"
for(var/datum/achievement_report/cheevo_report in GLOB.achievements_unlocked)
- parts += " [cheevo_report.winner_key] was [cheevo_report.winner], who earned the [span_greentext("'[cheevo_report.cheevo]'")] achievement at [cheevo_report.award_location]! "
+ parts += "[cheevo_report.winner] earned the [span_greentext("'[cheevo_report.cheevo]'")] achievement at [cheevo_report.award_location]! " // SKYRAT EDIT - No ckeys in the round end report - ORIGINAL: parts += " [cheevo_report.winner_key] was [cheevo_report.winner], who earned the [span_greentext("'[cheevo_report.cheevo]'")] achievement at [cheevo_report.award_location]! "
parts += "
"
return "
[parts.Join()]
"
diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm
index aa84927af03e86..50575d24b5b205 100644
--- a/code/_globalvars/lists/maintenance_loot.dm
+++ b/code/_globalvars/lists/maintenance_loot.dm
@@ -216,6 +216,7 @@ GLOBAL_LIST_INIT(uncommon_loot, list(//uncommon: useful items
/obj/item/stack/medical/gauze = 1,
/obj/item/stack/medical/mesh = 1,
/obj/item/stack/medical/suture = 1,
+ /obj/item/stack/medical/bandage = 1,
) = 1,
list(//medical chems
/obj/item/reagent_containers/cup/bottle/multiver = 1,
@@ -333,6 +334,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items
/obj/item/storage/box/hug/medical = 1,
/obj/item/storage/medkit/emergency = 1,
/obj/item/storage/medkit/regular = 1,
+ /obj/item/storage/box/bandages = 1,
) = 1,
list(//medical chems
/obj/item/reagent_containers/hypospray/medipen/oxandrolone = 1,
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index f87896e452123c..5ee1f4e643954d 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -52,6 +52,12 @@ GLOBAL_LIST_EMPTY(current_living_antags)
/// All observers with clients that joined as observers.
GLOBAL_LIST_EMPTY(current_observers_list)
+/// All living mobs which can hear blob telepathy
+GLOBAL_LIST_EMPTY(blob_telepathy_mobs)
+
+/// All "living" (because revenants are in between mortal planes or whatever) mobs that can hear revenants
+GLOBAL_LIST_EMPTY(revenant_relay_mobs)
+
///underages who have been reported to security for trying to buy things they shouldn't, so they can't spam
GLOBAL_LIST_EMPTY(narcd_underages)
diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm
index 018164e2c78786..01e6f3b59dc1f8 100644
--- a/code/_globalvars/lists/names.dm
+++ b/code/_globalvars/lists/names.dm
@@ -23,6 +23,7 @@ GLOBAL_LIST_INIT(posibrain_names, world.file2list("strings/names/posibrain.txt")
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(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/objects.dm b/code/_globalvars/lists/objects.dm
index 2fd384e69a94f1..8239dc29231f46 100644
--- a/code/_globalvars/lists/objects.dm
+++ b/code/_globalvars/lists/objects.dm
@@ -31,19 +31,6 @@ GLOBAL_LIST_EMPTY(deliverybeacontags)
/// List of all singularity components that exist
GLOBAL_LIST_EMPTY_TYPED(singularities, /datum/component/singularity)
-/// 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
-GLOBAL_LIST(chemical_reactions_list_reactant_index)
-/// list of all /datum/chemical_reaction datums. Used for the reaction lookup UI. Indexed by PRODUCT type
-GLOBAL_LIST(chemical_reactions_list_product_index) /// list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff
-GLOBAL_LIST_INIT(chemical_reagents_list, init_chemical_reagent_list())
-/// names of reagents used by plumbing UI.
-GLOBAL_LIST_INIT(chemical_name_list, init_chemical_name_list())
-/// List of all reactions with their associated product and result ids. Used for reaction lookups
-GLOBAL_LIST(chemical_reactions_results_lookup_list)
-/// List of all reagents that are parent types used to define a bunch of children - but aren't used themselves as anything.
-GLOBAL_LIST(fake_reagent_blacklist)
/// list of all /datum/tech datums indexed by id.
GLOBAL_LIST_EMPTY(tech_list)
/// list of all surgeries by name, associated with their path.
diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm
index 4d4f10ddc8acde..1a56a44f5e5b65 100644
--- a/code/_globalvars/lists/poll_ignore.dm
+++ b/code/_globalvars/lists/poll_ignore.dm
@@ -5,6 +5,7 @@
#define POLL_IGNORE_ALIEN_LARVA "alien_larva"
#define POLL_IGNORE_ASH_SPIRIT "ash_spirit"
#define POLL_IGNORE_ASHWALKER "ashwalker"
+#define POLL_IGNORE_BLOB "blob"
#define POLL_IGNORE_BOTS "bots"
#define POLL_IGNORE_CARGORILLA "cargorilla"
#define POLL_IGNORE_CONTRACTOR_SUPPORT "contractor_support"
@@ -43,6 +44,7 @@ GLOBAL_LIST_INIT(poll_ignore_desc, list(
POLL_IGNORE_ALIEN_LARVA = "Xenomorph larva",
POLL_IGNORE_ASH_SPIRIT = "Ash Spirit",
POLL_IGNORE_ASHWALKER = "Ashwalker eggs",
+ POLL_IGNORE_BLOB = "Blob spores",
POLL_IGNORE_BOTS = "Bots",
POLL_IGNORE_CARGORILLA = "Cargorilla",
POLL_IGNORE_CONTRACTOR_SUPPORT = "Contractor Support Unit",
diff --git a/code/_globalvars/lists/reagents.dm b/code/_globalvars/lists/reagents.dm
new file mode 100644
index 00000000000000..dec2724cfebd11
--- /dev/null
+++ b/code/_globalvars/lists/reagents.dm
@@ -0,0 +1,154 @@
+/// 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
+GLOBAL_LIST(chemical_reactions_list_reactant_index)
+/// list of all /datum/chemical_reaction datums. Used for the reaction lookup UI. Indexed by PRODUCT type
+GLOBAL_LIST(chemical_reactions_list_product_index)
+/// list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff
+GLOBAL_LIST_INIT(chemical_reagents_list, init_chemical_reagent_list())
+/// list of all reactions with their associated product and result ids. Used for reaction lookups
+GLOBAL_LIST(chemical_reactions_results_lookup_list)
+/// list of all reagents that are parent types used to define a bunch of children - but aren't used themselves as anything.
+GLOBAL_LIST(fake_reagent_blacklist)
+/// Turfs metalgen cant touch
+GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
+ /turf/closed/indestructible, //indestructible turfs should be indestructible, metalgen transmutation to plasma allows them to be destroyed
+ /turf/open/indestructible
+)))
+/// Names of human readable reagents used by plumbing UI.
+GLOBAL_LIST_INIT(chemical_name_list, init_chemical_name_list())
+/// Map of reagent names to its datum path
+GLOBAL_LIST_INIT(name2reagent, build_name2reagentlist())
+
+/// Initialises all /datum/reagent into a list indexed by reagent id
+/proc/init_chemical_reagent_list()
+ var/list/reagent_list = list()
+
+ for(var/datum/reagent/path as anything in subtypesof(/datum/reagent))
+ if(path in GLOB.fake_reagent_blacklist)
+ continue
+ var/datum/reagent/target_object = new path()
+ target_object.mass = rand(10, 800)
+ reagent_list[path] = target_object
+
+ return reagent_list
+
+/// Creates an list which is indexed by reagent name . used by plumbing reaction chamber and chemical filter UI
+/proc/init_chemical_name_list()
+ var/list/name_list = list()
+
+ for(var/X in GLOB.chemical_reagents_list)
+ var/datum/reagent/Reagent = GLOB.chemical_reagents_list[X]
+ name_list += Reagent.name
+
+ return sort_list(name_list)
+
+/**
+ * Chemical Reactions - Initialises all /datum/chemical_reaction into a list
+ * It is filtered into multiple lists within a list.
+ * For example:
+ * chemical_reactions_list_reactant_index[/datum/reagent/toxin/plasma] is a list of all reactions relating to plasma
+ * For chemical reaction list product index - indexes reactions based off the product reagent type - see get_recipe_from_reagent_product() in helpers
+ * For chemical reactions list lookup list - creates a bit list of info passed to the UI. This is saved to reduce lag from new windows opening, since it's a lot of data.
+ */
+/proc/build_chemical_reactions_lists()
+ if(GLOB.chemical_reactions_list_reactant_index)
+ return
+
+ //Prevent these reactions from appearing in lookup tables (UI code)
+ var/list/blacklist = typecacheof(/datum/chemical_reaction/randomized)
+
+ //Randomized need to go last since they need to check against conflicts with normal recipes
+ var/paths = subtypesof(/datum/chemical_reaction) - typesof(/datum/chemical_reaction/randomized) + subtypesof(/datum/chemical_reaction/randomized)
+ GLOB.chemical_reactions_list = list() //typepath to reaction list
+ GLOB.chemical_reactions_list_reactant_index = list() //reagents to reaction list
+ GLOB.chemical_reactions_results_lookup_list = list() //UI glob
+ GLOB.chemical_reactions_list_product_index = list() //product to reaction list
+
+ var/list/datum/chemical_reaction/reactions = list()
+ for(var/path in paths)
+ var/datum/chemical_reaction/reaction = new path()
+ reactions += reaction
+
+ // Ok so we're gonna do a thingTM here
+ // I want to distribute all our reactions such that each reagent id links to as few as possible
+ // I get the feeling there's a canonical way of doing this, but I don't know it
+ // So instead, we're gonna wing it
+ var/list/reagent_to_react_count = list()
+ for(var/datum/chemical_reaction/reaction as anything in reactions)
+ for(var/reagent_id as anything in reaction.required_reagents)
+ reagent_to_react_count[reagent_id] += 1
+
+ var/list/reaction_lookup = GLOB.chemical_reactions_list_reactant_index
+ // Create filters based on a random reagent id in the required reagents list - this is used to speed up handle_reactions()
+ // Basically, we only really need to care about ONE reagent, at least when initially filtering, since any others are ignorable
+ // Doing this separately because it relies on the loop above, and this is easier to parse
+ for(var/datum/chemical_reaction/reaction as anything in reactions)
+ var/preferred_id = null
+ for(var/reagent_id as anything in reaction.required_reagents)
+ if(isnull(preferred_id))
+ preferred_id = reagent_id
+ continue
+ // If we would have less then they would, take it
+ if(length(reaction_lookup[reagent_id]) < length(reaction_lookup[preferred_id]))
+ preferred_id = reagent_id
+ continue
+ // If they potentially have more then us, we take it
+ if(reagent_to_react_count[reagent_id] < reagent_to_react_count[preferred_id])
+ preferred_id = reagent_id
+ continue
+ if (!isnull(preferred_id))
+ if(!reaction_lookup[preferred_id])
+ reaction_lookup[preferred_id] = list()
+ reaction_lookup[preferred_id] += reaction
+
+ for(var/datum/chemical_reaction/reaction as anything in reactions)
+ var/list/product_ids = list()
+ var/list/reagents = list()
+ var/list/product_names = list()
+ var/bitflags = reaction.reaction_tags
+
+ if(!length(reaction.required_reagents)) //Skip impossible reactions
+ continue
+
+ GLOB.chemical_reactions_list[reaction.type] = reaction
+
+ for(var/reagent_path in reaction.required_reagents)
+ var/datum/reagent/reagent = find_reagent_object_from_type(reagent_path)
+ if(!istype(reagent))
+ stack_trace("Invalid reagent found in [reaction] required_reagents: [reagent_path]")
+ continue
+ reagents += list(list("name" = reagent.name, "id" = reagent.type))
+
+ for(var/product in reaction.results)
+ var/datum/reagent/reagent = find_reagent_object_from_type(product)
+ if(!istype(reagent))
+ stack_trace("Invalid reagent found in [reaction] results: [product]")
+ continue
+ product_names += reagent.name
+ product_ids += product
+
+ var/product_name
+ if(!length(product_names))
+ var/list/names = splittext("[reaction.type]", "/")
+ product_name = names[names.len]
+ else
+ product_name = product_names[1]
+
+ if(!is_type_in_typecache(reaction.type, blacklist))
+ //Master list of ALL reactions that is used in the UI lookup table. This is expensive to make, and we don't want to lag the server by creating it on UI request, so it's cached to send to UIs instantly.
+ GLOB.chemical_reactions_results_lookup_list += list(list("name" = product_name, "id" = reaction.type, "bitflags" = bitflags, "reactants" = reagents))
+
+ // Create filters based on each reagent id in the required reagents list - this is specifically for finding reactions from product(reagent) ids/typepaths.
+ for(var/id in product_ids)
+ if(!GLOB.chemical_reactions_list_product_index[id])
+ GLOB.chemical_reactions_list_product_index[id] = list()
+ GLOB.chemical_reactions_list_product_index[id] += reaction
+
+/// Builds map of reagent name to its datum path
+/proc/build_name2reagentlist()
+ . = list()
+ for (var/datum/reagent/R as anything in subtypesof(/datum/reagent))
+ var/name = initial(R.name)
+ if (length(name))
+ .[ckey(name)] = R
diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm
index a913410214da73..b37c61bc6815fd 100644
--- a/code/_globalvars/phobias.dm
+++ b/code/_globalvars/phobias.dm
@@ -72,7 +72,6 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
"doctors" = typecacheof(list(/mob/living/simple_animal/bot/medbot)),
"heresy" = typecacheof(list(
/mob/living/basic/heretic_summon,
- /mob/living/simple_animal/hostile/heretic_summon,
)),
"insects" = typecacheof(list(
/mob/living/basic/cockroach,
@@ -92,18 +91,18 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
"the supernatural" = typecacheof(list(
/mob/dead/observer,
/mob/living/basic/bat,
+ /mob/living/basic/construct,
/mob/living/basic/demon,
/mob/living/basic/faithless,
/mob/living/basic/ghost,
/mob/living/basic/heretic_summon,
+ /mob/living/basic/revenant,
/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/heretic_summon,
/mob/living/simple_animal/hostile/skeleton,
/mob/living/simple_animal/hostile/wizard,
/mob/living/simple_animal/hostile/zombie,
- /mob/living/simple_animal/revenant,
/mob/living/simple_animal/shade,
)),
))
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index d35c97ef68a967..da03288a0ca7e0 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -30,6 +30,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_CHUNKYFINGERS" = TRAIT_CHUNKYFINGERS,
"TRAIT_CHUNKYFINGERS_IGNORE_BATON" = TRAIT_CHUNKYFINGERS_IGNORE_BATON,
"TRAIT_FIST_MINING" = TRAIT_FIST_MINING,
+ "TRAIT_CAN_HOLD_ITEMS" = TRAIT_CAN_HOLD_ITEMS,
+ "TRAIT_FENCE_CLIMBER" = TRAIT_FENCE_CLIMBER,
"TRAIT_DUMB" = TRAIT_DUMB,
"TRAIT_ADVANCEDTOOLUSER" = TRAIT_ADVANCEDTOOLUSER,
"TRAIT_DISCOORDINATED_TOOL_USER" = TRAIT_DISCOORDINATED_TOOL_USER,
@@ -272,6 +274,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_FISH_TOXIN_IMMUNE" = TRAIT_FISH_TOXIN_IMMUNE,
"TRAIT_FISH_CROSSBREEDER" = TRAIT_FISH_CROSSBREEDER,
"TRAIT_FISH_FED_LUBE" = TRAIT_FISH_FED_LUBE,
+ "TRAIT_FISH_NO_HUNGER" = TRAIT_FISH_NO_HUNGER,
),
))
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 95697862b3c96c..3d632a99f7389b 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -73,10 +73,6 @@
if(world.time <= next_move)
return
- if(aicamera.in_camera_mode)
- aicamera.toggle_camera_mode(sound = FALSE)
- aicamera.captureimage(pixel_turf, usr)
- return
if(waypoint_mode)
waypoint_mode = 0
set_waypoint(A)
diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm
index 92340c59bc90e1..8cc8603f552c49 100644
--- a/code/_onclick/cyborg.dm
+++ b/code/_onclick/cyborg.dm
@@ -48,11 +48,6 @@
face_atom(A) // change direction to face what you clicked on
- if(aicamera.in_camera_mode) //Cyborg picture taking
- aicamera.toggle_camera_mode(sound = FALSE)
- aicamera.captureimage(A, usr)
- return
-
var/obj/item/W = get_active_held_item()
if(!W && get_dist(src,A) <= interaction_range)
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index afedfa5befe6ef..79f857e8cbef07 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -26,7 +26,7 @@
screens -= category
- if(animated)
+ if(!QDELETED(src) && animated)
animate(screen, alpha = 0, time = animated)
addtimer(CALLBACK(src, PROC_REF(clear_fullscreen_after_animate), screen), animated, TIMER_CLIENT_TIME)
else
@@ -216,3 +216,10 @@
layer = LIGHTING_ABOVE_ALL
blend_mode = BLEND_ADD
show_when_dead = TRUE
+
+/atom/movable/screen/fullscreen/static_vision
+ icon = 'icons/hud/screen_gen.dmi'
+ screen_loc = "WEST,SOUTH to EAST,NORTH"
+ icon_state = "noise"
+ color = "#04a8d1"
+ alpha = 80
diff --git a/code/_onclick/hud/generic_dextrous.dm b/code/_onclick/hud/generic_dextrous.dm
index bf09fa33717139..64ad896d57a1f5 100644
--- a/code/_onclick/hud/generic_dextrous.dm
+++ b/code/_onclick/hud/generic_dextrous.dm
@@ -43,7 +43,7 @@
using.icon = ui_style
static_inventory += using
- mymob.canon_client.clear_screen()
+ mymob.canon_client?.clear_screen()
for(var/atom/movable/screen/inventory/inv in (static_inventory + toggleable_inventory))
if(inv.slot_id)
diff --git a/code/_onclick/hud/screentip.dm b/code/_onclick/hud/screentip.dm
index 107f4ce1be523c..94b8f591f650f8 100644
--- a/code/_onclick/hud/screentip.dm
+++ b/code/_onclick/hud/screentip.dm
@@ -14,7 +14,7 @@
/atom/movable/screen/screentip/proc/update_view(datum/source)
SIGNAL_HANDLER
- if(!hud || !hud.mymob.canon_client.view_size) //Might not have been initialized by now
+ if(!hud || !hud.mymob.canon_client?.view_size) //Might not have been initialized by now
return
maptext_width = view_to_pixels(hud.mymob.canon_client.view_size.getView())[1]
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 1868057d82c18c..ed1cdc57b76147 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -287,34 +287,13 @@
/atom/proc/attack_pai_secondary(mob/user, list/modifiers)
return SECONDARY_ATTACK_CALL_NORMAL
-/*
- Simple animals
-*/
-
-/mob/living/simple_animal/resolve_unarmed_attack(atom/attack_target, list/modifiers)
- if(dextrous && (isitem(attack_target) || !combat_mode))
- attack_target.attack_hand(src, modifiers)
- update_held_items()
- else
- return ..()
-
-/mob/living/simple_animal/resolve_right_click_attack(atom/target, list/modifiers)
- if(dextrous && (isitem(target) || !combat_mode))
- . = target.attack_hand_secondary(src, modifiers)
- update_held_items()
- else
- return ..()
-
/*
Hostile animals
*/
/mob/living/simple_animal/hostile/resolve_unarmed_attack(atom/attack_target, list/modifiers)
GiveTarget(attack_target)
- if(dextrous && (isitem(attack_target) || !combat_mode))
- return ..()
- else
- INVOKE_ASYNC(src, PROC_REF(AttackingTarget), attack_target)
+ return ..()
#undef LIVING_UNARMED_ATTACK_BLOCKED
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index b5fc855da3196c..16791b7fd7e623 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -262,10 +262,25 @@
/datum/config_entry/string/hostedby
-/datum/config_entry/flag/norespawn
-
+/// Determines if a player can respawn after dying.
+/// 0 / RESPAWN_FLAG_DISABLED = Cannot respawn (default)
+/// 1 / RESPAWN_FLAG_FREE = Can respawn
+/// 2 / RESPAWN_FLAG_NEW_CHARACTER = Can respawn if choosing a different character
+/datum/config_entry/flag/allow_respawn
+ default = RESPAWN_FLAG_DISABLED
+
+/datum/config_entry/flag/allow_respawn/ValidateAndSet(str_val)
+ if(!VASProcCallGuard(str_val))
+ return FALSE
+ var/val_as_num = text2num(str_val)
+ if(val_as_num in list(RESPAWN_FLAG_DISABLED, RESPAWN_FLAG_FREE, RESPAWN_FLAG_NEW_CHARACTER))
+ config_entry_value = val_as_num
+ return TRUE
+ return FALSE
+
+/// Determines how long (in deciseconds) before a player is allowed to respawn.
/datum/config_entry/number/respawn_delay
- default = 0
+ default = 0 SECONDS
/datum/config_entry/flag/usewhitelist
diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm
index 488debb48e5a8a..1de3f900c8ca08 100644
--- a/code/controllers/subsystem/air.dm
+++ b/code/controllers/subsystem/air.dm
@@ -452,7 +452,7 @@ SUBSYSTEM_DEF(air)
border += item
net.air.volume += item.volume
- item.parent = net
+ item.replace_pipenet(item.parent, net)
if(item.air_temporary)
net.air.merge(item.air_temporary)
diff --git a/code/controllers/subsystem/economy.dm b/code/controllers/subsystem/economy.dm
index e396bcb8445632..511a438bf9a60c 100644
--- a/code/controllers/subsystem/economy.dm
+++ b/code/controllers/subsystem/economy.dm
@@ -123,6 +123,8 @@ SUBSYSTEM_DEF(economy)
var/effective_mailcount = round(living_player_count()/(inflation_value - 0.5)) //More mail at low inflation, and vis versa.
mail_waiting += clamp(effective_mailcount, 1, MAX_MAIL_PER_MINUTE * seconds_per_tick)
+ SSstock_market.news_string = ""
+
/**
* Handy proc for obtaining a department's bank account, given the department ID, AKA the define assigned for what department they're under.
*/
@@ -173,7 +175,7 @@ SUBSYSTEM_DEF(economy)
fluff_string = ", but company countermeasures protect YOU from being affected!"
else
fluff_string = ", and company countermeasures are failing to protect YOU from being affected. We're all doomed!"
- earning_report = "Sector Economic Report
Sector vendor prices is currently at [SSeconomy.inflation_value()*100]%[fluff_string]
The station spending power is currently [station_total] Credits, and the crew's targeted allowance is at [station_target] Credits.
That's all from the Nanotrasen Economist Division."
+ earning_report = "Sector Economic Report
Sector vendor prices is currently at [SSeconomy.inflation_value()*100]%[fluff_string]
The station spending power is currently [station_total] Credits, and the crew's targeted allowance is at [station_target] Credits.
[SSstock_market.news_string] That's all from the Nanotrasen Economist Division."
GLOB.news_network.submit_article(earning_report, "Station Earnings Report", "Station Announcements", null, update_alert = FALSE)
return TRUE
diff --git a/code/controllers/subsystem/id_access.dm b/code/controllers/subsystem/id_access.dm
index 963957741f2021..86f2e124a8f422 100644
--- a/code/controllers/subsystem/id_access.dm
+++ b/code/controllers/subsystem/id_access.dm
@@ -329,6 +329,7 @@ SUBSYSTEM_DEF(id_access)
desc_by_access["[ACCESS_CENT_SPECOPS]"] = "Code Black"
desc_by_access["[ACCESS_CENT_CAPTAIN]"] = "Code Gold"
desc_by_access["[ACCESS_CENT_BAR]"] = "Code Scotch"
+ desc_by_access["[ACCESS_BIT_DEN]"] = "Bitrunner Den"
desc_by_access["[ACCESS_BARBER]"] = "Barber" // SKYRAT EDIT ADD - BARBER UPDATE
/**
diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm
index 976b4bab24923f..8376059e910375 100644
--- a/code/controllers/subsystem/job.dm
+++ b/code/controllers/subsystem/job.dm
@@ -1,5 +1,3 @@
-#define VERY_LATE_ARRIVAL_TOAST_PROB 20
-
SUBSYSTEM_DEF(job)
name = "Jobs"
init_order = INIT_ORDER_JOBS
@@ -546,21 +544,16 @@ SUBSYSTEM_DEF(job)
/datum/controller/subsystem/job/proc/EquipRank(mob/living/equipping, datum/job/job, client/player_client)
// SKYRAT EDIT ADDITION BEGIN - ALTERNATIVE_JOB_TITLES
// The alt job title, if user picked one, or the default
- var/chosen_title = player_client?.prefs.alt_job_titles[job.title] || job.title
- var/default_title = job.title
- // SKYRAT EDIT ADDITION END - job.title
+ var/alt_title = player_client?.prefs.alt_job_titles[job.title]
+ // SKYRAT EDIT ADDITION END
equipping.job = job.title
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
- equipping.mind?.set_assigned_role_with_greeting(job, player_client)
- if(player_client)
- to_chat(player_client, span_infoplain("You are the [chosen_title].")) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: to_chat(player_client, span_infoplain("You are the [job.title]."))
-
- equipping.on_job_equipping(job, player_client?.prefs) //SKYRAT EDIT CHANGE
-
- job.announce_job(equipping, chosen_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: job.announce_job(equipping)
+ equipping.mind?.set_assigned_role_with_greeting(job, player_client, alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: equipping.mind?.set_assigned_role_with_greeting(job, player_client)
+ equipping.on_job_equipping(job, player_client?.prefs, player_client) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: equipping.on_job_equipping(job)
+ job.announce_job(equipping, alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: job.announce_job(equipping)
if(player_client?.holder)
if(CONFIG_GET(flag/auto_deadmin_players) || (player_client.prefs?.toggles & DEADMIN_ALWAYS))
@@ -568,46 +561,7 @@ SUBSYSTEM_DEF(job)
else
handle_auto_deadmin_roles(player_client, job.title)
-
- if(player_client)
- to_chat(player_client, span_infoplain("As the [chosen_title == job.title ? chosen_title : "[chosen_title] ([job.title])"] you answer directly to [job.supervisors]. Special circumstances may change this.")) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: to_chat(player_client, span_infoplain("As the [job.title] you answer directly to [job.supervisors]. Special circumstances may change this."))
-
- job.radio_help_message(equipping)
-
- if(player_client)
- if(job.req_admin_notify)
- to_chat(player_client, span_infoplain("You are playing a job that is important for Game Progression. \
- If you have to disconnect, please notify the admins via adminhelp."))
- if(CONFIG_GET(number/minimal_access_threshold))
- to_chat(player_client, span_boldnotice("As this station was initially staffed with a \
- [CONFIG_GET(flag/jobs_have_minimal_access) ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] \
- have been added to your ID card."))
- //SKYRAT EDIT START - ALTERNATIVE_JOB_TITLES
- if(chosen_title != default_title)
- to_chat(player_client, span_infoplain(span_warning("Remember that alternate titles are purely for flavor and roleplay.")))
- to_chat(player_client, span_infoplain(span_doyourjobidiot("Do not use your \"[chosen_title]\" alt title as an excuse to forego your duties as a [job.title].")))
- //SKYRAT EDIT END
- var/related_policy = get_policy(job.title)
- if(related_policy)
- to_chat(player_client, related_policy)
- to_chat(player_client, span_boldnotice("As this station was initially staffed with a \
- [CONFIG_GET(flag/jobs_have_minimal_access) ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] \
- have been added to your ID card."))
-
- if(ishuman(equipping))
- var/mob/living/carbon/human/wageslave = equipping
- wageslave.add_mob_memory(/datum/memory/key/account, remembered_id = wageslave.account_id)
-
- setup_alt_job_items(wageslave, job, player_client) // SKYRAT EDIT ADDITION - ALTERNATIVE_JOB_TITLES
-
- if(EMERGENCY_PAST_POINT_OF_NO_RETURN && prob(VERY_LATE_ARRIVAL_TOAST_PROB))
- // SKYRAT EDIT CHANGE START - Lizards
- if(islizard(equipping))
- equipping.equip_to_slot_or_del(new /obj/item/food/breadslice/root(equipping), ITEM_SLOT_MASK)
- else
- equipping.equip_to_slot_or_del(new /obj/item/food/griddle_toast(equipping), ITEM_SLOT_MASK)
- // SKYRAT EDIT CHANGE END - Lizards
-
+ setup_alt_job_items(equipping, job, player_client) // SKYRAT EDIT ADDITION - ALTERNATIVE_JOB_TITLES
job.after_spawn(equipping, player_client)
/datum/controller/subsystem/job/proc/handle_auto_deadmin_roles(client/C, rank)
@@ -1010,5 +964,3 @@ SUBSYSTEM_DEF(job)
return TRUE
return FALSE
-
-#undef VERY_LATE_ARRIVAL_TOAST_PROB
diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm
index 7067b3abbfc7bf..6ff9d39d5313f4 100644
--- a/code/controllers/subsystem/movement/movement_types.dm
+++ b/code/controllers/subsystem/movement/movement_types.dm
@@ -384,7 +384,7 @@
src.simulated_only = simulated_only
src.avoid = avoid
src.skip_first = skip_first
- movement_path = initial_path.Copy()
+ 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
diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm
index 6e6fc6547e2e71..c34d97b28f9c45 100644
--- a/code/controllers/subsystem/processing/quirks.dm
+++ b/code/controllers/subsystem/processing/quirks.dm
@@ -15,7 +15,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list(
list(/datum/quirk/prosthetic_limb, /datum/quirk/quadruple_amputee, /datum/quirk/body_purist),
list(/datum/quirk/prosthetic_organ, /datum/quirk/tin_man, /datum/quirk/body_purist),
list(/datum/quirk/quadruple_amputee, /datum/quirk/paraplegic, /datum/quirk/hemiplegic),
- list(/datum/quirk/quadruple_amputee, /datum/quirk/frail),
+ //list(/datum/quirk/quadruple_amputee, /datum/quirk/frail), // SKYRAT EDIT REMOVAL- Since we have synth wounds now, frail has a large downside for prosthetics and such
list(/datum/quirk/social_anxiety, /datum/quirk/mute),
list(/datum/quirk/mute, /datum/quirk/softspoken),
list(/datum/quirk/poor_aim, /datum/quirk/bighands),
diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm
index 7f0b52d471bc84..1cc3468fb7db48 100644
--- a/code/controllers/subsystem/research.dm
+++ b/code/controllers/subsystem/research.dm
@@ -313,6 +313,8 @@ SUBSYSTEM_DEF(research)
*/
/datum/controller/subsystem/research/proc/get_available_servers(turf/location)
var/list/local_servers = list()
+ if(!location)
+ return local_servers
for (var/datum/techweb/individual_techweb as anything in techwebs)
var/list/servers = find_valid_servers(location, individual_techweb)
if(length(servers))
diff --git a/code/controllers/subsystem/stock_market.dm b/code/controllers/subsystem/stock_market.dm
new file mode 100644
index 00000000000000..7c2cb71dc4972b
--- /dev/null
+++ b/code/controllers/subsystem/stock_market.dm
@@ -0,0 +1,154 @@
+
+SUBSYSTEM_DEF(stock_market)
+ name = "Stock Market"
+ wait = 20 SECONDS
+ init_order = INIT_ORDER_DEFAULT
+ runlevels = RUNLEVEL_GAME
+
+ /// Associated list of materials and their prices at the given time.
+ var/list/materials_prices = list()
+ /// Associated list of materials alongside their market trends. 1 is up, 0 is stable, -1 is down.
+ var/list/materials_trends = list()
+ /// Associated list of materials alongside the life of it's current trend. After it's life is up, it will change to a new trend.
+ var/list/materials_trend_life = list()
+ /// Associated list of materials alongside their available quantity. This is used to determine how much of a material is available to buy, and how much buying and selling affects the price.
+ var/list/materials_quantity = list()
+ /// HTML string that is used to display the market events to the player.
+ var/news_string = ""
+
+/datum/controller/subsystem/stock_market/Initialize()
+ for(var/datum/material/possible_market as anything in subtypesof(/datum/material)) // I need to make this work like this, but lets hardcode it for now
+ if(initial(possible_market.tradable))
+ materials_prices += possible_market
+ materials_prices[possible_market] = initial(possible_market.value_per_unit) * SHEET_MATERIAL_AMOUNT
+
+ materials_trends += possible_market
+ materials_trends[possible_market] = rand(MARKET_TREND_DOWNWARD,MARKET_TREND_UPWARD) //aka -1 to 1
+
+ materials_trend_life += possible_market
+ materials_trend_life[possible_market] = rand(1,10)
+
+ materials_quantity += possible_market
+ materials_quantity[possible_market] = initial(possible_market.tradable_base_quantity) + (rand(-initial(possible_market.tradable_base_quantity) * 0.5, initial(possible_market.tradable_base_quantity) * 0.5))
+ return SS_INIT_SUCCESS
+/datum/controller/subsystem/stock_market/fire(resumed)
+ for(var/datum/material/market as anything in materials_prices)
+ handle_trends_and_price(market)
+
+/**
+ * Handles shifts in the cost of materials, and in what direction the material is most likely to move.
+ */
+/datum/controller/subsystem/stock_market/proc/handle_trends_and_price(datum/material/mat)
+ if(prob(MARKET_EVENT_PROBABILITY))
+ handle_market_event(mat)
+ return
+ var/trend = materials_trends[mat]
+ var/trend_life = materials_trend_life[mat]
+
+ var/price_units = materials_prices[mat]
+ var/price_minimum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 0.5)
+ if(!isnull(initial(mat.minimum_value_override)))
+ price_minimum = round(initial(mat.minimum_value_override) * SHEET_MATERIAL_AMOUNT)
+ var/price_maximum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 3)
+ var/price_baseline = initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT
+
+ var/stock_quantity = materials_quantity[mat]
+
+ if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)) //We hardset to the worst possible price and lowest possible impact if sold
+ materials_prices[mat] = price_minimum
+ materials_quantity[mat] = stock_quantity * 2
+ materials_trends[mat] = MARKET_TREND_DOWNWARD
+ trend_life = materials_trend_life[mat] = 1
+ return
+
+ if(trend_life == 0)
+ ///We want to scale our trend so that if we're closer to our minimum or maximum price, we're more likely to trend the other way.
+ if((price_units < price_baseline))
+ var/chance_swap = 100 - ((clamp((price_units - price_minimum), 1, 1000) / (price_baseline - price_minimum))*100)
+ if(prob(chance_swap))
+ materials_trends[mat] = MARKET_TREND_UPWARD
+ else
+ materials_trends[mat] = MARKET_TREND_STABLE
+ else if((price_units > price_baseline))
+ var/chance_swap = 100 - ((clamp((price_units - price_maximum), 1, 1000) / (price_maximum - price_baseline))*100)
+ if(prob(chance_swap))
+ materials_trends[mat] = MARKET_TREND_DOWNWARD
+ else
+ materials_trends[mat] = MARKET_TREND_STABLE
+ materials_trend_life[mat] = rand(3,10) // Change our trend life for x number of cycles
+ else
+ materials_trend_life[mat] -= 1
+
+ var/price_change = 0
+ var/quantity_change = 0
+ switch(trend)
+ if(MARKET_TREND_UPWARD)
+ price_change = ROUND_UP(gaussian(price_units * 0.1, price_baseline * 0.05)) //If we don't ceil, small numbers will get trapped at low values
+ quantity_change = -round(gaussian(stock_quantity * 0.1, stock_quantity * 0.05))
+ if(MARKET_TREND_STABLE)
+ price_change = round(gaussian(0, price_baseline * 0.01))
+ quantity_change = round(gaussian(0, stock_quantity * 0.01))
+ if(MARKET_TREND_DOWNWARD)
+ price_change = -ROUND_UP(gaussian(price_units * 0.1, price_baseline * 0.05))
+ quantity_change = round(gaussian(stock_quantity * 0.1, stock_quantity * 0.05))
+ materials_prices[mat] = round(clamp(price_units + price_change, price_minimum, price_maximum))
+ materials_quantity[mat] = round(clamp(stock_quantity + quantity_change, 0, initial(mat.tradable_base_quantity) * 2))
+
+/**
+ * Market events are a way to spice up the market and make it more interesting.
+ * Randomly one will occur to a random material, and it will change the price of that material more drastically, or reset it to a stable price.
+ * Events are also broadcast to the newscaster as a fun little fluff piece. Good way to tell some lore as well, or just make a joke.
+ */
+/datum/controller/subsystem/stock_market/proc/handle_market_event(datum/material/mat)
+
+ var/company_name = list( // Pick a random company name from the list, I let copilot make a few up for me which is why some suck
+ "Nakamura Engineering",
+ "Robust Industries, LLC",
+ "MODular Solutions",
+ "SolGov",
+ "Australicus Industrial Mining",
+ "Vey-Medical",
+ "Aussec Armory",
+ "Dreamland Robotics"
+ )
+ var/circumstance
+ var/event = rand(1,3)
+
+ var/price_units = materials_prices[mat]
+ var/price_minimum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 0.5)
+ if(!isnull(initial(mat.minimum_value_override)))
+ price_minimum = round(initial(mat.minimum_value_override) * SHEET_MATERIAL_AMOUNT)
+ var/price_maximum = round(initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 3)
+ var/price_baseline = initial(mat.value_per_unit) * SHEET_MATERIAL_AMOUNT
+
+ switch(event)
+ if(1) //Reset to stable
+ materials_prices[mat] = price_baseline
+ materials_trends[mat] = MARKET_TREND_STABLE
+ materials_trend_life[mat] = 1
+ circumstance = pick(list(
+ "[pick(company_name)] has been bought out by a private investment firm. As a result, [initial(mat.name)] is now stable at [materials_prices[mat]] cr.",
+ "Due to a corporate restructuring, the largest supplier of [initial(mat.name)] has had the price changed to [materials_prices[mat]] cr.",
+ "[initial(mat.name)] is now under a monopoly by [pick(company_name)]. The price has been changed to [materials_prices[mat]] cr accordingly."
+ ))
+ if(2) //Big boost
+ materials_prices[mat] += round(gaussian(price_units * 0.5, price_units * 0.1))
+ materials_prices[mat] = clamp(materials_prices[mat], price_minimum, price_maximum)
+ materials_trends[mat] = MARKET_TREND_UPWARD
+ materials_trend_life[mat] = rand(1,5)
+ circumstance = pick(list(
+ "[pick(company_name)] has just released a new product that uses [initial(mat.name)]! As a result, the price has been raised to [materials_prices[mat]] cr.",
+ "Due to [pick(company_name)] finding a new property of [initial(mat.name)], its price has been raised to [materials_prices[mat]] cr.",
+ "A study has found that [initial(mat.name)] may run out within the next 100 years. The price has raised to [materials_prices[mat]] cr due to panic."
+ ))
+ if(3) //Big drop
+ materials_prices[mat] -= round(gaussian(price_units * 1.5, price_units * 0.1))
+ materials_prices[mat] = clamp(materials_prices[mat], price_minimum, price_maximum)
+ materials_trends[mat] = MARKET_TREND_DOWNWARD
+ materials_trend_life[mat] = rand(1,5)
+ circumstance = pick(list(
+ "[pick(company_name)]'s latest product has seen major controversy, and as a result, the price of [initial(mat.name)] has dropped to [materials_prices[mat]] cr.",
+ "Due to a new competitor, the price of [initial(mat.name)] has dropped to [materials_prices[mat]] cr.",
+ "[initial(mat.name)] has been found to be a carcinogen. The price has dropped to [materials_prices[mat]] cr due to panic."
+ ))
+ news_string += circumstance + " " // Add the event to the news_string, formatted for newscasters.
diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm
index b43220fb7d4b4a..d81c72cc920080 100644
--- a/code/datums/actions/action.dm
+++ b/code/datums/actions/action.dm
@@ -96,13 +96,15 @@
if(check_flags & AB_CHECK_CONSCIOUS)
RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(update_status_on_signal))
if(check_flags & AB_CHECK_INCAPACITATED)
- RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), PROC_REF(update_status_on_signal))
+ RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED)), PROC_REF(update_status_on_signal))
if(check_flags & AB_CHECK_IMMOBILE)
- RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), PROC_REF(update_status_on_signal))
+ RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED)), PROC_REF(update_status_on_signal))
if(check_flags & AB_CHECK_HANDS_BLOCKED)
- RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), PROC_REF(update_status_on_signal))
+ RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED)), PROC_REF(update_status_on_signal))
if(check_flags & AB_CHECK_LYING)
RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(update_status_on_signal))
+ if(check_flags & AB_CHECK_PHASED)
+ RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED), SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED)), PROC_REF(update_status_on_signal))
if(owner_has_control)
GiveAction(grant_to)
@@ -115,7 +117,7 @@
if(!hud.mymob)
continue
HideFrom(hud.mymob)
- LAZYREMOVE(remove_from.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
+ 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)
@@ -130,6 +132,11 @@
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)
@@ -174,6 +181,10 @@
if (feedback)
owner.balloon_alert(owner, "unconscious!")
return FALSE
+ if((check_flags & AB_CHECK_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED))
+ if (feedback)
+ owner.balloon_alert(owner, "incorporeal!")
+ return FALSE
return TRUE
/// Builds / updates all buttons we have shared or given out
diff --git a/code/datums/actions/mobs/lava_swoop.dm b/code/datums/actions/mobs/lava_swoop.dm
index 9c305ceb13b41f..7532ccfda08823 100644
--- a/code/datums/actions/mobs/lava_swoop.dm
+++ b/code/datums/actions/mobs/lava_swoop.dm
@@ -147,7 +147,7 @@
if(isindestructiblefloor(T))
continue
if(!isindestructiblewall(T))
- T.ChangeTurf(/turf/open/misc/asteroid/basalt/lava_land_surface, flags = CHANGETURF_INHERIT_AIR)
+ T.TerraformTurf(/turf/open/misc/asteroid/basalt/lava_land_surface, flags = CHANGETURF_INHERIT_AIR)
else
indestructible_turfs += T
SLEEP_CHECK_DEATH(1 SECONDS, owner) // give them a bit of time to realize what attack is actually happening
diff --git a/code/datums/actions/mobs/open_mob_commands.dm b/code/datums/actions/mobs/open_mob_commands.dm
index 008e7c4dc1bb7c..e7ffd104effbe0 100644
--- a/code/datums/actions/mobs/open_mob_commands.dm
+++ b/code/datums/actions/mobs/open_mob_commands.dm
@@ -5,6 +5,7 @@
overlay_icon_state = "bg_heretic_border"
button_icon = 'icons/mob/actions/actions_ecult.dmi'
button_icon_state = "stargazer_menu"
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED | AB_CHECK_PHASED
/// Weakref for storing our stargazer
var/datum/weakref/our_mob
diff --git a/code/datums/ai/basic_mobs/base_basic_controller.dm b/code/datums/ai/basic_mobs/base_basic_controller.dm
index 3eb79a815ad50e..cd025b28bcb2bd 100644
--- a/code/datums/ai/basic_mobs/base_basic_controller.dm
+++ b/code/datums/ai/basic_mobs/base_basic_controller.dm
@@ -18,7 +18,10 @@
if(!isliving(pawn))
return
var/mob/living/living_pawn = pawn
- if(!(ai_traits & CAN_ACT_WHILE_DEAD) && IS_DEAD_OR_INCAP(living_pawn))
+ var/incap_flags = NONE
+ if (ai_traits & CAN_ACT_IN_STASIS)
+ incap_flags |= IGNORE_STASIS
+ if(!(ai_traits & CAN_ACT_WHILE_DEAD) && (living_pawn.incapacitated(incap_flags) || living_pawn.stat))
return FALSE
if(ai_traits & PAUSE_DURING_DO_AFTER && LAZYLEN(living_pawn.do_afters))
return FALSE
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/find_mineable_wall.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/find_mineable_wall.dm
deleted file mode 100644
index ad5749c9161116..00000000000000
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/find_mineable_wall.dm
+++ /dev/null
@@ -1,33 +0,0 @@
-//behavior to find mineable mineral walls
-/datum/ai_behavior/find_mineral_wall
-
-/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key)
- . = ..()
- var/mob/living_pawn = controller.pawn
-
- for(var/turf/closed/mineral/potential_wall in oview(9, living_pawn))
- if(!check_if_mineable(controller, potential_wall)) //check if its surrounded by walls
- continue
- controller.set_blackboard_key(found_wall_key, potential_wall) //closest wall first!
- finish_action(controller, TRUE)
- return
-
- finish_action(controller, FALSE)
-
-/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
- var/mob/living/source = controller.pawn
- var/direction_to_turf = get_dir(target_wall, source)
- if(!ISDIAGONALDIR(direction_to_turf))
- return TRUE
- var/list/directions_to_check = list()
- for(var/direction_check in GLOB.cardinals)
- if(direction_check & direction_to_turf)
- directions_to_check += direction_check
-
- for(var/direction in directions_to_check)
- var/turf/test_turf = get_step(target_wall, direction)
- if(isnull(test_turf))
- continue
- if(!test_turf.is_blocked_turf(ignore_atoms = list(source)))
- return TRUE
- return FALSE
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 bd86260ee89ce6..551cb12f3b1458 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
@@ -19,13 +19,13 @@
/datum/ai_behavior/run_away_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if (QDELETED(target) || !can_see(controller.pawn, target, run_distance))
finish_action(controller, succeeded = TRUE, target_key = target_key, hiding_location_key = hiding_location_key)
return
- if (get_dist(controller.pawn, controller.current_movement_target) >= required_distance)
+ if (get_dist(controller.pawn, controller.current_movement_target) > required_distance)
return // Still heading over
if (plot_path_away_from(controller, target))
return
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm
index 04cb9b171ddb60..ba167b34f29514 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm
@@ -11,7 +11,8 @@
finish_action(controller, FALSE, ability_key, target_key)
return
var/mob/pawn = controller.pawn
- var/result = ability.InterceptClickOn(pawn, null, target)
+ pawn.face_atom(target)
+ var/result = ability.Trigger(target = target)
finish_action(controller, result, ability_key, target_key)
/datum/ai_behavior/targeted_mob_ability/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
index 0b9e31db667e15..376f62a5855b53 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
@@ -23,7 +23,8 @@
var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
controller.clear_blackboard_key(target_key)
- var/list/potential_targets = hearers(aggro_range, controller.pawn) - living_mob //Remove self, so we don't suicide
+
+ var/list/potential_targets = hearers(aggro_range, get_turf(controller.pawn)) - living_mob //Remove self, so we don't suicide
for(var/HM in typecache_filter_list(range(aggro_range, living_mob), hostile_machines)) //Can we see any hostile machines?
if(can_see(living_mob, HM, aggro_range))
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
index bbc1a43e322482..55f6ef4c4c00e6 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
@@ -6,6 +6,8 @@
/datum/ai_behavior/travel_towards
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ /// If true we will get rid of our target on completion
+ var/clear_target = FALSE
/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key)
. = ..()
@@ -16,7 +18,15 @@
/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
. = ..()
- finish_action(controller, TRUE)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key)
+ . = ..()
+ if (clear_target)
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/travel_towards/stop_on_arrival
+ clear_target = TRUE
/**
* # Travel Towards Atom
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm b/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm
index 1beab6ec907459..bad349030f1b46 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm
@@ -1,4 +1,5 @@
/datum/ai_planning_subtree/climb_trees
+ operational_datums = list(/datum/component/tree_climber)
///chance to climb a tree
var/climb_chance = 35
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
index 8d1391f7c7dda5..4f901745eeea5e 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
@@ -10,7 +10,7 @@
/datum/ai_planning_subtree/flee_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/flee_from = controller.blackboard[target_key]
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING] || QDELETED(flee_from))
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING] || QDELETED(flee_from))
return
var/flee_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE
if (get_dist(controller.pawn, flee_from) >= flee_distance)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
index c09e7cdbf75f98..2a85e9e902b2cc 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
@@ -8,6 +8,8 @@
var/maximum_distance = 6
/// How far do we look for our target?
var/view_distance = 10
+ /// the run away behavior we will use
+ var/run_away_behavior = /datum/ai_behavior/step_away
/datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
@@ -16,12 +18,15 @@
return // Don't run away from cucumbers, they're not snakes
var/range = get_dist(controller.pawn, target)
if (range < minimum_distance)
- controller.queue_behavior(/datum/ai_behavior/step_away, target_key)
+ controller.queue_behavior(run_away_behavior, target_key, minimum_distance)
return
if (range > maximum_distance)
controller.queue_behavior(/datum/ai_behavior/pursue_to_range, target_key, maximum_distance)
return
+/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance
+ run_away_behavior = /datum/ai_behavior/cover_minimum_distance
+
/// Take one step away
/datum/ai_behavior/step_away
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
@@ -80,3 +85,32 @@
if (!QDELETED(current_target) && get_dist(controller.pawn, current_target) > range)
return
finish_action(controller, succeeded = TRUE)
+
+///instead of taking a single step, we cover the entire distance
+/datum/ai_behavior/cover_minimum_distance
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ required_distance = 0
+ action_cooldown = 0.2 SECONDS
+
+/datum/ai_behavior/cover_minimum_distance/setup(datum/ai_controller/controller, target_key, minimum_distance)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ var/required_distance = minimum_distance - get_dist(controller.pawn, target) //the distance we need to move
+ var/distance = 0
+ var/turf/chosen_turf
+ for(var/turf/open/potential_turf in oview(required_distance, controller.pawn))
+ var/new_distance_from_target = get_dist(potential_turf, target)
+ if(potential_turf.is_blocked_turf())
+ continue
+ if(new_distance_from_target > distance)
+ chosen_turf = potential_turf
+ distance = new_distance_from_target
+ if(isnull(chosen_turf))
+ return FALSE
+ set_movement_target(controller, target = chosen_turf)
+
+/datum/ai_behavior/cover_minimum_distance/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ finish_action(controller, succeeded = TRUE)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
new file mode 100644
index 00000000000000..3c03702b69947b
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
@@ -0,0 +1,73 @@
+//behavior to find mineable mineral walls
+
+/datum/ai_planning_subtree/mine_walls
+ var/find_wall_behavior = /datum/ai_behavior/find_mineral_wall
+
+/datum/ai_planning_subtree/mine_walls/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard_key_exists(BB_TARGET_MINERAL_WALL))
+ controller.queue_behavior(/datum/ai_behavior/mine_wall, BB_TARGET_MINERAL_WALL)
+ return SUBTREE_RETURN_FINISH_PLANNING
+ controller.queue_behavior(find_wall_behavior, BB_TARGET_MINERAL_WALL)
+
+/datum/ai_behavior/mine_wall
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ action_cooldown = 15 SECONDS
+
+/datum/ai_behavior/mine_wall/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/turf/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/mine_wall/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/turf/closed/mineral/target = controller.blackboard[target_key]
+ var/is_gibtonite_turf = istype(target, /turf/closed/mineral/gibtonite)
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key)
+ return
+ living_pawn.melee_attack(target)
+ if(is_gibtonite_turf)
+ living_pawn.manual_emote("sighs...") //accept whats about to happen to us
+
+ finish_action(controller, TRUE, target_key)
+ return
+
+/datum/ai_behavior/mine_wall/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/find_mineral_wall
+
+/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key)
+ . = ..()
+ var/mob/living_pawn = controller.pawn
+
+ for(var/turf/closed/mineral/potential_wall in oview(9, living_pawn))
+ if(!check_if_mineable(controller, potential_wall)) //check if its surrounded by walls
+ continue
+ controller.set_blackboard_key(found_wall_key, potential_wall) //closest wall first!
+ finish_action(controller, TRUE)
+ return
+
+ finish_action(controller, FALSE)
+
+/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
+ var/mob/living/source = controller.pawn
+ var/direction_to_turf = get_dir(target_wall, source)
+ if(!ISDIAGONALDIR(direction_to_turf))
+ return TRUE
+ var/list/directions_to_check = list()
+ for(var/direction_check in GLOB.cardinals)
+ if(direction_check & direction_to_turf)
+ directions_to_check += direction_check
+
+ for(var/direction in directions_to_check)
+ var/turf/test_turf = get_step(target_wall, direction)
+ if(isnull(test_turf))
+ continue
+ if(!test_turf.is_blocked_turf(ignore_atoms = list(source)))
+ return TRUE
+ return FALSE
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm
index 1ff752d925ffa4..be395f3dfe49d2 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm
@@ -4,28 +4,28 @@
/// Blackboard key holding target atom
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// What AI behaviour do we actually run?
- var/datum/ai_behavior/ranged_skirmish/attack_behavior = /datum/ai_behavior/ranged_skirmish
+ var/attack_behavior = /datum/ai_behavior/ranged_skirmish
+ /// If target is further away than this we don't fire
+ var/max_range = 9
+ /// If target is closer than this we don't fire
+ var/min_range = 2
/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(!controller.blackboard_key_exists(target_key))
return
- controller.queue_behavior(attack_behavior, target_key, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(attack_behavior, target_key, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION, max_range, min_range)
/// How often will we try to perform our ranged attack?
/datum/ai_behavior/ranged_skirmish
action_cooldown = 1 SECONDS
- /// If target is further away than this we don't fire
- var/max_range = 9
- /// If target is closer than this we don't fire
- var/min_range = 2
-/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key, max_range, min_range)
. = ..()
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
return !QDELETED(target)
-/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key, max_range, min_range)
. = ..()
var/atom/target = controller.blackboard[target_key]
if (QDELETED(target))
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/run_emote.dm b/code/datums/ai/basic_mobs/basic_subtrees/run_emote.dm
new file mode 100644
index 00000000000000..6f2f5cdc2035c0
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/run_emote.dm
@@ -0,0 +1,33 @@
+/// Intermittently run an emote
+/datum/ai_planning_subtree/run_emote
+ var/emote_key = BB_EMOTE_KEY
+ var/emote_chance_key = BB_EMOTE_CHANCE
+
+/datum/ai_planning_subtree/run_emote/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/emote_chance = controller.blackboard[emote_chance_key] || 0
+ if (!SPT_PROB(emote_chance, seconds_per_tick))
+ return
+ controller.queue_behavior(/datum/ai_behavior/run_emote, emote_key)
+
+/// Emote from a blackboard key
+/datum/ai_behavior/run_emote
+
+/datum/ai_behavior/run_emote/perform(seconds_per_tick, datum/ai_controller/controller, emote_key)
+ var/mob/living/living_pawn = controller.pawn
+ if (!isliving(living_pawn))
+ finish_action(controller, FALSE)
+ return
+
+ var/list/emote_list = controller.blackboard[emote_key]
+ var/emote
+ if (islist(emote_list))
+ emote = length(emote_list) ? pick(emote_list) : null
+ else
+ emote = emote_list
+
+ if(isnull(emote))
+ finish_action(controller, FALSE)
+ return
+
+ living_pawn.emote(emote)
+ finish_action(controller, TRUE)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/shapechange_ambush.dm b/code/datums/ai/basic_mobs/basic_subtrees/shapechange_ambush.dm
new file mode 100644
index 00000000000000..ff01eb804ff711
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/shapechange_ambush.dm
@@ -0,0 +1,41 @@
+/// Shapeshift when we have no target, until someone has been nearby for long enough
+/datum/ai_planning_subtree/shapechange_ambush
+ operational_datums = list(/datum/component/ai_target_timer)
+ /// Key where we keep our ability
+ var/ability_key = BB_SHAPESHIFT_ACTION
+ /// Key where we keep our target
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// How long to lull our target into a false sense of security
+ var/minimum_target_time = 8 SECONDS
+
+/datum/ai_planning_subtree/shapechange_ambush/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ var/is_shifted = ismob(living_pawn.loc)
+ var/has_target = controller.blackboard_key_exists(target_key)
+ var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
+
+ if (!is_shifted)
+ if (has_target)
+ return // We're busy
+
+ if (using_action?.IsAvailable())
+ controller.queue_behavior(/datum/ai_behavior/use_mob_ability/shapeshift, BB_SHAPESHIFT_ACTION) // Shift
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if (!has_target || !using_action?.IsAvailable())
+ return SUBTREE_RETURN_FINISH_PLANNING // Lie in wait
+ var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
+ if (time_on_target < minimum_target_time)
+ return // Wait a bit longer
+ controller.queue_behavior(/datum/ai_behavior/use_mob_ability/shapeshift, BB_SHAPESHIFT_ACTION) // Surprise!
+
+/// Selects a random shapeshift ability before shifting
+/datum/ai_behavior/use_mob_ability/shapeshift
+
+/datum/ai_behavior/use_mob_ability/shapeshift/setup(datum/ai_controller/controller, ability_key)
+ var/datum/action/cooldown/spell/shapeshift/using_action = controller.blackboard[ability_key]
+ if (!using_action?.IsAvailable())
+ return FALSE
+ if (isnull(using_action.shapeshift_type)) // If we don't have a shape then pick one, AI can't use context wheels
+ using_action.shapeshift_type = pick(using_action.possible_shapes)
+ return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
index 42a361c25cd865..3fe1ada33ba994 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
@@ -3,7 +3,7 @@
/datum/ai_planning_subtree/simple_find_nearest_target_to_flee/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
controller.queue_behavior(/datum/ai_behavior/find_potential_targets/nearest, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
@@ -13,7 +13,7 @@
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list/nearest, BB_BASIC_MOB_RETALIATE_LIST, BB_BASIC_MOB_CURRENT_TARGET, targeting_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
index 52f4a3459bf914..7a3d5470b1a435 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
@@ -228,6 +228,6 @@
emote_see = speech_lines[BB_EMOTE_SEE] || list()
emote_hear = speech_lines[BB_EMOTE_HEAR] || list()
sound = speech_lines[BB_EMOTE_SOUND] || list()
- speech_chance = speech_lines[BB_EMOTE_CHANCE] ? speech_lines[BB_EMOTE_CHANCE] : initial(speech_chance)
+ speech_chance = speech_lines[BB_SPEAK_CHANCE] ? speech_lines[BB_SPEAK_CHANCE] : initial(speech_chance)
return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm
new file mode 100644
index 00000000000000..dadba992e9f105
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm
@@ -0,0 +1,56 @@
+///behavior to activate ability to escape from target
+/datum/ai_planning_subtree/teleport_away_from_target
+ ///minimum distance away from the target before we execute behavior
+ var/minimum_distance = 2
+ ///the ability we will execute
+ var/ability_key
+
+/datum/ai_planning_subtree/teleport_away_from_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ return
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ var/distance_from_target = get_dist(target, controller.pawn)
+ if(distance_from_target >= minimum_distance)
+ controller.clear_blackboard_key(BB_ESCAPE_DESTINATION)
+ return
+ var/datum/action/cooldown/ability = controller.blackboard[ability_key]
+ if(!ability?.IsAvailable())
+ return
+ var/turf/location_turf = controller.blackboard[BB_ESCAPE_DESTINATION]
+
+ if(isnull(location_turf))
+ controller.queue_behavior(/datum/ai_behavior/find_furthest_turf_from_target, BB_BASIC_MOB_CURRENT_TARGET, BB_ESCAPE_DESTINATION, minimum_distance)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if(get_dist(location_turf, target) < minimum_distance || !can_see(controller.pawn, location_turf)) //target moved close too close or we moved too far since finding the target turf
+ controller.clear_blackboard_key(BB_ESCAPE_DESTINATION)
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target, ability_key, BB_ESCAPE_DESTINATION)
+
+///find furtherst turf target so we may teleport to it
+/datum/ai_behavior/find_furthest_turf_from_target
+
+/datum/ai_behavior/find_furthest_turf_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, set_key, range)
+ var/mob/living/living_target = controller.blackboard[target_key]
+ if(QDELETED(living_target))
+ return
+
+ var/distance = 0
+ var/turf/chosen_turf
+ for(var/turf/open/potential_destination in oview(range, living_target))
+ if(potential_destination.is_blocked_turf())
+ continue
+ var/new_distance_to_target = get_dist(potential_destination, living_target)
+ if(new_distance_to_target > distance)
+ chosen_turf = potential_destination
+ distance = new_distance_to_target
+ if(distance == range)
+ break //we have already found the max distance
+
+ if(isnull(chosen_turf))
+ finish_action(controller, FALSE)
+ return
+
+ controller.set_blackboard_key(set_key, chosen_turf)
+ finish_action(controller, TRUE)
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
new file mode 100644
index 00000000000000..9ce7cc95c07da6
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
@@ -0,0 +1,18 @@
+/// Simply walk to a location
+/datum/ai_planning_subtree/travel_to_point
+ /// Blackboard key where we travel a place we walk to
+ var/location_key = BB_TRAVEL_DESTINATION
+ /// What do we do in order to travel
+ var/travel_behaviour = /datum/ai_behavior/travel_towards
+
+/datum/ai_planning_subtree/travel_to_point/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ var/atom/target = controller.blackboard[location_key]
+ if (QDELETED(target))
+ return
+ controller.queue_behavior(travel_behaviour, location_key)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+
+/datum/ai_planning_subtree/travel_to_point/and_clear_target
+ travel_behaviour = /datum/ai_behavior/travel_towards/stop_on_arrival
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 570088ce4d6e2c..978d7b7f8a75c2 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
@@ -26,7 +26,7 @@
if(isnull(our_controller))
return FALSE
- if(isturf(the_target) || !the_target) // bail out on invalids
+ if(isturf(the_target) || isnull(the_target)) // bail out on invalids
return FALSE
if(isobj(the_target.loc))
@@ -35,6 +35,8 @@
return FALSE
if(ismob(the_target)) //Target is in godmode, ignore it.
+ if(living_mob.loc == the_target)
+ return FALSE // We've either been eaten or are shapeshifted, let's assume the latter because we're still alive
var/mob/M = the_target
if(M.status_flags & GODMODE)
return FALSE
@@ -45,7 +47,7 @@
if(living_mob.see_invisible < the_target.invisibility) //Target's invisible to us, forget it
return FALSE
- if(isturf(the_target.loc) && living_mob.z != the_target.z) // z check will always fail if target is in a mech
+ if(isturf(living_mob.loc) && isturf(the_target.loc) && living_mob.z != the_target.z) // z check will always fail if target is in a mech or pawn is shapeshifted or jaunting
return FALSE
if(isliving(the_target)) //Targeting vs living mobs
diff --git a/code/datums/ai/basic_mobs/targetting_datums/with_object.dm b/code/datums/ai/basic_mobs/targetting_datums/with_object.dm
new file mode 100644
index 00000000000000..039027b6aa5922
--- /dev/null
+++ b/code/datums/ai/basic_mobs/targetting_datums/with_object.dm
@@ -0,0 +1,39 @@
+/**
+ * Find mobs who are holding the configurable object type
+ *
+ * This is an extension of basic targeting behaviour, that allows you to
+ * only target the mob if they have a specific item in their hand.
+ *
+ */
+/datum/targetting_datum/basic/holding_object
+ // We will find mobs who are holding this object in their hands
+ var/object_type_path = null
+
+/**
+ * Create an instance of the holding object targeting datum
+ *
+ * * object_type_path Pass an object type path, this will be compared to the items
+ * in targets hands to filter the target list.
+ */
+/datum/targetting_datum/basic/holding_object/New(object_type_path)
+ if (!ispath(object_type_path))
+ stack_trace("trying to create an item targeting datum with no valid typepath")
+ // Leaving object type as null will make this basically a noop
+ return
+ src.object_type_path = object_type_path
+
+///Returns true or false depending on if the target can be attacked by the mob
+/datum/targetting_datum/basic/holding_object/can_attack(mob/living/living_mob, atom/target, vision_range, check_faction = FALSE)
+ if (object_type_path == null)
+ return FALSE // no op
+ if(!ismob(target))
+ return FALSE // no hands no problems
+
+ // Look at me, type casting like a grown up
+ var/mob/targetmob = target
+ // Check if our parent behaviour agrees we can attack this target (we ignore faction by default)
+ var/can_attack = ..()
+ if(can_attack && targetmob.is_holding_item_of_type(object_type_path))
+ return TRUE // they have the item
+ // No valid target
+ return FALSE
diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm
index 5a42cb43a1eb2c..6883642b689184 100644
--- a/code/datums/ai/dog/dog_controller.dm
+++ b/code/datums/ai/dog/dog_controller.dm
@@ -20,6 +20,8 @@
BB_DOG_HARASS_HARM = TRUE,
BB_VISION_RANGE = AI_DOG_VISION_RANGE,
BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/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),
BB_BABIES_CHILD_TYPES = list(/mob/living/basic/pet/dog/corgi/puppy = 95, /mob/living/basic/pet/dog/corgi/puppy/void = 5),
)
@@ -29,6 +31,10 @@
/datum/ai_planning_subtree/make_babies, // Ian WILL prioritise sex over following your instructions
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/dog_harassment,
+ // Find targets to run away from (uses the targetting datum from above)
+ /datum/ai_planning_subtree/simple_find_target,
+ // Flee from that target
+ /datum/ai_planning_subtree/flee_target,
)
/datum/ai_controller/basic_controller/dog/corgi/get_access()
diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
index 036176dc85ea09..468cfed33fb98e 100644
--- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm
+++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
@@ -93,9 +93,7 @@
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
- if(isnull(hunted))
- //Target is gone for some reason. forget about this task!
- controller[hunting_target_key] = null
+ if(QDELETED(hunted))
finish_action(controller, FALSE, hunting_target_key)
else
target_caught(hunter, hunted)
@@ -136,7 +134,7 @@
/datum/ai_behavior/hunt_target/use_ability_on_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
- if(QDELETED(ability) || !ability.IsAvailable())
+ if(!ability?.IsAvailable())
finish_action(controller, FALSE, hunting_target_key)
return ..()
diff --git a/code/datums/ai/hunting_behavior/hunting_corpses.dm b/code/datums/ai/hunting_behavior/hunting_corpses.dm
new file mode 100644
index 00000000000000..e720e4da947afa
--- /dev/null
+++ b/code/datums/ai/hunting_behavior/hunting_corpses.dm
@@ -0,0 +1,17 @@
+/// Find and attack corpses
+/datum/ai_planning_subtree/find_and_hunt_target/corpses
+ finding_behavior = /datum/ai_behavior/find_hunt_target/corpses
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target
+ hunt_targets = list(/mob/living)
+
+/// Find nearby dead mobs
+/datum/ai_behavior/find_hunt_target/corpses
+
+/datum/ai_behavior/find_hunt_target/corpses/valid_dinner(mob/living/source, mob/living/dinner, radius)
+ if (!isliving(dinner) || dinner.stat != DEAD)
+ return FALSE
+ return can_see(source, dinner, radius)
+
+/// Find and attack specifically human corpses
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/human
+ hunt_targets = list(/mob/living/carbon/human)
diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm
index d5a3972a3d0270..d99957f419bb16 100644
--- a/code/datums/ai/idle_behaviors/idle_random_walk.dm
+++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm
@@ -24,3 +24,44 @@
if (!controller.blackboard_key_exists(target_key))
return
return ..()
+
+/// walk randomly however stick near a target
+/datum/idle_behavior/walk_near_target
+ /// chance to walk
+ var/walk_chance = 25
+ /// distance we are to target
+ var/minimum_distance = 20
+ /// key that holds target
+ var/target_key
+
+/datum/idle_behavior/walk_near_target/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ if(LAZYLEN(living_pawn.do_afters))
+ return
+
+ if(!SPT_PROB(walk_chance, seconds_per_tick) || !(living_pawn.mobility_flags & MOBILITY_MOVE) || !isturf(living_pawn.loc) || living_pawn.pulledby)
+ return
+
+ var/atom/target = controller.blackboard[target_key]
+ var/distance = get_dist(target, living_pawn)
+ if(isnull(target) || distance > minimum_distance) //if we are too far away from target, just walk randomly
+ var/move_dir = pick(GLOB.alldirs)
+ living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
+ return
+
+ var/list/possible_turfs = list()
+ for(var/direction in GLOB.alldirs)
+ var/turf/possible_step = get_step(living_pawn, direction)
+ if(get_dist(possible_step, target) > minimum_distance)
+ continue
+ if(possible_step.is_blocked_turf())
+ continue
+ possible_turfs += possible_step
+
+ if(!length(possible_turfs))
+ return
+
+ var/turf/picked_turf = pick(possible_turfs)
+
+ living_pawn.Move(picked_turf, get_dir(living_pawn, picked_turf))
diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm
index 3523da7ecec22e..da46735ec363d0 100644
--- a/code/datums/ai/movement/ai_movement_jps.dm
+++ b/code/datums/ai/movement/ai_movement_jps.dm
@@ -2,7 +2,7 @@
* This movement datum represents smart-pathing
*/
/datum/ai_movement/jps
- max_pathing_attempts = 4
+ max_pathing_attempts = 20
/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
. = ..()
@@ -12,13 +12,13 @@
var/datum/move_loop/loop = SSmove_manager.jps_move(moving,
current_movement_target,
delay,
- repath_delay = 2 SECONDS,
+ repath_delay = 0.5 SECONDS,
max_path_length = AI_MAX_PATH_LENGTH,
minimum_distance = controller.get_minimum_distance(),
id = controller.get_access(),
subsystem = SSai_movement,
extra_info = controller,
- initial_path = controller.blackboard[BB_PATH_TO_USE])
+ )
RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
diff --git a/code/datums/alarm.dm b/code/datums/alarm.dm
index b266362f3ca950..e25f96b95a5b0f 100644
--- a/code/datums/alarm.dm
+++ b/code/datums/alarm.dm
@@ -201,4 +201,5 @@
for(var/area_name as anything in alarms_of_type)
var/list/alarm_packet = alarms_of_type[area_name]
var/list/cameras = alarm_packet[2]
- cameras -= source // REF FOUND AND CLEARED BOYSSSS
+ if(cameras)
+ cameras -= source // REF FOUND AND CLEARED BOYSSSS
diff --git a/code/datums/armor/_armor.dm b/code/datums/armor/_armor.dm
index c6dbf1d5fdd66c..bfd15af4189245 100644
--- a/code/datums/armor/_armor.dm
+++ b/code/datums/armor/_armor.dm
@@ -62,6 +62,7 @@ GLOBAL_LIST_INIT(armor_by_type, generate_armor_type_cache())
return FALSE
/datum/armor/vv_get_dropdown()
+ SHOULD_CALL_PARENT(FALSE)
return list("", "MUST MODIFY ARMOR VALUES ON THE PARENT ATOM")
/datum/armor/CanProcCall(procname)
diff --git a/code/datums/brain_damage/split_personality.dm b/code/datums/brain_damage/split_personality.dm
index 4c7b6a46100ecb..f2120505f72f4b 100644
--- a/code/datums/brain_damage/split_personality.dm
+++ b/code/datums/brain_damage/split_personality.dm
@@ -11,6 +11,8 @@
var/initialized = FALSE //to prevent personalities deleting themselves while we wait for ghosts
var/mob/living/split_personality/stranger_backseat //there's two so they can swap without overwriting
var/mob/living/split_personality/owner_backseat
+ ///The role to display when polling ghost
+ var/poll_role = "split personality"
/datum/brain_trauma/severe/split_personality/on_gain()
var/mob/living/M = owner
@@ -33,7 +35,7 @@
/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 split personality?", ROLE_PAI, null, 7.5 SECONDS, stranger_backseat, POLL_IGNORE_SPLITPERSONALITY)
+ 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
@@ -238,5 +240,53 @@
if(objective)
to_chat(src, span_notice("Your master left you an objective: [objective]. Follow it at all costs when in control."))
+/datum/brain_trauma/severe/split_personality/blackout
+ name = "Alcohol-Induced CNS Impairment"
+ desc = "Patient's CNS has been temporarily impaired by imbibed alcohol, blocking memory formation, and causing reduced cognition and stupefaction."
+ scan_desc = "alcohol-induced CNS impairment"
+ 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"
+ /// 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))
+
+/datum/brain_trauma/severe/split_personality/blackout/on_lose()
+ . = ..()
+ owner.add_mood_event("hang_over", /datum/mood_event/hang_over)
+ UnregisterSignal(owner, COMSIG_ATOM_SPLASHED)
+
+/datum/brain_trauma/severe/split_personality/blackout/proc/on_splashed()
+ SIGNAL_HANDLER
+ if(prob(20))//we don't want every single splash to wake them up now do we
+ qdel(src)
+
+/datum/brain_trauma/severe/split_personality/blackout/on_life(seconds_per_tick, times_fired)
+ if(current_controller == OWNER)
+ switch_personalities()
+ if(owner.stat == DEAD)
+ if(current_controller != OWNER)
+ switch_personalities(TRUE)
+ qdel(src)
+ return
+ if(duration_in_seconds <= 0)
+ qdel(src)
+ return
+ duration_in_seconds -= seconds_per_tick
+
+/mob/living/split_personality/blackout
+ name = "blacked-out drunkard"
+ real_name = "drunken consciousness"
+
+/mob/living/split_personality/blackout/Login()
+ . = ..()
+ if(!. || !client)
+ return FALSE
+ to_chat(src, span_notice("You're the incredibly inebriated leftovers of your host's consciousness! Make sure to act the part and leave a trail of confusion and chaos in your wake."))
+ to_chat(src, span_boldwarning("Do not commit suicide or put the body in danger, you have a minor liscense to grief just like a clown, do not kill anyone or create a situation leading to the body being in danger or in harm ways. While you're drunk, you're not suicidal."))
+
#undef OWNER
#undef STRANGER
diff --git a/code/datums/components/bakeable.dm b/code/datums/components/bakeable.dm
index 5aa60cd89a7096..b4cde3c5752e97 100644
--- a/code/datums/components/bakeable.dm
+++ b/code/datums/components/bakeable.dm
@@ -14,7 +14,10 @@
/// REF() to the mind which placed us in an oven
var/who_baked_us
-/datum/component/bakeable/Initialize(bake_result, required_bake_time, positive_result, use_large_steam_sprite)
+ /// Reagents that should be added to the result
+ var/list/added_reagents
+
+/datum/component/bakeable/Initialize(bake_result, required_bake_time, positive_result, use_large_steam_sprit, list/added_reagents)
. = ..()
if(!isitem(parent)) //Only items support baking at the moment
return COMPONENT_INCOMPATIBLE
@@ -22,6 +25,7 @@
src.bake_result = bake_result
src.required_bake_time = required_bake_time
src.positive_result = positive_result
+ src.added_reagents = added_reagents
// Inherit the new values passed to the component
/datum/component/bakeable/InheritComponent(datum/component/bakeable/new_comp, original, bake_result, required_bake_time, positive_result, use_large_steam_sprite)
@@ -67,8 +71,11 @@
var/atom/original_object = parent
var/obj/item/plate/oven_tray/used_tray = original_object.loc
var/atom/baked_result = new bake_result(used_tray)
- baked_result.reagents.clear_reagents()
- original_object.reagents?.trans_to(baked_result, original_object.reagents.total_volume)
+ if(baked_result.reagents && positive_result) //make space and tranfer reagents if it has any & the resulting item isn't bad food or other bad baking result
+ baked_result.reagents.clear_reagents()
+ original_object.reagents.trans_to(baked_result, original_object.reagents.total_volume)
+ if(added_reagents) // Add any new reagents that should be added
+ baked_result.reagents.add_reagent_list(added_reagents)
if(who_baked_us)
ADD_TRAIT(baked_result, TRAIT_FOOD_CHEF_MADE, who_baked_us)
diff --git a/code/datums/components/basic_inhands.dm b/code/datums/components/basic_inhands.dm
new file mode 100644
index 00000000000000..ac50f618861f23
--- /dev/null
+++ b/code/datums/components/basic_inhands.dm
@@ -0,0 +1,50 @@
+/**
+ * Basic handling for showing held items in a mob's hands
+ */
+/datum/component/basic_inhands
+ /// Layer index we show our inhands upon
+ var/display_layer
+ /// Y offset to apply to inhands
+ var/y_offset
+ /// X offset to apply to inhands, is inverted for the left hand
+ var/x_offset
+ /// What overlays are we currently showing?
+ var/list/cached_overlays
+
+/datum/component/basic_inhands/Initialize(display_layer = 1, y_offset = 0, x_offset = 0)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.display_layer = display_layer
+ src.y_offset = y_offset
+ src.x_offset = x_offset
+ cached_overlays = list()
+
+/datum/component/basic_inhands/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_updated_overlays))
+ RegisterSignal(parent, COMSIG_MOB_UPDATE_HELD_ITEMS, PROC_REF(on_updated_held_items))
+
+/datum/component/basic_inhands/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(COMSIG_ATOM_UPDATE_OVERLAYS, COMSIG_MOB_UPDATE_HELD_ITEMS))
+
+/// When your overlays update, add your held overlays
+/datum/component/basic_inhands/proc/on_updated_overlays(atom/parent_atom, list/overlays)
+ SIGNAL_HANDLER
+ overlays += cached_overlays
+
+/// When your number of held items changes, regenerate held icons
+/datum/component/basic_inhands/proc/on_updated_held_items(mob/living/holding_mob)
+ SIGNAL_HANDLER
+ var/list/held_overlays = list()
+ for(var/obj/item/held in holding_mob.held_items)
+ var/is_right = holding_mob.get_held_index_of_item(held) % 2 == 0
+ var/icon_file = is_right ? held.righthand_file : held.lefthand_file
+ var/mutable_appearance/held_overlay = held.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE)
+ held_overlay.pixel_y += y_offset
+ held_overlay.pixel_x += x_offset * (is_right ? 1 : -1)
+ held_overlays += held_overlay
+
+ cached_overlays = held_overlays
+ holding_mob.update_appearance(UPDATE_OVERLAYS)
diff --git a/code/datums/components/blob_minion.dm b/code/datums/components/blob_minion.dm
new file mode 100644
index 00000000000000..8261a7ad1fdc3e
--- /dev/null
+++ b/code/datums/components/blob_minion.dm
@@ -0,0 +1,154 @@
+/**
+ * Common behaviour shared by things which are minions to a blob
+ */
+/datum/component/blob_minion
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Overmind who is our boss
+ var/mob/camera/blob/overmind
+ /// Callback to run if overmind strain changes
+ var/datum/callback/on_strain_changed
+
+/datum/component/blob_minion/Initialize(mob/camera/blob/overmind, datum/callback/on_strain_changed)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.on_strain_changed = on_strain_changed
+ register_overlord(overmind)
+
+/datum/component/blob_minion/InheritComponent(datum/component/new_comp, i_am_original, mob/camera/blob/overmind, datum/callback/on_strain_changed)
+ if (!isnull(on_strain_changed))
+ src.on_strain_changed = on_strain_changed
+ register_overlord(overmind)
+
+/datum/component/blob_minion/proc/register_overlord(mob/camera/blob/overmind)
+ if (isnull(overmind))
+ return
+ src.overmind = overmind
+ overmind.register_new_minion(parent)
+ RegisterSignal(overmind, COMSIG_QDELETING, PROC_REF(overmind_deleted))
+ RegisterSignal(overmind, COMSIG_BLOB_SELECTED_STRAIN, PROC_REF(overmind_properties_changed))
+ overmind_properties_changed(overmind, overmind.blobstrain)
+
+/// Our overmind is gone, uh oh!
+/datum/component/blob_minion/proc/overmind_deleted()
+ SIGNAL_HANDLER
+ overmind = null
+ overmind_properties_changed()
+
+/// Our overmind has changed colour and properties
+/datum/component/blob_minion/proc/overmind_properties_changed(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ SIGNAL_HANDLER
+ var/mob/living/living_parent = parent
+ living_parent.update_appearance(UPDATE_ICON)
+ on_strain_changed?.Invoke(overmind, new_strain)
+
+/datum/component/blob_minion/RegisterWithParent()
+ var/mob/living/living_parent = parent
+ living_parent.pass_flags |= PASSBLOB
+ living_parent.faction |= ROLE_BLOB
+ ADD_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
+ remove_verb(parent, /mob/living/verb/pulled) // No dragging people into the blob
+ RegisterSignal(parent, COMSIG_MOB_MIND_INITIALIZED, PROC_REF(on_mind_init))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, PROC_REF(on_update_appearance))
+ RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_update_status_tab))
+ RegisterSignal(parent, COMSIG_ATOM_BLOB_ACT, PROC_REF(on_blob_touched))
+ RegisterSignal(parent, COMSIG_ATOM_FIRE_ACT, PROC_REF(on_burned))
+ RegisterSignal(parent, COMSIG_ATOM_TRIED_PASS, PROC_REF(on_attempted_pass))
+ RegisterSignal(parent, COMSIG_MOVABLE_SPACEMOVE, PROC_REF(on_space_move))
+ RegisterSignal(parent, COMSIG_LIVING_TRY_SPEECH, PROC_REF(on_try_speech))
+ RegisterSignal(parent, COMSIG_MOB_CHANGED_TYPE, PROC_REF(on_transformed))
+ living_parent.update_appearance(UPDATE_ICON)
+ GLOB.blob_telepathy_mobs |= parent
+
+/datum/component/blob_minion/UnregisterFromParent()
+ if (!isnull(overmind))
+ overmind.blob_mobs -= parent
+ var/mob/living/living_parent = parent
+ living_parent.pass_flags &= ~PASSBLOB
+ living_parent.faction -= ROLE_BLOB
+ REMOVE_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
+ add_verb(parent, /mob/living/verb/pulled)
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_BLOB_ACT,
+ COMSIG_ATOM_FIRE_ACT,
+ COMSIG_ATOM_TRIED_PASS,
+ COMSIG_ATOM_UPDATE_ICON,
+ COMSIG_LIVING_TRY_SPEECH,
+ COMSIG_MOB_CHANGED_TYPE,
+ COMSIG_MOB_GET_STATUS_TAB_ITEMS,
+ COMSIG_MOB_MIND_INITIALIZED,
+ COMSIG_MOVABLE_SPACEMOVE,
+ ))
+ GLOB.blob_telepathy_mobs -= parent
+
+/// Become blobpilled when we gain a mind
+/datum/component/blob_minion/proc/on_mind_init(mob/living/minion, datum/mind/new_mind)
+ SIGNAL_HANDLER
+ if (isnull(overmind))
+ return
+ var/datum/antagonist/blob_minion/minion_motive = new(overmind)
+ new_mind.add_antag_datum(minion_motive)
+
+/// When our icon is updated, update our colour too
+/datum/component/blob_minion/proc/on_update_appearance(mob/living/minion)
+ SIGNAL_HANDLER
+ if(isnull(overmind))
+ minion.remove_atom_colour(FIXED_COLOUR_PRIORITY)
+ return
+ minion.add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
+
+/// When our icon is updated, update our colour too
+/datum/component/blob_minion/proc/on_update_status_tab(mob/living/minion, list/status_items)
+ SIGNAL_HANDLER
+ if (isnull(overmind))
+ return
+ status_items += "Blobs to Win: [length(overmind.blobs_legit)]/[overmind.blobwincount]"
+
+/// If we feel the gentle caress of a blob, we feel better
+/datum/component/blob_minion/proc/on_blob_touched(mob/living/minion)
+ SIGNAL_HANDLER
+ if(minion.stat == DEAD || minion.health >= minion.maxHealth)
+ return COMPONENT_CANCEL_BLOB_ACT // Don't hurt us in order to heal us
+ for(var/i in 1 to 2)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(parent)) // hello yes you are being healed
+ heal_effect.color = isnull(overmind) ? COLOR_BLACK : overmind.blobstrain.complementary_color
+ minion.heal_overall_damage(minion.maxHealth * BLOBMOB_HEALING_MULTIPLIER)
+ return COMPONENT_CANCEL_BLOB_ACT
+
+/// If we feel the fearsome bite of open flame, we feel worse
+/datum/component/blob_minion/proc/on_burned(mob/living/minion, exposed_temperature, exposed_volume)
+ SIGNAL_HANDLER
+ if(isnull(exposed_temperature))
+ minion.adjustFireLoss(5)
+ return
+ minion.adjustFireLoss(clamp(0.01 * exposed_temperature, 1, 5))
+
+/// Someone is attempting to move through us, allow it if it is a blob tile
+/datum/component/blob_minion/proc/on_attempted_pass(mob/living/minion, atom/movable/incoming)
+ SIGNAL_HANDLER
+ if(istype(incoming, /obj/structure/blob))
+ return COMSIG_COMPONENT_PERMIT_PASSAGE
+
+/// If we're near a blob, stop drifting
+/datum/component/blob_minion/proc/on_space_move(mob/living/minion)
+ SIGNAL_HANDLER
+ var/obj/structure/blob/blob_handhold = locate() in range(1, parent)
+ if (!isnull(blob_handhold))
+ return COMSIG_MOVABLE_STOP_SPACEMOVE
+
+/// We only speak telepathically to blobs
+/datum/component/blob_minion/proc/on_try_speech(mob/living/minion, message, ignore_spam, forced)
+ SIGNAL_HANDLER
+ var/spanned_message = minion.say_quote(message)
+ var/rendered = span_blob("\[Blob Telepathy\] [minion.real_name] [spanned_message]")
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, minion)
+ return COMPONENT_CANNOT_SPEAK
+
+/// Called when a blob minion is transformed into something else, hopefully a spore into a zombie
+/datum/component/blob_minion/proc/on_transformed(mob/living/minion, mob/living/replacement)
+ SIGNAL_HANDLER
+ overmind?.assume_direct_control(replacement)
+
+/datum/component/blob_minion/PostTransfer()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm
index 0ed00e7dd94ef6..689de30db378a8 100644
--- a/code/datums/components/butchering.dm
+++ b/code/datums/components/butchering.dm
@@ -99,50 +99,75 @@
*
* Arguments:
* - [butcher][/mob/living]: The mob doing the butchering
- * - [meat][/mob/living]: The mob being butchered
+ * - [target][/mob/living]: The mob being butchered
*/
-/datum/component/butchering/proc/on_butchering(atom/butcher, mob/living/meat)
+/datum/component/butchering/proc/on_butchering(atom/butcher, mob/living/target)
var/list/results = list()
- var/turf/T = meat.drop_location()
- var/final_effectiveness = effectiveness - meat.butcher_difficulty
+ var/turf/location = target.drop_location()
+ var/final_effectiveness = effectiveness - target.butcher_difficulty
var/bonus_chance = max(0, (final_effectiveness - 100) + bonus_modifier) //so 125 total effectiveness = 25% extra chance
- for(var/V in meat.butcher_results)
- var/obj/bones = V
- var/amount = meat.butcher_results[bones]
+
+ for(var/result_typepath in target.butcher_results)
+ var/obj/remains = result_typepath
+ var/amount = target.butcher_results[remains]
for(var/_i in 1 to amount)
if(!prob(final_effectiveness))
if(butcher)
- to_chat(butcher, span_warning("You fail to harvest some of the [initial(bones.name)] from [meat]."))
+ to_chat(butcher, span_warning("You fail to harvest some of the [initial(remains.name)] from [target]."))
continue
if(prob(bonus_chance))
if(butcher)
- to_chat(butcher, span_info("You harvest some extra [initial(bones.name)] from [meat]!"))
- results += new bones (T)
- results += new bones (T)
+ to_chat(butcher, span_info("You harvest some extra [initial(remains.name)] from [target]!"))
+ results += new remains (location)
+ results += new remains (location)
- meat.butcher_results.Remove(bones) //in case you want to, say, have it drop its results on gib
+ target.butcher_results.Remove(remains) //in case you want to, say, have it drop its results on gib
- for(var/V in meat.guaranteed_butcher_results)
- var/obj/sinew = V
- var/amount = meat.guaranteed_butcher_results[sinew]
+ for(var/guaranteed_result_typepath in target.guaranteed_butcher_results)
+ var/obj/guaranteed_remains = guaranteed_result_typepath
+ var/amount = target.guaranteed_butcher_results[guaranteed_remains]
for(var/i in 1 to amount)
- results += new sinew (T)
- meat.guaranteed_butcher_results.Remove(sinew)
+ results += new guaranteed_remains (location)
+ target.guaranteed_butcher_results.Remove(guaranteed_remains)
for(var/obj/item/carrion in results)
var/list/meat_mats = carrion.has_material_type(/datum/material/meat)
if(!length(meat_mats))
continue
- carrion.set_custom_materials((carrion.custom_materials - meat_mats) + list(GET_MATERIAL_REF(/datum/material/meat/mob_meat, meat) = counterlist_sum(meat_mats)))
+ carrion.set_custom_materials((carrion.custom_materials - meat_mats) + list(GET_MATERIAL_REF(/datum/material/meat/mob_meat, target) = counterlist_sum(meat_mats)))
+
+ // transfer delicious reagents to meat
+ if(target.reagents)
+ var/meat_produced = 0
+ for(var/obj/item/food/meat/slab/target_meat in results)
+ meat_produced += 1
+ for(var/obj/item/food/meat/slab/target_meat in results)
+ target.reagents.trans_to(target_meat, target.reagents.total_volume / meat_produced, remove_blacklisted = TRUE)
+
+ // dont forget yummy diseases either!
+ if(iscarbon(target))
+ var/mob/living/carbon/host_target = target
+ var/list/diseases = host_target.get_static_viruses()
+ if(LAZYLEN(diseases))
+ var/list/datum/disease/diseases_to_add = list()
+ for(var/datum/disease/disease as anything in diseases)
+ // admin or special viruses that should not be reproduced
+ if(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
+ continue
+
+ diseases_to_add += disease
+ if(LAZYLEN(diseases_to_add))
+ for(var/obj/diseased_remains in results)
+ diseased_remains.AddComponent(/datum/component/infective, diseases_to_add)
if(butcher)
- butcher.visible_message(span_notice("[butcher] butchers [meat]."), \
- span_notice("You butcher [meat]."))
- butcher_callback?.Invoke(butcher, meat)
- meat.harvest(butcher)
- meat.log_message("has been butchered by [key_name(butcher)]", LOG_ATTACK)
- meat.gib(FALSE, FALSE, TRUE)
+ butcher.visible_message(span_notice("[butcher] butchers [target]."), \
+ span_notice("You butcher [target]."))
+ butcher_callback?.Invoke(butcher, target)
+ target.harvest(butcher)
+ target.log_message("has been butchered by [key_name(butcher)]", LOG_ATTACK)
+ target.gib(DROP_BRAIN|DROP_ORGANS)
///Enables the butchering mechanic for the mob who has equipped us.
/datum/component/butchering/proc/enable_butchering(datum/source)
@@ -207,9 +232,9 @@
))
///When we are ready to drill through a mob
-/datum/component/butchering/mecha/proc/on_drill(datum/source, obj/vehicle/sealed/mecha/chassis, mob/living/meat)
+/datum/component/butchering/mecha/proc/on_drill(datum/source, obj/vehicle/sealed/mecha/chassis, mob/living/target)
SIGNAL_HANDLER
- INVOKE_ASYNC(src, PROC_REF(on_butchering), chassis, meat)
+ INVOKE_ASYNC(src, PROC_REF(on_butchering), chassis, target)
/datum/component/butchering/wearable
diff --git a/code/datums/components/chasm.dm b/code/datums/components/chasm.dm
index fc93123921c0dd..18420016c543fd 100644
--- a/code/datums/components/chasm.dm
+++ b/code/datums/components/chasm.dm
@@ -8,28 +8,29 @@
/// List of refs to falling objects -> how many levels deep we've fallen
var/static/list/falling_atoms = list()
var/static/list/forbidden_types = typecacheof(list(
- /obj/singularity,
- /obj/energy_ball,
- /obj/narsie,
/obj/docking_port,
- /obj/structure/lattice,
- /obj/structure/stone_tile,
- /obj/projectile,
- /obj/effect/projectile,
- /obj/effect/portal,
/obj/effect/abstract,
+ /obj/effect/collapse,
+ /obj/effect/constructing_effect,
+ /obj/effect/dummy/phased_mob,
+ /obj/effect/ebeam,
+ /obj/effect/fishing_lure,
/obj/effect/hotspot,
/obj/effect/landmark,
- /obj/effect/temp_visual,
/obj/effect/light_emitter/tendril,
- /obj/effect/collapse,
- /obj/effect/particle_effect/ion_trails,
- /obj/effect/dummy/phased_mob,
/obj/effect/mapping_helpers,
+ /obj/effect/particle_effect/ion_trails,
+ /obj/effect/portal,
+ /obj/effect/projectile,
+ /obj/effect/spectre_of_resurrection,
+ /obj/effect/temp_visual,
/obj/effect/wisp,
- /obj/effect/ebeam,
- /obj/effect/fishing_lure,
- /obj/effect/constructing_effect,
+ /obj/energy_ball,
+ /obj/narsie,
+ /obj/projectile,
+ /obj/singularity,
+ /obj/structure/lattice,
+ /obj/structure/stone_tile,
))
/datum/component/chasm/Initialize(turf/target, mapload)
diff --git a/code/datums/components/crafting/ranged_weapon.dm b/code/datums/components/crafting/ranged_weapon.dm
index a8d631c6266e0e..94a943b42c8fd0 100644
--- a/code/datums/components/crafting/ranged_weapon.dm
+++ b/code/datums/components/crafting/ranged_weapon.dm
@@ -222,18 +222,19 @@
time = 30 SECONDS //contemplate for a bit
category = CAT_WEAPON_RANGED
-/datum/crafting_recipe/deagle_prime //When you factor in the makarov (7 tc), the toolbox (1 tc), and the emag (3 tc), this comes to a total of 17 TC or thereabouts. Igorning the 20k pricetag, obviously.
+/datum/crafting_recipe/deagle_prime //When you factor in the makarov (7 tc), the toolbox (1 tc), and the emag (3 tc), this comes to a total of 18 TC or thereabouts. Igorning the 20k pricetag, obviously.
name = "Regal Condor"
always_available = FALSE
- result = /obj/item/gun/ballistic/automatic/pistol/deagle/regal/no_mag
+ result = /obj/item/gun/ballistic/automatic/pistol/deagle/regal
reqs = list(
/obj/item/gun/ballistic/automatic/pistol = 1,
/obj/item/stack/sheet/mineral/gold = 25,
/obj/item/stack/sheet/mineral/silver = 25,
/obj/item/food/donkpocket = 1,
- /obj/item/stack/telecrystal = 3,
+ /obj/item/stack/telecrystal = 4,
/obj/item/clothing/head/costume/crown/fancy = 1, //the captain's crown
/obj/item/storage/toolbox/syndicate = 1,
+ /obj/item/stack/sheet/iron = 10,
)
tool_behaviors = list(TOOL_SCREWDRIVER)
tool_paths = list(
@@ -249,18 +250,22 @@
blacklist += subtypesof(/obj/item/gun/ballistic/automatic/pistol)
/datum/crafting_recipe/deagle_prime_mag
- name = "Regal Condor Magazine (10mm)"
+ name = "Regal Condor Magazine (10mm Reaper)"
always_available = FALSE
- result = /obj/item/ammo_box/magazine/r10mm/empty
+ result = /obj/item/ammo_box/magazine/r10mm
reqs = list(
/obj/item/stack/sheet/iron = 10,
- /obj/item/stack/telecrystal = 2,
+ /obj/item/stack/sheet/mineral/gold = 10,
+ /obj/item/stack/sheet/mineral/silver = 10,
+ /obj/item/stack/sheet/mineral/plasma = 10,
+ /obj/item/food/donkpocket = 1, //Station mass murder, as sponsored by Donk Co.
)
tool_behaviors = list(TOOL_SCREWDRIVER, TOOL_WELDER)
tool_paths = list(
/obj/item/clothing/under/syndicate,
/obj/item/clothing/mask/gas/syndicate,
- /obj/item/card/emag
+ /obj/item/card/emag,
+ /obj/item/gun/ballistic/automatic/pistol/deagle/regal
)
time = 5 SECONDS
category = CAT_WEAPON_RANGED
diff --git a/code/datums/components/crafting/tools.dm b/code/datums/components/crafting/tools.dm
index cc2d510bd255dd..f126bdff53873b 100644
--- a/code/datums/components/crafting/tools.dm
+++ b/code/datums/components/crafting/tools.dm
@@ -46,3 +46,12 @@
)
result = /obj/item/pickaxe/improvised
category = CAT_TOOLS
+
+/datum/crafting_recipe/bandage
+ name = "Makeshift Bandage"
+ reqs = list(
+ /obj/item/stack/sheet/cloth = 3,
+ /datum/reagent/medicine/c2/libital = 10,
+ )
+ result = /obj/item/stack/medical/bandage/makeshift
+ category = CAT_TOOLS
diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm
index 561789d1b6deeb..7565268cef6dfc 100644
--- a/code/datums/components/cult_ritual_item.dm
+++ b/code/datums/components/cult_ritual_item.dm
@@ -304,7 +304,7 @@
)
if(cultist.blood_volume)
- cultist.apply_damage(initial(rune_to_scribe.scribe_damage), BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM), wound_bonus = CANT_WOUND) // *cuts arm* *bone explodes* ever have one of those days?
+ cultist.apply_damage(initial(rune_to_scribe.scribe_damage), BRUTE, pick(GLOB.arm_zones), wound_bonus = CANT_WOUND) // *cuts arm* *bone explodes* ever have one of those days?
var/scribe_mod = initial(rune_to_scribe.scribe_delay)
if(!initial(rune_to_scribe.no_scribe_boost) && (our_turf.type in turfs_that_boost_us))
@@ -357,7 +357,7 @@
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", ANNOUNCER_SPANOMALIES, has_important_message = TRUE)
+ 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)
for(var/shielded_turf in spiral_range_turfs(1, cultist, 1))
LAZYADD(shields, new /obj/structure/emergency_shield/cult/narsie(shielded_turf))
diff --git a/code/datums/components/damage_aura.dm b/code/datums/components/damage_aura.dm
index 2438eca8d7918a..6eec1903eefc85 100644
--- a/code/datums/components/damage_aura.dm
+++ b/code/datums/components/damage_aura.dm
@@ -91,14 +91,16 @@
/// What effect the damage aura has if it has an owner.
/datum/component/damage_aura/proc/owner_effect(mob/living/owner_mob, seconds_per_tick)
- owner_mob.adjustStaminaLoss(-20 * seconds_per_tick, updating_stamina = FALSE)
- owner_mob.adjustBruteLoss(-1 * seconds_per_tick, updating_health = FALSE)
- owner_mob.adjustFireLoss(-1 * seconds_per_tick, updating_health = FALSE)
- owner_mob.adjustToxLoss(-1 * seconds_per_tick, updating_health = FALSE, forced = TRUE)
- owner_mob.adjustOxyLoss(-1 * seconds_per_tick, updating_health = FALSE)
+ var/need_mob_update = FALSE
+ need_mob_update += owner_mob.adjustStaminaLoss(-20 * seconds_per_tick, updating_stamina = FALSE)
+ need_mob_update += owner_mob.adjustBruteLoss(-1 * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += owner_mob.adjustFireLoss(-1 * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += owner_mob.adjustToxLoss(-1 * seconds_per_tick, updating_health = FALSE, forced = TRUE)
+ need_mob_update += owner_mob.adjustOxyLoss(-1 * seconds_per_tick, updating_health = FALSE)
if (owner_mob.blood_volume < BLOOD_VOLUME_NORMAL)
owner_mob.blood_volume += 2 * seconds_per_tick
- owner_mob.updatehealth()
+ if(need_mob_update)
+ owner_mob.updatehealth()
/datum/component/damage_aura/process(seconds_per_tick)
var/should_show_effect = COOLDOWN_FINISHED(src, last_damage_effect_time)
diff --git a/code/datums/components/death_linked.dm b/code/datums/components/death_linked.dm
new file mode 100644
index 00000000000000..c20c810019564e
--- /dev/null
+++ b/code/datums/components/death_linked.dm
@@ -0,0 +1,30 @@
+/**
+ * ## Death link component
+ *
+ * When the owner of this component dies it also gibs a linked mob
+ */
+/datum/component/death_linked
+ ///The mob that also dies when the user dies
+ var/datum/weakref/linked_mob
+
+/datum/component/death_linked/Initialize(mob/living/target_mob)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ if(isnull(target_mob))
+ stack_trace("[type] added to [parent] with no linked mob.")
+ src.linked_mob = WEAKREF(target_mob)
+
+/datum/component/death_linked/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+
+/datum/component/death_linked/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, COMSIG_LIVING_DEATH)
+
+///signal called by the stat of the target changing
+/datum/component/death_linked/proc/on_death(mob/living/target, gibbed)
+ SIGNAL_HANDLER
+ var/mob/living/linked_mob_resolved = linked_mob?.resolve()
+ linked_mob_resolved?.gib(DROP_ALL_REMAINS)
diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm
index 3f230b8754dbe0..f88c27a7135306 100644
--- a/code/datums/components/fishing_spot.dm
+++ b/code/datums/components/fishing_spot.dm
@@ -12,6 +12,7 @@
fish_source = configuration
else
return COMPONENT_INCOMPATIBLE
+ fish_source.on_fishing_spot_init()
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(handle_attackby))
RegisterSignal(parent, COMSIG_FISHING_ROD_CAST, PROC_REF(handle_cast))
diff --git a/code/datums/components/focused_attacker.dm b/code/datums/components/focused_attacker.dm
new file mode 100644
index 00000000000000..eda6bd1797912e
--- /dev/null
+++ b/code/datums/components/focused_attacker.dm
@@ -0,0 +1,71 @@
+/**
+ * Increases our attack damage every time we attack the same target
+ * Not compatible with any other component or status effect which modifies attack damage
+ */
+/datum/component/focused_attacker
+ /// Amount of damage we gain per attack
+ var/gain_per_attack
+ /// Maximum amount by which we can increase our attack power
+ var/maximum_gain
+ /// The last thing we attacked
+ var/atom/last_target
+
+/datum/component/focused_attacker/Initialize(gain_per_attack = 5, maximum_gain = 25)
+ . = ..()
+ if (!isliving(parent) && !isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.maximum_gain = maximum_gain
+ src.gain_per_attack = gain_per_attack
+
+/datum/component/focused_attacker/Destroy(force, silent)
+ if (!isnull(last_target))
+ UnregisterSignal(last_target, COMSIG_QDELETING)
+ return ..()
+
+/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))
+ 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))
+
+/// Before a mob attacks, try increasing its attack power
+/datum/component/focused_attacker/proc/pre_mob_attack(mob/living/attacker, atom/target)
+ SIGNAL_HANDLER
+ if (isnull(target) || isturf(target))
+ return
+ if (target == last_target)
+ if (attacker.melee_damage_lower - initial(attacker.melee_damage_lower) >= maximum_gain)
+ return
+ attacker.melee_damage_lower += gain_per_attack
+ attacker.melee_damage_upper += gain_per_attack
+ return
+
+ attacker.melee_damage_lower = initial(attacker.melee_damage_lower)
+ attacker.melee_damage_upper = initial(attacker.melee_damage_upper)
+ register_new_target(target)
+
+/// Before an item attacks, try increasing its attack power
+/datum/component/focused_attacker/proc/pre_item_attack(obj/item/weapon, atom/target, mob/user, params)
+ SIGNAL_HANDLER
+ if (target == last_target)
+ if (weapon.force - initial(weapon.force) < maximum_gain)
+ weapon.force += gain_per_attack
+ return
+
+ weapon.force = initial(weapon.force)
+ register_new_target(target)
+
+/// Register a new target
+/datum/component/focused_attacker/proc/register_new_target(atom/target)
+ if (!isnull(last_target))
+ UnregisterSignal(last_target, COMSIG_QDELETING)
+ last_target = target
+ RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_deleted))
+
+/// Drop our target ref on deletion
+/datum/component/focused_attacker/proc/on_target_deleted(target)
+ SIGNAL_HANDLER
+ last_target = null
diff --git a/code/datums/components/fullauto.dm b/code/datums/components/fullauto.dm
index d02f090ae202fa..8663de5adc6913 100644
--- a/code/datums/components/fullauto.dm
+++ b/code/datums/components/fullauto.dm
@@ -8,8 +8,12 @@
var/turf/target_loc //For dealing with locking on targets due to BYOND engine limitations (the mouse input only happening when mouse moves).
var/autofire_stat = AUTOFIRE_STAT_IDLE
var/mouse_parameters
- var/autofire_shot_delay = 0.3 SECONDS //Time between individual shots.
- var/mouse_status = AUTOFIRE_MOUSEUP //This seems hacky but there can be two MouseDown() without a MouseUp() in between if the user holds click and uses alt+tab, printscreen or similar.
+ /// Time between individual shots.
+ var/autofire_shot_delay = 0.3 SECONDS
+ /// This seems hacky but there can be two MouseDown() without a MouseUp() in between if the user holds click and uses alt+tab, printscreen or similar.
+ var/mouse_status = AUTOFIRE_MOUSEUP
+ /// Should dual wielding be allowed?
+ var/allow_akimbo
///windup autofire vars
///Whether the delay between shots increases over time, simulating a spooling weapon
@@ -26,7 +30,7 @@
var/timerid
COOLDOWN_DECLARE(next_shot_cd)
-/datum/component/automatic_fire/Initialize(autofire_shot_delay, windup_autofire, windup_autofire_reduction_multiplier, windup_autofire_cap, windup_spindown)
+/datum/component/automatic_fire/Initialize(autofire_shot_delay, windup_autofire, windup_autofire_reduction_multiplier, windup_autofire_cap, windup_spindown, allow_akimbo = TRUE)
. = ..()
if(!isgun(parent))
return COMPONENT_INCOMPATIBLE
@@ -34,6 +38,7 @@
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(wake_up))
if(autofire_shot_delay)
src.autofire_shot_delay = autofire_shot_delay
+ src.allow_akimbo = allow_akimbo
if(windup_autofire)
src.windup_autofire = windup_autofire
src.windup_autofire_reduction_multiplier = windup_autofire_reduction_multiplier
@@ -256,7 +261,7 @@
if(HAS_TRAIT(shooter, TRAIT_DOUBLE_TAP))
next_delay = round(next_delay * 0.5)
COOLDOWN_START(src, next_shot_cd, next_delay)
- if(SEND_SIGNAL(parent, COMSIG_AUTOFIRE_SHOT, target, shooter, mouse_parameters) & COMPONENT_AUTOFIRE_SHOT_SUCCESS)
+ if(SEND_SIGNAL(parent, COMSIG_AUTOFIRE_SHOT, target, shooter, allow_akimbo, mouse_parameters) & COMPONENT_AUTOFIRE_SHOT_SUCCESS)
return TRUE
stop_autofiring()
return FALSE
@@ -288,21 +293,21 @@
return COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS
-/obj/item/gun/proc/do_autofire(datum/source, atom/target, mob/living/shooter, params)
+/obj/item/gun/proc/do_autofire(datum/source, atom/target, mob/living/shooter, allow_akimbo, params)
SIGNAL_HANDLER
if(semicd || shooter.incapacitated())
return NONE
if(!can_shoot())
shoot_with_empty_chamber(shooter)
return NONE
- INVOKE_ASYNC(src, PROC_REF(do_autofire_shot), source, target, shooter, params)
+ INVOKE_ASYNC(src, PROC_REF(do_autofire_shot), source, target, shooter, allow_akimbo, params)
return COMPONENT_AUTOFIRE_SHOT_SUCCESS //All is well, we can continue shooting.
-/obj/item/gun/proc/do_autofire_shot(datum/source, atom/target, mob/living/shooter, params)
+/obj/item/gun/proc/do_autofire_shot(datum/source, atom/target, mob/living/shooter, allow_akimbo, params)
var/obj/item/gun/akimbo_gun = shooter.get_inactive_held_item()
var/bonus_spread = 0
- if(istype(akimbo_gun) && weapon_weight < WEAPON_MEDIUM)
+ if(istype(akimbo_gun) && weapon_weight < WEAPON_MEDIUM && allow_akimbo)
if(akimbo_gun.weapon_weight < WEAPON_MEDIUM && akimbo_gun.can_trigger_gun(shooter))
bonus_spread = dual_wield_spread
addtimer(CALLBACK(akimbo_gun, TYPE_PROC_REF(/obj/item/gun, process_fire), target, shooter, TRUE, params, null, bonus_spread), 1)
diff --git a/code/datums/components/grillable.dm b/code/datums/components/grillable.dm
index f1fe80fe9fb536..72c2e75e8cb0e4 100644
--- a/code/datums/components/grillable.dm
+++ b/code/datums/components/grillable.dm
@@ -12,11 +12,10 @@
var/use_large_steam_sprite = FALSE
/// REF() to the mind which placed us on the griddle
var/who_placed_us
+ /// Reagents that should be added to the result
+ var/list/added_reagents
- /// What type of pollutant we spread around as we are grilleed, can be none // SKYRAT EDIT ADDITION
- var/pollutant_type // SKYRAT EDIT ADDITION
-
-/datum/component/grillable/Initialize(cook_result, required_cook_time, positive_result, use_large_steam_sprite, pollutant_type) //SKYRAT EDIT CHANGE
+/datum/component/grillable/Initialize(cook_result, required_cook_time, positive_result, use_large_steam_sprite, list/added_reagents)
. = ..()
if(!isitem(parent)) //Only items support grilling at the moment
return COMPONENT_INCOMPATIBLE
@@ -25,7 +24,7 @@
src.required_cook_time = required_cook_time
src.positive_result = positive_result
src.use_large_steam_sprite = use_large_steam_sprite
- src.pollutant_type = pollutant_type //SKYRAT EDIT ADDITION
+ src.added_reagents = added_reagents
/datum/component/grillable/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_GRILL_PLACED, PROC_REF(on_grill_placed))
@@ -108,10 +107,12 @@
if(original_object.custom_materials)
grilled_result.set_custom_materials(original_object.custom_materials)
- if(IsEdible(grilled_result))
+ if(IsEdible(grilled_result) && positive_result)
BLACKBOX_LOG_FOOD_MADE(grilled_result.type)
grilled_result.reagents.clear_reagents()
original_object.reagents?.trans_to(grilled_result, original_object.reagents.total_volume)
+ if(added_reagents) // Add any new reagents that should be added
+ grilled_result.reagents.add_reagent_list(added_reagents)
SEND_SIGNAL(parent, COMSIG_ITEM_GRILLED, grilled_result)
if(who_placed_us)
diff --git a/code/datums/components/healing_touch.dm b/code/datums/components/healing_touch.dm
index 4b953fc6289469..029b0f660ef33f 100644
--- a/code/datums/components/healing_touch.dm
+++ b/code/datums/components/healing_touch.dm
@@ -32,6 +32,10 @@
var/action_text
/// Text to print when action completes, replaces %SOURCE% with healer and %TARGET% with healed mob
var/complete_text
+ /// Whether to print the target's remaining health after healing (for non-carbon targets only)
+ var/show_health
+ /// Color for the healing effect
+ var/heal_color
/datum/component/healing_touch/Initialize(
heal_brute = 20,
@@ -46,6 +50,8 @@
self_targetting = HEALING_TOUCH_NOT_SELF,
action_text = "%SOURCE% begins healing %TARGET%",
complete_text = "%SOURCE% finishes healing %TARGET%",
+ show_health = FALSE,
+ heal_color = COLOR_HEALING_CYAN,
)
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
@@ -62,6 +68,8 @@
src.self_targetting = self_targetting
src.action_text = action_text
src.complete_text = complete_text
+ src.show_health = show_health
+ src.heal_color = heal_color
RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(try_healing)) // Players
RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(try_healing)) // NPCs
@@ -147,7 +155,11 @@
healer.visible_message(span_notice("[format_string(complete_text, healer, target)]"))
target.heal_overall_damage(brute = heal_brute, burn = heal_burn, stamina = heal_stamina, required_bodytype = required_bodytype)
- new /obj/effect/temp_visual/heal(get_turf(target), COLOR_HEALING_CYAN)
+ new /obj/effect/temp_visual/heal(get_turf(target), heal_color)
+
+ 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))
/// 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/joint_damage.dm b/code/datums/components/joint_damage.dm
new file mode 100644
index 00000000000000..5397bd307cab95
--- /dev/null
+++ b/code/datums/components/joint_damage.dm
@@ -0,0 +1,35 @@
+/*
+ * A component given to mobs to damage a linked mob
+ */
+/datum/component/joint_damage
+ ///the mob we will damage
+ var/datum/weakref/overlord_mob
+ ///our last health count
+ var/previous_health_count
+
+/datum/component/joint_damage/Initialize(mob/overlord_mob)
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/mob/living/parent_mob = parent
+ previous_health_count = parent_mob.health
+ if(overlord_mob)
+ src.overlord_mob = WEAKREF(overlord_mob)
+
+/datum/component/joint_damage/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(damage_overlord))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(damage_overlord))
+
+/datum/component/joint_damage/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_DEATH))
+
+/datum/component/joint_damage/Destroy()
+ overlord_mob = null
+ return ..()
+
+/datum/component/joint_damage/proc/damage_overlord(mob/living/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/overlord_to_damage = overlord_mob?.resolve()
+ if(!isnull(overlord_to_damage))
+ overlord_to_damage.adjustBruteLoss(previous_health_count - source.health) ///damage or heal overlord
+ previous_health_count = source.health
diff --git a/code/datums/components/manual_heart.dm b/code/datums/components/manual_heart.dm
new file mode 100644
index 00000000000000..09448acfc67229
--- /dev/null
+++ b/code/datums/components/manual_heart.dm
@@ -0,0 +1,179 @@
+/// If a beat is missed, how long to give before the next is missed
+#define MANUAL_HEART_GRACE_PERIOD 2 SECONDS
+
+/**
+ * Manual heart pumping component. Requires the holder to pump their heart manually every
+ * so often or die.
+ *
+ * Mainly used by the cursed heart.
+ */
+/datum/component/manual_heart
+ /// The action for pumping your heart
+ var/datum/action/cooldown/manual_heart/pump_action
+ /// Cooldown before harm is caused to the owner
+ COOLDOWN_DECLARE(heart_timer)
+ /// If true, add a screen tint on the next process
+ var/add_colour = TRUE
+ /// How long between needed pumps; you can pump one second early
+ var/pump_delay = 3 SECONDS
+ /// How much blood volume you lose every missed pump, this is a flat amount not a percentage!
+ var/blood_loss = BLOOD_VOLUME_NORMAL * 0.2 // 20% of normal volume, missing five pumps is instant death
+
+ //How much to heal per pump - negative numbers harm the owner instead
+ /// The amount of brute damage to heal per pump
+ var/heal_brute = 0
+ /// The amount of burn damage to heal per pump
+ var/heal_burn = 0
+ /// The amount of oxygen damage to heal per pump
+ var/heal_oxy = 0
+
+/datum/component/manual_heart/Initialize(pump_delay = 3 SECONDS, blood_loss = BLOOD_VOLUME_NORMAL * 0.2, heal_brute = 0, heal_burn = 0, heal_oxy = 0)
+ //Non-Carbon mobs can't have hearts, and should never receive this component.
+ if (!iscarbon(parent))
+ stack_trace("Manual Heart component added to [parent] ([parent?.type]) which is not a /mob/living/carbon subtype.")
+ return COMPONENT_INCOMPATIBLE
+
+ src.pump_delay = pump_delay
+ src.blood_loss = blood_loss
+ src.heal_brute = heal_brute
+ src.heal_burn = heal_burn
+ src.heal_oxy = heal_oxy
+
+ pump_action = new(src)
+
+/datum/component/manual_heart/Destroy()
+ QDEL_NULL(pump_action)
+ return ..()
+
+/datum/component/manual_heart/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(check_removed_organ))
+ RegisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(check_added_organ))
+ RegisterSignal(parent, COMSIG_HEART_MANUAL_PULSE, PROC_REF(on_pump))
+ RegisterSignals(parent, list(COMSIG_LIVING_DEATH, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD)), PROC_REF(pause))
+ RegisterSignals(parent, list(COMSIG_LIVING_REVIVE, SIGNAL_REMOVETRAIT(TRAIT_NOBLOOD)), PROC_REF(restart))
+
+ pump_action.cooldown_time = pump_delay - (1 SECONDS) //you can pump up to a second early
+ pump_action.Grant(parent)
+
+ var/mob/living/carbon/carbon_parent = parent
+ var/obj/item/organ/internal/heart/parent_heart = carbon_parent.get_organ_slot(ORGAN_SLOT_HEART)
+ if(parent_heart && !HAS_TRAIT(carbon_parent, TRAIT_NOBLOOD) && carbon_parent.stat != DEAD)
+ START_PROCESSING(SSdcs, src)
+ COOLDOWN_START(src, heart_timer, pump_delay)
+
+ to_chat(parent, span_userdanger("Your heart no longer beats automatically! You have to pump it manually - otherwise you'll die!"))
+
+/datum/component/manual_heart/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_CARBON_GAIN_ORGAN, COMSIG_CARBON_LOSE_ORGAN, COMSIG_HEART_MANUAL_PULSE, COMSIG_LIVING_REVIVE, COMSIG_LIVING_DEATH, SIGNAL_ADDTRAIT(TRAIT_NOBLOOD), SIGNAL_REMOVETRAIT(TRAIT_NOBLOOD)))
+
+ to_chat(parent, span_userdanger("You feel your heart start beating normally again!"))
+ var/mob/living/carbon/carbon_parent = parent
+ if(istype(carbon_parent))
+ carbon_parent.remove_client_colour(/datum/client_colour/manual_heart_blood)
+
+/datum/component/manual_heart/proc/restart()
+ SIGNAL_HANDLER
+
+ if(!check_valid())
+ return
+ COOLDOWN_START(src, heart_timer, pump_delay)
+ pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS) //make sure the action button always shows as available when it is
+ START_PROCESSING(SSdcs, src)
+
+/datum/component/manual_heart/proc/pause()
+ SIGNAL_HANDLER
+ pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
+ var/mob/living/carbon/carbon_parent = parent
+ if(istype(carbon_parent))
+ carbon_parent.remove_client_colour(/datum/client_colour/manual_heart_blood) //prevents red overlay from getting stuck
+ STOP_PROCESSING(SSdcs, src)
+
+/// Worker proc that checks logic for if a pump can happen, and applies effects from doing so
+/datum/component/manual_heart/proc/on_pump(mob/owner)
+ COOLDOWN_START(src, heart_timer, pump_delay)
+ playsound(owner,'sound/effects/singlebeat.ogg', 40, TRUE)
+
+ var/mob/living/carbon/carbon_owner = owner
+
+ if(HAS_TRAIT(carbon_owner, TRAIT_NOBLOOD))
+ return
+ carbon_owner.blood_volume = min(carbon_owner.blood_volume + (blood_loss * 0.5), BLOOD_VOLUME_MAXIMUM)
+ carbon_owner.remove_client_colour(/datum/client_colour/manual_heart_blood)
+ add_colour = TRUE
+ carbon_owner.adjustBruteLoss(-heal_brute)
+ carbon_owner.adjustFireLoss(-heal_burn)
+ carbon_owner.adjustOxyLoss(-heal_oxy)
+
+/datum/component/manual_heart/process()
+ var/mob/living/carbon/carbon_parent = parent
+
+ //If they aren't connected, don't kill them.
+ if(!carbon_parent.client)
+ COOLDOWN_START(src, heart_timer, pump_delay)
+ return
+
+ if(!COOLDOWN_FINISHED(src, heart_timer))
+ return
+
+ carbon_parent.blood_volume = max(carbon_parent.blood_volume - blood_loss, 0)
+ to_chat(carbon_parent, span_userdanger("You have to keep pumping your blood!"))
+ COOLDOWN_START(src, heart_timer, MANUAL_HEART_GRACE_PERIOD) //give two full seconds before losing more blood
+ if(add_colour)
+ carbon_parent.add_client_colour(/datum/client_colour/manual_heart_blood)
+ add_colour = FALSE
+
+///If a new heart is added, start processing.
+/datum/component/manual_heart/proc/check_added_organ(mob/organ_owner, obj/item/organ/new_organ)
+ SIGNAL_HANDLER
+
+ var/obj/item/organ/internal/heart/new_heart = new_organ
+
+ if(!istype(new_heart) || !check_valid())
+ return
+ COOLDOWN_START(src, heart_timer, pump_delay)
+ pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
+ var/mob/living/carbon/carbon_parent = parent
+ if(istype(carbon_parent))
+ carbon_parent.remove_client_colour(/datum/client_colour/manual_heart_blood) //prevents red overlay from getting stuck
+ START_PROCESSING(SSdcs, src)
+
+///If the heart is removed, stop processing.
+/datum/component/manual_heart/proc/check_removed_organ(mob/organ_owner, obj/item/organ/removed_organ)
+ SIGNAL_HANDLER
+
+ var/obj/item/organ/internal/heart/removed_heart = removed_organ
+
+ if(istype(removed_heart))
+ pump_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
+ STOP_PROCESSING(SSdcs, src)
+
+///Helper proc to check if processing can be restarted.
+/datum/component/manual_heart/proc/check_valid()
+ var/mob/living/carbon/carbon_parent = parent
+ var/obj/item/organ/internal/heart/parent_heart = carbon_parent.get_organ_slot(ORGAN_SLOT_HEART)
+ return !isnull(parent_heart) && !HAS_TRAIT(carbon_parent, TRAIT_NOBLOOD) && carbon_parent.stat != DEAD
+
+///Action to pump your heart. Cooldown will always be set to 1 second less than the pump delay.
+/datum/action/cooldown/manual_heart
+ name = "Pump your blood"
+ cooldown_time = 2 SECONDS
+ check_flags = NONE
+ button_icon = 'icons/obj/medical/organs/organs.dmi'
+ button_icon_state = "cursedheart-off"
+
+/datum/action/cooldown/manual_heart/Activate(atom/atom_target)
+ . = ..()
+
+ SEND_SIGNAL(owner, COMSIG_HEART_MANUAL_PULSE)
+
+///The action button is only available when you're a living carbon with blood and a heart.
+/datum/action/cooldown/manual_heart/IsAvailable(feedback = FALSE)
+ var/mob/living/carbon/heart_haver = owner
+ if(!istype(heart_haver) || HAS_TRAIT(heart_haver, TRAIT_NOBLOOD) || heart_haver.stat == DEAD)
+ return FALSE
+ var/obj/item/organ/internal/heart/heart_havers_heart = heart_haver.get_organ_slot(ORGAN_SLOT_HEART)
+ if(isnull(heart_havers_heart))
+ return FALSE
+ return ..()
+
+#undef MANUAL_HEART_GRACE_PERIOD
diff --git a/code/datums/components/mob_chain.dm b/code/datums/components/mob_chain.dm
new file mode 100644
index 00000000000000..8312d9d5504764
--- /dev/null
+++ b/code/datums/components/mob_chain.dm
@@ -0,0 +1,224 @@
+/**
+ * Component allowing you to create a linked list of mobs.
+ * These mobs will follow each other and attack as one, as well as sharing damage taken.
+ */
+/datum/component/mob_chain
+
+ /// If true then damage we take is passed backwards along the line
+ var/pass_damage_back
+ /// If true then we will set our icon state based on line position
+ var/vary_icon_state
+
+ /// Mob in front of us in the chain
+ var/mob/living/front
+ /// Mob behind us in the chain
+ var/mob/living/back
+
+/datum/component/mob_chain/Initialize(mob/living/front, pass_damage_back = TRUE, vary_icon_state = FALSE)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.front = front
+ src.pass_damage_back = pass_damage_back
+ src.vary_icon_state = vary_icon_state
+ if (!isnull(front))
+ SEND_SIGNAL(front, COMSIG_MOB_GAINED_CHAIN_TAIL, parent)
+ parent.AddComponent(/datum/component/leash, owner = front, distance = 1) // Handles catching up gracefully
+ var/mob/living/living_parent = parent
+ living_parent.set_glide_size(front.glide_size)
+
+/datum/component/mob_chain/Destroy(force, silent)
+ if (!isnull(front))
+ SEND_SIGNAL(front, COMSIG_MOB_LOST_CHAIN_TAIL, parent)
+ front = null
+ back = null
+ return ..()
+
+/datum/component/mob_chain/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_GAINED_CHAIN_TAIL, PROC_REF(on_gained_tail))
+ RegisterSignal(parent, COMSIG_MOB_LOST_CHAIN_TAIL, PROC_REF(on_lost_tail))
+ RegisterSignal(parent, COMSIG_MOB_CHAIN_CONTRACT, PROC_REF(on_contracted))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ 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))
+ 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))
+ update_mob_appearance()
+ if (pass_damage_back)
+ RegisterSignals(parent, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_adjust_damage))
+ RegisterSignal(parent, COMSIG_LIVING_ADJUST_STAMINA_DAMAGE, PROC_REF(on_adjust_stamina))
+ RegisterSignal(parent, COMSIG_CARBON_LIMB_DAMAGED, PROC_REF(on_limb_damage))
+
+ var/datum/action/cooldown/worm_contract/shrink = new(parent)
+ shrink.Grant(parent)
+
+/datum/component/mob_chain/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ 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,
+ COMSIG_LIVING_DEATH,
+ COMSIG_LIVING_ADJUST_OXY_DAMAGE,
+ COMSIG_LIVING_ADJUST_STAMINA_DAMAGE,
+ COMSIG_LIVING_ADJUST_TOX_DAMAGE,
+ COMSIG_LIVING_UNARMED_ATTACK,
+ COMSIG_MOB_ATTACK_RANGED,
+ COMSIG_MOB_CHAIN_CONTRACT,
+ COMSIG_MOB_GAINED_CHAIN_TAIL,
+ COMSIG_MOB_LOST_CHAIN_TAIL,
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_MOVABLE_UPDATE_GLIDE_SIZE,
+ COMSIG_QDELETING,
+ ))
+ qdel(parent.GetComponent(/datum/component/leash))
+ var/mob/living/living_parent = parent
+ var/datum/action/cooldown/worm_contract/shrink = locate() in living_parent.actions
+ qdel(shrink)
+
+/// Update how we look
+/datum/component/mob_chain/proc/update_mob_appearance()
+ if (!vary_icon_state)
+ return
+ var/mob/living/body = parent
+ body.update_appearance(UPDATE_ICON_STATE)
+
+/// Called when something sets us as IT'S front
+/datum/component/mob_chain/proc/on_gained_tail(mob/living/body, mob/living/tail)
+ SIGNAL_HANDLER
+ back = tail
+ update_mob_appearance()
+
+/// Called when our tail loses its chain component
+/datum/component/mob_chain/proc/on_lost_tail()
+ SIGNAL_HANDLER
+ back = null
+ update_mob_appearance()
+
+/// Called when our tail gets pulled up to our body
+/datum/component/mob_chain/proc/on_contracted(mob/living/shrinking)
+ SIGNAL_HANDLER
+ if (isnull(back))
+ return
+ back.forceMove(shrinking.loc)
+ var/datum/action/cooldown/worm_contract/shrink = locate() in back.actions
+ if (isnull(shrink))
+ return
+ INVOKE_ASYNC(shrink, TYPE_PROC_REF(/datum/action, Trigger))
+
+/// If we die so does the guy behind us, then stop following the leader
+/datum/component/mob_chain/proc/on_death()
+ SIGNAL_HANDLER
+ back?.death()
+ qdel(src)
+
+/// If we get deleted so does the guy behind us
+/datum/component/mob_chain/proc/on_deletion()
+ SIGNAL_HANDLER
+ QDEL_NULL(back)
+ front?.update_appearance(UPDATE_ICON)
+
+/// Pull our tail behind us when we move
+/datum/component/mob_chain/proc/on_moved(mob/living/mover, turf/old_loc)
+ SIGNAL_HANDLER
+ if(isnull(back) || back.loc == old_loc)
+ return
+ back.Move(old_loc)
+
+/// Update our visuals based on if we have someone in front and behind
+/datum/component/mob_chain/proc/on_update_icon_state(mob/living/our_mob)
+ SIGNAL_HANDLER
+ var/current_icon_state = our_mob.base_icon_state
+ if(isnull(front))
+ current_icon_state = "[current_icon_state]_start"
+ else if(isnull(back))
+ current_icon_state = "[current_icon_state]_end"
+ else
+ current_icon_state = "[current_icon_state]_mid"
+
+ our_mob.icon_state = current_icon_state
+ if (isanimal_or_basicmob(our_mob))
+ var/mob/living/basic/basic_parent = our_mob
+ basic_parent.icon_living = current_icon_state
+
+/// Do not allow someone to be pulled out of the chain
+/datum/component/mob_chain/proc/on_pulled(mob/living/our_mob)
+ SIGNAL_HANDLER
+ if (!isnull(front))
+ return COMSIG_ATOM_CANT_PULL
+
+/// Tell our tail to attack too
+/datum/component/mob_chain/proc/on_attack(mob/living/our_mob, atom/target)
+ SIGNAL_HANDLER
+ if (target == back || target == front)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ if (isnull(back) || QDELETED(target))
+ return
+ INVOKE_ASYNC(back, TYPE_PROC_REF(/mob, ClickOn), target)
+
+/// Maintain glide size backwards
+/datum/component/mob_chain/proc/on_glide_size_changed(mob/living/our_mob, new_size)
+ SIGNAL_HANDLER
+ back?.set_glide_size(new_size)
+
+/// On gain or lose stamina, adjust our tail too
+/datum/component/mob_chain/proc/on_adjust_stamina(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (forced)
+ return
+ back?.adjustStaminaLoss(amount, forced = forced)
+
+/// On damage or heal, affect our furthest segment
+/datum/component/mob_chain/proc/on_adjust_damage(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (isnull(back) || forced)
+ return
+ switch (type)
+ if(BRUTE)
+ back.adjustBruteLoss(amount, forced = forced)
+ if(BURN)
+ back.adjustFireLoss(amount, forced = forced)
+ if(TOX)
+ back.adjustToxLoss(amount, forced = forced)
+ if(OXY) // If all segments are suffocating we pile damage backwards until our ass starts dying forwards
+ back.adjustOxyLoss(amount, forced = forced)
+ if(CLONE)
+ back.adjustCloneLoss(amount, forced = forced)
+ return COMPONENT_IGNORE_CHANGE
+
+/// Special handling for if damage is delegated to a mob's limbs instead of its overall damage
+/datum/component/mob_chain/proc/on_limb_damage(mob/living/our_mob, limb, brute, burn)
+ SIGNAL_HANDLER
+ if (isnull(back))
+ return
+ if (brute != 0)
+ back.adjustBruteLoss(brute, updating_health = FALSE)
+ if (burn != 0)
+ back.adjustFireLoss(burn, updating_health = FALSE)
+ if (brute != 0 || burn != 0)
+ back.updatehealth()
+ return COMPONENT_PREVENT_LIMB_DAMAGE
+
+/**
+ * Shrink the chain of mobs into one tile.
+ */
+/datum/action/cooldown/worm_contract
+ name = "Force Contract"
+ desc = "Forces your body to contract onto a single tile."
+ background_icon_state = "bg_heretic"
+ overlay_icon_state = "bg_heretic_border"
+ button_icon = 'icons/mob/actions/actions_ecult.dmi'
+ button_icon_state = "worm_contract"
+ cooldown_time = 30 SECONDS
+ melee_cooldown_time = 0 SECONDS
+
+/datum/action/cooldown/worm_contract/Activate(atom/target)
+ SEND_SIGNAL(owner, COMSIG_MOB_CHAIN_CONTRACT)
+ StartCooldown()
diff --git a/code/datums/components/omen.dm b/code/datums/components/omen.dm
index 4f3b17a7473528..f499d22968dbb5 100644
--- a/code/datums/components/omen.dm
+++ b/code/datums/components/omen.dm
@@ -238,7 +238,7 @@
return ..()
death_explode(our_guy)
- our_guy.gib()
+ our_guy.gib(DROP_ALL_REMAINS)
/**
* The quirk omen. Permanent.
@@ -259,7 +259,7 @@
/datum/component/omen/quirk/check_death(mob/living/our_guy)
if(!iscarbon(our_guy))
- our_guy.gib()
+ our_guy.gib(DROP_ALL_REMAINS)
return
// Don't explode if buckled to a stasis bed
@@ -270,7 +270,7 @@
death_explode(our_guy)
var/mob/living/carbon/player = our_guy
- player.spread_bodyparts(skip_head = TRUE)
+ player.spread_bodyparts()
player.spawn_gibs()
return
diff --git a/code/datums/components/pet_commands/fetch.dm b/code/datums/components/pet_commands/fetch.dm
index a8723f8bd4bd1a..ae33983e1d00d3 100644
--- a/code/datums/components/pet_commands/fetch.dm
+++ b/code/datums/components/pet_commands/fetch.dm
@@ -18,6 +18,8 @@
/datum/pet_command/point_targetting/fetch/New(mob/living/parent)
. = ..()
+ if(isnull(parent))
+ return
parent.AddElement(/datum/element/ai_held_item) // We don't remove this on destroy because they might still be holding something
/datum/pet_command/point_targetting/fetch/add_new_friend(mob/living/tamer)
diff --git a/code/datums/components/plumbing/_plumbing.dm b/code/datums/components/plumbing/_plumbing.dm
index b140b003e6c9a9..af27b4bbb4611b 100644
--- a/code/datums/components/plumbing/_plumbing.dm
+++ b/code/datums/components/plumbing/_plumbing.dm
@@ -73,8 +73,8 @@
/datum/component/plumbing/process()
if(!demand_connects || !reagents)
- STOP_PROCESSING(SSplumbing, src)
- return
+ return PROCESS_KILL
+
if(reagents.total_volume < reagents.maximum_volume)
for(var/D in GLOB.cardinals)
if(D & demand_connects)
@@ -93,23 +93,29 @@
///called from in process(). only calls process_request(), but can be overwritten for children with special behaviour
/datum/component/plumbing/proc/send_request(dir)
- process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = null, dir = dir)
+ process_request(dir = dir)
///check who can give us what we want, and how many each of them will give us
-/datum/component/plumbing/proc/process_request(amount, reagent, dir)
- var/list/valid_suppliers = list()
+/datum/component/plumbing/proc/process_request(amount = MACHINE_REAGENT_TRANSFER, reagent, dir)
+ //find the duct to take from
var/datum/ductnet/net
if(!ducts.Find(num2text(dir)))
return
net = ducts[num2text(dir)]
+
+ //find all valid suppliers in the duct
+ var/list/valid_suppliers = list()
for(var/datum/component/plumbing/supplier as anything in net.suppliers)
if(supplier.can_give(amount, reagent, net))
valid_suppliers += supplier
- // Need to ask for each in turn very carefully, making sure we get the total volume. This is to avoid a division that would always round down and become 0
- var/targetVolume = reagents.total_volume + amount
var/suppliersLeft = valid_suppliers.len
+ if(!suppliersLeft)
+ return
+
+ //take an equal amount from each supplier
+ var/currentRequest
for(var/datum/component/plumbing/give as anything in valid_suppliers)
- var/currentRequest = (targetVolume - reagents.total_volume) / suppliersLeft
+ currentRequest = amount / suppliersLeft
give.transfer_to(src, currentRequest, reagent, net)
suppliersLeft--
@@ -122,7 +128,7 @@
for(var/datum/reagent/contained_reagent as anything in reagents.reagent_list)
if(contained_reagent.type == reagent)
return TRUE
- else if(reagents.total_volume > 0) //take whatever
+ else if(reagents.total_volume) //take whatever
return TRUE
///this is where the reagent is actually transferred and is thus the finish point of our process()
@@ -132,7 +138,7 @@
if(reagent)
reagents.trans_id_to(target.recipient_reagents_holder, reagent, amount)
else
- reagents.trans_to(target.recipient_reagents_holder, amount, round_robin = TRUE, methods = methods)//we deal with alot of precise calculations so we round_robin=TRUE. Otherwise we get floating point errors, 1 != 1 and 2.5 + 2.5 = 6
+ reagents.trans_to(target.recipient_reagents_holder, amount, methods = methods)
///We create our luxurious piping overlays/underlays, to indicate where we do what. only called once if use_overlays = TRUE in Initialize()
/datum/component/plumbing/proc/create_overlays(atom/movable/parent_movable, list/overlays)
diff --git a/code/datums/components/plumbing/reaction_chamber.dm b/code/datums/components/plumbing/reaction_chamber.dm
index c750fda714255f..f728ce315e6ee1 100644
--- a/code/datums/components/plumbing/reaction_chamber.dm
+++ b/code/datums/components/plumbing/reaction_chamber.dm
@@ -18,16 +18,20 @@
if(chamber.emptying)
return
+ var/present_amount
+ var/diff
for(var/required_reagent in chamber.required_reagents)
- var/has_reagent = FALSE
+ //find how much amount is already present if at all
+ present_amount = 0
for(var/datum/reagent/containg_reagent as anything in reagents.reagent_list)
if(required_reagent == containg_reagent.type)
- has_reagent = TRUE
- if(containg_reagent.volume + CHEMICAL_QUANTISATION_LEVEL < chamber.required_reagents[required_reagent])
- process_request(min(chamber.required_reagents[required_reagent] - containg_reagent.volume, MACHINE_REAGENT_TRANSFER) , required_reagent, dir)
- return
- if(!has_reagent)
- process_request(min(chamber.required_reagents[required_reagent], MACHINE_REAGENT_TRANSFER), required_reagent, dir)
+ present_amount = containg_reagent.volume
+ break
+
+ //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)
return
reagents.flags &= ~NO_REACT
@@ -45,16 +49,15 @@
ducting_layer = SECOND_DUCT_LAYER
/datum/component/plumbing/acidic_input/send_request(dir)
- process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = /datum/reagent/reaction_agent/acidic_buffer, dir = dir)
+ process_request(reagent = /datum/reagent/reaction_agent/acidic_buffer, dir = dir)
///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs separate, like into a special internal base container
/datum/component/plumbing/alkaline_input
demand_connects = EAST
demand_color = COLOR_VIBRANT_LIME
-
ducting_layer = FOURTH_DUCT_LAYER
/datum/component/plumbing/alkaline_input/send_request(dir)
- process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = /datum/reagent/reaction_agent/basic_buffer, dir = dir)
+ process_request(reagent = /datum/reagent/reaction_agent/basic_buffer, dir = dir)
diff --git a/code/datums/components/rot.dm b/code/datums/components/rot.dm
index 11c4f2cb617490..64ddde2b2db9b2 100644
--- a/code/datums/components/rot.dm
+++ b/code/datums/components/rot.dm
@@ -114,7 +114,7 @@
/datum/component/rot/proc/rot_react_touch(datum/source, mob/living/react_to)
SIGNAL_HANDLER
- rot_react(source, react_to, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ rot_react(source, react_to, pick(GLOB.arm_zones))
/// Triggered when something enters the component's parent.
/datum/component/rot/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
diff --git a/code/datums/components/squashable.dm b/code/datums/components/squashable.dm
index 5fc489873bf2da..8174127aeb39cd 100644
--- a/code/datums/components/squashable.dm
+++ b/code/datums/components/squashable.dm
@@ -72,7 +72,7 @@
/datum/component/squashable/proc/Squish(mob/living/target)
if(squash_flags & SQUASHED_SHOULD_BE_GIBBED)
- target.gib()
+ target.gib(DROP_ALL_REMAINS)
else
target.adjustBruteLoss(squash_damage)
diff --git a/code/datums/components/stationstuck.dm b/code/datums/components/stationstuck.dm
index 63a1dcabbbd19e..5634186f04e02c 100644
--- a/code/datums/components/stationstuck.dm
+++ b/code/datums/components/stationstuck.dm
@@ -49,7 +49,7 @@ It has a punishment variable that is what happens to the parent when they leave
escapee.death()
if(PUNISHMENT_GIB)
escapee.investigate_log("has been gibbed by stationstuck component.", INVESTIGATE_DEATHS)
- escapee.gib()
+ escapee.gib(DROP_ALL_REMAINS)
if(PUNISHMENT_TELEPORT)
var/targetturf = find_safe_turf(stuck_zlevel)
if(!targetturf)
diff --git a/code/datums/components/supermatter_crystal.dm b/code/datums/components/supermatter_crystal.dm
index a9bdfc5b1549ed..38968d1e3d1f5d 100644
--- a/code/datums/components/supermatter_crystal.dm
+++ b/code/datums/components/supermatter_crystal.dm
@@ -4,6 +4,11 @@
var/datum/callback/tool_act_callback
///Callback used by the SM to get the damage and matter power increase/decrease
var/datum/callback/consume_callback
+ // A whitelist of items that can interact with the SM without dusting the user
+ var/static/list/sm_item_whitelist = typecacheof(list(
+ /obj/item/melee/roastingstick,
+ /obj/item/toy/crayon/spraycan
+ ))
/datum/component/supermatter_crystal/Initialize(datum/callback/tool_act_callback, datum/callback/consume_callback)
@@ -15,8 +20,10 @@
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(hand_hit))
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(attackby_hit))
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_WRENCH), PROC_REF(tool_hit))
+ RegisterSignal(parent, COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_WRENCH), PROC_REF(tool_hit))
RegisterSignal(parent, COMSIG_ATOM_BUMPED, PROC_REF(bumped_hit))
RegisterSignal(parent, COMSIG_ATOM_INTERCEPT_Z_FALL, PROC_REF(intercept_z_fall))
+ RegisterSignal(parent, COMSIG_ATOM_ON_Z_IMPACT, PROC_REF(on_z_impact))
src.tool_act_callback = tool_act_callback
src.consume_callback = consume_callback
@@ -36,8 +43,10 @@
COMSIG_ATOM_ATTACK_HAND,
COMSIG_ATOM_ATTACKBY,
COMSIG_ATOM_TOOL_ACT(TOOL_WRENCH),
+ COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_WRENCH),
COMSIG_ATOM_BUMPED,
COMSIG_ATOM_INTERCEPT_Z_FALL,
+ COMSIG_ATOM_ON_Z_IMPACT,
)
UnregisterSignal(parent, signals_to_remove)
@@ -150,7 +159,7 @@
var/atom/atom_source = source
if(!istype(item) || (item.item_flags & ABSTRACT) || !istype(user))
return
- if(istype(item, /obj/item/melee/roastingstick))
+ if(is_type_in_typecache(item, sm_item_whitelist))
return FALSE
if(istype(item, /obj/item/clothing/mask/cigarette))
var/obj/item/clothing/mask/cigarette/cig = item
@@ -232,11 +241,36 @@
/datum/component/supermatter_crystal/proc/intercept_z_fall(datum/source, list/falling_movables, levels)
SIGNAL_HANDLER
for(var/atom/movable/hit_object as anything in falling_movables)
- if(hit_object == source)
- continue
+ if(parent == hit_object)
+ return
+
bumped_hit(parent, hit_object)
return FALL_INTERCEPTED | FALL_NO_MESSAGE
+/datum/component/supermatter_crystal/proc/on_z_impact(datum/source, turf/impacted_turf, levels)
+ SIGNAL_HANDLER
+
+ var/atom/atom_source = source
+
+ for(var/mob/living/poor_target in impacted_turf)
+ consume(atom_source, poor_target)
+ playsound(get_turf(atom_source), 'sound/effects/supermatter.ogg', 50, TRUE)
+ poor_target.visible_message(span_danger("\The [atom_source] slams into \the [poor_target] out of nowhere inducing a resonance... [poor_target.p_their()] body starts to glow and burst into flames before flashing into dust!"),
+ span_userdanger("\The [atom_source] slams into you out of nowhere as your ears are filled with unearthly ringing. Your last thought is \"The fuck.\""),
+ span_hear("You hear an unearthly noise as a wave of heat washes over you."))
+
+ for(var/atom/movable/hit_object as anything in impacted_turf)
+ if(parent == hit_object)
+ return
+
+ if(iseffect(hit_object))
+ continue
+
+ consume(atom_source, hit_object)
+ playsound(get_turf(atom_source), 'sound/effects/supermatter.ogg', 50, TRUE)
+ atom_source.visible_message(span_danger("\The [atom_source], smacks into the plating out of nowhere, reducing everything below to ash."), null,
+ span_hear("You hear a loud crack as you are washed with a wave of heat."))
+
/datum/component/supermatter_crystal/proc/dust_mob(datum/source, mob/living/nom, vis_msg, mob_msg, cause)
if(nom.incorporeal_move || nom.status_flags & GODMODE) //try to keep supermatter sliver's + hemostat's dust conditions in sync with this too
return
diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm
index 4ae3a973d7ee18..a5b5606bf724cb 100644
--- a/code/datums/components/tackle.dm
+++ b/code/datums/components/tackle.dm
@@ -445,7 +445,7 @@
if(86 to 92)
user.visible_message(span_danger("[user] slams head-first into [hit], suffering major cranial trauma!"), span_userdanger("You slam head-first into [hit], and the world explodes around you!"))
- user.adjustStaminaLoss(30, updating_stamina=FALSE)
+ user.adjustStaminaLoss(30, updating_stamina = FALSE)
user.adjustBruteLoss(30)
user.adjust_confusion(15 SECONDS)
if(prob(80))
@@ -457,7 +457,7 @@
if(68 to 85)
user.visible_message(span_danger("[user] slams hard into [hit], knocking [user.p_them()] senseless!"), span_userdanger("You slam hard into [hit], knocking yourself senseless!"))
- user.adjustStaminaLoss(30, updating_stamina=FALSE)
+ user.adjustStaminaLoss(30, updating_stamina = FALSE)
user.adjustBruteLoss(10)
user.adjust_confusion(10 SECONDS)
user.Knockdown(3 SECONDS)
@@ -465,7 +465,7 @@
if(1 to 67)
user.visible_message(span_danger("[user] slams into [hit]!"), span_userdanger("You slam into [hit]!"))
- user.adjustStaminaLoss(20, updating_stamina=FALSE)
+ user.adjustStaminaLoss(20, updating_stamina = FALSE)
user.adjustBruteLoss(10)
user.Knockdown(2 SECONDS)
shake_camera(user, 2, 2)
@@ -545,10 +545,11 @@
HOW_big_of_a_miss_did_we_just_make = ", making a ginormous mess!" // an extra exclamation point!! for emphasis!!!
owner.visible_message(span_danger("[owner] trips over [kevved] and slams into it face-first[HOW_big_of_a_miss_did_we_just_make]!"), span_userdanger("You trip over [kevved] and slam into it face-first[HOW_big_of_a_miss_did_we_just_make]!"))
- owner.adjustStaminaLoss(15 + messes.len * 2, FALSE)
- owner.adjustBruteLoss(8 + messes.len)
+ owner.adjustStaminaLoss(15 + messes.len * 2, updating_stamina = FALSE)
+ owner.adjustBruteLoss(8 + messes.len, updating_health = FALSE)
owner.Paralyze(0.4 SECONDS * messes.len) // .4 seconds of paralyze for each thing you knock around
owner.Knockdown(2 SECONDS + 0.4 SECONDS * messes.len) // 2 seconds of knockdown after the paralyze
+ owner.updatehealth()
for(var/obj/item/I in messes)
var/dist = rand(1, 3)
diff --git a/code/datums/components/temporary_description.dm b/code/datums/components/temporary_description.dm
new file mode 100644
index 00000000000000..1ff5e6dccdba3d
--- /dev/null
+++ b/code/datums/components/temporary_description.dm
@@ -0,0 +1,44 @@
+/**
+ * Adds examine text to something which is removed when receiving specified signals, by default the revive signal.
+ * The default settings are set up to be applied to a corpse to add some kind of immersive storytelling text which goes away upon revival.
+ */
+/datum/component/temporary_description
+ /// What do we display on examine?
+ var/description_text = ""
+ /// What do we display if examined by a clown? Usually only applied if this is put on a corpse, but go nuts.
+ var/naive_description = ""
+ /// When are we removed?
+ var/list/removal_signals
+
+/datum/component/temporary_description/Initialize(
+ description_text = "There's something unusual about them.",
+ naive_description = "",
+ list/removal_signals = list(COMSIG_LIVING_REVIVE),
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ if (!description_text)
+ stack_trace("[type] applied to [parent] with empty description, which is pointless.")
+ src.description_text = description_text
+ src.naive_description = naive_description
+ if (length(removal_signals))
+ src.removal_signals = removal_signals
+
+/datum/component/temporary_description/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+ RegisterSignals(parent, removal_signals, PROC_REF(remove_component))
+
+/datum/component/temporary_description/UnregisterFromParent()
+ UnregisterSignal(parent, removal_signals + COMSIG_ATOM_EXAMINE)
+
+/datum/component/temporary_description/proc/on_examined(atom/corpse, mob/thing_inspector, list/examine_list)
+ SIGNAL_HANDLER
+ if (naive_description && HAS_MIND_TRAIT(thing_inspector, TRAIT_NAIVE))
+ examine_list += span_notice(naive_description)
+ return
+ examine_list += span_notice(description_text)
+
+/datum/component/temporary_description/proc/remove_component()
+ SIGNAL_HANDLER
+ qdel(src) // It wouldn't be immersive if the circumstances of my grisly death remained after I was revived
diff --git a/code/datums/components/tippable.dm b/code/datums/components/tippable.dm
index eafb234581812f..ee1f2c40547399 100644
--- a/code/datums/components/tippable.dm
+++ b/code/datums/components/tippable.dm
@@ -133,6 +133,8 @@
/datum/component/tippable/proc/do_tip(mob/living/tipped_mob, mob/tipper)
if(QDELETED(tipped_mob))
CRASH("Tippable component: do_tip() called with QDELETED tipped_mob!")
+ if (is_tipped) // sanity check in case multiple people try to tip at the same time
+ return
to_chat(tipper, span_warning("You tip over [tipped_mob]."))
if (!isnull(tipped_mob.client))
@@ -185,6 +187,8 @@
/datum/component/tippable/proc/do_untip(mob/living/tipped_mob, mob/untipper)
if(QDELETED(tipped_mob))
return
+ if (!is_tipped) // sanity check in case multiple people try to untip at the same time
+ return
to_chat(untipper, span_notice("You right [tipped_mob]."))
tipped_mob.visible_message(
diff --git a/code/datums/components/tree_climber.dm b/code/datums/components/tree_climber.dm
index b3c70153c719cd..9f506ae516f8ac 100644
--- a/code/datums/components/tree_climber.dm
+++ b/code/datums/components/tree_climber.dm
@@ -12,6 +12,8 @@
return COMPONENT_INCOMPATIBLE
src.climbing_distance = climbing_distance
+ ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+
/datum/component/tree_climber/RegisterWithParent()
RegisterSignals(parent, list(COMSIG_HOSTILE_PRE_ATTACKINGTARGET, COMSIG_LIVING_CLIMB_TREE), PROC_REF(climb_tree))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
diff --git a/code/datums/components/wall_mounted.dm b/code/datums/components/wall_mounted.dm
index 6164d39b001663..8d1722f89feb80 100644
--- a/code/datums/components/wall_mounted.dm
+++ b/code/datums/components/wall_mounted.dm
@@ -17,7 +17,7 @@
/datum/component/wall_mounted/RegisterWithParent()
RegisterSignal(hanging_wall_turf, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
- RegisterSignal(hanging_wall_turf, COMSIG_TURF_CHANGE, PROC_REF(drop_wallmount))
+ RegisterSignal(hanging_wall_turf, COMSIG_TURF_CHANGE, PROC_REF(on_turf_changing))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(drop_wallmount))
RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(on_linked_destroyed))
@@ -41,6 +41,14 @@
SIGNAL_HANDLER
examine_list += span_notice("\The [hanging_wall_turf] is currently supporting [span_bold("[parent]")]. Deconstruction or excessive damage would cause it to [span_bold("fall to the ground")].")
+/**
+ * When the type of turf changes, if it is changing into a floor we should drop our contents
+ */
+/datum/component/wall_mounted/proc/on_turf_changing(datum/source, path, new_baseturfs, flags, post_change_callbacks)
+ SIGNAL_HANDLER
+ if (ispath(path, /turf/open))
+ drop_wallmount()
+
/**
* Handles the dropping of the linked object. This is done via deconstruction, as that should be the most sane way to handle it for most objects.
* Except for intercoms, which are handled by creating a new wallframe intercom, as they're apparently items.
diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm
index b8f26a89b3cb43..aeea8b22cbaabb 100644
--- a/code/datums/datumvars.dm
+++ b/code/datums/datumvars.dm
@@ -4,7 +4,8 @@
/datum/proc/can_vv_get(var_name)
return TRUE
-/datum/proc/vv_edit_var(var_name, var_value) //called whenever a var is edited
+/// Called when a var is edited with the new value to change to
+/datum/proc/vv_edit_var(var_name, var_value)
if(var_name == NAMEOF(src, vars))
return FALSE
vars[var_name] = var_value
@@ -20,9 +21,14 @@
/datum/proc/can_vv_mark()
return TRUE
-//please call . = ..() first and append to the result, that way parent items are always at the top and child items are further down
-//add separaters by doing . += "---"
+/**
+ * Gets all the dropdown options in the vv menu.
+ * When overriding, make sure to call . = ..() first and appent to the result, that way parent items are always at the top and child items are further down.
+ * Add seperators by doing VV_DROPDOWN_OPTION("", "---")
+ */
/datum/proc/vv_get_dropdown()
+ SHOULD_CALL_PARENT(TRUE)
+
. = list()
VV_DROPDOWN_OPTION("", "---")
VV_DROPDOWN_OPTION(VV_HK_CALLPROC, "Call Proc")
@@ -35,9 +41,11 @@
VV_DROPDOWN_OPTION(VV_HK_MASS_REMOVECOMPONENT, "Mass Remove Component/Element")
VV_DROPDOWN_OPTION(VV_HK_MODIFY_TRAITS, "Modify Traits")
-//This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks!
-//href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables!
-//This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm incase this runtimes.
+/**
+ * This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks!
+ * href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables!
+ * This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm incase this runtimes.
+ */
/datum/proc/vv_do_topic(list/href_list)
if(!usr || !usr.client || !usr.client.holder || !check_rights(NONE))
return FALSE //This is VV, not to be called by anything else.
diff --git a/code/datums/diseases/advance/symptoms/heal.dm b/code/datums/diseases/advance/symptoms/heal.dm
index 8754eb3b71d43b..55e2cdd6c656b9 100644
--- a/code/datums/diseases/advance/symptoms/heal.dm
+++ b/code/datums/diseases/advance/symptoms/heal.dm
@@ -161,8 +161,8 @@
if(!parts.len)
return
- for(var/obj/item/bodypart/L in parts)
- if(L.heal_damage(heal_amt/parts.len, heal_amt/parts.len, BODYTYPE_ORGANIC))
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len, required_bodytype = BODYTYPE_ORGANIC))
M.update_damage_overlays()
return 1
@@ -303,8 +303,8 @@
if(prob(5))
to_chat(M, span_notice("The darkness soothes and mends your wounds."))
- for(var/obj/item/bodypart/L in parts)
- if(L.heal_damage(heal_amt/parts.len, heal_amt/parts.len * 0.5, BODYTYPE_ORGANIC)) //more effective on brute
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len * 0.5, required_bodytype = BODYTYPE_ORGANIC)) //more effective on brute
M.update_damage_overlays()
return 1
@@ -405,8 +405,8 @@
if(!parts.len)
return
- for(var/obj/item/bodypart/L in parts)
- if(L.heal_damage(heal_amt/parts.len, heal_amt/parts.len, BODYTYPE_ORGANIC))
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len, required_bodytype = BODYTYPE_ORGANIC))
M.update_damage_overlays()
if(active_coma && M.getBruteLoss() + M.getFireLoss() == 0)
@@ -469,8 +469,8 @@
if(prob(5))
to_chat(M, span_notice("You feel yourself absorbing the water around you to soothe your damaged skin."))
- for(var/obj/item/bodypart/L in parts)
- if(L.heal_damage(heal_amt/parts.len * 0.5, heal_amt/parts.len, BODYTYPE_ORGANIC))
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len * 0.5, heal_amt/parts.len, required_bodytype = BODYTYPE_ORGANIC))
M.update_damage_overlays()
return 1
@@ -592,8 +592,8 @@
return
if(prob(5))
to_chat(M, span_notice("The pain from your wounds fades rapidly."))
- for(var/obj/item/bodypart/L in parts)
- if(L.heal_damage(heal_amt/parts.len, heal_amt/parts.len, BODYTYPE_ORGANIC))
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len, required_bodytype = BODYTYPE_ORGANIC))
M.update_damage_overlays()
return 1
@@ -637,10 +637,13 @@
/datum/symptom/heal/radiation/Heal(mob/living/carbon/M, datum/disease/advance/A, actual_power)
var/heal_amt = actual_power
+ var/need_mob_update = FALSE
if(cellular_damage)
- M.adjustCloneLoss(-heal_amt * 0.5)
+ need_mob_update += M.adjustCloneLoss(-heal_amt * 0.5, updating_health = FALSE)
- M.adjustToxLoss(-(2 * heal_amt))
+ need_mob_update += M.adjustToxLoss(-(2 * heal_amt), updating_health = FALSE)
+ if(need_mob_update)
+ M.updatehealth()
var/list/parts = M.get_damaged_bodyparts(1,1, BODYTYPE_ORGANIC)
@@ -650,8 +653,8 @@
if(prob(4))
to_chat(M, span_notice("Your skin glows faintly, and you feel your wounds mending themselves."))
- for(var/obj/item/bodypart/L in parts)
- if(L.heal_damage(heal_amt/parts.len, heal_amt/parts.len, BODYTYPE_ORGANIC))
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len, required_bodytype = BODYTYPE_ORGANIC))
M.update_damage_overlays()
return 1
diff --git a/code/datums/diseases/advance/symptoms/oxygen.dm b/code/datums/diseases/advance/symptoms/oxygen.dm
index 89da211b06b99a..fad70aff23f72e 100644
--- a/code/datums/diseases/advance/symptoms/oxygen.dm
+++ b/code/datums/diseases/advance/symptoms/oxygen.dm
@@ -38,8 +38,8 @@
var/mob/living/carbon/infected_mob = advanced_disease.affected_mob
switch(advanced_disease.stage)
if(4, 5)
- infected_mob.adjustOxyLoss(-7, 0)
infected_mob.losebreath = max(0, infected_mob.losebreath - 4)
+ infected_mob.adjustOxyLoss(-7)
if(regenerate_blood && infected_mob.blood_volume < BLOOD_VOLUME_NORMAL)
infected_mob.blood_volume += 1
else
diff --git a/code/datums/diseases/chronic_illness.dm b/code/datums/diseases/chronic_illness.dm
index 77c162d6d85c30..129883c17b5f0f 100644
--- a/code/datums/diseases/chronic_illness.dm
+++ b/code/datums/diseases/chronic_illness.dm
@@ -31,19 +31,23 @@
to_chat(affected_mob, span_notice("You look at your hand. Your vision blurs."))
affected_mob.set_eye_blur_if_lower(10 SECONDS)
if(3)
+ var/need_mob_update = FALSE
if(SPT_PROB(0.5, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel a very sharp pain in your chest!"))
if(prob(45))
affected_mob.vomit(VOMIT_CATEGORY_BLOOD, lost_nutrition = 20)
if(SPT_PROB(0.5, seconds_per_tick))
to_chat(affected_mob, span_userdanger("[pick("You feel your heart slowing...", "You relax and slow your heartbeat.")]"))
- affected_mob.adjustStaminaLoss(70, FALSE)
+ need_mob_update += affected_mob.adjustStaminaLoss(70, updating_stamina = FALSE)
if(SPT_PROB(1, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel a buzzing in your brain."))
SEND_SOUND(affected_mob, sound('sound/weapons/flash_ring.ogg'))
if(SPT_PROB(0.5, seconds_per_tick))
- affected_mob.adjustBruteLoss(1)
+ need_mob_update += affected_mob.adjustBruteLoss(1, updating_health = FALSE)
+ if(need_mob_update)
+ affected_mob.updatehealth()
if(4)
+ var/need_mob_update = FALSE
if(prob(30))
affected_mob.playsound_local(affected_mob, 'sound/effects/singlebeat.ogg', 100, FALSE, use_reverb = FALSE)
if(SPT_PROB(1, seconds_per_tick))
@@ -51,7 +55,7 @@
if(prob(75))
affected_mob.vomit(VOMIT_CATEGORY_BLOOD, lost_nutrition = 45)
if(SPT_PROB(1, seconds_per_tick))
- affected_mob.adjustStaminaLoss(100, FALSE)
+ need_mob_update += affected_mob.adjustStaminaLoss(100, updating_stamina = FALSE)
affected_mob.visible_message(span_warning("[affected_mob] collapses!"))
if(prob(30))
to_chat(affected_mob, span_danger("Your vision blurs as you faint!"))
@@ -59,7 +63,9 @@
if(SPT_PROB(0.5, seconds_per_tick))
to_chat(affected_mob, span_danger("[pick("You feel as though your atoms are accelerating in place.", "You feel like you're being torn apart!")]"))
affected_mob.emote("scream")
- affected_mob.adjustBruteLoss(10)
+ need_mob_update += affected_mob.adjustBruteLoss(10, updating_health = FALSE)
+ if(need_mob_update)
+ affected_mob.updatehealth()
if(5)
switch(rand(1,2))
if(1)
diff --git a/code/datums/diseases/gbs.dm b/code/datums/diseases/gbs.dm
index 22f84cf73a1f40..abf2116a92f062 100644
--- a/code/datums/diseases/gbs.dm
+++ b/code/datums/diseases/gbs.dm
@@ -30,5 +30,5 @@
to_chat(affected_mob, span_userdanger("Your body feels as if it's trying to rip itself apart!"))
if(SPT_PROB(30, seconds_per_tick))
affected_mob.investigate_log("has been gibbed by GBS.", INVESTIGATE_DEATHS)
- affected_mob.gib()
+ affected_mob.gib(DROP_ALL_REMAINS)
return FALSE
diff --git a/code/datums/diseases/rhumba_beat.dm b/code/datums/diseases/rhumba_beat.dm
index 01188137915fbf..e64002ab528f7c 100644
--- a/code/datums/diseases/rhumba_beat.dm
+++ b/code/datums/diseases/rhumba_beat.dm
@@ -18,7 +18,7 @@
switch(stage)
if(2)
if(SPT_PROB(26, seconds_per_tick))
- affected_mob.adjustFireLoss(5, FALSE)
+ affected_mob.adjustFireLoss(5)
if(SPT_PROB(0.5, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel strange..."))
if(3)
diff --git a/code/datums/diseases/tuberculosis.dm b/code/datums/diseases/tuberculosis.dm
index dd75ea7cc62c7b..de87cab6f3f673 100644
--- a/code/datums/diseases/tuberculosis.dm
+++ b/code/datums/diseases/tuberculosis.dm
@@ -28,23 +28,27 @@
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel a cold sweat form."))
if(4)
+ var/need_mob_update = FALSE
if(SPT_PROB(1, seconds_per_tick))
to_chat(affected_mob, span_userdanger("You see four of everything!"))
affected_mob.set_dizzy_if_lower(10 SECONDS)
if(SPT_PROB(1, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel a sharp pain from your lower chest!"))
- affected_mob.adjustOxyLoss(5, FALSE)
+ need_mob_update += affected_mob.adjustOxyLoss(5, updating_health = FALSE)
affected_mob.emote("gasp")
if(SPT_PROB(5, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel air escape from your lungs painfully."))
- affected_mob.adjustOxyLoss(25, FALSE)
+ need_mob_update += affected_mob.adjustOxyLoss(25, updating_health = FALSE)
affected_mob.emote("gasp")
+ if(need_mob_update)
+ affected_mob.updatehealth()
if(5)
+ var/need_mob_update = FALSE
if(SPT_PROB(1, seconds_per_tick))
to_chat(affected_mob, span_userdanger("[pick("You feel your heart slowing...", "You relax and slow your heartbeat.")]"))
- affected_mob.adjustStaminaLoss(70, FALSE)
+ need_mob_update += affected_mob.adjustStaminaLoss(70, updating_stamina = FALSE)
if(SPT_PROB(5, seconds_per_tick))
- affected_mob.adjustStaminaLoss(100, FALSE)
+ need_mob_update += affected_mob.adjustStaminaLoss(100, updating_stamina = FALSE)
affected_mob.visible_message(span_warning("[affected_mob] faints!"), span_userdanger("You surrender yourself and feel at peace..."))
affected_mob.AdjustSleeping(100)
if(SPT_PROB(1, seconds_per_tick))
@@ -59,3 +63,5 @@
if(SPT_PROB(7.5, seconds_per_tick))
to_chat(affected_mob, span_danger("[pick("You feel uncomfortably hot...", "You feel like unzipping your jumpsuit...", "You feel like taking off some clothes...")]"))
affected_mob.adjust_bodytemperature(40)
+ if(need_mob_update)
+ affected_mob.updatehealth()
diff --git a/code/datums/dna.dm b/code/datums/dna.dm
index 5034a863f5619c..785b70eb4759ab 100644
--- a/code/datums/dna.dm
+++ b/code/datums/dna.dm
@@ -927,7 +927,7 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
switch(rand(0,6))
if(0)
investigate_log("has been gibbed by DNA instability.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
if(1)
investigate_log("has been dusted by DNA instability.", INVESTIGATE_DEATHS)
dust()
@@ -943,7 +943,7 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block())
BP.dismember()
else
investigate_log("has been gibbed by DNA instability.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
else
set_species(/datum/species/dullahan)
if(4)
diff --git a/code/datums/elements/ai_flee_while_injured.dm b/code/datums/elements/ai_flee_while_injured.dm
index fc1a2e33281684..0bbc08eb88bffd 100644
--- a/code/datums/elements/ai_flee_while_injured.dm
+++ b/code/datums/elements/ai_flee_while_injured.dm
@@ -32,14 +32,14 @@
return
var/current_health_percentage = source.health / source.maxHealth
- if (source.ai_controller.blackboard[BB_BASIC_MOB_FLEEING])
- if (current_health_percentage < stop_fleeing_at)
+ if (source.ai_controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ if (current_health_percentage > start_fleeing_below)
return
- source.ai_controller.CancelActions() // Stop fleeing go back to whatever you were doing
- source.ai_controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, FALSE)
+ source.ai_controller.CancelActions()
+ source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, FALSE)
return
- if (current_health_percentage > start_fleeing_below)
+ if (current_health_percentage < stop_fleeing_at)
return
- source.ai_controller.CancelActions()
- source.ai_controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, TRUE)
+ source.ai_controller.CancelActions() // Stop fleeing go back to whatever you were doing
+ source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE)
diff --git a/code/datums/elements/amputating_limbs.dm b/code/datums/elements/amputating_limbs.dm
index c2fe7c454a96e0..16a99e96f6c197 100644
--- a/code/datums/elements/amputating_limbs.dm
+++ b/code/datums/elements/amputating_limbs.dm
@@ -1,4 +1,4 @@
-/// This component will intercept bare-handed attacks by the owner on critically injured carbons and amputate random limbs instead
+/// This component will intercept bare-handed attacks by the owner on sufficiently injured carbons and amputate random limbs instead
/datum/element/amputating_limbs
element_flags = ELEMENT_BESPOKE
argument_hash_start_idx = 2
@@ -6,6 +6,10 @@
var/surgery_time
/// What is the means by which we describe the act of amputation?
var/surgery_verb
+ /// How awake must our target be?
+ var/minimum_stat
+ /// How likely are we to perform this action?
+ var/snip_chance
/// The types of limb we can remove
var/list/target_zones
@@ -13,6 +17,8 @@
datum/target,
surgery_time = 5 SECONDS,
surgery_verb = "prying",
+ minimum_stat = SOFT_CRIT,
+ snip_chance = 100,
list/target_zones = list(BODY_ZONE_L_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_ARM, BODY_ZONE_R_LEG),
)
. = ..()
@@ -23,6 +29,8 @@
src.surgery_time = surgery_time
src.surgery_verb = surgery_verb
+ 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))
@@ -33,11 +41,11 @@
/// 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)
SIGNAL_HANDLER
- if (!iscarbon(victim) || HAS_TRAIT(victim, TRAIT_NODISMEMBER))
+ if (!iscarbon(victim) || HAS_TRAIT(victim, TRAIT_NODISMEMBER) || !prob(snip_chance))
return
var/mob/living/carbon/limbed_victim = victim
- if (limbed_victim.stat == CONSCIOUS)
+ if (limbed_victim.stat < minimum_stat)
return
if (DOING_INTERACTION_WITH_TARGET(surgeon, victim))
@@ -60,7 +68,7 @@
/// Chop one off
/datum/element/amputating_limbs/proc/amputate(mob/living/surgeon, mob/living/carbon/victim, obj/item/bodypart/to_remove)
- surgeon.visible_message(span_warning("[surgeon] begins [surgery_verb] [to_remove] off of [victim]!"))
- if (!do_after(surgeon, delay = surgery_time, target = victim))
+ surgeon.visible_message(span_warning("[surgeon] [surgery_verb] [to_remove] off of [victim]!"))
+ if (surgery_time > 0 && !do_after(surgeon, delay = surgery_time, target = victim))
return
to_remove.dismember()
diff --git a/code/datums/elements/climbable.dm b/code/datums/elements/climbable.dm
index e953766571c2e5..b26990c59119bd 100644
--- a/code/datums/elements/climbable.dm
+++ b/code/datums/elements/climbable.dm
@@ -114,15 +114,13 @@
///Handles climbing onto the atom when you click-drag
/datum/element/climbable/proc/mousedrop_receive(atom/climbed_thing, atom/movable/dropped_atom, mob/user, params)
SIGNAL_HANDLER
- if(user == dropped_atom && isliving(dropped_atom))
- var/mob/living/living_target = dropped_atom
- if(isanimal(living_target))
- var/mob/living/simple_animal/animal = dropped_atom
- if (!animal.dextrous)
- return
- if(living_target.mobility_flags & MOBILITY_MOVE)
- INVOKE_ASYNC(src, PROC_REF(climb_structure), climbed_thing, living_target, params)
- return
+ if(user != dropped_atom || !isliving(dropped_atom))
+ return
+ if(!HAS_TRAIT(dropped_atom, TRAIT_FENCE_CLIMBER) && !HAS_TRAIT(dropped_atom, TRAIT_CAN_HOLD_ITEMS)) // If you can hold items you can probably climb a fence
+ return
+ var/mob/living/living_target = dropped_atom
+ if(living_target.mobility_flags & MOBILITY_MOVE)
+ INVOKE_ASYNC(src, PROC_REF(climb_structure), climbed_thing, living_target, params)
///Tries to climb onto the target if the forced movement of the mob allows it
/datum/element/climbable/proc/try_speedrun(datum/source, mob/bumpee)
diff --git a/code/datums/elements/death_linked.dm b/code/datums/elements/death_linked.dm
deleted file mode 100644
index c5d2c6c422c6ae..00000000000000
--- a/code/datums/elements/death_linked.dm
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * ## death linkage element!
- *
- * Bespoke element that when the owner dies, the linked mob dies too.
- */
-/datum/element/death_linked
- element_flags = ELEMENT_BESPOKE
- argument_hash_start_idx = 3
- ///The mob that also dies when the user dies
- var/datum/weakref/linked_mob
-
-/datum/element/death_linked/Attach(datum/target, mob/living/target_mob)
- . = ..()
- if(!isliving(target))
- return ELEMENT_INCOMPATIBLE
- if(!target_mob)
- stack_trace("[type] added to [target] with NO MOB.")
- src.linked_mob = WEAKREF(target_mob)
- RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
-
-/datum/element/death_linked/Detach(datum/target)
- . = ..()
- UnregisterSignal(target, COMSIG_LIVING_DEATH)
-
-///signal called by the stat of the target changing
-/datum/element/death_linked/proc/on_death(mob/living/target, gibbed)
- SIGNAL_HANDLER
- var/mob/living/linked_mob_resolved = linked_mob?.resolve()
- linked_mob_resolved?.death(TRUE)
diff --git a/code/datums/elements/dextrous.dm b/code/datums/elements/dextrous.dm
new file mode 100644
index 00000000000000..335c7c196d1cea
--- /dev/null
+++ b/code/datums/elements/dextrous.dm
@@ -0,0 +1,69 @@
+/**
+ * Sets up the attachee to have hands and manages things like dropping items on death and displaying them on examine
+ * Actual hand performance is managed by code on /living/ and not encapsulated here, we just enable it
+ */
+/datum/element/dextrous
+
+/datum/element/dextrous/Attach(datum/target, hands_count = 2, hud_type = /datum/hud/dextrous)
+ . = ..()
+ if (!isliving(target) || iscarbon(target))
+ return ELEMENT_INCOMPATIBLE // Incompatible with the carbon typepath because that already has its own hand handling and doesn't need hand holding
+
+ var/mob/living/mob_parent = target
+ set_available_hands(mob_parent, hands_count)
+ mob_parent.set_hud_used(new hud_type(target))
+ mob_parent.hud_used.show_hud(mob_parent.hud_used.hud_version)
+ ADD_TRAIT(target, TRAIT_CAN_HOLD_ITEMS, REF(src))
+ RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_hand_clicked))
+ RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+
+/datum/element/dextrous/Detach(datum/source)
+ . = ..()
+ var/mob/living/mob_parent = source
+ set_available_hands(mob_parent, initial(mob_parent.default_num_hands))
+ var/initial_hud = initial(mob_parent.hud_type)
+ mob_parent.set_hud_used(new initial_hud(source))
+ mob_parent.hud_used.show_hud(mob_parent.hud_used.hud_version)
+ REMOVE_TRAIT(source, TRAIT_CAN_HOLD_ITEMS, REF(src))
+ UnregisterSignal(source, list(
+ COMSIG_ATOM_EXAMINE,
+ COMSIG_LIVING_DEATH,
+ COMSIG_LIVING_UNARMED_ATTACK,
+ ))
+
+/// Set up how many hands we should have
+/datum/element/dextrous/proc/set_available_hands(mob/living/hand_owner, hands_count)
+ hand_owner.drop_all_held_items()
+ var/held_items = list()
+ for (var/i in 1 to hands_count)
+ held_items += null
+ hand_owner.held_items = held_items
+ hand_owner.set_num_hands(hands_count)
+ hand_owner.set_usable_hands(hands_count)
+
+/// Drop our shit when we die
+/datum/element/dextrous/proc/on_death(mob/living/died, gibbed)
+ SIGNAL_HANDLER
+ died.drop_all_held_items()
+
+/// Try picking up items
+/datum/element/dextrous/proc/on_hand_clicked(mob/living/hand_haver, atom/target, proximity, modifiers)
+ SIGNAL_HANDLER
+ if (!isitem(target) && hand_haver.combat_mode)
+ return
+ if (LAZYACCESS(modifiers, RIGHT_CLICK))
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand_secondary), hand_haver, modifiers)
+ else
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand), hand_haver, modifiers)
+ INVOKE_ASYNC(hand_haver, TYPE_PROC_REF(/mob, update_held_items))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Tell people what we are holding
+/datum/element/dextrous/proc/on_examined(mob/living/examined, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ for(var/obj/item/held_item in examined.held_items)
+ if(held_item.item_flags & (ABSTRACT|EXAMINE_SKIP|HAND_ITEM))
+ continue
+ examine_list += span_info("[examined.p_They()] [examined.p_have()] [held_item.get_examine_string(user)] in [examined.p_their()] \
+ [examined.get_held_index_name(examined.get_held_index_of_item(held_item))].")
diff --git a/code/datums/elements/food/microwavable.dm b/code/datums/elements/food/microwavable.dm
index 3ad3e272d345dd..8e7305545c0b0d 100644
--- a/code/datums/elements/food/microwavable.dm
+++ b/code/datums/elements/food/microwavable.dm
@@ -6,14 +6,18 @@
var/atom/default_typepath = /obj/item/food/badrecipe
/// Resulting atom typepath on a completed microwave.
var/atom/result_typepath
+ /// Reagents that should be added to the result
+ var/list/added_reagents
-/datum/element/microwavable/Attach(datum/target, microwave_type)
+/datum/element/microwavable/Attach(datum/target, microwave_type, list/reagents)
. = ..()
if(!isitem(target))
return ELEMENT_INCOMPATIBLE
result_typepath = microwave_type || default_typepath
+ added_reagents = reagents
+
RegisterSignal(target, COMSIG_ITEM_MICROWAVE_ACT, PROC_REF(on_microwaved))
if(!ispath(result_typepath, default_typepath))
@@ -41,13 +45,15 @@
var/efficiency = istype(used_microwave) ? used_microwave.efficiency : 1
SEND_SIGNAL(result, COMSIG_ITEM_MICROWAVE_COOKED, source, efficiency)
- if(IS_EDIBLE(result))
- if(microwaver && microwaver.mind)
- ADD_TRAIT(result, TRAIT_FOOD_CHEF_MADE, REF(microwaver.mind))
+ if(IS_EDIBLE(result) && (result_typepath != default_typepath))
+ BLACKBOX_LOG_FOOD_MADE(result.type)
result.reagents.clear_reagents()
source.reagents?.trans_to(result, source.reagents.total_volume)
+ if(added_reagents) // Add any new reagents that should be added
+ result.reagents.add_reagent_list(added_reagents)
- BLACKBOX_LOG_FOOD_MADE(result.type)
+ if(microwaver && microwaver.mind)
+ ADD_TRAIT(result, TRAIT_FOOD_CHEF_MADE, REF(microwaver.mind))
qdel(source)
diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm
index 6c55b595563820..3a0a9961843c45 100644
--- a/code/datums/elements/footstep.dm
+++ b/code/datums/elements/footstep.dm
@@ -40,6 +40,8 @@
footstep_sounds = GLOB.heavyfootstep
if(FOOTSTEP_MOB_SHOE)
footstep_sounds = GLOB.footstep
+ if(FOOTSTEP_MOB_RUST)
+ footstep_sounds = 'sound/effects/footstep/rustystep1.ogg'
if(FOOTSTEP_MOB_SLIME)
footstep_sounds = 'sound/effects/footstep/slime1.ogg'
if(FOOTSTEP_OBJ_MACHINE)
@@ -64,11 +66,12 @@
if(!istype(turf))
return
- if(!turf.footstep || source.buckled || source.throwing || source.movement_type & (VENTCRAWLING | FLYING) || HAS_TRAIT(source, TRAIT_IMMOBILIZED) || CHECK_MOVE_LOOP_FLAGS(source, MOVEMENT_LOOP_OUTSIDE_CONTROL))
+ if(source.buckled || source.throwing || source.movement_type & (VENTCRAWLING | FLYING) || HAS_TRAIT(source, TRAIT_IMMOBILIZED) || CHECK_MOVE_LOOP_FLAGS(source, MOVEMENT_LOOP_OUTSIDE_CONTROL))
return
if(source.body_position == LYING_DOWN) //play crawling sound if we're lying
- playsound(turf, 'sound/effects/footstep/crawl1.ogg', 15 * volume, falloff_distance = 1, vary = sound_vary)
+ if(turf.footstep)
+ playsound(turf, 'sound/effects/footstep/crawl1.ogg', 15 * volume, falloff_distance = 1, vary = sound_vary)
return
if(iscarbon(source))
@@ -92,6 +95,9 @@
. = list(FOOTSTEP_MOB_SHOE = turf.footstep, FOOTSTEP_MOB_BAREFOOT = turf.barefootstep, FOOTSTEP_MOB_HEAVY = turf.heavyfootstep, FOOTSTEP_MOB_CLAW = turf.clawfootstep, STEP_SOUND_PRIORITY = STEP_SOUND_NO_PRIORITY)
SEND_SIGNAL(turf, COMSIG_TURF_PREPARE_STEP_SOUND, .)
+ //The turf has no footstep sound (e.g. open space) and none of the objects on that turf (e.g. catwalks) overrides it
+ if(isnull(turf.footstep))
+ return null
return .
/datum/element/footstep/proc/play_simplestep(mob/living/source, atom/oldloc, direction, forced, list/old_locs, momentum_change)
diff --git a/code/datums/elements/sideway_movement.dm b/code/datums/elements/sideway_movement.dm
index dfe23187807f54..e6d94745e6cdb0 100644
--- a/code/datums/elements/sideway_movement.dm
+++ b/code/datums/elements/sideway_movement.dm
@@ -23,5 +23,5 @@
return
var/new_dir = old_dir
if(direction == old_dir || direction == REVERSE_DIR(old_dir))
- new_dir = angle2dir(dir2angle(direction) + pick(90, -90))
+ new_dir = turn(source.dir, pick(90, -90))
source.setDir(new_dir)
diff --git a/code/datums/elements/structure_repair.dm b/code/datums/elements/structure_repair.dm
new file mode 100644
index 00000000000000..d3b26eed815be9
--- /dev/null
+++ b/code/datums/elements/structure_repair.dm
@@ -0,0 +1,45 @@
+/// Intercepts attacks from mobs with this component to instead repair specified structures.
+/datum/element/structure_repair
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// How much to heal structures by
+ var/heal_amount
+ /// Typecache of types of structures to repair
+ var/list/structure_types_typecache
+
+/datum/element/structure_repair/Attach(
+ datum/target,
+ heal_amount = 5,
+ structure_types_typecache = typecacheof(list(/obj/structure)),
+)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.heal_amount = heal_amount
+ src.structure_types_typecache = structure_types_typecache
+ RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(try_repair))
+
+/datum/element/structure_repair/Detach(datum/source)
+ UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ 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)
+ SIGNAL_HANDLER
+
+ if (!is_type_in_typecache(target, structure_types_typecache))
+ return
+
+ if (target.get_integrity() >= target.max_integrity)
+ target.balloon_alert(fixer, "not damaged!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ target.repair_damage(heal_amount)
+ fixer.Beam(target, icon_state = "sendbeam", time = 0.4 SECONDS)
+ fixer.visible_message(
+ span_danger("[fixer] repairs [target]."),
+ span_danger("You repair [target], leaving it at [round(target.get_integrity() * 100 / target.max_integrity)]% stability."),
+ )
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/code/datums/elements/waddling.dm b/code/datums/elements/waddling.dm
index 9d9aede83edf44..825b0c4e4cb829 100644
--- a/code/datums/elements/waddling.dm
+++ b/code/datums/elements/waddling.dm
@@ -4,30 +4,21 @@
. = ..()
if(!ismovable(target))
return ELEMENT_INCOMPATIBLE
- if(isliving(target))
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(LivingWaddle))
- else
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Waddle))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Waddle))
/datum/element/waddling/Detach(datum/source)
. = ..()
UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
-
-/datum/element/waddling/proc/LivingWaddle(mob/living/target, atom/oldloc, direction, forced)
+/datum/element/waddling/proc/Waddle(atom/movable/moved, atom/oldloc, direction, forced)
SIGNAL_HANDLER
-
- if(forced || target.incapacitated() || target.body_position == LYING_DOWN || CHECK_MOVE_LOOP_FLAGS(target, MOVEMENT_LOOP_OUTSIDE_CONTROL))
- return
- waddling_animation(target)
-
-
-/datum/element/waddling/proc/Waddle(atom/movable/target, atom/oldloc, direction, forced)
- SIGNAL_HANDLER
-
- if(forced || CHECK_MOVE_LOOP_FLAGS(target, MOVEMENT_LOOP_OUTSIDE_CONTROL))
+ if(forced || CHECK_MOVE_LOOP_FLAGS(moved, MOVEMENT_LOOP_OUTSIDE_CONTROL))
return
- waddling_animation(target)
+ if(isliving(moved))
+ var/mob/living/living_moved = moved
+ if (living_moved.incapacitated() || living_moved.body_position == LYING_DOWN)
+ return
+ waddling_animation(moved)
/datum/element/waddling/proc/waddling_animation(atom/movable/target)
animate(target, pixel_z = 4, time = 0)
diff --git a/code/datums/elements/wall_walker.dm b/code/datums/elements/wall_walker.dm
new file mode 100644
index 00000000000000..92ac3318c12879
--- /dev/null
+++ b/code/datums/elements/wall_walker.dm
@@ -0,0 +1,49 @@
+/// This element will allow the mob it's attached to to pass through a specified type of wall, and drag anything through it.
+/datum/element/wall_walker
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// What kind of walls can we pass through?
+ var/wall_type
+
+/datum/element/wall_walker/Attach(
+ datum/target,
+ wall_type = /turf/closed/wall,
+)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.wall_type = wall_type
+ RegisterSignal(target, COMSIG_LIVING_WALL_BUMP, PROC_REF(try_pass_wall))
+ RegisterSignal(target, COMSIG_LIVING_WALL_EXITED, PROC_REF(exit_wall))
+
+/datum/element/wall_walker/Detach(datum/source)
+ UnregisterSignal(source, list(COMSIG_LIVING_WALL_BUMP, COMSIG_LIVING_WALL_EXITED))
+ return ..()
+
+/// If the wall is of the proper type, pass into it and keep hold on whatever you're pulling
+/datum/element/wall_walker/proc/try_pass_wall(mob/living/passing_mob, turf/closed/bumped_wall)
+ if(!istype(bumped_wall, wall_type))
+ return
+
+ var/atom/movable/stored_pulling = passing_mob.pulling
+ if(stored_pulling) //force whatever you're pulling to come with you
+ stored_pulling.setDir(get_dir(stored_pulling.loc, passing_mob.loc))
+ stored_pulling.forceMove(passing_mob.loc)
+ passing_mob.forceMove(bumped_wall)
+
+ if(stored_pulling) //don't drop them because we went into a wall
+ passing_mob.start_pulling(stored_pulling, supress_message = TRUE)
+
+/// If the wall is of the proper type, pull whatever you're pulling into it
+/datum/element/wall_walker/proc/exit_wall(mob/living/passing_mob, turf/closed/exited_wall)
+ if(!istype(exited_wall, wall_type))
+ return
+
+ var/atom/movable/stored_pulling = passing_mob.pulling
+ if(isnull(stored_pulling))
+ return
+
+ stored_pulling.setDir(get_dir(stored_pulling.loc, passing_mob.loc))
+ stored_pulling.forceMove(exited_wall)
+ passing_mob.start_pulling(stored_pulling, supress_message = TRUE)
diff --git a/code/datums/elements/wheel.dm b/code/datums/elements/wheel.dm
new file mode 100644
index 00000000000000..2bb8977ca5cae9
--- /dev/null
+++ b/code/datums/elements/wheel.dm
@@ -0,0 +1,28 @@
+/// Element which spins you as you move
+/datum/element/wheel
+
+/datum/element/wheel/Attach(datum/target)
+ . = ..()
+ if(!ismovable(target))
+ return ELEMENT_INCOMPATIBLE
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+
+/datum/element/wheel/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
+
+/datum/element/wheel/proc/on_moved(atom/movable/moved, atom/oldloc, direction, forced)
+ SIGNAL_HANDLER
+ if(forced || CHECK_MOVE_LOOP_FLAGS(moved, MOVEMENT_LOOP_OUTSIDE_CONTROL))
+ return
+ if(isliving(moved))
+ var/mob/living/living_moved = moved
+ if (living_moved.incapacitated() || living_moved.body_position == LYING_DOWN)
+ return
+ var/rotation_degree = (360 / 3)
+ if(direction & SOUTHWEST)
+ rotation_degree *= -1
+
+ var/matrix/to_turn = matrix(moved.transform)
+ to_turn = turn(moved.transform, rotation_degree)
+ animate(moved, transform = to_turn, time = 0.1 SECONDS, flags = ANIMATION_PARALLEL)
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
index d247117f3a176e..a4c7c372525bf4 100644
--- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
@@ -34,3 +34,8 @@
name = "Garden Gnome"
icon_file = 'icons/mob/simple/garden_gnome.dmi'
json_config = 'code/datums/greyscale/json_configs/garden_gnome.json'
+
+/datum/greyscale_config/pony
+ name = "Pony"
+ icon_file = 'icons/mob/simple/animal.dmi'
+ json_config = 'code/datums/greyscale/json_configs/pony.json'
diff --git a/code/datums/greyscale/json_configs/pony.json b/code/datums/greyscale/json_configs/pony.json
new file mode 100644
index 00000000000000..a08437c7cb6647
--- /dev/null
+++ b/code/datums/greyscale/json_configs/pony.json
@@ -0,0 +1,30 @@
+{
+ "pony": [
+ {
+ "type": "icon_state",
+ "icon_state": "pony",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "pony_hair",
+ "blend_mode": "overlay",
+ "color_ids": [ 2 ]
+ }
+ ],
+ "pony_dead": [
+ {
+ "type": "icon_state",
+ "icon_state": "pony_dead",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "pony_hair_dead",
+ "blend_mode": "overlay",
+ "color_ids": [ 2 ]
+ }
+ ]
+}
diff --git a/code/datums/hud.dm b/code/datums/hud.dm
index 1d112acf3237ae..604dea3ff712e0 100644
--- a/code/datums/hud.dm
+++ b/code/datums/hud.dm
@@ -18,7 +18,8 @@ GLOBAL_LIST_INIT(huds, list(
DATA_HUD_SENTIENT_DISEASE = new/datum/atom_hud/sentient_disease(),
DATA_HUD_AI_DETECT = new/datum/atom_hud/ai_detector(),
DATA_HUD_FAN = new/datum/atom_hud/data/human/fan_hud(),
- DATA_HUD_PERMIT = new/datum/atom_hud/data/human/permit(), // SKYRAT EDIT
+ DATA_HUD_PERMIT = new/datum/atom_hud/data/human/permit(), // SKYRAT EDIT ADDITION
+ DATA_HUD_DNR = new/datum/atom_hud/data/human/dnr(), // SKYRAT EDIT ADDITION
))
/datum/atom_hud
diff --git a/code/datums/id_trim/jobs.dm b/code/datums/id_trim/jobs.dm
index 2a62d19452ddd2..6d50347aa3d78b 100644
--- a/code/datums/id_trim/jobs.dm
+++ b/code/datums/id_trim/jobs.dm
@@ -155,6 +155,30 @@
)
job = /datum/job/bartender
+/datum/id_trim/job/bitrunner
+ assignment = "Bitrunner"
+ trim_state = "trim_bitrunner"
+ department_color = COLOR_CARGO_BROWN
+ subdepartment_color = COLOR_CARGO_BROWN
+ sechud_icon_state = SECHUD_BITRUNNER
+ minimal_access = list(
+ ACCESS_BIT_DEN,
+ ACCESS_CARGO,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_MINING,
+ ACCESS_MINERAL_STOREROOM,
+ )
+ extra_access = list(
+ ACCESS_MINING,
+ ACCESS_MINING_STATION,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_QM,
+ )
+ job = /datum/job/bitrunner
+
/datum/id_trim/job/botanist
assignment = "Botanist"
trim_state = "trim_botanist"
@@ -215,6 +239,7 @@
ACCESS_SHIPPING,
)
extra_access = list(
+ ACCESS_BIT_DEN,
ACCESS_MINING,
ACCESS_MINING_STATION,
)
@@ -576,6 +601,7 @@
ACCESS_ALL_PERSONAL_LOCKERS,
ACCESS_ARMORY,
ACCESS_AUX_BASE,
+ ACCESS_BIT_DEN,
ACCESS_BRIG,
ACCESS_BRIG_ENTRANCE,
ACCESS_CARGO,
@@ -712,6 +738,7 @@
subdepartment_color = COLOR_MEDICAL_BLUE
sechud_icon_state = SECHUD_PARAMEDIC
minimal_access = list(
+ ACCESS_BIT_DEN,
ACCESS_CARGO,
ACCESS_CONSTRUCTION,
ACCESS_HYDROPONICS,
@@ -739,7 +766,7 @@
assignment = "Prisoner"
trim_state = "trim_warden"
department_color = COLOR_PRISONER_BLACK
- subdepartment_color = COLOR_PRISONER_ORANGE
+ subdepartment_color = COLOR_PRISONER_BLACK
sechud_icon_state = SECHUD_PRISONER
template_access = list(
ACCESS_CAPTAIN,
@@ -810,6 +837,7 @@
sechud_icon_state = SECHUD_QUARTERMASTER
minimal_access = list(
ACCESS_AUX_BASE,
+ ACCESS_BIT_DEN,
ACCESS_CARGO,
ACCESS_MAINT_TUNNELS,
ACCESS_MECH_MINING,
@@ -1008,6 +1036,7 @@
assignment = "Security Officer (Cargo)"
subdepartment_color = COLOR_CARGO_BROWN
department_access = list(
+ ACCESS_BIT_DEN,
ACCESS_CARGO,
ACCESS_MINING,
ACCESS_SHIPPING,
@@ -1076,6 +1105,7 @@
ACCESS_MINING_STATION,
)
extra_access = list(
+ ACCESS_BIT_DEN,
ACCESS_MAINT_TUNNELS,
)
template_access = list(
diff --git a/code/datums/id_trim/outfits.dm b/code/datums/id_trim/outfits.dm
index f62e451340f98c..2a06434ecdb869 100644
--- a/code/datums/id_trim/outfits.dm
+++ b/code/datums/id_trim/outfits.dm
@@ -55,3 +55,22 @@
subdepartment_color = COLOR_PRISONER_BLACK
access = list(ACCESS_HUNTER)
+
+/// Trim for player controlled avatars in the Virtual Domain.
+/datum/id_trim/bit_avatar
+ assignment = "Bit Avatar"
+ trim_state = "trim_bitavatar"
+ department_color = COLOR_BLACK
+ subdepartment_color = COLOR_GREEN
+
+/// Trim for cyber police in the Virtual Domain.
+/datum/id_trim/cyber_police
+ assignment = "Cyber Police"
+ trim_state = "trim_deathcommando"
+ department_color = COLOR_BLACK
+ subdepartment_color = COLOR_GREEN
+
+/datum/id_trim/cyber_police/New()
+ . = ..()
+
+ access |= SSid_access.get_region_access_list(list(REGION_ALL_GLOBAL))
diff --git a/code/datums/lazy_template.dm b/code/datums/lazy_template.dm
index 0b8b2999f69938..e3006e13056df5 100644
--- a/code/datums/lazy_template.dm
+++ b/code/datums/lazy_template.dm
@@ -7,8 +7,11 @@
/// If this is true each load will increment an index keyed to the type and it will load [map_name]_[index]
var/list/datum/turf_reservation/reservations = list()
var/uses_multiple_allocations = FALSE
+ /// Key to identify this template - used in caching
var/key
+ /// Directory of maps to prefix to the filename
var/map_dir = "_maps/templates/lazy_templates"
+ /// The filename (without extension) of the map to load
var/map_name
/datum/lazy_template/New()
@@ -61,12 +64,18 @@
if(!reservation)
CRASH("Failed to reserve a block for lazy template: '[key]'")
+ // lists kept for overall loading
var/list/loaded_atom_movables = list()
var/list/loaded_turfs = list()
var/list/loaded_areas = list()
+
+ var/list/obj/structure/cable/loaded_cables = list()
+ var/list/obj/machinery/atmospherics/loaded_atmospherics = list()
+
for(var/z_idx in parsed_template.parsed_bounds[MAP_MAXZ] to 1 step -1)
var/turf/bottom_left = reservation.bottom_left_turfs[z_idx]
var/turf/top_right = reservation.top_right_turfs[z_idx]
+
load_map(
file(load_path),
bottom_left.x,
@@ -78,12 +87,20 @@
for(var/turf/turf as anything in block(bottom_left, top_right))
loaded_turfs += turf
loaded_areas |= get_area(turf)
- for(var/thing in turf.get_all_contents())
- // atoms can actually be in the contents of two or more turfs based on its icon/bound size
- // see https://www.byond.com/docs/ref/index.html#/atom/var/contents
+
+ // atoms can actually be in the contents of two or more turfs based on its icon/bound size
+ // see https://www.byond.com/docs/ref/index.html#/atom/var/contents
+ for(var/thing in (turf.get_all_contents() - turf))
+ if(istype(thing, /obj/structure/cable))
+ loaded_cables += thing
+ else if(istype(thing, /obj/machinery/atmospherics))
+ loaded_atmospherics += thing
loaded_atom_movables |= thing
- SSatoms.InitializeAtoms(loaded_atom_movables + loaded_turfs + loaded_areas)
+ SSatoms.InitializeAtoms(loaded_areas + loaded_atom_movables + loaded_turfs)
+ SSmachines.setup_template_powernets(loaded_cables)
+ SSair.setup_template_machinery(loaded_atmospherics)
+
SEND_SIGNAL(src, COMSIG_LAZY_TEMPLATE_LOADED, loaded_atom_movables, loaded_turfs, loaded_areas)
reservations += reservation
return reservation
diff --git a/code/datums/looping_sounds/burning.dm b/code/datums/looping_sounds/burning.dm
new file mode 100644
index 00000000000000..191ae88db8924b
--- /dev/null
+++ b/code/datums/looping_sounds/burning.dm
@@ -0,0 +1,9 @@
+/// Soundloop for the fire (bonfires, fireplaces, etc.)
+/datum/looping_sound/burning
+ start_sound = 'sound/items/match_strike.ogg'
+ start_length = 3 SECONDS
+ mid_sounds = 'sound/effects/comfyfire.ogg'
+ mid_length = 5 SECONDS
+ volume = 50
+ vary = TRUE
+ extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE
diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
index 7d7437ccda6c80..b0fcd471db2fff 100644
--- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm
+++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
@@ -62,12 +62,12 @@
weighted_closed_turf_types = list(/turf/closed/mineral/random/snow/underground = 1)
weighted_mob_spawn_list = list(
SPAWN_MEGAFAUNA = 1,
+ /mob/living/basic/mining/ice_demon = 100,
/mob/living/basic/mining/ice_whelp = 60,
/mob/living/basic/mining/legion/snow = 100,
- /mob/living/simple_animal/hostile/asteroid/ice_demon = 100,
/obj/structure/spawner/ice_moon/demonic_portal = 6,
- /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6,
/obj/structure/spawner/ice_moon/demonic_portal/ice_whelp = 6,
+ /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6,
)
weighted_megafauna_spawn_list = list(/mob/living/simple_animal/hostile/megafauna/colossus = 1)
weighted_flora_spawn_list = list(
diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm
index c982f0e086c374..e5b959b340f509 100644
--- a/code/datums/martial/cqc.dm
+++ b/code/datums/martial/cqc.dm
@@ -80,9 +80,21 @@
return TRUE
/datum/martial_art/cqc/proc/Kick(mob/living/attacker, mob/living/defender)
- if(!can_use(attacker))
+ if(!can_use(attacker) || defender.stat != CONSCIOUS)
return FALSE
- if(!defender.stat || !defender.IsParalyzed())
+
+ if(defender.body_position == LYING_DOWN && !defender.IsUnconscious() && defender.getStaminaLoss() >= 100)
+ log_combat(attacker, defender, "knocked out (Head kick)(CQC)")
+ defender.visible_message(span_danger("[attacker] kicks [defender]'s head, knocking [defender.p_them()] out!"), \
+ span_userdanger("You're knocked unconscious by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), null, attacker)
+ to_chat(attacker, span_danger("You kick [defender]'s head, knocking [defender.p_them()] out!"))
+ playsound(get_turf(attacker), 'sound/weapons/genhit1.ogg', 50, TRUE, -1)
+
+ var/helmet_protection = defender.run_armor_check(BODY_ZONE_HEAD, MELEE)
+ defender.apply_effect(20 SECONDS, EFFECT_KNOCKDOWN, helmet_protection)
+ defender.apply_effect(10 SECONDS, EFFECT_UNCONSCIOUS, helmet_protection)
+ defender.adjustOrganLoss(ORGAN_SLOT_BRAIN, 15, 150)
+ else
defender.visible_message(span_danger("[attacker] kicks [defender] back!"), \
span_userdanger("You're kicked back by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), COMBAT_MESSAGE_RANGE, attacker)
to_chat(attacker, span_danger("You kick [defender] back!"))
@@ -90,17 +102,9 @@
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)
log_combat(attacker, defender, "kicked (CQC)")
- . = TRUE
- if(defender.IsParalyzed() && !defender.stat)
- log_combat(attacker, defender, "knocked out (Head kick)(CQC)")
- defender.visible_message(span_danger("[attacker] kicks [defender]'s head, knocking [defender.p_them()] out!"), \
- span_userdanger("You're knocked unconscious by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), null, attacker)
- to_chat(attacker, span_danger("You kick [defender]'s head, knocking [defender.p_them()] out!"))
- playsound(get_turf(attacker), 'sound/weapons/genhit1.ogg', 50, TRUE, -1)
- defender.SetSleeping(30 SECONDS)
- defender.adjustOrganLoss(ORGAN_SLOT_BRAIN, 15, 150)
- . = TRUE
+ . = TRUE
/datum/martial_art/cqc/proc/Pressure(mob/living/attacker, mob/living/defender)
if(!can_use(attacker))
@@ -166,6 +170,19 @@
/datum/martial_art/cqc/harm_act(mob/living/attacker, mob/living/defender)
if(!can_use(attacker))
return FALSE
+
+ if(attacker.resting && defender.stat != DEAD && defender.body_position == STANDING_UP)
+ defender.visible_message(span_danger("[attacker] leg sweeps [defender]!"), \
+ span_userdanger("Your legs are sweeped by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), null, attacker)
+ to_chat(attacker, span_danger("You leg sweep [defender]!"))
+ playsound(get_turf(attacker), 'sound/effects/hit_kick.ogg', 50, TRUE, -1)
+ attacker.do_attack_animation(defender)
+ defender.apply_damage(10, BRUTE)
+ defender.Knockdown(5 SECONDS)
+ log_combat(attacker, defender, "sweeped (CQC)")
+ reset_streak()
+ return TRUE
+
add_to_streak("H", defender)
if(check_streak(attacker, defender))
return TRUE
@@ -185,14 +202,7 @@
span_userdanger("You're [picked_hit_type]ed by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), COMBAT_MESSAGE_RANGE, attacker)
to_chat(attacker, span_danger("You [picked_hit_type] [defender]!"))
log_combat(attacker, defender, "[picked_hit_type]s (CQC)")
- if(attacker.resting && !defender.stat && !defender.IsParalyzed())
- defender.visible_message(span_danger("[attacker] leg sweeps [defender]!"), \
- span_userdanger("Your legs are sweeped by [attacker]!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), null, attacker)
- to_chat(attacker, span_danger("You leg sweep [defender]!"))
- playsound(get_turf(attacker), 'sound/effects/hit_kick.ogg', 50, TRUE, -1)
- defender.apply_damage(10, BRUTE)
- defender.Paralyze(6 SECONDS)
- log_combat(attacker, defender, "sweeped (CQC)")
+
return TRUE
/datum/martial_art/cqc/disarm_act(mob/living/attacker, mob/living/defender)
@@ -239,7 +249,7 @@
to_chat(usr, "You try to remember some of the basics of CQC.")
to_chat(usr, "[span_notice("Slam")]: Grab Punch. Slam opponent into the ground, knocking them down.")
- to_chat(usr, "[span_notice("CQC Kick")]: Punch Punch. Knocks opponent away. Knocks out stunned or knocked down opponents.")
+ to_chat(usr, "[span_notice("CQC Kick")]: Punch Punch. Knocks opponent away. Knocks out stunned opponents and does stamina damage.")
to_chat(usr, "[span_notice("Restrain")]: Grab Grab. Locks opponents into a restraining position, disarm to knock them out with a chokehold.")
to_chat(usr, "[span_notice("Pressure")]: Shove Grab. Decent stamina damage.")
to_chat(usr, "[span_notice("Consecutive CQC")]: Shove Shove Punch. Mainly offensive move, huge damage and decent stamina damage.")
diff --git a/code/datums/martial/plasma_fist.dm b/code/datums/martial/plasma_fist.dm
index 1bc353cc6982fb..f4c89177ac363f 100644
--- a/code/datums/martial/plasma_fist.dm
+++ b/code/datums/martial/plasma_fist.dm
@@ -68,7 +68,7 @@
log_combat(attacker, defender, "gibbed (Plasma Fist)")
var/turf/Dturf = get_turf(defender)
defender.investigate_log("has been gibbed by plasma fist.", INVESTIGATE_DEATHS)
- defender.gib()
+ defender.gib(DROP_ALL_REMAINS)
if(nobomb)
return
if(!hasclient)
diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm
index d91884d972e961..06d26f31ea3081 100644
--- a/code/datums/materials/_material.dm
+++ b/code/datums/materials/_material.dm
@@ -33,8 +33,16 @@ Simple datum which is instanced once per type and is used for every object of sa
var/strength_modifier = 1
///This is a modifier for integrity, and resembles the strength of the material
var/integrity_modifier = 1
+
///This is the amount of value per 1 unit of the material
var/value_per_unit = 0
+ ///This is the minimum value of the material, used in the stock market for any mat that isn't set to null
+ var/minimum_value_override = null
+ ///Is this material traded on the stock market?
+ var/tradable = FALSE
+ ///If this material is tradable, what is the base quantity of the material on the stock market?
+ var/tradable_base_quantity = 0
+
///Armor modifiers, multiplies an items normal armor vars by these amounts.
var/armor_modifiers = list(MELEE = 1, BULLET = 1, LASER = 1, ENERGY = 1, BOMB = 1, BIO = 1, FIRE = 1, ACID = 1)
///How beautiful is this material per unit.
diff --git a/code/datums/materials/basemats.dm b/code/datums/materials/basemats.dm
index fcef1b5d3d8a00..f79b9f7e4224f5 100644
--- a/code/datums/materials/basemats.dm
+++ b/code/datums/materials/basemats.dm
@@ -7,6 +7,9 @@
categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE)
sheet_type = /obj/item/stack/sheet/iron
value_per_unit = 5 / SHEET_MATERIAL_AMOUNT
+ minimum_value_override = 0
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_COMMON
/datum/material/iron/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item)
victim.apply_damage(10, BRUTE, BODY_ZONE_HEAD, wound_bonus = 5)
@@ -25,6 +28,9 @@
shard_type = /obj/item/shard
debris_type = /obj/effect/decal/cleanable/glass
value_per_unit = 5 / SHEET_MATERIAL_AMOUNT
+ minimum_value_override = 0
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_COMMON
beauty_modifier = 0.05
armor_modifiers = list(MELEE = 0.2, BULLET = 0.2, ENERGY = 1, BIO = 0.2, FIRE = 1, ACID = 0.2)
@@ -56,6 +62,8 @@ Unless you know what you're doing, only use the first three numbers. They're in
categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE)
sheet_type = /obj/item/stack/sheet/mineral/silver
value_per_unit = 50 / SHEET_MATERIAL_AMOUNT
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_UNCOMMON
beauty_modifier = 0.075
/datum/material/silver/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item)
@@ -72,6 +80,8 @@ Unless you know what you're doing, only use the first three numbers. They're in
categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE)
sheet_type = /obj/item/stack/sheet/mineral/gold
value_per_unit = 125 / SHEET_MATERIAL_AMOUNT
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_RARE
beauty_modifier = 0.15
armor_modifiers = list(MELEE = 1.1, BULLET = 1.1, LASER = 1.15, ENERGY = 1.15, BOMB = 1, BIO = 1, FIRE = 0.7, ACID = 1.1)
@@ -90,6 +100,8 @@ Unless you know what you're doing, only use the first three numbers. They're in
alpha = 132
starlight_color = COLOR_BLUE_LIGHT
value_per_unit = 500 / SHEET_MATERIAL_AMOUNT
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_EXOTIC
beauty_modifier = 0.3
armor_modifiers = list(MELEE = 1.3, BULLET = 1.3, LASER = 0.6, ENERGY = 1, BOMB = 1.2, BIO = 1, FIRE = 1, ACID = 1)
@@ -106,6 +118,8 @@ Unless you know what you're doing, only use the first three numbers. They're in
categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE)
sheet_type = /obj/item/stack/sheet/mineral/uranium
value_per_unit = 100 / SHEET_MATERIAL_AMOUNT
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_RARE
beauty_modifier = 0.3 //It shines so beautiful
armor_modifiers = list(MELEE = 1.5, BULLET = 1.4, LASER = 0.5, ENERGY = 0.5, FIRE = 1, ACID = 1)
@@ -173,6 +187,8 @@ Unless you know what you're doing, only use the first three numbers. They're in
beauty_modifier = 0.5
sheet_type = /obj/item/stack/sheet/bluespace_crystal
value_per_unit = 300 / SHEET_MATERIAL_AMOUNT
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_EXOTIC
/datum/material/bluespace/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item)
victim.reagents.add_reagent(/datum/reagent/bluespace, rand(5, 8))
@@ -216,6 +232,8 @@ Unless you know what you're doing, only use the first three numbers. They're in
categories = list(MAT_CATEGORY_ORE = TRUE, MAT_CATEGORY_RIGID = TRUE, MAT_CATEGORY_BASE_RECIPES = TRUE, MAT_CATEGORY_ITEM_MATERIAL=TRUE)
sheet_type = /obj/item/stack/sheet/mineral/titanium
value_per_unit = 125 / SHEET_MATERIAL_AMOUNT
+ tradable = TRUE
+ tradable_base_quantity = MATERIAL_QUANTITY_UNCOMMON
beauty_modifier = 0.05
armor_modifiers = list(MELEE = 1.35, BULLET = 1.3, LASER = 1.3, ENERGY = 1.25, BOMB = 1.25, BIO = 1, FIRE = 0.7, ACID = 1)
diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm
index 35d1a6683f38ae..2b3e250a3fbf28 100644
--- a/code/datums/memory/_memory.dm
+++ b/code/datums/memory/_memory.dm
@@ -246,6 +246,7 @@
var/static/list/something_pool = list(
/mob/living/basic/bat,
/mob/living/basic/bear,
+ /mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
@@ -255,6 +256,8 @@
/mob/living/basic/cow,
/mob/living/basic/cow/wisdom,
/mob/living/basic/crab,
+ /mob/living/basic/goat,
+ /mob/living/basic/gorilla,
/mob/living/basic/headslug,
/mob/living/basic/killer_tomato,
/mob/living/basic/lizard,
@@ -272,10 +275,7 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
- /mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
- /mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet/cat,
/mob/living/simple_animal/pet/cat/cak,
diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm
index fa9114a5284782..deb55e92ecb40d 100644
--- a/code/datums/mind/_mind.dm
+++ b/code/datums/mind/_mind.dm
@@ -569,16 +569,14 @@
/// Sets us to the passed job datum, then greets them to their new job.
/// Use this one for when you're assigning this mind to a new job for the first time,
/// or for when someone's recieving a job they'd really want to be greeted to.
-/datum/mind/proc/set_assigned_role_with_greeting(datum/job/new_role, client/incoming_client)
+/datum/mind/proc/set_assigned_role_with_greeting(datum/job/new_role, client/incoming_client, alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: /datum/mind/proc/set_assigned_role_with_greeting(datum/job/new_role, client/incoming_client)
. = set_assigned_role(new_role)
if(assigned_role != new_role)
return
- to_chat(incoming_client || src, span_infoplain("You are the [new_role.title]."))
-
- var/related_policy = get_policy(new_role.title)
- if(related_policy)
- to_chat(incoming_client || src, related_policy)
+ var/intro_message = new_role.get_spawn_message(alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: var/intro_message = new_role.get_spawn_message()
+ if(incoming_client && intro_message)
+ to_chat(incoming_client, intro_message)
/mob/proc/sync_mind()
mind_initialize() //updates the mind (or creates and initializes one if one doesn't exist)
diff --git a/code/datums/mind/initialization.dm b/code/datums/mind/initialization.dm
index 12a5dddb229cb7..eb622cc5af5494 100644
--- a/code/datums/mind/initialization.dm
+++ b/code/datums/mind/initialization.dm
@@ -11,6 +11,7 @@
mind.set_current(src)
// There's nowhere else to set this up, mind code makes me depressed
mind.antag_hud = add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/antagonist_hud, "combo_hud", mind)
+ SEND_SIGNAL(src, COMSIG_MOB_MIND_INITIALIZED, mind)
/mob/living/carbon/mind_initialize()
..()
diff --git a/code/datums/mind/skills.dm b/code/datums/mind/skills.dm
index 5847236435b934..474291d5ae0d4a 100644
--- a/code/datums/mind/skills.dm
+++ b/code/datums/mind/skills.dm
@@ -18,12 +18,10 @@
experience_multiplier += experience_multiplier_reasons[key]
known_skills[skill][SKILL_EXP] = max(0, known_skills[skill][SKILL_EXP] + amt*experience_multiplier) //Update exp. Prevent going below 0
known_skills[skill][SKILL_LVL] = update_skill_level(skill)//Check what the current skill level is based on that skill's exp
- if(silent)
- return
if(known_skills[skill][SKILL_LVL] > old_level)
- S.level_gained(src, known_skills[skill][SKILL_LVL], old_level)
+ S.level_gained(src, known_skills[skill][SKILL_LVL], old_level, silent)
else if(known_skills[skill][SKILL_LVL] < old_level)
- S.level_lost(src, known_skills[skill][SKILL_LVL], old_level)
+ S.level_lost(src, known_skills[skill][SKILL_LVL], old_level, silent)
///Set experience of a specific skill to a number
/datum/mind/proc/set_experience(skill, amt, silent = FALSE)
diff --git a/code/datums/mood_events/drug_events.dm b/code/datums/mood_events/drug_events.dm
index c734f2797b48cb..8ac323dda7a769 100644
--- a/code/datums/mood_events/drug_events.dm
+++ b/code/datums/mood_events/drug_events.dm
@@ -19,6 +19,10 @@
else
description = initial(description)
+/datum/mood_event/hang_over
+ mood_change = -4
+ description = "I have a killer hang over!"
+ timeout = 1 MINUTES
/datum/mood_event/smoked
description = "I have had a smoke recently."
diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index 53bcce6c6ff0e0..23a3364adc318c 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -316,6 +316,11 @@
description = "I need something to cover my head..."
mood_change = -3
+/datum/mood_event/bald_reminder
+ description = "I was reminded that I can't grow my hair back at all! This is awful!"
+ mood_change = -5
+ timeout = 4 MINUTES
+
/datum/mood_event/bad_touch
description = "I don't like when people touch me."
mood_change = -3
diff --git a/code/datums/mutations/body.dm b/code/datums/mutations/body.dm
index 094e650fe33f00..24aa41321f3690 100644
--- a/code/datums/mutations/body.dm
+++ b/code/datums/mutations/body.dm
@@ -266,9 +266,9 @@
name = "Anti-Glow"
desc = "Your skin seems to attract and absorb nearby light creating 'darkness' around you."
text_gain_indication = "The light around you seems to disappear."
- glow = -1.5
conflicts = list(/datum/mutation/human/glow)
locked = TRUE
+ glow_power = -1.5
/datum/mutation/human/glow/anti/get_glow_color()
return COLOR_BLACK
@@ -508,7 +508,7 @@
to_chat(borgo, span_userdanger("Your sensors are disabled by a shower of blood!"))
borgo.Paralyze(6 SECONDS)
owner.investigate_log("has been gibbed by the martyrdom mutation.", INVESTIGATE_DEATHS)
- owner.gib()
+ owner.gib(DROP_ALL_REMAINS)
/datum/mutation/human/headless
name = "H.A.R.S."
diff --git a/code/datums/quirks/positive_quirks/drunk_healing.dm b/code/datums/quirks/positive_quirks/drunk_healing.dm
index fbab2503b4e9c4..e1c4ba911255ef 100644
--- a/code/datums/quirks/positive_quirks/drunk_healing.dm
+++ b/code/datums/quirks/positive_quirks/drunk_healing.dm
@@ -10,13 +10,16 @@
mail_goodies = list(/obj/effect/spawner/random/food_or_drink/booze)
/datum/quirk/drunkhealing/process(seconds_per_tick)
+ var/need_mob_update = FALSE
switch(quirk_holder.get_drunk_amount())
if (6 to 40)
- quirk_holder.adjustBruteLoss(-0.1 * seconds_per_tick, FALSE, required_bodytype = BODYTYPE_ORGANIC)
- quirk_holder.adjustFireLoss(-0.05 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += quirk_holder.adjustBruteLoss(-0.1 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += quirk_holder.adjustFireLoss(-0.05 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
if (41 to 60)
- quirk_holder.adjustBruteLoss(-0.4 * seconds_per_tick, FALSE, required_bodytype = BODYTYPE_ORGANIC)
- quirk_holder.adjustFireLoss(-0.2 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += quirk_holder.adjustBruteLoss(-0.4 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += quirk_holder.adjustFireLoss(-0.2 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
if (61 to INFINITY)
- quirk_holder.adjustBruteLoss(-0.8 * seconds_per_tick, FALSE, required_bodytype = BODYTYPE_ORGANIC)
- quirk_holder.adjustFireLoss(-0.4 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += quirk_holder.adjustBruteLoss(-0.8 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += quirk_holder.adjustFireLoss(-0.4 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ if(need_mob_update)
+ quirk_holder.updatehealth()
diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm
index 968e6df544e828..b3bc5a4dc18dd8 100644
--- a/code/datums/ruins/lavaland.dm
+++ b/code/datums/ruins/lavaland.dm
@@ -53,7 +53,7 @@
Probably best to stay clear."
prefix = "_maps/RandomRuins/LavaRuins/skyrat/" // SKYRAT ADDITION
suffix = "lavaland_surface_ash_walker1_skyrat.dmm" // SKYRAT EDIT - ORIGINAL: lavaland_surface_ash_walker1.dmm
- always_place = TRUE //SKYRAT EDIT CHANGE
+ cost = 1000 //SKYRAT EDIT: Original: 20
allow_duplicates = FALSE
//SKYRAT EDIT REMOVAL BEGIN - MAPPING
/*
@@ -286,3 +286,19 @@
suffix = "lavaland_battle_site.dmm"
allow_duplicates = TRUE
cost = 3
+
+/datum/map_template/ruin/lavaland/watcher_grave
+ name = "Watchers' Grave"
+ id = "watcher-grave"
+ description = "A lonely cave where an orphaned child awaits a new parent."
+ suffix = "lavaland_surface_watcher_grave.dmm"
+ cost = 5
+ allow_duplicates = FALSE
+
+/datum/map_template/ruin/lavaland/mook_village
+ name = "Mook Village"
+ id = "mook_village"
+ description = "A village hosting a community of friendly mooks!"
+ suffix = "lavaland_surface_mookvillage.dmm"
+ allow_duplicates = FALSE
+ cost = 5
diff --git a/code/datums/skills/_skill.dm b/code/datums/skills/_skill.dm
index ec52ebd9e96688..b8438c67927ca8 100644
--- a/code/datums/skills/_skill.dm
+++ b/code/datums/skills/_skill.dm
@@ -27,8 +27,7 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill))
span_nicegreen("I'm getting a little better at [name]!"),
span_nicegreen("I'm getting much better at [name]!"),
span_nicegreen("I feel like I've become quite proficient at [name]!"),
- "After lots of practice, I've begun to truly understand the intricacies \
- and surprising depth behind [name]. I now consider myself a master [title].",
+ span_nicegreen("After lots of practice, I've begun to truly understand the intricacies and surprising depth behind [name]. I now consider myself a master [title]."),
span_nicegreen("Through incredible determination and effort, I've reached the peak of my [name] abiltities. I'm finally able to consider myself a legendary [title]!") )
levelDownMessages = list(span_nicegreen("I have somehow completely lost all understanding of [name]. Please tell an admin if you see this."),
span_nicegreen("I'm starting to forget what [name] really even is. I need more practice..."),
@@ -46,13 +45,18 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill))
* * mind - The mind that you'll want to send messages
* * new_level - The newly gained level. Can check the actual level to give different messages at different levels, see defines in skills.dm
* * old_level - Similar to the above, but the level you had before levelling up.
+ * * silent - Silences the announcement if TRUE
*/
-/datum/skill/proc/level_gained(datum/mind/mind, new_level, old_level)//just for announcements (doesn't go off if the xp gain is silent)
+/datum/skill/proc/level_gained(datum/mind/mind, new_level, old_level, silent)
+ if(silent)
+ return
to_chat(mind.current, levelUpMessages[new_level]) //new_level will be a value from 1 to 6, so we get appropriate message from the 6-element levelUpMessages list
/**
* level_lost: See level_gained, same idea but fires on skill level-down
*/
-/datum/skill/proc/level_lost(datum/mind/mind, new_level, old_level)
+/datum/skill/proc/level_lost(datum/mind/mind, new_level, old_level, silent)
+ if(silent)
+ return
to_chat(mind.current, levelDownMessages[old_level]) //old_level will be a value from 1 to 6, so we get appropriate message from the 6-element levelUpMessages list
/**
diff --git a/code/datums/skills/fishing.dm b/code/datums/skills/fishing.dm
index d5ecff3c8f5769..ddf90e1a0a3ac5 100644
--- a/code/datums/skills/fishing.dm
+++ b/code/datums/skills/fishing.dm
@@ -8,3 +8,17 @@
desc = "How empty and alone you are on this barren Earth."
modifiers = list(SKILL_VALUE_MODIFIER = list(1, 1, 0, -1, -2, -4, -6))
skill_item_path = /obj/item/clothing/head/soft/fishing_hat
+
+/datum/skill/fishing/New()
+ . = ..()
+ levelUpMessages[SKILL_LEVEL_MASTER] = span_nicegreen("After lots of practice, I've begun to truly understand the surprising depth behind [name]. As a master [title], I can take an easier guess of what I'm trying to catch now.")
+
+/datum/skill/fishing/level_gained(datum/mind/mind, new_level, old_level, silent)
+ . = ..()
+ if(new_level >= SKILL_LEVEL_MASTER && old_level < SKILL_LEVEL_MASTER)
+ ADD_TRAIT(mind, TRAIT_REVEAL_FISH, SKILL_TRAIT)
+
+/datum/skill/fishing/level_lost(datum/mind/mind, new_level, old_level, silent)
+ . = ..()
+ if(old_level >= SKILL_LEVEL_MASTER && new_level < SKILL_LEVEL_MASTER)
+ REMOVE_TRAIT(mind, TRAIT_REVEAL_FISH, SKILL_TRAIT)
diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm
index f57f6c2e9dd64d..8402c70236baae 100644
--- a/code/datums/station_traits/neutral_traits.dm
+++ b/code/datums/station_traits/neutral_traits.dm
@@ -148,7 +148,7 @@
show_in_report = FALSE // Selective attention test. Did you spot the gorilla?
/// The gorilla we created, we only hold this ref until the round starts.
- var/mob/living/simple_animal/hostile/gorilla/cargo_domestic/cargorilla
+ var/mob/living/basic/gorilla/cargorilla/cargorilla
/datum/station_trait/cargorilla/New()
. = ..()
@@ -158,8 +158,8 @@
/datum/station_trait/cargorilla/proc/replace_cargo(datum/source)
SIGNAL_HANDLER
- var/mob/living/simple_animal/sloth/cargo_sloth = GLOB.cargo_sloth
- if(!cargo_sloth)
+ var/mob/living/basic/sloth/cargo_sloth = GLOB.cargo_sloth
+ if(isnull(cargo_sloth))
return
cargorilla = new(cargo_sloth.loc)
@@ -189,7 +189,7 @@
cargorilla = null
/// Get us a ghost for the gorilla.
-/datum/station_trait/cargorilla/proc/get_ghost_for_gorilla(mob/living/simple_animal/hostile/gorilla/cargo_domestic/gorilla)
+/datum/station_trait/cargorilla/proc/get_ghost_for_gorilla(mob/living/basic/gorilla/cargorilla/gorilla)
if(QDELETED(gorilla))
return
diff --git a/code/datums/station_traits/positive_traits.dm b/code/datums/station_traits/positive_traits.dm
index 165cc4c2ad4834..7c36ed24db9e1a 100644
--- a/code/datums/station_traits/positive_traits.dm
+++ b/code/datums/station_traits/positive_traits.dm
@@ -283,6 +283,7 @@
/datum/job/assistant = /obj/item/organ/internal/heart/cybernetic, //real cardiac
/datum/job/atmospheric_technician = /obj/item/organ/internal/cyberimp/mouth/breathing_tube,
/datum/job/bartender = /obj/item/organ/internal/liver/cybernetic/tier3,
+ /datum/job/bitrunner = /obj/item/organ/internal/eyes/robotic/thermals,
/datum/job/botanist = /obj/item/organ/internal/cyberimp/chest/nutriment,
/datum/job/captain = /obj/item/organ/internal/heart/cybernetic/tier3,
/datum/job/cargo_technician = /obj/item/organ/internal/stomach/cybernetic/tier2,
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index cb0b55be059afb..fd9e3d9aee5695 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -44,13 +44,15 @@
owner.apply_status_effect(/datum/status_effect/his_wrath)
qdel(src)
return
- var/grace_heal = bloodlust * 0.05
- owner.adjustBruteLoss(-grace_heal)
- owner.adjustFireLoss(-grace_heal)
- owner.adjustToxLoss(-grace_heal, TRUE, TRUE)
- owner.adjustOxyLoss(-(grace_heal * 2))
- owner.adjustCloneLoss(-grace_heal)
-
+ var/grace_heal = bloodlust * 0.02
+ var/need_mob_update = FALSE
+ need_mob_update += owner.adjustBruteLoss(-grace_heal * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += owner.adjustFireLoss(-grace_heal * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += owner.adjustToxLoss(-grace_heal * seconds_between_ticks, forced = TRUE)
+ need_mob_update += owner.adjustOxyLoss(-(grace_heal * 2) * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += owner.adjustCloneLoss(-grace_heal * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ if(need_mob_update)
+ owner.updatehealth()
/datum/status_effect/wish_granters_gift //Fully revives after ten seconds.
id = "wish_granters_gift"
@@ -61,7 +63,6 @@
to_chat(owner, span_notice("Death is not your end! The Wish Granter's energy suffuses you, and you begin to rise..."))
return ..()
-
/datum/status_effect/wish_granters_gift/on_remove()
owner.revive(ADMIN_HEAL_ALL)
owner.visible_message(span_warning("[owner] appears to wake from the dead, having healed all wounds!"), span_notice("You have regenerated."))
@@ -140,9 +141,12 @@
if(owner.on_fire)
return
- owner.adjustBruteLoss(-10, FALSE)
- owner.adjustFireLoss(-5, FALSE)
- owner.adjustOxyLoss(-10)
+ var/need_mob_update = FALSE
+ need_mob_update += owner.adjustBruteLoss(-4 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustFireLoss(-2 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustOxyLoss(-4 * seconds_between_ticks, updating_health = FALSE)
+ if(need_mob_update)
+ owner.updatehealth()
/datum/status_effect/fleshmend/proc/on_ignited(datum/source)
SIGNAL_HANDLER
@@ -254,13 +258,16 @@
//Because a servant of medicines stops at nothing to help others, lets keep them on their toes and give them an additional boost.
if(itemUser.health < itemUser.maxHealth)
new /obj/effect/temp_visual/heal(get_turf(itemUser), "#375637")
- itemUser.adjustBruteLoss(-1.5)
- itemUser.adjustFireLoss(-1.5)
- itemUser.adjustToxLoss(-1.5, forced = TRUE) //Because Slime People are people too
- itemUser.adjustOxyLoss(-1.5, forced = TRUE)
- itemUser.adjustStaminaLoss(-1.5)
- itemUser.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1.5)
- itemUser.adjustCloneLoss(-0.5) //Becasue apparently clone damage is the bastion of all health
+ var/need_mob_update = FALSE
+ need_mob_update += itemUser.adjustBruteLoss(-0.6 * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += itemUser.adjustFireLoss(-0.6 * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += itemUser.adjustToxLoss(-0.6 * seconds_between_ticks, updating_health = FALSE, forced = TRUE) //Because Slime People are people too
+ need_mob_update += itemUser.adjustOxyLoss(-0.6 * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += itemUser.adjustStaminaLoss(-0.6 * seconds_between_ticks, updating_stamina = FALSE, forced = TRUE)
+ need_mob_update += itemUser.adjustOrganLoss(ORGAN_SLOT_BRAIN, -0.6 * seconds_between_ticks)
+ need_mob_update += itemUser.adjustCloneLoss(-0.2 * seconds_between_ticks, updating_health = FALSE, forced = TRUE) //Because apparently clone damage is the bastion of all health
+ if(need_mob_update)
+ itemUser.updatehealth()
/datum/status_effect/hippocratic_oath/proc/consume_owner()
owner.visible_message(span_notice("[owner]'s soul is absorbed into the rod, relieving the previous snake of its duty."))
@@ -274,7 +281,6 @@
owner.investigate_log("has been consumed by the Rod of Asclepius.", INVESTIGATE_DEATHS)
qdel(owner)
-
/datum/status_effect/good_music
id = "Good Music"
alert_type = null
@@ -444,10 +450,13 @@
qdel(src)
return
- owner.adjustBruteLoss(-2 * seconds_between_ticks, updating_health = FALSE)
- owner.adjustFireLoss(-2 * seconds_between_ticks, updating_health = FALSE)
- owner.adjustOxyLoss(-4 * seconds_between_ticks, updating_health = FALSE)
- owner.adjustStaminaLoss(-4 * seconds_between_ticks, updating_stamina = FALSE)
+ var/need_mob_update = FALSE
+ need_mob_update += owner.adjustBruteLoss(-2 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustFireLoss(-2 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustOxyLoss(-4 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustStaminaLoss(-4 * seconds_between_ticks, updating_stamina = FALSE)
+ if(need_mob_update)
+ owner.updatehealth()
owner.adjust_bodytemperature(BODYTEMP_NORMAL, 0, BODYTEMP_NORMAL) //Won't save you from the void of space, but it will stop you from freezing or suffocating in low pressure
diff --git a/code/datums/status_effects/debuffs/cursed.dm b/code/datums/status_effects/debuffs/cursed.dm
index 285fb86348e0d0..8d331bbe90add7 100644
--- a/code/datums/status_effects/debuffs/cursed.dm
+++ b/code/datums/status_effects/debuffs/cursed.dm
@@ -102,7 +102,7 @@
to_chat(owner, span_userdanger("Why couldn't I get one more try?!"))
owner.investigate_log("has been gibbed by the cursed status effect after accumulating [curse_count] curses.", INVESTIGATE_DEATHS)
- owner.gib()
+ owner.gib(DROP_ALL_REMAINS)
qdel(src)
return
diff --git a/code/datums/status_effects/debuffs/debuffs.dm b/code/datums/status_effects/debuffs/debuffs.dm
index 8c8f910fd572e6..f4c78b69101f1c 100644
--- a/code/datums/status_effects/debuffs/debuffs.dm
+++ b/code/datums/status_effects/debuffs/debuffs.dm
@@ -206,6 +206,7 @@
if(locate(/obj/item/pillow) in owner.loc)
healing += 0.1
+ var/need_mob_update = FALSE
if(healing > 0)
if(iscarbon(owner))
var/mob/living/carbon/carbon_owner = owner
@@ -219,10 +220,12 @@
target_organ.apply_organ_damage(-healing_bonus * target_organ.maxHealth)
if(health_ratio > 0.8) // only heals minor physical damage
- owner.adjustBruteLoss(-1 * healing, required_bodytype = BODYTYPE_ORGANIC)
- owner.adjustFireLoss(-1 * healing, required_bodytype = BODYTYPE_ORGANIC)
- owner.adjustToxLoss(-1 * healing * 0.5, TRUE, TRUE, required_biotype = MOB_ORGANIC)
- owner.adjustStaminaLoss(min(-1 * healing, -1 * HEALING_SLEEP_DEFAULT))
+ 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)
+ if(need_mob_update)
+ owner.updatehealth()
// Drunkenness gets reduced by 0.3% per tick (6% per 2 seconds)
owner.set_drunk_effect(owner.get_drunk_amount() * 0.997)
@@ -311,9 +314,12 @@
for(var/obj/item/his_grace/HG in owner.held_items)
qdel(src)
return
- owner.adjustBruteLoss(0.1)
- owner.adjustFireLoss(0.1)
- owner.adjustToxLoss(0.2, TRUE, TRUE)
+ var/need_mob_update
+ need_mob_update = owner.adjustBruteLoss(0.04 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustFireLoss(0.04 * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustToxLoss(0.08 * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ if(need_mob_update)
+ owner.updatehealth()
/datum/status_effect/cultghost //is a cult ghost and can't use manifest runes
id = "cult_ghost"
@@ -688,7 +694,7 @@
/datum/status_effect/dna_melt/on_remove()
if(!ishuman(owner))
- owner.gib() //fuck you in particular
+ owner.gib(DROP_ALL_REMAINS) //fuck you in particular
return
var/mob/living/carbon/human/H = owner
INVOKE_ASYNC(H, TYPE_PROC_REF(/mob/living/carbon/human, something_horrible), kill_either_way)
@@ -821,7 +827,8 @@
/datum/status_effect/ants/tick(seconds_between_ticks)
var/mob/living/carbon/human/victim = owner
- victim.adjustBruteLoss(max(0.1, round((ants_remaining * 0.004),0.1))) //Scales with # of ants (lowers with time). Roughly 10 brute over 50 seconds.
+ var/need_mob_update
+ need_mob_update = victim.adjustBruteLoss(max(0.1, round((ants_remaining * 0.0016) * seconds_between_ticks,0.1)), updating_health = FALSE) //Scales with # of ants (lowers with time). Roughly 10 brute over 50 seconds.
if(victim.stat <= SOFT_CRIT) //Makes sure people don't scratch at themselves while they're in a critical condition
if(prob(15))
switch(rand(1,2))
@@ -834,20 +841,22 @@
if (1 to 8) //16% Chance
var/obj/item/bodypart/head/hed = victim.get_bodypart(BODY_ZONE_HEAD)
to_chat(victim, span_danger("You scratch at the ants on your scalp!."))
- hed.receive_damage(1,0)
+ need_mob_update += hed.receive_damage(brute = 0.4 * seconds_between_ticks, burn = 0, updating_health = FALSE)
if (9 to 29) //40% chance
var/obj/item/bodypart/arm = victim.get_bodypart(pick(BODY_ZONE_L_ARM,BODY_ZONE_R_ARM))
to_chat(victim, span_danger("You scratch at the ants on your arms!"))
- arm.receive_damage(3,0)
+ need_mob_update += arm.receive_damage(brute = 1.2 * seconds_between_ticks, burn = 0, updating_health = FALSE)
if (30 to 49) //38% chance
var/obj/item/bodypart/leg = victim.get_bodypart(pick(BODY_ZONE_L_LEG,BODY_ZONE_R_LEG))
to_chat(victim, span_danger("You scratch at the ants on your leg!"))
- leg.receive_damage(3,0)
+ need_mob_update += leg.receive_damage(brute = 1.2 * seconds_between_ticks, burn = 0, updating_health = FALSE)
if(50) // 2% chance
to_chat(victim, span_danger("You rub some ants away from your eyes!"))
victim.set_eye_blur_if_lower(6 SECONDS)
ants_remaining -= 5 // To balance out the blindness, it'll be a little shorter.
ants_remaining--
+ if(need_mob_update)
+ victim.updatehealth()
if(ants_remaining <= 0 || victim.stat >= HARD_CRIT)
victim.remove_status_effect(/datum/status_effect/ants) //If this person has no more ants on them or are dead, they are no longer affected.
@@ -961,5 +970,71 @@
/datum/movespeed_modifier/careful_driving
multiplicative_slowdown = 3
+/datum/status_effect/midas_blight
+ id = "midas_blight"
+ alert_type = /atom/movable/screen/alert/status_effect/midas_blight
+ status_type = STATUS_EFFECT_REPLACE
+ tick_interval = 0.2 SECONDS
+ remove_on_fullheal = TRUE
+
+ /// The visual overlay state, helps tell both you and enemies how much gold is in your system
+ var/midas_state = "midas_1"
+ /// How fast the gold in a person's system scales.
+ var/goldscale = 30 // x2.8 - Gives ~ 15u for 1 second
+
+/datum/status_effect/midas_blight/on_creation(mob/living/new_owner, duration = 1)
+ // Duration is already input in SECONDS
+ src.duration = duration
+ RegisterSignal(new_owner, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays))
+ return ..()
+
+/atom/movable/screen/alert/status_effect/midas_blight
+ name = "Midas Blight"
+ desc = "Your blood is being turned to gold, slowing your movements!"
+ icon_state = "midas_blight"
+
+/datum/status_effect/midas_blight/tick(seconds_between_ticks)
+ var/mob/living/carbon/human/victim = owner
+ // We're transmuting blood, time to lose some.
+ if(victim.blood_volume > BLOOD_VOLUME_SURVIVE + 50 && !HAS_TRAIT(victim, TRAIT_NOBLOOD))
+ victim.blood_volume -= 5 * seconds_between_ticks
+ // This has been hell to try and balance so that you'll actually get anything out of it
+ victim.reagents.add_reagent(/datum/reagent/gold/cursed, amount = seconds_between_ticks * goldscale, no_react = TRUE)
+ var/current_gold_amount = victim.reagents.get_reagent_amount(/datum/reagent/gold, include_subtypes = TRUE)
+ switch(current_gold_amount)
+ if(-INFINITY to 50)
+ victim.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/midas_blight/soft, update = TRUE)
+ victim.add_actionspeed_modifier(/datum/actionspeed_modifier/status_effect/midas_blight/soft, update = TRUE)
+ midas_state = "midas_1"
+ if(50 to 100)
+ victim.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/midas_blight/medium, update = TRUE)
+ victim.add_actionspeed_modifier(/datum/actionspeed_modifier/status_effect/midas_blight/medium, update = TRUE)
+ midas_state = "midas_2"
+ if(100 to 200)
+ victim.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/midas_blight/hard, update = TRUE)
+ victim.add_actionspeed_modifier(/datum/actionspeed_modifier/status_effect/midas_blight/hard, update = TRUE)
+ midas_state = "midas_3"
+ if(200 to INFINITY)
+ victim.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/midas_blight/gold, update = TRUE)
+ victim.add_actionspeed_modifier(/datum/actionspeed_modifier/status_effect/midas_blight/gold, update = TRUE)
+ midas_state = "midas_4"
+ victim.update_icon()
+ if(victim.stat == DEAD)
+ qdel(src) // Dead people stop being turned to gold. Don't want people sitting on dead bodies.
+
+/datum/status_effect/midas_blight/proc/on_update_overlays(atom/parent_atom, list/overlays)
+ SIGNAL_HANDLER
+
+ if(midas_state)
+ var/mutable_appearance/midas_overlay = mutable_appearance('icons/mob/effects/debuff_overlays.dmi', midas_state)
+ midas_overlay.blend_mode = BLEND_MULTIPLY
+ overlays += midas_overlay
+
+/datum/status_effect/midas_blight/on_remove()
+ owner.remove_movespeed_modifier(MOVESPEED_ID_MIDAS_BLIGHT, update = TRUE)
+ owner.remove_actionspeed_modifier(ACTIONSPEED_ID_MIDAS_BLIGHT, update = TRUE)
+ UnregisterSignal(owner, COMSIG_ATOM_UPDATE_OVERLAYS)
+ owner.update_icon()
+
#undef HEALING_SLEEP_DEFAULT
#undef HEALING_SLEEP_ORGAN_MULTIPLIER
diff --git a/code/datums/status_effects/debuffs/drunk.dm b/code/datums/status_effects/debuffs/drunk.dm
index 705fcc60eeb97b..cdac6b59225005 100644
--- a/code/datums/status_effects/debuffs/drunk.dm
+++ b/code/datums/status_effects/debuffs/drunk.dm
@@ -213,19 +213,28 @@
if(drunk_value >= 91)
owner.adjustToxLoss(1)
owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.4)
- if(owner.stat == CONSCIOUS && prob(20))
- // 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))
- to_chat(owner, span_warning("You're so tired... but you can't miss that shuttle..."))
-
- else
- to_chat(owner, span_warning("Just a quick nap..."))
- owner.Sleeping(90 SECONDS)
+ if(owner.stat == CONSCIOUS)
+ attempt_to_blackout()
// And finally, over 100 - let's be honest, you shouldn't be alive by now.
if(drunk_value >= 101)
owner.adjustToxLoss(2)
+/datum/status_effect/inebriated/drunk/proc/attempt_to_blackout()
+ /* SKYRAT EDIT REMOVAL - Blackout drunk begone
+ var/mob/living/carbon/drunkard = owner
+ 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
+ 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)
+ */ // SKYRAT EDIT REMOVAL END (also removed the else on the line after)
+ 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)
+
/// Status effect for being fully drunk (not tipsy).
/atom/movable/screen/alert/status_effect/drunk
name = "Drunk"
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
index 112e8bd861409e..6f31faaefaae6b 100644
--- a/code/datums/status_effects/debuffs/fire_stacks.dm
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -147,7 +147,10 @@
if(!on_fire)
return TRUE
- adjust_stacks(owner.fire_stack_decay_rate * seconds_between_ticks)
+ if(HAS_TRAIT(owner, TRAIT_HUSK))
+ adjust_stacks(-2 * seconds_between_ticks)
+ else
+ adjust_stacks(owner.fire_stack_decay_rate * seconds_between_ticks)
if(stacks <= 0)
qdel(src)
diff --git a/code/datums/status_effects/debuffs/static_vision.dm b/code/datums/status_effects/debuffs/static_vision.dm
new file mode 100644
index 00000000000000..7132c189b9d449
--- /dev/null
+++ b/code/datums/status_effects/debuffs/static_vision.dm
@@ -0,0 +1,29 @@
+/datum/status_effect/static_vision
+ id = "static_vision"
+ status_type = STATUS_EFFECT_REPLACE
+ alert_type = null
+
+/datum/status_effect/static_vision/on_creation(mob/living/new_owner, duration = 3 SECONDS)
+ src.duration = duration
+ return ..()
+
+/datum/status_effect/static_vision/on_apply()
+ RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(remove_static_vision))
+
+ owner.overlay_fullscreen(id, /atom/movable/screen/fullscreen/static_vision)
+ owner.sound_environment_override = SOUND_ENVIRONMENT_UNDERWATER
+
+ return TRUE
+
+/datum/status_effect/static_vision/on_remove()
+ UnregisterSignal(owner, COMSIG_LIVING_DEATH)
+
+ owner.clear_fullscreen(id)
+ if(owner.sound_environment_override == SOUND_ENVIRONMENT_UNDERWATER)
+ owner.sound_environment_override = SOUND_ENVIRONMENT_NONE
+
+/// Handles clearing on death
+/datum/status_effect/static_vision/proc/remove_static_vision(datum/source, admin_revive)
+ SIGNAL_HANDLER
+
+ qdel(src)
diff --git a/code/datums/status_effects/drug_effects.dm b/code/datums/status_effects/drug_effects.dm
index d01a92743b5890..1d37c8f0e43eba 100644
--- a/code/datums/status_effects/drug_effects.dm
+++ b/code/datums/status_effects/drug_effects.dm
@@ -70,7 +70,7 @@
/datum/status_effect/stoned/on_apply()
if(!ishuman(owner))
- CRASH("[type] status effect added to non-human owner: [owner ? owner.type : "null owner"]")
+ return FALSE
var/mob/living/carbon/human/human_owner = owner
original_eye_color_left = human_owner.eye_color_left
original_eye_color_right = human_owner.eye_color_right
@@ -85,7 +85,7 @@
/datum/status_effect/stoned/on_remove()
if(!ishuman(owner))
- stack_trace("[type] status effect being removed from non-human owner: [owner ? owner.type : "null owner"]")
+ return
var/mob/living/carbon/human/human_owner = owner
human_owner.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/cannabis)
human_owner.eye_color_left = original_eye_color_left
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 7161946c9ff9f5..a2aa81ebb698a0 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -109,11 +109,14 @@
for(var/datum/action/cooldown/spell/spell in rewarded.actions)
spell.reset_spell_cooldown()
- rewarded.adjustBruteLoss(-25)
- rewarded.adjustFireLoss(-25)
- rewarded.adjustToxLoss(-25)
- rewarded.adjustOxyLoss(-25)
- rewarded.adjustCloneLoss(-25)
+ var/need_mob_update = FALSE
+ need_mob_update += rewarded.adjustBruteLoss(-25, updating_health = FALSE)
+ need_mob_update += rewarded.adjustFireLoss(-25, updating_health = FALSE)
+ need_mob_update += rewarded.adjustToxLoss(-25, updating_health = FALSE)
+ need_mob_update += rewarded.adjustOxyLoss(-25, updating_health = FALSE)
+ need_mob_update += rewarded.adjustCloneLoss(-25, updating_health = FALSE)
+ if(need_mob_update)
+ rewarded.updatehealth()
// heldup is for the person being aimed at
/datum/status_effect/grouped/heldup
diff --git a/code/datums/storage/subtypes/bag_of_holding.dm b/code/datums/storage/subtypes/bag_of_holding.dm
index 9176588b5fca7f..9e3a486cdcd4ae 100644
--- a/code/datums/storage/subtypes/bag_of_holding.dm
+++ b/code/datums/storage/subtypes/bag_of_holding.dm
@@ -32,6 +32,6 @@
user.log_message("detonated a bag of holding at [loc_name(loccheck)].", LOG_ATTACK, color="red")
user.investigate_log("has been gibbed by a bag of holding recursive insertion.", INVESTIGATE_DEATHS)
- user.gib(TRUE, TRUE, TRUE)
+ user.gib()
new/obj/boh_tear(loccheck)
qdel(resolve_parent)
diff --git a/code/datums/storage/subtypes/surgery_tray.dm b/code/datums/storage/subtypes/surgery_tray.dm
index 358865813188b0..42b369b4ce9221 100644
--- a/code/datums/storage/subtypes/surgery_tray.dm
+++ b/code/datums/storage/subtypes/surgery_tray.dm
@@ -6,6 +6,7 @@
/datum/storage/surgery_tray/New()
. = ..()
set_holdable(list(
+ /obj/item/autopsy_scanner,
/obj/item/blood_filter,
/obj/item/bonesetter,
/obj/item/cautery,
diff --git a/code/datums/weather/weather_types/void_storm.dm b/code/datums/weather/weather_types/void_storm.dm
index becfa9859a81a8..4d3638c5827116 100644
--- a/code/datums/weather/weather_types/void_storm.dm
+++ b/code/datums/weather/weather_types/void_storm.dm
@@ -33,8 +33,11 @@
return FALSE
/datum/weather/void_storm/weather_act(mob/living/victim)
- victim.adjustFireLoss(1)
- victim.adjustOxyLoss(rand(1, 3))
+ var/need_mob_update = FALSE
+ victim.adjustFireLoss(1, updating_health = FALSE)
+ victim.adjustOxyLoss(rand(1, 3), updating_health = FALSE)
+ if(need_mob_update)
+ victim.updatehealth()
victim.adjust_eye_blur(rand(0 SECONDS, 2 SECONDS))
victim.adjust_bodytemperature(-30 * TEMPERATURE_DAMAGE_COEFFICIENT)
diff --git a/code/datums/wires/explosive.dm b/code/datums/wires/explosive.dm
index 925c948183124e..800b5b884449f5 100644
--- a/code/datums/wires/explosive.dm
+++ b/code/datums/wires/explosive.dm
@@ -151,4 +151,4 @@
/datum/wires/explosive/gibtonite/explode()
var/obj/item/gibtonite/P = holder
- P.GibtoniteReaction(null, 2)
+ P.GibtoniteReaction(null, "A wire signal has primed a")
diff --git a/code/datums/wires/microwave.dm b/code/datums/wires/microwave.dm
index abce90e8de5e24..e3efabac08c4d6 100644
--- a/code/datums/wires/microwave.dm
+++ b/code/datums/wires/microwave.dm
@@ -4,7 +4,8 @@
/datum/wires/microwave/New(atom/holder)
wires = list(
- WIRE_ACTIVATE
+ WIRE_ACTIVATE,
+ WIRE_MODE_SELECT
)
..()
@@ -12,18 +13,23 @@
if(!..())
return FALSE
. = FALSE
- var/obj/machinery/microwave/M = holder
- if(M.panel_open)
+ var/obj/machinery/microwave/mw = holder
+ if(mw.panel_open)
. = TRUE
/datum/wires/microwave/on_pulse(wire)
- var/obj/machinery/microwave/M = holder
+ var/obj/machinery/microwave/mw = holder
switch(wire)
if(WIRE_ACTIVATE)
- M.cook()
+ mw.cook()
+ if(WIRE_MODE_SELECT)
+ if(mw.vampire_charging_capable)
+ mw.vampire_charging_enabled = !mw.vampire_charging_enabled
/datum/wires/microwave/on_cut(wire, mend, source)
- var/obj/machinery/microwave/M = holder
+ var/obj/machinery/microwave/mw = holder
switch(wire)
if(WIRE_ACTIVATE)
- M.wire_disabled = !mend
+ mw.wire_disabled = !mend
+ if(WIRE_MODE_SELECT)
+ mw.wire_mode_swap = !mend
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
index 265b9d49815920..d930c496de6ce9 100644
--- a/code/datums/world_topic.dm
+++ b/code/datums/world_topic.dm
@@ -195,7 +195,7 @@
/datum/world_topic/status/Run(list/input)
. = list()
.["version"] = GLOB.game_version
- .["respawn"] = config ? !CONFIG_GET(flag/norespawn) : FALSE
+ .["respawn"] = config ? !!CONFIG_GET(flag/allow_respawn) : FALSE // show respawn as true regardless of "respawn as char" or "free respawn"
.["enter"] = !LAZYACCESS(SSlag_switch.measures, DISABLE_NON_OBSJOBS)
.["ai"] = CONFIG_GET(flag/allow_ai)
.["host"] = world.host ? world.host : null
@@ -240,4 +240,3 @@
// Shuttle status, see /__DEFINES/stat.dm
.["shuttle_timer"] = SSshuttle.emergency.timeLeft()
// Shuttle timer, in seconds
-
diff --git a/code/datums/wounds/_wounds.dm b/code/datums/wounds/_wounds.dm
index faf1ef87057221..7d211b9ae29247 100644
--- a/code/datums/wounds/_wounds.dm
+++ b/code/datums/wounds/_wounds.dm
@@ -185,7 +185,7 @@
* * attack_direction: For bloodsplatters, if relevant
* * wound_source: The source of the wound, such as a weapon.
*/
-/datum/wound/proc/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null, wound_source = "Unknown")
+/datum/wound/proc/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null, wound_source = "Unknown", replacing = FALSE)
if (!can_be_applied_to(L, old_wound))
qdel(src)
@@ -198,7 +198,7 @@
src.wound_source = wound_source
set_victim(L.owner)
- set_limb(L)
+ set_limb(L, replacing)
LAZYADD(victim.all_wounds, src)
LAZYADD(limb.wounds, src)
update_descriptions()
@@ -290,7 +290,7 @@
. = limb
if(limb) // if we're nulling limb, we're basically detaching from it, so we should remove ourselves in that case
UnregisterSignal(limb, COMSIG_QDELETING)
- UnregisterSignal(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_GAUZE_DESTROYED))
+ UnregisterSignal(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_UNGAUZED))
LAZYREMOVE(limb.wounds, src)
limb.update_wounds(replaced)
if (disabling)
@@ -302,7 +302,7 @@
if (limb)
RegisterSignal(limb, COMSIG_QDELETING, PROC_REF(source_died))
- RegisterSignals(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_GAUZE_DESTROYED), PROC_REF(gauze_state_changed))
+ RegisterSignals(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_UNGAUZED), PROC_REF(gauze_state_changed))
if (disabling)
limb.add_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src))
@@ -371,7 +371,7 @@
already_scarred = TRUE
var/obj/item/bodypart/cached_limb = limb // remove_wound() nulls limb so we have to track it locally
remove_wound(replaced=TRUE)
- new_wound.apply_wound(cached_limb, old_wound = src, smited = smited, attack_direction = attack_direction, wound_source = wound_source)
+ new_wound.apply_wound(cached_limb, old_wound = src, smited = smited, attack_direction = attack_direction, wound_source = wound_source, replacing = TRUE)
. = new_wound
qdel(src)
@@ -435,14 +435,14 @@
else
limp_slowdown = initial(limp_slowdown)
limp_chance = initial(limp_chance)
- else if(limb.body_zone in list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ else if(limb.body_zone in GLOB.arm_zones)
if(limb.current_gauze?.splint_factor)
set_interaction_efficiency_penalty(1 + ((get_effective_actionspeed_modifier()) * limb.current_gauze.splint_factor))
else
set_interaction_efficiency_penalty(initial(interaction_efficiency_penalty))
if(initial(disabling))
- set_disabling(!limb.current_gauze)
+ set_disabling(isnull(limb.current_gauze))
limb.update_wounds(replaced_or_replacing)
diff --git a/code/datums/wounds/bones.dm b/code/datums/wounds/bones.dm
index 59e64db1ccf3d6..270d2a23f42868 100644
--- a/code/datums/wounds/bones.dm
+++ b/code/datums/wounds/bones.dm
@@ -384,7 +384,7 @@
threshold_minimum = 115
// doesn't make much sense for "a" bone to stick out of your head
-/datum/wound/blunt/bone/critical/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null, wound_source = "Unknown")
+/datum/wound/blunt/bone/critical/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null, wound_source = "Unknown", replacing = FALSE)
if(L.body_zone == BODY_ZONE_HEAD)
occur_text = "splits open, exposing a bare, cracked skull through the flesh and blood"
examine_desc = "has an unsettling indent, with bits of skull poking out"
diff --git a/code/datums/wounds/burns.dm b/code/datums/wounds/burns.dm
index a97706c8e16d8d..39e91d06fb65f4 100644
--- a/code/datums/wounds/burns.dm
+++ b/code/datums/wounds/burns.dm
@@ -48,7 +48,7 @@
for(var/datum/reagent/reagent as anything in victim.reagents.reagent_list)
if(reagent.chemical_flags & REAGENT_AFFECTS_WOUNDS)
- reagent.on_burn_wound_processing()
+ reagent.on_burn_wound_processing(src)
if(HAS_TRAIT(victim, TRAIT_VIRUS_RESISTANCE))
sanitization += 0.9
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 4912a69f7b8e6b..add1cdf887c19b 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -129,6 +129,11 @@
var/base_pixel_x = 0
///Default pixel y shifting for the atom's icon.
var/base_pixel_y = 0
+ // Use SET_BASE_VISUAL_PIXEL(x, y) to set these in typepath definitions, it'll handle pixel_w and z for you
+ ///Default pixel w shifting for the atom's icon.
+ var/base_pixel_w = 0
+ ///Default pixel z shifting for the atom's icon.
+ var/base_pixel_z = 0
///Used for changing icon states for different base sprites.
var/base_icon_state
@@ -318,6 +323,8 @@
/atom/proc/CanPass(atom/movable/mover, border_dir)
SHOULD_CALL_PARENT(TRUE)
SHOULD_BE_PURE(TRUE)
+ if(SEND_SIGNAL(src, COMSIG_ATOM_TRIED_PASS, mover, border_dir) & COMSIG_COMPONENT_PERMIT_PASSAGE)
+ return TRUE
if(mover.movement_type & PHASING)
return TRUE
. = CanAllowThrough(mover, border_dir)
@@ -672,11 +679,11 @@
var/reagent_sigreturn = SEND_SIGNAL(src, COMSIG_ATOM_REAGENT_EXAMINE, user, ., user_sees_reagents)
if(!(reagent_sigreturn & STOP_GENERIC_REAGENT_EXAMINE))
if(reagents.flags & TRANSPARENT)
- if(reagents.total_volume > 0)
+ if(reagents.total_volume)
. += "It contains [round(reagents.total_volume, 0.01)] units of various reagents[user_sees_reagents ? ":" : "."]"
- if(user_sees_reagents) //Show each individual reagent
+ if(user_sees_reagents) //Show each individual reagent for detailed examination
for(var/datum/reagent/current_reagent as anything in reagents.reagent_list)
- . += "• [round(current_reagent.volume, 0.01)] units of [current_reagent.name]"
+ . += "• [FLOOR(current_reagent.volume, CHEMICAL_QUANTISATION_LEVEL)] 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.")
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index 0be75a321ef88a..dbc9a19a6c5152 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -18,7 +18,7 @@
/datum/atom_hud/data
/datum/atom_hud/data/human/medical
- hud_icons = list(STATUS_HUD, HEALTH_HUD)
+ hud_icons = list(STATUS_HUD, HEALTH_HUD, DNR_HUD) // SKYRAT EDIT ADDITION - DNR_HUD
/datum/atom_hud/data/human/medical/basic
@@ -47,7 +47,7 @@
hud_icons = list(ID_HUD)
/datum/atom_hud/data/human/security/advanced
- hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, PERMIT_HUD) //SKYRAT EDIT: ADD PERMIT_HUD
+ hud_icons = list(ID_HUD, IMPTRACK_HUD, IMPLOYAL_HUD, IMPCHEM_HUD, WANTED_HUD, PERMIT_HUD, DNR_HUD) //SKYRAT EDIT ADDITION - PERMIT_HUD, DNR_HUD
/datum/atom_hud/data/human/fan_hud
hud_icons = list(FAN_HUD)
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index 6208df36de1b67..ce64f2004587af 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -17,6 +17,8 @@ GLOBAL_LIST_EMPTY(dynamic_forced_roundstart_ruleset)
GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Modify the threat level for station traits before dynamic can be Initialized. List(instance = threat_reduction)
GLOBAL_LIST_EMPTY(dynamic_station_traits)
+/// Rulesets which have been forcibly enabled or disabled
+GLOBAL_LIST_EMPTY(dynamic_forced_rulesets)
/datum/game_mode/dynamic
// Threat logging vars
diff --git a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
index 5079035834be55..c3e295ae875fee 100644
--- a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
+++ b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
@@ -45,7 +45,11 @@
continue
if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level))
- log_dynamic("FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [GLOB.alive_player_list.len], threat level: [threat_level]")
+ var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED
+ if (ruleset_forced == RULESET_NOT_FORCED)
+ log_dynamic("FAIL: [ruleset] is not acceptable with the current parameters. Alive players: [GLOB.alive_player_list.len], threat level: [threat_level]")
+ else
+ log_dynamic("FAIL: [ruleset] was disabled.")
continue
if (mid_round_budget < ruleset.cost)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
index a1c4c9232e2bf4..0af18e0b0aa81f 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
@@ -33,6 +33,7 @@
JOB_DETECTIVE,
JOB_HEAD_OF_SECURITY,
JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
)
/// If enemy_roles was set, this is the amount of enemy job workers needed per threat_level range (0-10,10-20,etc) IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS.
var/required_enemies = list(1,1,0,0,0,0,0,0,0,0)
@@ -105,23 +106,39 @@
/// By default, a rule is acceptable if it satisfies the threat level/population requirements.
/// If your rule has extra checks, such as counting security officers, do that in ready() instead
/datum/dynamic_ruleset/proc/acceptable(population = 0, threat_level = 0)
- pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement
- indice_pop = min(requirements.len,round(population/pop_per_requirement)+1)
-
- if(minimum_players > population)
- log_dynamic("FAIL: [src] failed acceptable: minimum_players ([minimum_players]) > population ([population])")
+ var/ruleset_forced = GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED
+ if (ruleset_forced != RULESET_NOT_FORCED)
+ if (ruleset_forced == RULESET_FORCE_ENABLED)
+ return TRUE
+ else
+ log_dynamic("FAIL: [src] was disabled in admin panel.")
+ return FALSE
+
+ if(!is_valid_population(population))
+ var/range = maximum_players > 0 ? "([minimum_players] - [maximum_players])" : "(minimum: [minimum_players])"
+ log_dynamic("FAIL: [src] failed acceptable: min/max players out of range [range] vs population ([population])")
return FALSE
- if(maximum_players > 0 && population > maximum_players)
- log_dynamic("FAIL: [src] failed acceptable: maximum_players ([maximum_players]) < population ([population])")
- return FALSE
-
- if (threat_level < requirements[indice_pop])
+ if (!is_valid_threat(population, threat_level))
log_dynamic("FAIL: [src] failed acceptable: threat_level ([threat_level]) < requirement ([requirements[indice_pop]])")
return FALSE
return TRUE
+/// Returns true if we have enough players to run
+/datum/dynamic_ruleset/proc/is_valid_population(population)
+ if(minimum_players > population)
+ return FALSE
+ if(maximum_players > 0 && population > maximum_players)
+ return FALSE
+ return TRUE
+
+/// Sets the current threat indices and returns true if we're inside of them
+/datum/dynamic_ruleset/proc/is_valid_threat(population, threat_level)
+ pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement
+ indice_pop = min(requirements.len,round(population/pop_per_requirement)+1)
+ return threat_level >= requirements[indice_pop]
+
/// When picking rulesets, if dynamic picks the same one multiple times, it will "scale up".
/// However, doing this blindly would result in lowpop rounds (think under 10 people) where over 80% of the crew is antags!
/// This function is here to ensure the antag ratio is kept under control while scaling up.
@@ -174,7 +191,7 @@
candidates = list()
assigned = list()
antag_datum = null
-
+
/// Here you can perform any additional checks you want. (such as checking the map etc)
/// Remember that on roundstart no one knows what their job is at this point.
/// IMPORTANT: If ready() returns TRUE, that means pre_execute() or execute() should never fail!
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 3610d8aad91086..7b125439d53d50 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -38,8 +38,8 @@
job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it
var/threat = round(mode.threat_level/10)
-
- if (job_check < required_enemies[threat])
+ var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED
+ if (!ruleset_forced && job_check < required_enemies[threat])
log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found")
return FALSE
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 91f82f29f1ca43..80fcbc54db88ce 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -52,42 +52,50 @@
dead_players = trim_list(GLOB.dead_player_list)
list_observers = trim_list(GLOB.current_observers_list)
-/datum/dynamic_ruleset/midround/proc/trim_list(list/L = list())
- var/list/trimmed_list = L.Copy()
- for(var/mob/M in trimmed_list)
- if (!istype(M, required_type))
- trimmed_list.Remove(M)
+/datum/dynamic_ruleset/midround/proc/trim_list(list/to_trim = list())
+ var/list/trimmed_list = to_trim.Copy()
+ for(var/mob/creature in trimmed_list)
+ if (!istype(creature, required_type))
+ trimmed_list.Remove(creature)
continue
- if (!M.client) // Are they connected?
- trimmed_list.Remove(M)
+ if (isnull(creature.client)) // Are they connected?
+ trimmed_list.Remove(creature)
+ continue
+ if (isnull(creature.mind))
+ trimmed_list.Remove(creature)
continue
//SKYRAT EDIT ADDITION
- if(is_banned_from(M.client.ckey, BAN_ANTAGONIST))
- trimmed_list.Remove(M)
+ if(is_banned_from(creature.client.ckey, BAN_ANTAGONIST))
+ trimmed_list.Remove(creature)
continue
- if(!M.client?.prefs?.read_preference(/datum/preference/toggle/be_antag))
- trimmed_list.Remove(M)
+ if(!creature.client?.prefs?.read_preference(/datum/preference/toggle/be_antag))
+ trimmed_list.Remove(creature)
continue
//SKYRAT EDIT END
- if(M.client.get_remaining_days(minimum_required_age) > 0)
- trimmed_list.Remove(M)
+ if(creature.client.get_remaining_days(minimum_required_age) > 0)
+ trimmed_list.Remove(creature)
continue
- if (!((antag_preference || antag_flag) in M.client.prefs.be_special))
- trimmed_list.Remove(M)
+ if (!((antag_preference || antag_flag) in creature.client.prefs.be_special))
+ trimmed_list.Remove(creature)
continue
- if (is_banned_from(M.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
- trimmed_list.Remove(M)
+ if (is_banned_from(creature.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
+ trimmed_list.Remove(creature)
+ 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
+ if (creature.mind.assigned_role.title in restricted_roles) // Does their job allow it?
+ trimmed_list.Remove(creature)
+ continue
+ if (length(exclusive_roles) && !(creature.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job?
+ trimmed_list.Remove(creature)
+ continue
+ if(HAS_TRAIT(creature, TRAIT_MIND_TEMPORARILY_GONE)) // are they out of body?
+ trimmed_list.Remove(creature)
+ continue
+ if(HAS_TRAIT(creature, TRAIT_TEMPORARY_BODY)) // are they an avatar?
+ trimmed_list.Remove(creature)
continue
- if (M.mind)
- if (restrict_ghost_roles && (M.mind.assigned_role.title in GLOB.exp_specialmap[EXP_TYPE_SPECIAL])) // Are they playing a ghost role?
- trimmed_list.Remove(M)
- continue
- if (M.mind.assigned_role.title in restricted_roles) // Does their job allow it?
- trimmed_list.Remove(M)
- continue
- if ((exclusive_roles.len > 0) && !(M.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job?
- trimmed_list.Remove(M)
- continue
return trimmed_list
// You can then for example prompt dead players in execute() to join as strike teams or whatever
@@ -108,8 +116,8 @@
job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it
var/threat = round(mode.threat_level/10)
-
- if (job_check < required_enemies[threat])
+ var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED
+ if (!ruleset_forced && job_check < required_enemies[threat])
log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found")
return FALSE
@@ -366,6 +374,7 @@
JOB_DETECTIVE,
JOB_HEAD_OF_SECURITY,
JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
)
required_enemies = list(3,3,3,3,3,2,1,1,0,0)
required_candidates = 5
@@ -378,7 +387,7 @@
var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5)
-/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat=0)
+/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat_level=0)
if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in mode.executed_rules)
return FALSE // Unavailable if nuke ops were already sent at roundstart
indice_pop = min(operative_cap.len, round(living_players.len/5)+1)
@@ -533,7 +542,7 @@
minimum_players = 15
repeatable = TRUE
-/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat = 0)
+/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat_level = 0)
var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) //Checks if there's a single safe, dark tile on station.
if(!spawn_loc)
return FALSE
@@ -710,7 +719,7 @@
spawn_locs = list()
return ..()
-/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat=0)
+/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat_level=0)
if(GLOB.dead_mob_list.len < dead_mobs_required)
return FALSE
return ..()
@@ -734,7 +743,7 @@
. = ..()
/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant)
- var/mob/living/simple_animal/revenant/revenant = new(pick(spawn_locs))
+ var/mob/living/basic/revenant/revenant = new(pick(spawn_locs))
revenant.key = applicant.key
message_admins("[ADMIN_LOOKUPFLW(revenant)] has been made into a revenant by the midround ruleset.")
log_game("[key_name(revenant)] was spawned as a revenant by the midround ruleset.")
@@ -773,7 +782,7 @@
minimum_players = 20
repeatable = TRUE
-/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat=0)
+/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat_level=0)
if (SSmapping.is_planetary() || GLOB.light_pirate_gangs.len == 0)
return FALSE
return ..()
@@ -795,7 +804,7 @@
minimum_players = 25
repeatable = TRUE
-/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat=0)
+/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat_level=0)
if (SSmapping.is_planetary() || GLOB.heavy_pirate_gangs.len == 0)
return FALSE
return ..()
@@ -873,6 +882,7 @@
JOB_DETECTIVE,
JOB_HEAD_OF_SECURITY,
JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
)
required_enemies = list(2, 2, 1, 1, 1, 1, 1, 0, 0, 0)
required_candidates = 1
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index fde5c89b88de94..494f1cca77ef79 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -591,13 +591,7 @@
if(!isliving(user))
return FALSE //no ghosts allowed, sorry
- var/is_dextrous = FALSE
- if(isanimal(user))
- var/mob/living/simple_animal/user_as_animal = user
- if (user_as_animal.dextrous)
- is_dextrous = TRUE
-
- if(!issilicon(user) && !is_dextrous && !user.can_hold_items())
+ if(!issilicon(user) && !user.can_hold_items())
return FALSE //spiders gtfo
if(issilicon(user)) // If we are a silicon, make sure the machine allows silicons to interact with it
diff --git a/code/game/machinery/airlock_control.dm b/code/game/machinery/airlock_control.dm
index 4c3532e3616b0a..f4d1b29da186f6 100644
--- a/code/game/machinery/airlock_control.dm
+++ b/code/game/machinery/airlock_control.dm
@@ -6,10 +6,6 @@
var/airlock_state
var/frequency
-/obj/machinery/door/airlock/Initialize(mapload)
- . = ..()
- RegisterSignal(SSdcs, COMSIG_GLOB_GREY_TIDE, PROC_REF(grey_tide))
-
/// Forces the airlock to unbolt and open
/obj/machinery/door/airlock/proc/secure_open()
locked = FALSE
@@ -35,17 +31,6 @@
locked = FALSE
return ..()
-/obj/machinery/door/airlock/proc/grey_tide(datum/source, list/grey_tide_areas)
- SIGNAL_HANDLER
-
- if(!is_station_level(z) || critical_machine)
- return //Skip doors in critical positions, such as the SM chamber.
-
- for(var/area_type in grey_tide_areas)
- if(!istype(get_area(src), area_type))
- continue
- INVOKE_ASYNC(src, PROC_REF(prison_open)) //Sleep gets called further down in open(), so we have to invoke async
-
/obj/machinery/airlock_sensor
icon = 'icons/obj/machines/wallmounts.dmi'
icon_state = "airlock_sensor_off"
diff --git a/code/game/machinery/computer/aifixer.dm b/code/game/machinery/computer/aifixer.dm
index 87e53ebb368de1..2bc13e156d441d 100644
--- a/code/game/machinery/computer/aifixer.dm
+++ b/code/game/machinery/computer/aifixer.dm
@@ -65,10 +65,12 @@
/obj/machinery/computer/aifixer/proc/Fix()
use_power(1000)
- occupier.adjustOxyLoss(-5, FALSE)
- occupier.adjustFireLoss(-5, FALSE)
- occupier.adjustBruteLoss(-5, FALSE)
- occupier.updatehealth()
+ var/need_mob_update = FALSE
+ need_mob_update += occupier.adjustOxyLoss(-5, updating_health = FALSE)
+ need_mob_update += occupier.adjustFireLoss(-5, updating_health = FALSE)
+ need_mob_update += occupier.adjustBruteLoss(-5, updating_health = FALSE)
+ if(need_mob_update)
+ occupier.updatehealth()
if(occupier.health >= 0 && occupier.stat == DEAD)
occupier.revive()
if(!occupier.radio_enabled)
diff --git a/code/game/machinery/computer/arcade/arcade.dm b/code/game/machinery/computer/arcade/arcade.dm
index 28034872289e3a..58a7280e8643e9 100644
--- a/code/game/machinery/computer/arcade/arcade.dm
+++ b/code/game/machinery/computer/arcade/arcade.dm
@@ -582,7 +582,7 @@ GLOBAL_LIST_INIT(arcade_prize_pool, list(
var/mob/living/living_user = user
if (istype(living_user))
living_user.investigate_log("has been gibbed by an emagged Orion Trail game.", INVESTIGATE_DEATHS)
- living_user.gib()
+ living_user.gib(DROP_ALL_REMAINS)
SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("loss", "hp", (obj_flags & EMAGGED ? "emagged":"normal")))
user.lost_game()
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 5a604cd969b7d4..54e622fb12d384 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -82,17 +82,18 @@
/obj/machinery/computer/security/ui_data()
var/list/data = list()
- data["network"] = network
data["activeCamera"] = null
if(active_camera)
data["activeCamera"] = list(
name = active_camera.c_tag,
+ ref = REF(active_camera),
status = active_camera.status,
)
return data
/obj/machinery/computer/security/ui_static_data()
var/list/data = list()
+ data["network"] = network
data["mapRef"] = cam_screen.assigned_map
var/list/cameras = get_camera_list(network)
data["cameras"] = list()
@@ -100,6 +101,7 @@
var/obj/machinery/camera/C = cameras[i]
data["cameras"] += list(list(
name = C.c_tag,
+ ref = REF(C),
))
return data
@@ -110,13 +112,11 @@
return
if(action == "switch_camera")
- var/c_tag = params["name"]
- var/list/cameras = get_camera_list(network)
- var/obj/machinery/camera/selected_camera = cameras[c_tag]
+ var/obj/machinery/camera/selected_camera = locate(params["camera"]) in GLOB.cameranet.cameras
active_camera = selected_camera
playsound(src, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
- if(!selected_camera)
+ if(isnull(active_camera))
return TRUE
update_active_camera_screen()
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index c433565dbf8b3d..bc4a4ecabed14e 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -134,7 +134,8 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new)
JOB_QUARTERMASTER = 50,
JOB_SHAFT_MINER = 51,
JOB_CARGO_TECHNICIAN = 52,
- JOB_CUSTOMS_AGENT = 53, // SKYRAT EDIT ADDITION
+ JOB_BITRUNNER = 53,
+ JOB_CUSTOMS_AGENT = 54, // SKYRAT EDIT ADDITION
// 60+: Civilian/other
JOB_HEAD_OF_PERSONNEL = 60,
JOB_BARTENDER = 61,
diff --git a/code/game/machinery/computer/operating_computer.dm b/code/game/machinery/computer/operating_computer.dm
index f7b0ef24ffdc9d..3b639a9ce573f0 100644
--- a/code/game/machinery/computer/operating_computer.dm
+++ b/code/game/machinery/computer/operating_computer.dm
@@ -25,11 +25,15 @@
if(!CONFIG_GET(flag/no_default_techweb_link) && !linked_techweb)
CONNECT_TO_RND_SERVER_ROUNDSTART(linked_techweb, src)
- experiment_handler = AddComponent( \
+ var/list/operating_signals = list(
+ COMSIG_OPERATING_COMPUTER_AUTOPSY_COMPLETE = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_autopsy_experiment),
+ )
+ experiment_handler = AddComponent(
/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/autopsy), \
config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE, \
config_mode = EXPERIMENT_CONFIG_ALTCLICK, \
+ experiment_signals = operating_signals, \
)
/obj/machinery/computer/operating/Destroy()
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index 2786ed0f7691db..9d18b7ecb8501b 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -191,12 +191,23 @@
diag_hud_set_electrified()
- RegisterSignal(src, COMSIG_MACHINERY_BROKEN, PROC_REF(on_break))
-
// Click on the floor to close airlocks
AddComponent(/datum/component/redirect_attack_hand_from_turf)
- return INITIALIZE_HINT_LATELOAD
+ RegisterSignal(src, COMSIG_MACHINERY_BROKEN, PROC_REF(on_break))
+
+ RegisterSignal(SSdcs, COMSIG_GLOB_GREY_TIDE, PROC_REF(grey_tide))
+
+/obj/machinery/door/airlock/proc/grey_tide(datum/source, list/grey_tide_areas)
+ SIGNAL_HANDLER
+
+ if(!is_station_level(z) || critical_machine)
+ return //Skip doors in critical positions, such as the SM chamber.
+
+ for(var/area_type in grey_tide_areas)
+ if(!istype(get_area(src), area_type))
+ continue
+ INVOKE_ASYNC(src, PROC_REF(prison_open)) //Sleep gets called further down in open(), so we have to invoke async
/obj/machinery/door/airlock/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
if(id_tag)
diff --git a/code/game/machinery/doors/poddoor.dm b/code/game/machinery/doors/poddoor.dm
index 4b49e7ee963e46..7eb1163871e459 100644
--- a/code/game/machinery/doors/poddoor.dm
+++ b/code/game/machinery/doors/poddoor.dm
@@ -49,7 +49,7 @@
return
if (deconstruction != BLASTDOOR_FINISHED)
return
- var/change_id = tgui_input_number(user, "Set the door controllers ID", "Door Controller ID", id, 100)
+ var/change_id = tgui_input_text(user, "Set the door controllers ID", "Door Controller ID", id, 100)
if(!change_id || QDELETED(usr) || QDELETED(src) || !usr.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return
id = change_id
diff --git a/code/game/machinery/harvester.dm b/code/game/machinery/harvester.dm
index 63a48dcd976351..1a16c00e42da86 100644
--- a/code/game/machinery/harvester.dm
+++ b/code/game/machinery/harvester.dm
@@ -75,21 +75,18 @@
/obj/machinery/harvester/proc/can_harvest()
if(!powered() || state_open || !occupant || !iscarbon(occupant))
return
- var/mob/living/carbon/C = occupant
+ var/mob/living/carbon/carbon_occupant = occupant
if(!allow_clothing)
- for(var/A in C.held_items + C.get_equipped_items())
- if(!isitem(A))
- continue
- var/obj/item/I = A
- if(!(HAS_TRAIT(I, TRAIT_NODROP)))
+ for(var/obj/item/abiotic_item in carbon_occupant.held_items + carbon_occupant.get_equipped_items())
+ if(!(HAS_TRAIT(abiotic_item, TRAIT_NODROP)))
say("Subject may not have abiotic items on.")
playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE)
return
- if(!(C.mob_biotypes & MOB_ORGANIC))
+ if(!(carbon_occupant.mob_biotypes & MOB_ORGANIC))
say("Subject is not organic.")
playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE)
return
- if(!allow_living && !(C.stat == DEAD || HAS_TRAIT(C, TRAIT_FAKEDEATH))) //I mean, the machines scanners arent advanced enough to tell you're alive
+ if(!allow_living && !(carbon_occupant.stat == DEAD || HAS_TRAIT(carbon_occupant, TRAIT_FAKEDEATH))) //I mean, the machines scanners arent advanced enough to tell you're alive
say("Subject is still alive.")
playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE)
return
@@ -124,21 +121,21 @@
end_harvesting(success = FALSE)
return
playsound(src, 'sound/machines/juicer.ogg', 20, TRUE)
- var/mob/living/carbon/C = occupant
+ var/mob/living/carbon/carbon_occupant = occupant
if(!LAZYLEN(operation_order)) //The list is empty, so we're done here
end_harvesting(success = TRUE)
return
var/turf/target = get_step(src, output_dir)
- for(var/obj/item/bodypart/BP in operation_order) //first we do non-essential limbs
- BP.drop_limb()
- C.emote("scream")
- if(BP.body_zone != "chest")
- BP.forceMove(target) //Move the limbs right next to it, except chest, that's a weird one
- BP.drop_organs()
+ for(var/obj/item/bodypart/limb_to_remove as anything in operation_order) //first we do non-essential limbs
+ limb_to_remove.drop_limb()
+ carbon_occupant.emote("scream")
+ if(limb_to_remove.body_zone != "chest")
+ limb_to_remove.forceMove(target) //Move the limbs right next to it, except chest, that's a weird one
+ limb_to_remove.drop_organs()
else
- for(var/obj/item/organ/O in BP.dismember())
- O.forceMove(target) //Some organs, like chest ones, are different so we need to manually move them
- operation_order.Remove(BP)
+ for(var/obj/item/organ/organ_to_remove in limb_to_remove.dismember())
+ organ_to_remove.forceMove(target) //Some organs, like chest ones, are different so we need to manually move them
+ operation_order.Remove(limb_to_remove)
break
use_power(active_power_usage)
addtimer(CALLBACK(src, PROC_REF(harvest)), interval)
@@ -154,7 +151,7 @@
say("Subject has been successfully harvested.")
playsound(src, 'sound/machines/microwave/microwave-end.ogg', 100, FALSE)
-/obj/machinery/harvester/screwdriver_act(mob/living/user, obj/item/I)
+/obj/machinery/harvester/screwdriver_act(mob/living/user, obj/item/tool)
. = TRUE
if(..())
return
@@ -164,20 +161,20 @@
if(state_open)
to_chat(user, span_warning("[src] must be closed to [panel_open ? "close" : "open"] its maintenance hatch!"))
return
- if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-o", initial(icon_state), I))
+ if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-o", initial(icon_state), tool))
return
return FALSE
-/obj/machinery/harvester/crowbar_act(mob/living/user, obj/item/I)
- if(default_pry_open(I))
+/obj/machinery/harvester/crowbar_act(mob/living/user, obj/item/tool)
+ if(default_pry_open(tool))
return TRUE
- if(default_deconstruction_crowbar(I))
+ if(default_deconstruction_crowbar(tool))
return TRUE
-/obj/machinery/harvester/default_pry_open(obj/item/I) //wew
- . = !(state_open || panel_open || (flags_1 & NODECONSTRUCT_1)) && I.tool_behaviour == TOOL_CROWBAR //We removed is_operational here
+/obj/machinery/harvester/default_pry_open(obj/item/tool) //wew
+ . = !(state_open || panel_open || (flags_1 & NODECONSTRUCT_1)) && tool.tool_behaviour == TOOL_CROWBAR //We removed is_operational here
if(.)
- I.play_tool_sound(src, 50)
+ tool.play_tool_sound(src, 50)
visible_message(span_notice("[usr] pries open \the [src]."), span_notice("You pry open [src]."))
open_machine()
diff --git a/code/game/machinery/shieldgen.dm b/code/game/machinery/shieldgen.dm
index 0e53fa8e36caca..f1cbc110a3e50f 100644
--- a/code/game/machinery/shieldgen.dm
+++ b/code/game/machinery/shieldgen.dm
@@ -504,7 +504,7 @@
for(var/mob/living/L in get_turf(src))
visible_message(span_danger("\The [src] is suddenly occupying the same space as \the [L]!"))
L.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- L.gib()
+ L.gib(DROP_ALL_REMAINS)
RegisterSignal(src, COMSIG_ATOM_SINGULARITY_TRY_MOVE, PROC_REF(block_singularity))
/obj/machinery/shieldwall/Destroy()
diff --git a/code/game/machinery/suit_storage_unit.dm b/code/game/machinery/suit_storage_unit.dm
index d5d0e9c732e13b..15c740a31e4bdb 100644
--- a/code/game/machinery/suit_storage_unit.dm
+++ b/code/game/machinery/suit_storage_unit.dm
@@ -138,6 +138,9 @@
storage_type = /obj/item/tank/jetpack/oxygen/harness
mod_type = /obj/item/mod/control/pre_equipped/nuclear
+/obj/machinery/suit_storage_unit/syndicate/lavaland
+ mod_type = /obj/item/mod/control/pre_equipped/nuclear/no_jetpack
+
/obj/machinery/suit_storage_unit/interdyne
mask_type = /obj/item/clothing/mask/gas/syndicate
storage_type = /obj/item/tank/jetpack/oxygen/harness
diff --git a/code/game/machinery/syndicatebeacon.dm b/code/game/machinery/syndicatebeacon.dm
index 188f3b4f52ee9e..9015eaacedc697 100644
--- a/code/game/machinery/syndicatebeacon.dm
+++ b/code/game/machinery/syndicatebeacon.dm
@@ -146,3 +146,7 @@
/obj/item/sbeacondrop/clownbomb
desc = "A label on it reads: Warning: Activating this device will send a silly explosive to your location."
droptype = /obj/machinery/syndicatebomb/badmin/clown
+
+/obj/item/sbeacondrop/horse
+ desc = "A label on it reads: Warning: Activating this device will send a live horse to your location."
+ droptype = /mob/living/basic/pony/syndicate
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
index dae527c3e6b7ec..2d524e190429a0 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
@@ -394,6 +394,15 @@
reagents.expose(smoker, INGEST, fraction)
return TRUE
+/// Helper to quickly create a cloud of reagent smoke
+/proc/do_chem_smoke(range = 0, amount = DIAMOND_AREA(range), atom/holder = null, location = null, reagent_type = /datum/reagent/water, reagent_volume = 10, log = FALSE)
+ var/datum/reagents/smoke_reagents = new/datum/reagents(reagent_volume)
+ smoke_reagents.add_reagent(reagent_type, reagent_volume)
+
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.attach(location)
+ smoke.set_up(amount = amount, holder = holder, location = location, carry = smoke_reagents, silent = TRUE)
+ smoke.start(log = log)
/// A factory which produces clouds of chemical bearing smoke.
/datum/effect_system/fluid_spread/smoke/chem
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index 2c4a594b806035..4e5b36bf5e8d7b 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -84,6 +84,10 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark)
name = "Cargo Technician"
icon_state = "Cargo Technician"
+/obj/effect/landmark/start/bitrunner
+ name = "Bitrunner"
+ icon_state = "x3"
+
/obj/effect/landmark/start/bartender
name = "Bartender"
icon_state = "Bartender"
diff --git a/code/game/objects/effects/spawners/random/decoration.dm b/code/game/objects/effects/spawners/random/decoration.dm
index 1beafc37df563f..6116d228733174 100644
--- a/code/game/objects/effects/spawners/random/decoration.dm
+++ b/code/game/objects/effects/spawners/random/decoration.dm
@@ -93,6 +93,14 @@
loot_type_path = /obj/structure/showcase
loot = list()
+/obj/effect/spawner/random/decoration/microwave
+ name = "microwave showcase spawner"
+ icon_state = "showcase"
+ loot = list(
+ /obj/structure/showcase/machinery/microwave,
+ /obj/structure/showcase/machinery/microwave_engineering,
+ )
+
/obj/effect/spawner/random/decoration/glowstick
name = "random colored glowstick"
icon_state = "glowstick"
diff --git a/code/game/objects/effects/spawners/random/entertainment.dm b/code/game/objects/effects/spawners/random/entertainment.dm
index 18876dfc927fee..1c635f07a5aaeb 100644
--- a/code/game/objects/effects/spawners/random/entertainment.dm
+++ b/code/game/objects/effects/spawners/random/entertainment.dm
@@ -271,7 +271,6 @@
/obj/item/toy/plush/carpplushie = 3,
/obj/item/toy/plush/lizard_plushie/green = 3,
/obj/item/toy/plush/lizard_plushie/space/green = 3,
- /obj/item/toy/plush/awakenedplushie = 3,
/obj/item/toy/plush/goatplushie = 3,
/obj/item/toy/plush/rouny = 3,
/obj/item/toy/plush/abductor = 3,
diff --git a/code/game/objects/effects/spawners/random/techstorage.dm b/code/game/objects/effects/spawners/random/techstorage.dm
index c1f9304039654e..4bfdeec27d1803 100644
--- a/code/game/objects/effects/spawners/random/techstorage.dm
+++ b/code/game/objects/effects/spawners/random/techstorage.dm
@@ -34,6 +34,7 @@
/obj/item/circuitboard/machine/ore_redemption,
/obj/item/circuitboard/computer/order_console/mining,
/obj/item/circuitboard/machine/microwave,
+ /obj/item/circuitboard/machine/microwave/engineering,
/obj/item/circuitboard/machine/deep_fryer,
/obj/item/circuitboard/machine/griddle,
/obj/item/circuitboard/machine/reagentgrinder,
diff --git a/code/game/objects/effects/spiderwebs.dm b/code/game/objects/effects/spiderwebs.dm
index 3bdac03396fa1d..9b44d3507db2e1 100644
--- a/code/game/objects/effects/spiderwebs.dm
+++ b/code/game/objects/effects/spiderwebs.dm
@@ -174,11 +174,11 @@
/obj/structure/spider/sticky/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
- if(isspider(mover))
+ if(HAS_TRAIT(mover, TRAIT_WEB_SURFER))
return TRUE
if(!isliving(mover))
return
- if(isspider(mover.pulledby))
+ if(!isnull(mover.pulledby) && HAS_TRAIT(mover.pulledby, TRAIT_WEB_SURFER))
return TRUE
loc.balloon_alert(mover, "stuck in web!")
return FALSE
diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
index e88753d25c1f86..c736aac35d8fda 100644
--- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm
+++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
@@ -594,11 +594,22 @@
var/datum/weakref/pinged_person
/// The icon state applied to the image created for this ping.
var/real_icon_state = "sonar_ping"
+ /// Does the visual follow the creature?
+ var/follow_creature = TRUE
+ /// Creature's X & Y coords, which can either be overridden or kept the same depending on follow_creature.
+ var/creature_x
+ var/creature_y
-/obj/effect/temp_visual/sonar_ping/Initialize(mapload, mob/living/looker, mob/living/creature)
+/obj/effect/temp_visual/sonar_ping/Initialize(mapload, mob/living/looker, mob/living/creature, ping_state, follow_creatures = TRUE)
. = ..()
if(!looker || !creature)
return INITIALIZE_HINT_QDEL
+ if(ping_state)
+ real_icon_state = ping_state
+ follow_creature = follow_creatures
+ creature_x = creature.x
+ creature_y = creature.y
+
modsuit_image = image(icon = icon, loc = looker.loc, icon_state = real_icon_state, layer = ABOVE_ALL_MOB_LAYER, pixel_x = ((creature.x - looker.x) * 32), pixel_y = ((creature.y - looker.y) * 32))
modsuit_image.plane = ABOVE_LIGHTING_PLANE
SET_PLANE_EXPLICIT(modsuit_image, ABOVE_LIGHTING_PLANE, creature)
@@ -631,8 +642,12 @@
if(isnull(looker) || isnull(creature))
return PROCESS_KILL
modsuit_image.loc = looker.loc
- modsuit_image.pixel_x = ((creature.x - looker.x) * 32)
- modsuit_image.pixel_y = ((creature.y - looker.y) * 32)
+ // Long pings follow, short pings stay put. We still need to update for looker.x&y though
+ if(follow_creature)
+ creature_y = creature.y
+ creature_x = creature.x
+ modsuit_image.pixel_x = ((creature_x - looker.x) * 32)
+ modsuit_image.pixel_y = ((creature_y - looker.y) * 32)
/obj/effect/temp_visual/block //color is white by default, set to whatever is needed
name = "blocking glow"
diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm
index eabf86811042f9..9dd7e125055f18 100644
--- a/code/game/objects/items/cards_ids.dm
+++ b/code/game/objects/items/cards_ids.dm
@@ -32,6 +32,7 @@
lefthand_file = 'icons/mob/inhands/equipment/idcards_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/idcards_righthand.dmi'
w_class = WEIGHT_CLASS_TINY
+
/// Cached icon that has been built for this card. Intended to be displayed in chat. Cardboards IDs and actual IDs use it.
var/icon/cached_flat_icon
@@ -757,10 +758,10 @@
break
/obj/item/card/id/examine_more(mob/user)
+ . = ..()
if(!user.can_read(src))
return
- . = ..()
. += span_notice("You examine [src] closer, and note the following...")
if(registered_age)
diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm
index f04c6e0058fa6b..79efe62a2250d0 100644
--- a/code/game/objects/items/circuitboards/computer_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm
@@ -540,6 +540,10 @@
name = "Golem Ship Equipment Vendor Console"
build_path = /obj/machinery/computer/order_console/mining/golem
+/obj/item/circuitboard/computer/order_console/bitrunning
+ name = "Bitrunning Vendor Console"
+ build_path = /obj/machinery/computer/order_console/bitrunning
+
/obj/item/circuitboard/computer/ferry
name = "Transport Ferry"
greyscale_colors = CIRCUIT_COLOR_SUPPLY
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index dae27c175b4e83..f41cb62c9deee5 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1251,10 +1251,22 @@
req_components = list(
/datum/stock_part/micro_laser = 1,
/datum/stock_part/matter_bin = 1,
+ /datum/stock_part/capacitor = 1,
/obj/item/stack/cable_coil = 2,
/obj/item/stack/sheet/glass = 2)
needs_anchored = FALSE
+/obj/item/circuitboard/machine/microwave/engineering
+ name = "Wireless Microwave Oven"
+ greyscale_colors = CIRCUIT_COLOR_SERVICE
+ build_path = /obj/machinery/microwave/engineering
+ req_components = list(
+ /datum/stock_part/micro_laser = 1,
+ /datum/stock_part/matter_bin = 1,
+ /datum/stock_part/capacitor/tier2 = 1,
+ /obj/item/stack/cable_coil = 4,
+ /obj/item/stack/sheet/glass = 2)
+
/obj/item/circuitboard/machine/processor
name = "Food Processor"
greyscale_colors = CIRCUIT_COLOR_SERVICE
@@ -1309,6 +1321,15 @@
req_components = list(
/datum/stock_part/card_reader = 1)
+/obj/item/circuitboard/machine/fishing_portal_generator
+ name = "Fishing Portal Generator"
+ greyscale_colors = CIRCUIT_COLOR_SERVICE
+ build_path = /obj/machinery/fishing_portal_generator
+ req_components = list(
+ /datum/stock_part/matter_bin = 2,
+ /datum/stock_part/capacitor = 1)
+ needs_anchored = FALSE
+
//Supply
/obj/item/circuitboard/machine/ore_redemption
name = "Ore Redemption"
@@ -1357,16 +1378,14 @@
greyscale_colors = CIRCUIT_COLOR_SUPPLY
build_path = /obj/machinery/rnd/production/techfab/department/cargo
-/obj/item/circuitboard/machine/bepis
- name = "BEPIS Chamber"
+/obj/item/circuitboard/machine/materials_market
+ name = "Galactic Materials Market"
greyscale_colors = CIRCUIT_COLOR_SUPPLY
- build_path = /obj/machinery/rnd/bepis
+ build_path = /obj/machinery/materials_market
req_components = list(
/obj/item/stack/cable_coil = 5,
- /datum/stock_part/capacitor = 1,
- /datum/stock_part/servo = 1,
- /datum/stock_part/micro_laser = 1,
- /datum/stock_part/scanning_module = 1)
+ /datum/stock_part/scanning_module = 1,
+ /datum/stock_part/card_reader = 1)
//Misc
/obj/item/circuitboard/machine/sheetifier
@@ -1527,3 +1546,4 @@
/obj/item/mod/module/rad_protection = 1,
/obj/item/stack/sheet/plasteel = 2,
)
+
diff --git a/code/game/objects/items/cosmetics.dm b/code/game/objects/items/cosmetics.dm
index 686bd40c698442..9e49a99d73b89e 100644
--- a/code/game/objects/items/cosmetics.dm
+++ b/code/game/objects/items/cosmetics.dm
@@ -1,3 +1,7 @@
+#define UPPER_LIP "Upper"
+#define MIDDLE_LIP "Middle"
+#define LOWER_LIP "Lower"
+
/obj/item/lipstick
gender = PLURAL
name = "red lipstick"
@@ -9,6 +13,8 @@
var/open = FALSE
/// Actual color of the lipstick, also gets applied to the human
var/lipstick_color = COLOR_RED
+ /// The style of lipstick. Upper, middle, or lower lip. Default is middle.
+ var/style = "lipstick"
/// A trait that's applied while someone has this lipstick applied, and is removed when the lipstick is removed
var/lipstick_trait
@@ -22,6 +28,10 @@
if(vname == NAMEOF(src, open))
update_appearance(UPDATE_ICON)
+/obj/item/lipstick/examine(mob/user)
+ . = ..()
+ . += "Alt-click to change the style."
+
/obj/item/lipstick/update_icon_state()
icon_state = "lipstick[open ? "_uncap" : null]"
inhand_icon_state = "lipstick[open ? "open" : null]"
@@ -35,6 +45,42 @@
colored_overlay.color = lipstick_color
. += colored_overlay
+/obj/item/lipstick/AltClick(mob/user)
+ . = ..()
+ if(.)
+ return TRUE
+
+ if(!user.can_perform_action(src, NEED_DEXTERITY|NEED_HANDS|ALLOW_RESTING))
+ return FALSE
+
+ return display_radial_menu(user)
+
+/obj/item/lipstick/proc/display_radial_menu(mob/living/carbon/human/user)
+ var/style_options = list(
+ UPPER_LIP = icon('icons/hud/radial.dmi', UPPER_LIP),
+ MIDDLE_LIP = icon('icons/hud/radial.dmi', MIDDLE_LIP),
+ LOWER_LIP = icon('icons/hud/radial.dmi', LOWER_LIP),
+ )
+ var/pick = show_radial_menu(user, src, style_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), radius = 36, require_near = TRUE)
+ if(!pick)
+ return TRUE
+
+ switch(pick)
+ if(MIDDLE_LIP)
+ style = "lipstick"
+ if(LOWER_LIP)
+ style = "lipstick_lower"
+ if(UPPER_LIP)
+ style = "lipstick_upper"
+ return TRUE
+
+/obj/item/lipstick/proc/check_menu(mob/living/user)
+ if(!istype(user))
+ return FALSE
+ if(user.incapacitated() || !user.is_holding(src))
+ return FALSE
+ return TRUE
+
/obj/item/lipstick/purple
name = "purple lipstick"
lipstick_color = COLOR_PURPLE
@@ -106,7 +152,7 @@
if(target == user)
user.visible_message(span_notice("[user] does [user.p_their()] lips with \the [src]."), \
span_notice("You take a moment to apply \the [src]. Perfect!"))
- target.update_lips("lipstick", lipstick_color, lipstick_trait)
+ target.update_lips(style, lipstick_color, lipstick_trait)
return
user.visible_message(span_warning("[user] begins to do [target]'s lips with \the [src]."), \
@@ -115,7 +161,7 @@
return
user.visible_message(span_notice("[user] does [target]'s lips with \the [src]."), \
span_notice("You apply \the [src] on [target]'s lips."))
- target.update_lips("lipstick", lipstick_color, lipstick_trait)
+ target.update_lips(style, lipstick_color, lipstick_trait)
//you can wipe off lipstick with paper!
/obj/item/paper/attack(mob/M, mob/user)
@@ -285,3 +331,7 @@
/obj/item/razor/surgery/get_surgery_tool_overlay(tray_extended)
return "razor"
+
+#undef UPPER_LIP
+#undef MIDDLE_LIP
+#undef LOWER_LIP
diff --git a/code/game/objects/items/debug_items.dm b/code/game/objects/items/debug_items.dm
index 68ae291e73645b..e2febe30fcfd06 100644
--- a/code/game/objects/items/debug_items.dm
+++ b/code/game/objects/items/debug_items.dm
@@ -163,7 +163,7 @@
playsound(src, 'sound/voice/borg_deathsound.ogg')
sleep(3 SECONDS)
living_user.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- living_user.gib()
+ living_user.gib(DROP_ALL_REMAINS)
return
var/turf/loc_turf = get_turf(src)
for(var/spawn_atom in (choice == "No" ? typesof(path) : subtypesof(path)))
diff --git a/code/game/objects/items/defib.dm b/code/game/objects/items/defib.dm
index 279f81010079a2..8e4a3a41171a92 100644
--- a/code/game/objects/items/defib.dm
+++ b/code/game/objects/items/defib.dm
@@ -578,6 +578,12 @@
do_cancel()
/obj/item/shockpaddles/proc/do_help(mob/living/carbon/H, mob/living/user)
+ var/target_synthetic = (H.mob_biotypes & MOB_ROBOTIC) // SKYRAT EDIT ADDITION BEGIN - SYNTH REVIVAL
+ if (target_synthetic)
+ to_chat(user, span_boldwarning("[H] is a synthetic lifeform! This defibrillator probably isn't calibrated to revive [H.p_them()] properly and could have some serious consequences! \
+ [span_warning("You might want to [span_blue("surgically revive [H.p_them()]")]...")]"))
+ balloon_alert(user, "target is synthetic!") // immediately grabs their attention even if they dont see chat
+ // SKYRAT EDIT ADDITION END - SYNTH REVIVAL
user.visible_message(span_warning("[user] begins to place [src] on [H]'s chest."), span_warning("You begin to place [src] on [H]'s chest..."))
busy = TRUE
update_appearance()
@@ -636,17 +642,19 @@
var/total_brute = H.getBruteLoss()
var/total_burn = H.getFireLoss()
+ var/need_mob_update = FALSE
//If the body has been fixed so that they would not be in crit when defibbed, give them oxyloss to put them back into crit
if (H.health > HALFWAYCRITDEATH)
- H.adjustOxyLoss(H.health - HALFWAYCRITDEATH, 0)
+ need_mob_update += H.adjustOxyLoss(H.health - HALFWAYCRITDEATH, updating_health = FALSE)
else
var/overall_damage = total_brute + total_burn + H.getToxLoss() + H.getOxyLoss()
var/mobhealth = H.health
- H.adjustOxyLoss((mobhealth - HALFWAYCRITDEATH) * (H.getOxyLoss() / overall_damage), 0)
- H.adjustToxLoss((mobhealth - HALFWAYCRITDEATH) * (H.getToxLoss() / overall_damage), 0, TRUE) // force tox heal for toxin lovers too
- H.adjustFireLoss((mobhealth - HALFWAYCRITDEATH) * (total_burn / overall_damage), 0)
- H.adjustBruteLoss((mobhealth - HALFWAYCRITDEATH) * (total_brute / overall_damage), 0)
- H.updatehealth() // Previous "adjust" procs don't update health, so we do it manually.
+ need_mob_update += H.adjustOxyLoss((mobhealth - HALFWAYCRITDEATH) * (H.getOxyLoss() / overall_damage), updating_health = FALSE)
+ need_mob_update += H.adjustToxLoss((mobhealth - HALFWAYCRITDEATH) * (H.getToxLoss() / overall_damage), updating_health = FALSE, forced = TRUE) // force tox heal for toxin lovers too
+ need_mob_update += H.adjustFireLoss((mobhealth - HALFWAYCRITDEATH) * (total_burn / overall_damage), updating_health = FALSE)
+ need_mob_update += H.adjustBruteLoss((mobhealth - HALFWAYCRITDEATH) * (total_brute / overall_damage), updating_health = FALSE)
+ if(need_mob_update)
+ H.updatehealth() // Previous "adjust" procs don't update health, so we do it manually.
user.visible_message(span_notice("[req_defib ? "[defib]" : "[src]"] pings: Resuscitation successful."))
playsound(src, 'sound/machines/defib_success.ogg', 50, FALSE)
H.set_heartattack(FALSE)
@@ -662,6 +670,23 @@
else
user.add_mood_event("saved_life", /datum/mood_event/saved_life)
log_combat(user, H, "revived", defib)
+ // SKYRAT EDIT ADDITION BEGIN - SYNTH REVIVAL
+ if (target_synthetic)
+ user.visible_message(span_boldwarning("[src] fire a powerful jolt of electricity into [H]'s vulnerable circuitry!"))
+ to_chat(H, span_userdanger("[user]'s defibrillator fires a powerful jolt of electricity into your vulnerable circuitry, overloading it!"))
+ // You may ask, why not just call H.emp_act()?
+ // well my dear reader, that EMPs contents. I only want to EMP bodyparts and organs specifically
+ for (var/obj/item/bodypart/iterated_part as anything in H.bodyparts)
+ iterated_part.emp_act(EMP_LIGHT)
+ for (var/obj/item/organ/iterated_organ as anything in H.organs)
+ iterated_organ.emp_act(EMP_LIGHT)
+ var/obj/item/organ/internal/brain/brain_organ = H.get_organ_slot(ORGAN_SLOT_BRAIN)
+ if (istype(brain_organ))
+ var/datum/brain_trauma/trauma = brain_organ.gain_trauma_type(SYNTH_DEFIBBED_TRAUMA_SEVERITY, TRAUMA_LIMIT_BASIC)
+ if (!QDELETED(trauma))
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(remove_synth_defib_trauma), brain_organ, trauma), SYNTH_DEFIBBED_TRAUMA_DURATION)
+ // SKYRAT EDIT ADDITION END - SYNTH REVIVAL
+
do_success()
return
else if (!H.get_organ_by_type(/obj/item/organ/internal/heart))
diff --git a/code/game/objects/items/devices/polycircuit.dm b/code/game/objects/items/devices/polycircuit.dm
index 9dbdbff993d742..5b7fd42d6f6bd3 100644
--- a/code/game/objects/items/devices/polycircuit.dm
+++ b/code/game/objects/items/devices/polycircuit.dm
@@ -51,7 +51,7 @@
else
to_chat(user, span_notice("You navigate the sharp edges of circuitry and remove a single board from [src]"))
else
- H.apply_damage(15, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ H.apply_damage(15, BRUTE, pick(GLOB.arm_zones))
to_chat(user, span_warning("You give yourself a wicked cut on [src]'s many sharp corners and edges!"))
/obj/item/stack/circuit_stack/full
diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm
index dcb7ddc1936ca4..5fe777f20dcce9 100644
--- a/code/game/objects/items/devices/radio/intercom.dm
+++ b/code/game/objects/items/devices/radio/intercom.dm
@@ -20,7 +20,7 @@
unscrewed = TRUE
/obj/item/radio/intercom/prison
- name = "prison intercom"
+ name = "receive-only intercom"
desc = "A station intercom. It looks like it has been modified to not broadcast."
/obj/item/radio/intercom/prison/Initialize(mapload, ndir, building)
diff --git a/code/game/objects/items/food/_food.dm b/code/game/objects/items/food/_food.dm
index 69cfdde4668bc9..0fb85e67589e40 100644
--- a/code/game/objects/items/food/_food.dm
+++ b/code/game/objects/items/food/_food.dm
@@ -115,7 +115,7 @@
///Set decomp_req_handle to TRUE to only make it decompose when someone picks it up.
///Requires /datum/component/germ_sensitive to detect exposure
/obj/item/food/proc/make_germ_sensitive(mapload)
- if(istype(src, /obj/item/food/bowled) || istype(src, /obj/item/food/canned) || !isnull(trash_type))
+ if(!isnull(trash_type))
return // You don't eat the package and it protects from decomposing
AddComponent(/datum/component/germ_sensitive, mapload)
if(!preserved_food)
diff --git a/code/game/objects/items/food/donkpocket.dm b/code/game/objects/items/food/donkpocket.dm
index f2495e0ee52ff0..d4b4636f15c9b3 100644
--- a/code/game/objects/items/food/donkpocket.dm
+++ b/code/game/objects/items/food/donkpocket.dm
@@ -20,12 +20,16 @@
var/baking_time_short = 25 SECONDS
/// The upper end for how long it takes to bake
var/baking_time_long = 30 SECONDS
+ /// The reagents added when microwaved. Needed since microwaving ignores food_reagents
+ var/static/list/added_reagents = list(/datum/reagent/medicine/omnizine = 6)
+ /// The reagents that most child types add when microwaved. Needed because you can't override static lists.
+ var/static/list/child_added_reagents = list(/datum/reagent/medicine/omnizine = 2)
/obj/item/food/donkpocket/make_bakeable()
- AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE)
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, added_reagents)
/obj/item/food/donkpocket/make_microwaveable()
- AddElement(/datum/element/microwavable, warm_type)
+ AddElement(/datum/element/microwavable, warm_type, added_reagents)
/obj/item/food/donkpocket/warm
name = "warm Donk-pocket"
@@ -69,6 +73,12 @@
foodtypes = GRAIN
warm_type = /obj/item/food/donkpocket/warm/spicy
+/obj/item/food/donkpocket/spicy/make_bakeable()
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, child_added_reagents)
+
+/obj/item/food/donkpocket/spicy/make_microwaveable()
+ AddElement(/datum/element/microwavable, warm_type, child_added_reagents)
+
/obj/item/food/donkpocket/warm/spicy
name = "warm Spicy-pocket"
desc = "The classic snack food, now maybe a bit too spicy."
@@ -95,6 +105,12 @@
foodtypes = GRAIN
warm_type = /obj/item/food/donkpocket/warm/teriyaki
+/obj/item/food/donkpocket/teriyaki/make_bakeable()
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, child_added_reagents)
+
+/obj/item/food/donkpocket/teriyaki/make_microwaveable()
+ AddElement(/datum/element/microwavable, warm_type, child_added_reagents)
+
/obj/item/food/donkpocket/warm/teriyaki
name = "warm Teriyaki-pocket"
desc = "An east-asian take on the classic stationside snack, now steamy and warm."
@@ -121,6 +137,12 @@
foodtypes = GRAIN
warm_type = /obj/item/food/donkpocket/warm/pizza
+/obj/item/food/donkpocket/pizza/make_bakeable()
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, child_added_reagents)
+
+/obj/item/food/donkpocket/pizza/make_microwaveable()
+ AddElement(/datum/element/microwavable, warm_type, child_added_reagents)
+
/obj/item/food/donkpocket/warm/pizza
name = "warm Pizza-pocket"
desc = "Delicious, cheesy, and even better when hot."
@@ -146,6 +168,16 @@
foodtypes = GRAIN
warm_type = /obj/item/food/donkpocket/warm/honk
crafting_complexity = FOOD_COMPLEXITY_3
+ var/static/list/honk_added_reagents = list(
+ /datum/reagent/medicine/omnizine = 2,
+ /datum/reagent/consumable/laughter = 6,
+ )
+
+/obj/item/food/donkpocket/honk/make_bakeable()
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, honk_added_reagents)
+
+/obj/item/food/donkpocket/honk/make_microwaveable()
+ AddElement(/datum/element/microwavable, warm_type, honk_added_reagents)
/obj/item/food/donkpocket/warm/honk
name = "warm Honk-pocket"
@@ -173,6 +205,12 @@
foodtypes = GRAIN
warm_type = /obj/item/food/donkpocket/warm/berry
+/obj/item/food/donkpocket/berry/make_bakeable()
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, child_added_reagents)
+
+/obj/item/food/donkpocket/berry/make_microwaveable()
+ AddElement(/datum/element/microwavable, warm_type, child_added_reagents)
+
/obj/item/food/donkpocket/warm/berry
name = "warm Berry-pocket"
desc = "A relentlessly sweet donk-pocket, now warm and delicious."
@@ -198,6 +236,16 @@
foodtypes = GRAIN
warm_type = /obj/item/food/donkpocket/warm/gondola
+ var/static/list/gondola_added_reagents = list(
+ /datum/reagent/medicine/omnizine = 2,
+ /datum/reagent/gondola_mutation_toxin = 5,
+ )
+
+/obj/item/food/donkpocket/gondola/make_bakeable()
+ AddComponent(/datum/component/bakeable, warm_type, rand(baking_time_short, baking_time_long), TRUE, TRUE, gondola_added_reagents)
+
+/obj/item/food/donkpocket/gondola/make_microwaveable()
+ AddElement(/datum/element/microwavable, warm_type, gondola_added_reagents)
/obj/item/food/donkpocket/warm/gondola
name = "warm Gondola-pocket"
diff --git a/code/game/objects/items/food/egg.dm b/code/game/objects/items/food/egg.dm
index 555b16e24b69e5..60bbb58e3ac873 100644
--- a/code/game/objects/items/food/egg.dm
+++ b/code/game/objects/items/food/egg.dm
@@ -28,6 +28,7 @@ GLOBAL_VAR_INIT(chicks_from_eggs, 0)
ant_attracting = FALSE
decomp_type = /obj/item/food/egg/rotten
decomp_req_handle = TRUE //so laid eggs can actually become chickens
+ /// How likely is it that a chicken will come out of here if we throw it?
var/chick_throw_prob = 13
/obj/item/food/egg/make_bakeable()
@@ -68,24 +69,25 @@ GLOBAL_VAR_INIT(chicks_from_eggs, 0)
var/turf/hit_turf = get_turf(hit_atom)
new /obj/effect/decal/cleanable/food/egg_smudge(hit_turf)
- //Chicken code uses this MAX_CHICKENS variable, so I figured that I'd use it again here. Even this check and the check in chicken code both use the MAX_CHICKENS variable, they use independent counter variables and thus are independent of each other.
- if(GLOB.chicks_from_eggs < MAX_CHICKENS) //Roughly a 1/8 (12.5%) chance to make a chick, as in Minecraft, with a 1/256 (~0.39%) chance to make four chicks instead.
- var/chance = rand(0, 255)
- switch(chance)
- if(0 to 30)
- new /mob/living/basic/chick(hit_turf)
- GLOB.chicks_from_eggs++
- visible_message(span_notice("A chick comes out of the cracked egg!"))
- if(31)
- var/spawned_chickens = min(4, MAX_CHICKENS - GLOB.chicks_from_eggs) // We don't want to go over the limit
- visible_message(span_notice("[spawned_chickens] chicks come out of the egg! Jackpot!"))
- for(var/i in 1 to spawned_chickens)
- new /mob/living/basic/chick(hit_turf)
- GLOB.chicks_from_eggs++
-
+ if (prob(chick_throw_prob))
+ spawn_impact_chick(hit_turf)
reagents.expose(hit_atom, TOUCH)
qdel(src)
+/// Spawn a baby chicken from throwing an egg
+/obj/item/food/egg/proc/spawn_impact_chick(turf/spawn_turf)
+ var/chickens_remaining = MAX_CHICKENS - GLOB.chicks_from_eggs
+ if (chickens_remaining < 1)
+ return
+ var/spawned_chickens = prob(97) ? 1 : min(4, chickens_remaining) // We don't want to go over the limit
+ if (spawned_chickens > 1) // Chicken jackpot!
+ visible_message(span_notice("[spawned_chickens] chicks come out of the egg! Jackpot!"))
+ else
+ visible_message(span_notice("A chick comes out of the cracked egg!"))
+ for(var/i in 1 to spawned_chickens)
+ new /mob/living/basic/chick(spawn_turf)
+ GLOB.chicks_from_eggs++
+
/obj/item/food/egg/attackby(obj/item/item, mob/user, params)
if(istype(item, /obj/item/toy/crayon))
var/obj/item/toy/crayon/crayon = item
diff --git a/code/game/objects/items/food/monkeycube.dm b/code/game/objects/items/food/monkeycube.dm
index 5cf9db79fd01ec..ffc9b63c62f09f 100644
--- a/code/game/objects/items/food/monkeycube.dm
+++ b/code/game/objects/items/food/monkeycube.dm
@@ -48,7 +48,7 @@
return
Expand()
user.visible_message(span_danger("[user]'s torso bursts open as a primate emerges!"))
- user.gib(null, TRUE, null, TRUE)
+ user.gib(DROP_BRAIN|DROP_BODYPARTS|DROP_ITEMS) // just remove the organs
/obj/item/food/monkeycube/syndicate
faction = list(FACTION_NEUTRAL, ROLE_SYNDICATE)
@@ -62,7 +62,7 @@
/datum/reagent/medicine/strange_reagent = 5,
)
tastes = list("the jungle" = 1, "bananas" = 1, "jimmies" = 1)
- spawned_mob = /mob/living/simple_animal/hostile/gorilla
+ spawned_mob = /mob/living/basic/gorilla
/obj/item/food/monkeycube/chicken
name = "chicken cube"
diff --git a/code/game/objects/items/food/moth.dm b/code/game/objects/items/food/moth.dm
index 3bb3c8738d41f0..41c2d8ec3a1662 100644
--- a/code/game/objects/items/food/moth.dm
+++ b/code/game/objects/items/food/moth.dm
@@ -522,7 +522,7 @@
desc = "A salad with added cotton and a basic dressing. Presumably either moths are around, or the South's risen again."
icon = 'icons/obj/food/moth.dmi'
icon_state = "cotton_salad"
- food_reagents = list(,
+ food_reagents = list(
/datum/reagent/consumable/nutriment = 8,
/datum/reagent/consumable/nutriment/vitamin = 14,
)
diff --git a/code/game/objects/items/food/packaged.dm b/code/game/objects/items/food/packaged.dm
index edcc0bd09ed611..1dabec57cdd1a2 100644
--- a/code/game/objects/items/food/packaged.dm
+++ b/code/game/objects/items/food/packaged.dm
@@ -16,6 +16,9 @@
w_class = WEIGHT_CLASS_SMALL
preserved_food = TRUE
+/obj/item/food/canned/make_germ_sensitive(mapload)
+ return // It's in a can
+
/obj/item/food/canned/proc/open_can(mob/user)
to_chat(user, span_notice("You pull back the tab of \the [src]."))
playsound(user.loc, 'sound/items/foodcanopen.ogg', 50)
@@ -209,11 +212,14 @@
/// What type of ready-donk are we warmed into?
var/warm_type = /obj/item/food/ready_donk/warm
+ /// What reagents should be added when this item is warmed?
+ var/static/list/added_reagents = list(/datum/reagent/medicine/omnizine = 3)
+
/obj/item/food/ready_donk/make_bakeable()
- AddComponent(/datum/component/bakeable, warm_type, rand(15 SECONDS, 20 SECONDS), TRUE, TRUE)
+ AddComponent(/datum/component/bakeable, warm_type, rand(15 SECONDS, 20 SECONDS), TRUE, TRUE, added_reagents)
/obj/item/food/ready_donk/make_microwaveable()
- AddElement(/datum/element/microwavable, warm_type)
+ AddElement(/datum/element/microwavable, warm_type, added_reagents)
/obj/item/food/ready_donk/examine_more(mob/user)
. = ..()
diff --git a/code/game/objects/items/food/pizza.dm b/code/game/objects/items/food/pizza.dm
index fdbb1e33c870e7..b93cd7ed7219c5 100644
--- a/code/game/objects/items/food/pizza.dm
+++ b/code/game/objects/items/food/pizza.dm
@@ -389,7 +389,7 @@
if(istype(item, /obj/item/food/pineappleslice))
to_chat(user, "If you want something crazy like pineapple, I'll kill you.") //this is in bigger text because it's hard to spam something that gibs you, and so that you're perfectly aware of the reason why you died
user.investigate_log("has been gibbed by putting pineapple on an arnold pizza.", INVESTIGATE_DEATHS)
- user.gib() //if you want something crazy like pineapple, i'll kill you
+ user.gib(DROP_ALL_REMAINS) //if you want something crazy like pineapple, i'll kill you
else if(istype(item, /obj/item/food/grown/mushroom) && iscarbon(user))
to_chat(user, span_userdanger("So, if you want mushroom, shut up.")) //not as large as the pineapple text, because you could in theory spam it
var/mob/living/carbon/shutup = user
diff --git a/code/game/objects/items/food/sandwichtoast.dm b/code/game/objects/items/food/sandwichtoast.dm
index fec714ad8503d8..8b91b04621e00b 100644
--- a/code/game/objects/items/food/sandwichtoast.dm
+++ b/code/game/objects/items/food/sandwichtoast.dm
@@ -302,5 +302,5 @@
/obj/item/food/sandwich/death/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] starts to shove [src] down [user.p_their()] throat the wrong way. It looks like [user.p_theyre()] trying to commit suicide!"))
qdel(src)
- user.gib(TRUE, TRUE, TRUE)
+ user.gib()
return MANUAL_SUICIDE
diff --git a/code/game/objects/items/food/soup.dm b/code/game/objects/items/food/soup.dm
index 08eb86ebaa0fe3..e1482b17a817b1 100644
--- a/code/game/objects/items/food/soup.dm
+++ b/code/game/objects/items/food/soup.dm
@@ -7,6 +7,9 @@
eatverbs = list("slurp", "sip", "inhale", "drink")
venue_value = FOOD_PRICE_CHEAP
+/obj/item/food/bowled/make_germ_sensitive(mapload)
+ return // It's in a bowl
+
/obj/item/food/bowled/wish
name = "wish soup"
desc = "I wish this was soup."
diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm
index 5902298032c3ab..d5f3b0dec9d431 100644
--- a/code/game/objects/items/grenades/plastic.dm
+++ b/code/game/objects/items/grenades/plastic.dm
@@ -161,7 +161,7 @@
user.visible_message(span_suicide("[user] activates [src] and holds it above [user.p_their()] head! It looks like [user.p_theyre()] going out with a bang!"))
shout_syndicate_crap(user)
explosion(user, heavy_impact_range = 2, explosion_cause = src) //Cheap explosion imitation because putting detonate() here causes runtimes
- user.gib(1, 1)
+ user.gib(DROP_BODYPARTS)
qdel(src)
// X4 is an upgraded directional variant of c4 which is relatively safe to be standing next to. And much less safe to be standing on the other side of.
diff --git a/code/game/objects/items/implants/implant_explosive.dm b/code/game/objects/items/implants/implant_explosive.dm
index 70471a8e99a183..c9f961b594e26e 100644
--- a/code/game/objects/items/implants/implant_explosive.dm
+++ b/code/game/objects/items/implants/implant_explosive.dm
@@ -166,7 +166,7 @@
explosion(src, devastation_range = explosion_devastate, heavy_impact_range = explosion_heavy, light_impact_range = explosion_light, flame_range = explosion_light, flash_range = explosion_light, explosion_cause = src)
if(imp_in)
imp_in.investigate_log("has been gibbed by an explosive implant.", INVESTIGATE_DEATHS)
- imp_in.gib(TRUE)
+ imp_in.gib(DROP_ORGANS|DROP_BODYPARTS)
qdel(src)
///Macrobomb has the strength and delay of 10 microbombs
diff --git a/code/game/objects/items/implants/implant_freedom.dm b/code/game/objects/items/implants/implant_freedom.dm
index d010b57e506774..1fa61c27510fa1 100644
--- a/code/game/objects/items/implants/implant_freedom.dm
+++ b/code/game/objects/items/implants/implant_freedom.dm
@@ -3,37 +3,45 @@
desc = "Use this to escape from those evil Red Shirts."
icon_state = "freedom"
implant_color = "r"
- uses = 4
+ uses = FREEDOM_IMPLANT_CHARGES
+/obj/item/implant/freedom/implant(mob/living/target, mob/user, silent, force)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!iscarbon(target)) //This is pretty much useless for anyone else since they can't be cuffed
+ balloon_alert(user, "that would be a waste!")
+ return FALSE
+ return TRUE
/obj/item/implant/freedom/activate()
. = ..()
+ var/mob/living/carbon/carbon_imp_in = imp_in
+ if(!carbon_imp_in.handcuffed && !carbon_imp_in.legcuffed)
+ balloon_alert(carbon_imp_in, "no restraints!")
+ return
+
uses--
- to_chat(imp_in, span_hear("You feel a faint click."))
- if(iscarbon(imp_in))
- var/mob/living/carbon/C_imp_in = imp_in
- C_imp_in.uncuff()
+
+ carbon_imp_in.uncuff()
if(!uses)
+ addtimer(CALLBACK(carbon_imp_in, TYPE_PROC_REF(/atom, balloon_alert), carbon_imp_in, "implant degraded!"), 1 SECONDS)
qdel(src)
-
/obj/item/implant/freedom/get_data()
var/dat = {"
-Implant Specifications:
-Name: Freedom Beacon
-Life: optimum 5 uses
-Important Notes: Illegal
-
-Implant Details:
-Function: Transmits a specialized cluster of signals to override handcuff locking
-mechanisms
-Special Features:
-Neuro-Scan- Analyzes certain shadow signals in the nervous system
-
-No Implant Specifics"}
+ Implant Specifications:
+ Name: Freedom Beacon
+ Life: Optimum [initial(uses)] uses
+ Important Notes: Illegal
+
+ Implant Details:
+ Function: Transmits a specialized cluster of signals to override handcuff locking
+ mechanisms. These signals will release any bindings on both the arms and legs.
+ Disclaimer: Heavy-duty restraints such as straightjackets are deemed "too complex" to release from.
+ "}
return dat
-
/obj/item/implanter/freedom
name = "implanter" // Skyrat edit , was implanter (freedom)
imp_type = /obj/item/implant/freedom
diff --git a/code/game/objects/items/manuals.dm b/code/game/objects/items/manuals.dm
index bcae55357e46b6..9d8cd8e9426e9d 100644
--- a/code/game/objects/items/manuals.dm
+++ b/code/game/objects/items/manuals.dm
@@ -431,9 +431,10 @@
H.gib_animation()
sleep(0.3 SECONDS)
H.adjustBruteLoss(1000) //to make the body super-bloody
+ // if we use gib() then the body gets deleted
H.spawn_gibs()
- H.spill_organs()
- H.spread_bodyparts()
+ H.spill_organs(DROP_ALL_REMAINS)
+ H.spread_bodyparts(DROP_BRAIN)
return BRUTELOSS
/obj/item/book/manual/wiki/plumbing
diff --git a/code/game/objects/items/plushes.dm b/code/game/objects/items/plushes.dm
index 77b76d852240d3..eab3f34d343fe1 100644
--- a/code/game/objects/items/plushes.dm
+++ b/code/game/objects/items/plushes.dm
@@ -584,6 +584,7 @@
squeak_override = list('sound/effects/blobattack.ogg' = 1)
gender = FEMALE //given all the jokes and drawings, I'm not sure the xenobiologists would make a slimeboy
+// This is supposed to be only in the bus ruin, don't spawn it elsewhere
/obj/item/toy/plush/awakenedplushie
name = "awakened plushie"
desc = "An ancient plushie that has grown enlightened to the true nature of reality."
diff --git a/code/game/objects/items/puzzle_pieces.dm b/code/game/objects/items/puzzle_pieces.dm
index fc80355a7cc59b..9bf33e36f2fe9e 100644
--- a/code/game/objects/items/puzzle_pieces.dm
+++ b/code/game/objects/items/puzzle_pieces.dm
@@ -61,6 +61,19 @@
fire = 100
acid = 100
+/obj/machinery/door/puzzle/Initialize(mapload)
+ . = ..()
+ RegisterSignal(SSdcs, COMSIG_GLOB_PUZZLE_COMPLETED, PROC_REF(try_signal))
+
+/obj/machinery/door/puzzle/Destroy(force)
+ . = ..()
+ UnregisterSignal(SSdcs, COMSIG_GLOB_PUZZLE_COMPLETED)
+
+/obj/machinery/door/puzzle/proc/try_signal(datum/source, try_id)
+ SIGNAL_HANDLER
+
+ INVOKE_ASYNC(src, PROC_REF(try_puzzle_open), try_id)
+
/obj/machinery/door/puzzle/Bumped(atom/movable/AM)
return !density && ..()
@@ -111,15 +124,6 @@
/obj/machinery/door/puzzle/light
desc = "This door only opens when a linked mechanism is powered. It looks virtually indestructible."
-/obj/machinery/door/puzzle/light/Initialize(mapload)
- . = ..()
- RegisterSignal(SSdcs, COMSIG_GLOB_LIGHT_MECHANISM_COMPLETED, PROC_REF(check_mechanism))
-
-/obj/machinery/door/puzzle/light/proc/check_mechanism(datum/source, try_id)
- SIGNAL_HANDLER
-
- INVOKE_ASYNC(src, PROC_REF(try_puzzle_open), try_id)
-
//*************************
//***Box Pushing Puzzles***
//*************************
@@ -268,5 +272,5 @@
return
visible_message(span_boldnotice("[src] becomes fully charged!"))
powered = TRUE
- SEND_GLOBAL_SIGNAL(COMSIG_GLOB_LIGHT_MECHANISM_COMPLETED, puzzle_id)
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PUZZLE_COMPLETED, puzzle_id)
playsound(src, 'sound/machines/synth_yes.ogg', 100, TRUE)
diff --git a/code/game/objects/items/rcd/RCD.dm b/code/game/objects/items/rcd/RCD.dm
index bf6f5c2ab1f0a4..33994c91774405 100644
--- a/code/game/objects/items/rcd/RCD.dm
+++ b/code/game/objects/items/rcd/RCD.dm
@@ -76,6 +76,7 @@
list(FURNISH_TYPE = /obj/structure/table/glass, ICON = "glass_table", TITLE = "Glass Table"),
list(FURNISH_TYPE = /obj/structure/rack, ICON = "rack", TITLE = "Rack"),
list(FURNISH_TYPE = /obj/structure/bed, ICON = "bed", TITLE = "Bed"),
+ list(FURNISH_TYPE = /obj/machinery/microwave/engineering, ICON = "engi_mw_complete", TITLE = "Wireless Microwave"),
),
),
@@ -259,7 +260,7 @@ GLOBAL_VAR_INIT(icon_holographic_window, init_holographic_window())
useResource(16, user)
activate()
playsound(loc, 'sound/machines/click.ogg', 50, 1)
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
return MANUAL_SUICIDE
user.visible_message(span_suicide("[user] pulls the trigger... But there is not enough ammo!"))
diff --git a/code/game/objects/items/religion.dm b/code/game/objects/items/religion.dm
index f58d3ad93cf636..e68a1b8c88324d 100644
--- a/code/game/objects/items/religion.dm
+++ b/code/game/objects/items/religion.dm
@@ -64,15 +64,18 @@
/obj/item/banner/proc/check_inspiration(mob/living/carbon/human/H) //Banner-specific conditions for being eligible
return
-/obj/item/banner/proc/inspiration(mob/living/carbon/human/H)
- H.adjustBruteLoss(-15)
- H.adjustFireLoss(-15)
- H.AdjustStun(-40)
- H.AdjustKnockdown(-40)
- H.AdjustImmobilized(-40)
- H.AdjustParalyzed(-40)
- H.AdjustUnconscious(-40)
- playsound(H, 'sound/magic/staff_healing.ogg', 25, FALSE)
+/obj/item/banner/proc/inspiration(mob/living/carbon/human/inspired_human)
+ var/need_mob_update = FALSE
+ need_mob_update += inspired_human.adjustBruteLoss(-15, updating_health = FALSE)
+ need_mob_update += inspired_human.adjustFireLoss(-15, updating_health = FALSE)
+ if(need_mob_update)
+ inspired_human.updatehealth()
+ inspired_human.AdjustStun(-40)
+ inspired_human.AdjustKnockdown(-40)
+ inspired_human.AdjustImmobilized(-40)
+ inspired_human.AdjustParalyzed(-40)
+ inspired_human.AdjustUnconscious(-40)
+ playsound(inspired_human, 'sound/magic/staff_healing.ogg', 25, FALSE)
/obj/item/banner/proc/special_inspiration(mob/living/carbon/human/H) //Any banner-specific inspiration effects go here
return
@@ -128,10 +131,13 @@
/obj/item/clothing/under/rank/medical/doctor = 1)
category = CAT_MISC
-/obj/item/banner/medical/special_inspiration(mob/living/carbon/human/H)
- H.adjustToxLoss(-15)
- H.setOxyLoss(0)
- H.reagents.add_reagent(/datum/reagent/medicine/inaprovaline, 5)
+/obj/item/banner/medical/special_inspiration(mob/living/carbon/human/inspired_human)
+ var/need_mob_update = FALSE
+ need_mob_update += inspired_human.adjustToxLoss(-15, updating_health = FALSE)
+ need_mob_update += inspired_human.setOxyLoss(0, updating_health = FALSE)
+ if(need_mob_update)
+ inspired_human.updatehealth()
+ inspired_human.reagents.add_reagent(/datum/reagent/medicine/inaprovaline, 5)
/obj/item/banner/science
name = "sciencia banner"
diff --git a/code/game/objects/items/robot/items/storage.dm b/code/game/objects/items/robot/items/storage.dm
index 6570e159b6a094..4995c7d9df6066 100644
--- a/code/game/objects/items/robot/items/storage.dm
+++ b/code/game/objects/items/robot/items/storage.dm
@@ -57,18 +57,20 @@
/obj/item/borg/apparatus/pre_attack(atom/atom, mob/living/user, params)
if(!stored)
- var/itemcheck = FALSE
- for(var/storable_type in storable)
- if(istype(atom, storable_type))
- itemcheck = TRUE
- break
- if(itemcheck)
- var/obj/item/item = atom
- item.forceMove(src)
- stored = item
- RegisterSignal(stored, COMSIG_ATOM_UPDATED_ICON, PROC_REF(on_stored_updated_icon))
- update_appearance()
- return TRUE
+ // Borgs should not be grabbing their own modules
+ if(!istype(atom.loc, /mob/living/silicon/robot))
+ var/itemcheck = FALSE
+ for(var/storable_type in storable)
+ if(istype(atom, storable_type))
+ itemcheck = TRUE
+ break
+ if(itemcheck)
+ var/obj/item/item = atom
+ item.forceMove(src)
+ stored = item
+ RegisterSignal(stored, COMSIG_ATOM_UPDATED_ICON, PROC_REF(on_stored_updated_icon))
+ update_appearance()
+ return TRUE
else
stored.melee_attack_chain(user, atom, params)
return TRUE
diff --git a/code/game/objects/items/skub.dm b/code/game/objects/items/skub.dm
index 7e9cd381e336b9..12e6da344d0b85 100644
--- a/code/game/objects/items/skub.dm
+++ b/code/game/objects/items/skub.dm
@@ -13,6 +13,6 @@
/obj/item/skub/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] has declared themself as anti-skub! The skub tears them apart!"))
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
playsound(src, 'sound/items/eatfood.ogg', 50, TRUE, -1)
return MANUAL_SUICIDE
diff --git a/code/game/objects/items/spear.dm b/code/game/objects/items/spear.dm
index d9f13911731db4..410cddd1fc079d 100644
--- a/code/game/objects/items/spear.dm
+++ b/code/game/objects/items/spear.dm
@@ -152,7 +152,7 @@
user.say("[war_cry]", forced="spear warcry")
explosive.forceMove(user)
explosive.detonate()
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
qdel(src)
return BRUTELOSS
diff --git a/code/game/objects/items/stacks/golem_food/golem_hand_actions.dm b/code/game/objects/items/stacks/golem_food/golem_hand_actions.dm
index fc87d09447744d..39c17d2346c6a5 100644
--- a/code/game/objects/items/stacks/golem_food/golem_hand_actions.dm
+++ b/code/game/objects/items/stacks/golem_food/golem_hand_actions.dm
@@ -35,7 +35,7 @@
playsound(src, 'sound/weapons/sonic_jackhammer.ogg', 50, TRUE)
held_gibtonite.forceMove(get_turf(src))
held_gibtonite.det_time = 2 SECONDS
- held_gibtonite.GibtoniteReaction(user)
+ held_gibtonite.GibtoniteReaction(user, "A [src] has targeted [target] with a thrown and primed")
held_gibtonite.throw_at(target, range = 10, speed = 3, thrower = user)
held_gibtonite = null
qdel(src)
@@ -44,7 +44,7 @@
/// Called when you can't hold it in any longer and just drop it on the ground
/obj/item/gibtonite_hand/proc/release_gibtonite()
held_gibtonite.forceMove(get_turf(src))
- held_gibtonite.GibtoniteReaction(isliving(loc) ? loc : null)
+ held_gibtonite.GibtoniteReaction(isliving(loc) ? loc : null, "A [src] has dropped and primed a")
held_gibtonite = null
qdel(src)
diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm
index 081ab5d78e015e..f4a61f2d7ca4d2 100644
--- a/code/game/objects/items/stacks/medical.dm
+++ b/code/game/objects/items/stacks/medical.dm
@@ -159,6 +159,15 @@
splint_factor = 0.7
burn_cleanliness_bonus = 0.35
merge_type = /obj/item/stack/medical/gauze
+ var/obj/item/bodypart/gauzed_bodypart
+
+/obj/item/stack/medical/gauze/Destroy(force)
+ . = ..()
+
+ if (gauzed_bodypart)
+ gauzed_bodypart.current_gauze = null
+ SEND_SIGNAL(gauzed_bodypart, COMSIG_BODYPART_UNGAUZED, src)
+ gauzed_bodypart = null
// gauze is only relevant for wounds, which are handled in the wounds themselves
/obj/item/stack/medical/gauze/try_heal(mob/living/patient, mob/user, silent)
@@ -474,3 +483,27 @@
/obj/item/stack/medical/poultice/post_heal_effects(amount_healed, mob/living/carbon/healed_mob, mob/user)
. = ..()
healed_mob.adjustOxyLoss(amount_healed)
+
+/obj/item/stack/medical/bandage
+ name = "first aid bandage"
+ desc = "A DeForest brand bandage designed for basic first aid on blunt-force trauma."
+ icon_state = "bandage"
+ inhand_icon_state = "bandage"
+ novariants = TRUE
+ amount = 1
+ max_amount = 1
+ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
+ heal_brute = 25
+ stop_bleeding = 0.2
+ self_delay = 3 SECONDS
+ other_delay = 1 SECONDS
+ grind_results = list(/datum/reagent/medicine/c2/libital = 2)
+
+/obj/item/stack/medical/bandage/makeshift
+ name = "makeshift bandage"
+ desc = "A hastily constructed bandage designed for basic first aid on blunt-force trauma."
+ icon_state = "bandage_makeshift"
+ icon_state_preview = "bandage_makeshift"
+ inhand_icon_state = "bandage"
+ novariants = TRUE
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 381cdd7a2962d8..1ad86868212f5c 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -609,7 +609,8 @@ GLOBAL_LIST_INIT(cardboard_recipes, list ( \
new /datum/stack_recipe("light bulbs box", /obj/item/storage/box/lights/bulbs, check_density = FALSE, category = CAT_CONTAINERS), \
new /datum/stack_recipe("mixed lights box", /obj/item/storage/box/lights/mixed, check_density = FALSE, category = CAT_CONTAINERS), \
new /datum/stack_recipe("mouse traps box", /obj/item/storage/box/mousetraps, check_density = FALSE, category = CAT_CONTAINERS), \
- new /datum/stack_recipe("candle box", /obj/item/storage/fancy/candle_box, check_density = FALSE, category = CAT_CONTAINERS)
+ new /datum/stack_recipe("candle box", /obj/item/storage/fancy/candle_box, check_density = FALSE, category = CAT_CONTAINERS), \
+ new /datum/stack_recipe("bandage box", /obj/item/storage/box/bandages, check_density = FALSE, category = CAT_CONTAINERS)
)),
null, \
diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm
index a67d85979f1040..abd9f8efb2f93c 100644
--- a/code/game/objects/items/storage/belt.dm
+++ b/code/game/objects/items/storage/belt.dm
@@ -235,7 +235,7 @@
/obj/item/healthanalyzer,
/obj/item/hemostat,
/obj/item/holosign_creator/medical,
- /obj/item/hypospray/mkii, //SKYRAT EDIT HYPOSPRAYS
+ /obj/item/hypospray/mkii, //SKYRAT EDIT ADDITION - HYPOSPRAYS
/obj/item/implant,
/obj/item/implantcase,
/obj/item/implanter,
@@ -248,7 +248,7 @@
/obj/item/reagent_containers/dropper,
/obj/item/reagent_containers/cup/beaker,
/obj/item/reagent_containers/cup/bottle,
- /obj/item/reagent_containers/cup/vial, //SKYRAT EDIT HYPOSPRAYS
+ /obj/item/reagent_containers/cup/vial, //SKYRAT EDIT ADDITION - HYPOSPRAYS
/obj/item/reagent_containers/cup/tube,
/obj/item/reagent_containers/hypospray,
/obj/item/reagent_containers/medigel,
@@ -694,6 +694,7 @@
atom_storage.max_slots = 6
atom_storage.max_specific_storage = WEIGHT_CLASS_NORMAL // Set to this so the light replacer can fit.
atom_storage.set_holdable(list(
+ /obj/item/access_key,
/obj/item/assembly/mousetrap,
/obj/item/clothing/gloves,
/obj/item/flashlight,
diff --git a/code/game/objects/items/storage/boxes/medical_boxes.dm b/code/game/objects/items/storage/boxes/medical_boxes.dm
index ad0fd9b37f54b1..ff4b232454c771 100644
--- a/code/game/objects/items/storage/boxes/medical_boxes.dm
+++ b/code/game/objects/items/storage/boxes/medical_boxes.dm
@@ -129,3 +129,43 @@
/obj/item/reagent_containers/cup/beaker/meta/rezadone = 1,
)
generate_items_inside(items_inside, src)
+
+/obj/item/storage/box/bandages
+ name = "box of bandages"
+ desc = "A box of DeForest brand gel bandages designed to treat blunt-force trauma."
+ icon = 'icons/obj/storage/box.dmi' // SKYRAT EDIT CHANGE
+ icon_state = "brutebox"
+ base_icon_state = "brutebox"
+ inhand_icon_state = "brutebox"
+ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
+ drop_sound = 'sound/items/handling/matchbox_drop.ogg'
+ pickup_sound = 'sound/items/handling/matchbox_pickup.ogg'
+ illustration = null
+ w_class = WEIGHT_CLASS_SMALL
+ custom_price = PAYCHECK_CREW * 1.75
+
+/obj/item/storage/box/bandages/Initialize(mapload)
+ . = ..()
+ atom_storage.max_slots = 6
+ atom_storage.set_holdable(list(
+ /obj/item/stack/medical/bandage,
+ /obj/item/reagent_containers/pill,
+ /obj/item/reagent_containers/pill/patch,
+ ))
+
+/obj/item/storage/box/bandages/PopulateContents()
+ for(var/i in 1 to 5)
+ new /obj/item/stack/medical/bandage(src)
+
+/obj/item/storage/box/bandages/update_icon_state()
+ . = ..()
+ switch(length(contents))
+ if(5)
+ icon_state = "[base_icon_state]_f"
+ if(3 to 4)
+ icon_state = "[base_icon_state]_almostfull"
+ if(1 to 2)
+ icon_state = "[base_icon_state]_almostempty"
+ if(0)
+ icon_state = base_icon_state
diff --git a/code/game/objects/items/storage/holsters.dm b/code/game/objects/items/storage/holsters.dm
index 7b8bc6a6716f48..cb722469a2c033 100644
--- a/code/game/objects/items/storage/holsters.dm
+++ b/code/game/objects/items/storage/holsters.dm
@@ -30,6 +30,8 @@
/obj/item/food/grown/banana,
/obj/item/gun/energy/laser/thermal,
/obj/item/gun/ballistic/rifle/boltaction, //fits if you make it an obrez
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
/obj/item/storage/belt/holster/energy
@@ -47,6 +49,8 @@
/obj/item/food/grown/banana,
/obj/item/gun/energy/laser/thermal,
/obj/item/gun/energy/recharge/ebow,
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
/obj/item/storage/belt/holster/energy/thermal
@@ -100,6 +104,8 @@
/obj/item/gun/energy/disabler,
/obj/item/gun/energy/dueling,
/obj/item/gun/energy/laser/thermal,
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
/obj/item/gun/ballistic/rifle/boltaction, //fits if you make it an obrez
))
@@ -150,7 +156,9 @@
/obj/item/gun/energy/recharge/ebow,
/obj/item/gun/energy/e_gun/mini,
/obj/item/gun/energy/disabler,
- /obj/item/gun/energy/dueling
+ /obj/item/gun/energy/dueling,
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
atom_storage.silent = TRUE
@@ -176,3 +184,18 @@
/obj/item/ammo_casing, // For shotgun shells, rockets, launcher grenades, and a few other things.
/obj/item/grenade, // All regular grenades, the big grenade launcher fires these.
))
+
+/obj/item/storage/belt/holster/nukie/cowboy
+ desc = "A deep shoulder holster capable of holding almost any form of small firearm and its ammo. This one's specialized for handguns."
+
+/obj/item/storage/belt/holster/nukie/cowboy/Initialize(mapload)
+ . = ..()
+ atom_storage.max_slots = 3
+ atom_storage.max_specific_storage = WEIGHT_CLASS_NORMAL
+
+/obj/item/storage/belt/holster/nukie/cowboy/full/PopulateContents()
+ generate_items_inside(list(
+ /obj/item/gun/ballistic/revolver/syndicate/cowboy = 1,
+ /obj/item/ammo_box/a357 = 2,
+ ), src)
+
diff --git a/code/game/objects/items/storage/medkit.dm b/code/game/objects/items/storage/medkit.dm
index 6ca84e415e01b0..4b08d1551a34e3 100644
--- a/code/game/objects/items/storage/medkit.dm
+++ b/code/game/objects/items/storage/medkit.dm
@@ -34,6 +34,7 @@
/obj/item/reagent_containers/medigel,
/obj/item/reagent_containers/spray,
/obj/item/lighter,
+ /obj/item/storage/box/bandages,
/obj/item/storage/fancy/cigarettes,
/obj/item/storage/pill_bottle,
/obj/item/stack/medical,
@@ -288,18 +289,19 @@
if(empty)
return
var/static/list/items_inside = list(
+ /obj/item/cautery = 1,
+ /obj/item/scalpel = 1,
+ /obj/item/healthanalyzer/advanced = 1,
+ /obj/item/hemostat = 1,
+ /obj/item/reagent_containers/medigel/sterilizine = 1,
+ /obj/item/storage/box/bandages = 1,
+ /obj/item/surgical_drapes = 1,
+ /obj/item/reagent_containers/hypospray/medipen/atropine = 2,
+ /obj/item/stack/medical/gauze = 2,
/obj/item/stack/medical/suture/medicated = 2,
/obj/item/stack/medical/mesh/advanced = 2,
/obj/item/reagent_containers/pill/patch/libital = 4,
/obj/item/reagent_containers/pill/patch/aiuri = 4,
- /obj/item/healthanalyzer/advanced = 1,
- /obj/item/stack/medical/gauze = 2,
- /obj/item/reagent_containers/hypospray/medipen/atropine = 2,
- /obj/item/reagent_containers/medigel/sterilizine = 1,
- /obj/item/surgical_drapes = 1,
- /obj/item/scalpel = 1,
- /obj/item/hemostat = 1,
- /obj/item/cautery = 1,
)
generate_items_inside(items_inside,src)
@@ -332,6 +334,7 @@
/obj/item/mod/module/health_analyzer = 1,
/obj/item/autosurgeon/syndicate/emaggedsurgerytoolset = 1,
/obj/item/reagent_containers/hypospray/combat/empty = 1,
+ /obj/item/storage/box/bandages = 1,
/obj/item/storage/box/evilmeds = 1,
/obj/item/reagent_containers/medigel/sterilizine = 1,
/obj/item/clothing/glasses/hud/health/night/science = 1,
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index 9fa66404aeda3a..fc1b264b811805 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -789,6 +789,21 @@
for(var/i in 1 to poster_count)
new /obj/item/poster/traitor(src)
+/obj/item/storage/box/syndie_kit/cowboy
+ name = "western outlaw pack"
+ desc = "Contains everything you'll need to be the rootin' tootin' cowboy you always wanted. Either play the Lone Ranger or go in with your posse of outlaws."
+
+/obj/item/storage/box/syndie_kit/cowboy/PopulateContents()
+ generate_items_inside(list(
+ /obj/item/clothing/shoes/cowboy/black/syndicate= 1,
+ /obj/item/clothing/head/cowboy/black/syndicate = 1,
+ /obj/item/storage/belt/holster/nukie/cowboy/full = 1,
+ /obj/item/clothing/under/costume/dutch/syndicate = 1,
+ /obj/item/lighter/skull = 1,
+ /obj/item/sbeacondrop/horse = 1,
+ /obj/item/food/grown/apple = 1,
+ ), src)
+
#undef KIT_RECON
#undef KIT_BLOODY_SPAI
#undef KIT_STEALTHY
diff --git a/code/game/objects/items/surgery_tray.dm b/code/game/objects/items/surgery_tray.dm
index 37494a39b555e0..edd92522446d1f 100644
--- a/code/game/objects/items/surgery_tray.dm
+++ b/code/game/objects/items/surgery_tray.dm
@@ -6,6 +6,7 @@
/datum/storage/surgery_tray/New()
. = ..()
set_holdable(list(
+ /obj/item/autopsy_scanner,
/obj/item/blood_filter,
/obj/item/bonesetter,
/obj/item/cautery,
diff --git a/code/game/objects/items/tail_pin.dm b/code/game/objects/items/tail_pin.dm
index 3052075c94d145..de3148dd06dea3 100644
--- a/code/game/objects/items/tail_pin.dm
+++ b/code/game/objects/items/tail_pin.dm
@@ -2,7 +2,7 @@
icon = 'icons/obj/poster.dmi'
icon_state = "tailpin"
name = "tail pin"
- desc = "Offically branded 'pin the tail on the corgi' style party implement. Not intended to be used on people."
+ desc = "Officially branded 'pin the tail on the corgi' style party implement. Not intended to be used on people."
force = 0
w_class = WEIGHT_CLASS_SMALL
throwforce = 0
diff --git a/code/game/objects/items/teleportation.dm b/code/game/objects/items/teleportation.dm
index 533fc755fa1881..07e178acba5f9c 100644
--- a/code/game/objects/items/teleportation.dm
+++ b/code/game/objects/items/teleportation.dm
@@ -489,7 +489,7 @@
destination.ex_act(EXPLODE_HEAVY)
victim.unequip_everything()
victim.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- victim.gib()
+ victim.gib(DROP_ALL_REMAINS)
///Damage and stun all mobs in fragging_location turf, called after a teleport
/obj/item/syndicate_teleporter/proc/telefrag(turf/fragging_location, mob/user) // Don't let this gib. Never let this gib.
diff --git a/code/game/objects/items/tongs.dm b/code/game/objects/items/tongs.dm
new file mode 100644
index 00000000000000..8e7753bf488493
--- /dev/null
+++ b/code/game/objects/items/tongs.dm
@@ -0,0 +1,100 @@
+/// Tongs, let you pick up and feed people food from further away.
+/obj/item/kitchen/tongs
+ name = "tongs"
+ desc = "So you never have to touch anything with your dirty, unwashed hands."
+ reach = 2
+ icon_state = "tongs"
+ base_icon_state = "tongs"
+ inhand_icon_state = "fork" // close enough
+ attack_verb_continuous = list("pinches", "tongs", "nips")
+ attack_verb_simple = list("pinch", "tong", "nip")
+ /// What are we holding in our tongs?
+ var/obj/item/tonged
+ /// Sound to play when we click our tongs together
+ var/clack_sound = 'sound/items/handling/component_drop.ogg'
+ /// Time to wait between clacking sounds
+ var/clack_delay = 2 SECONDS
+ /// Have we clacked recently?
+ COOLDOWN_DECLARE(clack_cooldown)
+
+/obj/item/kitchen/tongs/Destroy(force)
+ QDEL_NULL(tonged)
+ return ..()
+
+/obj/item/kitchen/tongs/examine(mob/user)
+ . = ..()
+ if (!isnull(tonged))
+ . += span_notice("It is holding [tonged].")
+
+/obj/item/kitchen/tongs/dropped(mob/user, silent)
+ . = ..()
+ drop_tonged()
+
+/obj/item/kitchen/tongs/attack_self(mob/user, modifiers)
+ . = ..()
+ if(.)
+ return TRUE
+ if (!isnull(tonged))
+ drop_tonged()
+ return TRUE
+ if (!COOLDOWN_FINISHED(src, clack_cooldown))
+ return TRUE
+ user.visible_message(span_notice("[user] clacks [user.p_their()] [src] together like a crab. Click clack!"))
+ click_clack()
+ return TRUE
+
+/// Release the food we are holding
+/obj/item/kitchen/tongs/proc/drop_tonged()
+ if (isnull(tonged))
+ return
+ visible_message(span_notice("[tonged] falls to the ground!"))
+ var/turf/location = drop_location()
+ tonged.forceMove(location)
+ tonged.do_drop_animation(location)
+
+/// Play a clacking sound and appear closed, then open again
+/obj/item/kitchen/tongs/proc/click_clack()
+ COOLDOWN_START(src, clack_cooldown, clack_delay)
+ playsound(src, clack_sound, vol = 100, vary = FALSE)
+ icon_state = "[base_icon_state]_closed"
+ var/delay = min(0.5 SECONDS, clack_delay / 2) // Just in case someone's been fucking with the cooldown
+ addtimer(CALLBACK(src, PROC_REF(clack)), delay, TIMER_DELETE_ME)
+
+/// Plays a clacking sound and appear open
+/obj/item/kitchen/tongs/proc/clack()
+ playsound(src, clack_sound, vol = 100, vary = FALSE)
+ icon_state = base_icon_state
+
+/obj/item/kitchen/tongs/Exited(atom/movable/leaving, direction)
+ . = ..()
+ if (leaving != tonged)
+ return
+ tonged = null
+ update_appearance(UPDATE_ICON)
+
+/obj/item/kitchen/tongs/pre_attack(obj/item/attacked, mob/living/user, params)
+ if (!isnull(tonged))
+ attacked.attackby(tonged, user)
+ return TRUE
+ if (isliving(attacked))
+ if (COOLDOWN_FINISHED(src, clack_cooldown))
+ click_clack()
+ return ..()
+ if (!IsEdible(attacked) || attacked.w_class > WEIGHT_CLASS_NORMAL || !isnull(tonged))
+ return ..()
+ tonged = attacked
+ attacked.do_pickup_animation(src)
+ attacked.forceMove(src)
+ update_appearance(UPDATE_ICON)
+
+/obj/item/kitchen/tongs/update_overlays()
+ . = ..()
+ if (isnull(tonged))
+ return
+ var/mutable_appearance/held_food = new /mutable_appearance(tonged.appearance)
+ held_food.layer = layer
+ held_food.plane = plane
+ held_food.transform = held_food.transform.Scale(0.7, 0.7)
+ held_food.pixel_x = 6
+ held_food.pixel_y = 6
+ . += held_food
diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm
index 21fc15572bfc25..8c949b36a79e49 100644
--- a/code/game/objects/items/weaponry.dm
+++ b/code/game/objects/items/weaponry.dm
@@ -922,7 +922,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
if(isliving(target))
var/mob/living/bug = target
bug.investigate_log("has been splatted by a flyswatter.", INVESTIGATE_DEATHS)
- bug.gib()
+ bug.gib(DROP_ALL_REMAINS)
else
qdel(target)
return
@@ -1099,7 +1099,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
if(living_target.stat == DEAD && prob(force*damage_mod*0.5))
living_target.visible_message(span_danger("[living_target] explodes in a shower of gore!"), blind_message = span_hear("You hear organic matter ripping and tearing!"))
living_target.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- living_target.gib()
+ living_target.gib(DROP_ALL_REMAINS)
log_combat(user, living_target, "gibbed", src)
else if(target.uses_integrity)
target.take_damage(force*damage_mod*3, BRUTE, MELEE, FALSE, null, 50)
diff --git a/code/game/objects/structures/bonfire.dm b/code/game/objects/structures/bonfire.dm
index 3ca135a5786003..a6cbbf8b0099ac 100644
--- a/code/game/objects/structures/bonfire.dm
+++ b/code/game/objects/structures/bonfire.dm
@@ -17,19 +17,14 @@
anchored = TRUE
buckle_lying = 0
pass_flags_self = PASSTABLE | LETPASSTHROW
- ///is the bonfire lit?
+ /// is the bonfire lit?
var/burning = FALSE
- ///icon for the bonfire while on. for a softer more burning embers icon, use "bonfire_warm"
+ /// icon for the bonfire while on. for a softer more burning embers icon, use "bonfire_warm"
var/burn_icon = "bonfire_on_fire"
- ///if the bonfire has a grill attached
+ /// if the bonfire has a grill attached
var/grill = FALSE
-
-/obj/structure/bonfire/dense
- density = TRUE
-
-/obj/structure/bonfire/prelit/Initialize(mapload)
- . = ..()
- start_burning()
+ /// the looping sound effect that is played while burning
+ var/datum/looping_sound/burning/burning_loop
/obj/structure/bonfire/Initialize(mapload)
. = ..()
@@ -37,6 +32,12 @@
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
+ burning_loop = new(src)
+
+/obj/structure/bonfire/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ QDEL_NULL(burning_loop)
+ . = ..()
/obj/structure/bonfire/attackby(obj/item/used_item, mob/living/user, params)
if(istype(used_item, /obj/item/stack/rods) && !can_buckle && !grill)
@@ -77,7 +78,6 @@
else
return ..()
-
/obj/structure/bonfire/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
@@ -107,6 +107,8 @@
/obj/structure/bonfire/proc/start_burning()
if(burning || !check_oxygen())
return
+
+ burning_loop.start()
icon_state = burn_icon
burning = TRUE
set_light(6)
@@ -169,6 +171,8 @@
. = ..()
if(!burning)
return
+
+ burning_loop.stop()
icon_state = "bonfire"
burning = FALSE
set_light(0)
@@ -183,4 +187,11 @@
if(..())
buckled_mob.pixel_y -= 13
+/obj/structure/bonfire/dense
+ density = TRUE
+
+/obj/structure/bonfire/prelit/Initialize(mapload)
+ . = ..()
+ start_burning()
+
#undef BONFIRE_FIRE_STACK_STRENGTH
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/security.dm b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
index 9700a3e80fa98f..ec46756140ca25 100755
--- a/code/game/objects/structures/crates_lockers/closets/secure/security.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
@@ -17,7 +17,7 @@
new /obj/item/radio/headset/heads/captain/alt(src)
new /obj/item/radio/headset/heads/captain(src)
new /obj/item/storage/belt/sabre(src)
- new /obj/item/storage/box/gunset/pdh(src) // SKYRAT EDIT CHANGE - ORIGINAL: new /obj/item/gun/energy/e_gun(src)
+ new /obj/item/gun/energy/e_gun(src)
new /obj/item/door_remote/captain(src)
new /obj/item/storage/photo_album/captain(src)
@@ -25,7 +25,6 @@
name = "head of personnel's locker"
icon_state = "hop"
req_access = list(ACCESS_HOP)
- storage_capacity = 40 //SKYRAT EDIT ADDITION
/obj/structure/closet/secure_closet/hop/PopulateContents()
..()
diff --git a/code/game/objects/structures/divine.dm b/code/game/objects/structures/divine.dm
index cda00d98d0b37f..ef9f650e42a562 100644
--- a/code/game/objects/structures/divine.dm
+++ b/code/game/objects/structures/divine.dm
@@ -18,7 +18,7 @@
return
to_chat(user, span_notice("Invoking the sacred ritual, you sacrifice [L]."))
L.investigate_log("has been sacrificially gibbed on an altar.", INVESTIGATE_DEATHS)
- L.gib()
+ L.gib(DROP_ALL_REMAINS)
message_admins("[ADMIN_LOOKUPFLW(user)] has sacrificed [key_name_admin(L)] on the sacrificial altar at [AREACOORD(src)].")
/obj/structure/healingfountain
diff --git a/code/game/objects/structures/fireaxe.dm b/code/game/objects/structures/fireaxe.dm
index 59a00618b0e915..31ba1cd47aa59f 100644
--- a/code/game/objects/structures/fireaxe.dm
+++ b/code/game/objects/structures/fireaxe.dm
@@ -211,6 +211,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fireaxecabinet, 32)
/obj/structure/fireaxecabinet/empty
populate_contents = FALSE
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fireaxecabinet/empty, 32)
+
/obj/item/wallframe/fireaxecabinet
name = "fire axe cabinet"
desc = "Home to a window's greatest nightmare. Apply to wall to use."
@@ -238,6 +240,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fireaxecabinet/mechremoval, 32)
/obj/structure/fireaxecabinet/mechremoval/empty
populate_contents = FALSE
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fireaxecabinet/mechremoval/empty, 32)
+
/obj/item/wallframe/fireaxecabinet/mechremoval
name = "mech removal tool cabinet"
desc = "Home to a very special crowbar. Apply to wall to use."
diff --git a/code/game/objects/structures/fireplace.dm b/code/game/objects/structures/fireplace.dm
index afd4d27efe6c63..8c9d09585c998a 100644
--- a/code/game/objects/structures/fireplace.dm
+++ b/code/game/objects/structures/fireplace.dm
@@ -14,17 +14,22 @@
light_color = LIGHT_COLOR_FIRE
light_angle = 170
light_flags = LIGHT_IGNORE_OFFSET
+ /// is the fireplace lit?
var/lit = FALSE
-
+ /// the amount of fuel for the fire
var/fuel_added = 0
+ /// how much time is left before fire runs out of fuel
var/flame_expiry_timer
+ /// the looping sound effect that is played while burning
+ var/datum/looping_sound/burning/burning_loop
/obj/structure/fireplace/Initialize(mapload)
. = ..()
- START_PROCESSING(SSobj, src)
+ burning_loop = new(src)
/obj/structure/fireplace/Destroy()
STOP_PROCESSING(SSobj, src)
+ QDEL_NULL(burning_loop)
. = ..()
/obj/structure/fireplace/setDir(newdir)
@@ -126,7 +131,6 @@
put_out()
return
- playsound(src, 'sound/effects/comfyfire.ogg',50,FALSE, FALSE, TRUE)
var/turf/T = get_turf(src)
T.hotspot_expose(700, 2.5 * seconds_per_tick)
update_appearance()
@@ -155,6 +159,8 @@
return max(0, fuel_added)
/obj/structure/fireplace/proc/ignite()
+ START_PROCESSING(SSobj, src)
+ burning_loop.start()
lit = TRUE
desc = "A large stone brick fireplace, warm and cozy."
flame_expiry_timer = world.time + fuel_added
@@ -163,6 +169,8 @@
adjust_light()
/obj/structure/fireplace/proc/put_out()
+ STOP_PROCESSING(SSobj, src)
+ burning_loop.stop()
lit = FALSE
update_appearance()
adjust_light()
diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm
index 7393c20758e245..add1f278569fc0 100644
--- a/code/game/objects/structures/icemoon/cave_entrance.dm
+++ b/code/game/objects/structures/icemoon/cave_entrance.dm
@@ -20,6 +20,9 @@ GLOBAL_LIST_INIT(ore_probability, list(
mob_types = list(/mob/living/simple_animal/hostile/asteroid/wolf)
move_resist = INFINITY
anchored = TRUE
+ scanner_taggable = TRUE
+ mob_gps_id = "WF" // wolf
+ spawner_gps_id = "Animal Den"
/obj/structure/spawner/ice_moon/Initialize(mapload)
. = ..()
@@ -65,6 +68,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
max_mobs = 1
spawn_time = 60 SECONDS
mob_types = list(/mob/living/simple_animal/hostile/asteroid/polarbear)
+ mob_gps_id = "BR" // bear
/obj/structure/spawner/ice_moon/polarbear/clear_rock()
for(var/turf/potential in RANGE_TURFS(1, src))
@@ -76,9 +80,11 @@ GLOBAL_LIST_INIT(ore_probability, list(
name = "demonic portal"
desc = "A portal that goes to another world, normal creatures couldn't survive there."
icon_state = "nether"
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/ice_demon)
+ mob_types = list(/mob/living/basic/mining/ice_demon)
light_range = 1
light_color = COLOR_SOFT_RED
+ mob_gps_id = "WT|B" // watcher | bluespace
+ spawner_gps_id = "Netheric Distortion"
/obj/structure/spawner/ice_moon/demonic_portal/Initialize(mapload)
. = ..()
@@ -100,9 +106,11 @@ GLOBAL_LIST_INIT(ore_probability, list(
/obj/structure/spawner/ice_moon/demonic_portal/ice_whelp
mob_types = list(/mob/living/basic/mining/ice_whelp)
+ mob_gps_id = "ID|W" // ice drake | whelp
/obj/structure/spawner/ice_moon/demonic_portal/snowlegion
mob_types = list(/mob/living/basic/mining/legion/snow/spawner_made)
+ mob_gps_id = "LG|S" // legion | snow
/obj/effect/collapsing_demonic_portal
name = "collapsing demonic portal"
diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm
index 22f7f0422cd191..6d6b2e6af37af9 100644
--- a/code/game/objects/structures/lavaland/necropolis_tendril.dm
+++ b/code/game/objects/structures/lavaland/necropolis_tendril.dm
@@ -14,19 +14,22 @@
move_resist=INFINITY // just killing it tears a massive hole in the ground, let's not move it
anchored = TRUE
resistance_flags = FIRE_PROOF | LAVA_PROOF
-
- var/gps = null
var/obj/effect/light_emitter/tendril/emitted_light
-
+ scanner_taggable = TRUE
+ mob_gps_id = "WT"
+ spawner_gps_id = "Necropolis Tendril"
/obj/structure/spawner/lavaland/goliath
mob_types = list(/mob/living/basic/mining/goliath)
+ mob_gps_id = "GL"
/obj/structure/spawner/lavaland/legion
mob_types = list(/mob/living/basic/mining/legion/spawner_made)
+ mob_gps_id = "LG"
/obj/structure/spawner/lavaland/icewatcher
mob_types = list(/mob/living/basic/mining/watcher/icewing)
+ mob_gps_id = "WT|I" // icewing
GLOBAL_LIST_INIT(tendrils, list())
/obj/structure/spawner/lavaland/Initialize(mapload)
@@ -63,7 +66,6 @@ GLOBAL_LIST_INIT(tendrils, list())
L.client.give_award(/datum/award/score/tendril_score, L) //Progresses score by one
GLOB.tendrils -= src
QDEL_NULL(emitted_light)
- QDEL_NULL(gps)
return ..()
/obj/effect/light_emitter/tendril
diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm
index 3c99aee7233d30..6072d905781d6b 100644
--- a/code/game/objects/structures/mirror.dm
+++ b/code/game/objects/structures/mirror.dm
@@ -1,3 +1,20 @@
+
+// Normal Mirrors
+
+#define CHANGE_HAIR "Change Hair"
+#define CHANGE_BEARD "Change Beard"
+
+// Magic Mirrors!
+
+#define CHANGE_RACE "Change Race"
+#define CHANGE_SEX "Change Sex"
+#define CHANGE_NAME "Change Name"
+#define CHANGE_EYES "Change Eyes"
+
+#define INERT_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD)
+#define PRIDE_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD, CHANGE_RACE, CHANGE_SEX, CHANGE_EYES)
+#define MAGIC_MIRROR_OPTIONS list(CHANGE_HAIR, CHANGE_BEARD, CHANGE_RACE, CHANGE_SEX, CHANGE_EYES, CHANGE_NAME)
+
/obj/structure/mirror
name = "mirror"
desc = "Mirror mirror on the wall, who's the most robust of them all?"
@@ -6,8 +23,28 @@
movement_type = FLOATING
density = FALSE
anchored = TRUE
- max_integrity = 200
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
+ ///List of all Races that can be chosen, decided by its Initialize.
+ var/list/selectable_races = list()
+
+/obj/structure/mirror/Initialize(mapload)
+ . = ..()
+ update_choices()
+
+/obj/structure/mirror/Destroy()
+ mirror_options = null
+ selectable_races = null
+ return ..()
+
+/obj/structure/mirror/proc/update_choices()
+ for(var/i in mirror_options)
+ mirror_options[i] = icon('icons/hud/radial.dmi', i)
/obj/structure/mirror/Initialize(mapload)
. = ..()
@@ -40,46 +77,160 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror, 28)
MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
-/* SKYRAT EDIT REMOVAL
-/obj/structure/mirror/attack_hand(mob/user, list/modifiers)
+/obj/structure/mirror/attack_hand(mob/living/carbon/human/user)
. = ..()
- if(.)
- return TRUE
- if(broken || !Adjacent(user))
- return TRUE
- if(!ishuman(user))
+ if(. || !ishuman(user) || broken || !magical_mirror) // SKYRAT EDIT CHANGE - MUNDANE MIRRORS DON'T LET YOU CHANGE - ORIGINAL: if(. || !ishuman(user) || broken)
return TRUE
- var/mob/living/carbon/human/hairdresser = user
- //handle facial hair (if necessary)
- if(hairdresser.gender != FEMALE)
- var/new_style = tgui_input_list(user, "Select a facial hairstyle", "Grooming", GLOB.facial_hairstyles_list)
+ if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH) && !magical_mirror)
+ return TRUE //no tele-grooming (if nonmagical)
+
+ return display_radial_menu(user)
+
+/obj/structure/mirror/proc/display_radial_menu(mob/living/carbon/human/user)
+ var/pick = show_radial_menu(user, src, mirror_options, user, radius = 36, require_near = TRUE)
+ if(!pick)
+ return TRUE //get out
+
+ switch(pick)
+ if(CHANGE_HAIR)
+ change_hair(user)
+ if(CHANGE_BEARD)
+ change_beard(user)
+ if(CHANGE_RACE)
+ change_race(user)
+ if(CHANGE_SEX) // sex: yes
+ change_sex(user)
+ if(CHANGE_NAME)
+ change_name(user)
+ if(CHANGE_EYES)
+ change_eyes(user)
+
+ 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(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE //no tele-grooming
- if(HAS_TRAIT(hairdresser, TRAIT_SHAVED))
- to_chat(hairdresser, span_notice("If only growing back facial hair were that easy for you..."))
+ 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
- hairdresser.set_facial_hairstyle(new_style, update = TRUE)
+ beard_dresser.set_facial_hairstyle(new_style, update = TRUE)
else
- hairdresser.set_facial_hairstyle("Shaved", update = TRUE)
+ 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)
- //handle normal hair
- var/new_style = tgui_input_list(user, "Select a hairstyle", "Grooming", GLOB.hairstyles_list)
+/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)
if(isnull(new_style))
return TRUE
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE //no tele-grooming
if(HAS_TRAIT(hairdresser, TRAIT_BALD))
- to_chat(hairdresser, span_notice("If only growing back hair were that easy for you..."))
+ to_chat(hairdresser, span_notice("If only growing back hair were that easy for you... The reminder makes you feel terrible."))
+ hairdresser.add_mood_event("bald_hair_day", /datum/mood_event/bald_reminder)
return TRUE
hairdresser.set_hairstyle(new_style, update = TRUE)
-*/
-/obj/structure/mirror/examine_status(mob/user)
+/obj/structure/mirror/proc/change_name(mob/living/carbon/human/user)
+ var/newname = sanitize_name(tgui_input_text(user, "Who are we again?", "Name change", user.name, MAX_NAME_LEN), allow_numbers = TRUE) //It's magic so whatever.
+ if(!newname)
+ return TRUE
+ user.real_name = newname
+ user.name = newname
+ if(user.dna)
+ user.dna.real_name = newname
+ if(user.mind)
+ user.mind.name = newname
+
+// Erm ackshually the proper term is species. Get it right??
+/obj/structure/mirror/proc/change_race(mob/living/carbon/human/race_changer)
+ 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/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")
+ change_race(race_changer) // try again
+ return
+
+ race_changer.set_species(newrace, icon_update = FALSE)
+ if(HAS_TRAIT(race_changer, TRAIT_USES_SKINTONES))
+ var/new_s_tone = tgui_input_list(race_changer, "Choose your skin tone", "Race change", GLOB.skin_tones)
+ if(new_s_tone)
+ race_changer.skin_tone = new_s_tone
+ race_changer.dna.update_ui_block(DNA_SKIN_TONE_BLOCK)
+ else if(HAS_TRAIT(race_changer, TRAIT_MUTANT_COLORS) && !HAS_TRAIT(race_changer, TRAIT_FIXED_MUTANT_COLORS))
+ var/new_mutantcolor = input(race_changer, "Choose your skin color:", "Race change", race_changer.dna.features["mcolor"]) as color|null
+ if(new_mutantcolor)
+ var/temp_hsv = RGBtoHSV(new_mutantcolor)
+
+ if(ReadHSV(temp_hsv)[3] >= ReadHSV("#7F7F7F")[3]) // mutantcolors must be bright
+ race_changer.dna.features["mcolor"] = sanitize_hexcolor(new_mutantcolor)
+ race_changer.dna.update_uf_block(DNA_MUTANT_COLOR_BLOCK)
+
+ else
+ to_chat(race_changer, span_notice("Invalid color. Your color is not bright enough."))
+ return TRUE
+
+ race_changer.update_body(is_creating = TRUE)
+ race_changer.update_mutations_overlay() // no hulk lizard
+
+// possible Genders: MALE, FEMALE, PLURAL, NEUTER
+// possible Physique: MALE, FEMALE
+// saved you a click (many)
+/obj/structure/mirror/proc/change_sex(mob/living/carbon/human/sexy)
+
+ var/chosen_sex = tgui_input_list(sexy, "Become a..", "Confirmation", list("Warlock", "Witch", "Wizard", "Itzard")) // YOU try coming up with the 'it' version of wizard
+
+ switch(chosen_sex)
+ if("Warlock")
+ sexy.gender = MALE
+ to_chat(sexy, span_notice("Man, you feel like a man!"))
+ if("Witch")
+ sexy.gender = FEMALE
+ to_chat(sexy, span_notice("Man, you feel like a woman!"))
+ if("Wizard")
+ sexy.gender = PLURAL
+ to_chat(sexy, span_notice("Woah dude, you feel like a dude!"))
+ if("Itzard")
+ sexy.gender = NEUTER
+ to_chat(sexy, span_notice("Woah dude, you feel like something else!"))
+
+ var/chosen_physique = tgui_input_list(sexy, "Alter your physique as well?", "Confirmation", list("Warlock Physique", "Witch Physique", "Wizards Don't Need Gender"))
+
+ if(chosen_physique && chosen_physique != "Wizards Don't Need Gender")
+ sexy.physique = (chosen_physique == "Warlock Physique") ? MALE : FEMALE
+
+ sexy.dna.update_ui_block(DNA_GENDER_BLOCK)
+ sexy.update_body(is_creating = TRUE) // or else physique won't change properly
+ sexy.update_mutations_overlay() //(hulk male/female)
+ sexy.update_clothing(ITEM_SLOT_ICLOTHING) // update gender shaped clothing
+
+/obj/structure/mirror/proc/change_eyes(mob/living/carbon/human/user)
+ var/new_eye_color = input(user, "Choose your eye color", "Eye Color", user.eye_color_left) as color|null
+ if(isnull(new_eye_color))
+ return TRUE
+ user.eye_color_left = sanitize_hexcolor(new_eye_color)
+ user.eye_color_right = sanitize_hexcolor(new_eye_color)
+ user.dna.update_ui_block(DNA_EYE_COLOR_LEFT_BLOCK)
+ user.dna.update_ui_block(DNA_EYE_COLOR_RIGHT_BLOCK)
+ user.update_body()
+ to_chat(user, span_notice("You gaze at your new eyes with your new eyes. Perfect!"))
+
+/obj/structure/mirror/examine_status(mob/living/carbon/human/user)
if(broken)
return list()// no message spam
return ..()
@@ -165,11 +316,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
name = "magic mirror"
desc = "Turn and face the strange... face."
icon_state = "magic_mirror"
-
- ///Flags this race must have to be selectable with this type of mirror.
- var/race_flags = MIRROR_MAGIC
- ///List of all Races that can be chosen, decided by its Initialize.
- var/list/selectable_races = list()
+ mirror_options = MAGIC_MIRROR_OPTIONS
+ magical_mirror = TRUE
/obj/structure/mirror/magic/Initialize(mapload)
. = ..()
@@ -181,133 +329,38 @@ 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/attack_hand(mob/user, list/modifiers)
- . = ..()
- if(.)
- return TRUE
- if(!ishuman(user))
- 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"))
+ if(hairchoice == "Style") //So you just want to use a mirror then?
+ return ..()
- var/mob/living/carbon/human/amazed_human = user
-// SKYRAT EDIT BEGIN - Magic Mirror Character Application
- var/choice
- var/ask = tgui_alert(user, "Would you like to apply your loaded character?","Confirm", list("Yes!", "No, I want to manually edit my character here."))
+ var/new_hair_color = input(user, "Choose your hair color", "Hair Color", user.hair_color) as color|null
- if(ask == "Yes!")
- user?.client?.prefs?.safe_transfer_prefs_to(amazed_human)
- else
- choice = tgui_input_list(user, "Something to change?", "Magical Grooming", list("name", "race", "gender", "hair", "eyes"))
-// SKYRAT EDIT END
- if(isnull(choice))
- return TRUE
+ if(new_hair_color)
+ user.set_haircolor(sanitize_hexcolor(new_hair_color), update = FALSE)
+ user.dna.update_ui_block(DNA_HAIR_COLOR_BLOCK)
+ if(user.physique == MALE)
+ var/new_face_color = input(user, "Choose your facial hair color", "Hair Color", user.facial_hair_color) as color|null
+ if(new_face_color)
+ user.set_facial_haircolor(sanitize_hexcolor(new_face_color), update = FALSE)
+ user.dna.update_ui_block(DNA_FACIAL_HAIR_COLOR_BLOCK)
+ user.update_body_parts()
+ user.update_mutant_bodyparts(force_update = TRUE) /// SKYRAT EDIT ADDITION - Mirrors are no longer scared of colored ears
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
+/obj/structure/mirror/magic/attack_hand(mob/living/carbon/human/user)
+ . = ..()
+ if(!.)
return TRUE
- switch(choice)
- if("name")
- var/newname = sanitize_name(tgui_input_text(amazed_human, "Who are we again?", "Name change", amazed_human.name, MAX_NAME_LEN), allow_numbers = TRUE) //It's magic so whatever.
- if(!newname)
- return TRUE
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- amazed_human.real_name = newname
- amazed_human.name = newname
- if(amazed_human.dna)
- amazed_human.dna.real_name = newname
- if(amazed_human.mind)
- amazed_human.mind.name = newname
-
- if("race")
- var/racechoice = tgui_input_list(amazed_human, "What are we again?", "Race change", selectable_races)
- if(isnull(racechoice))
- return TRUE
- if(!selectable_races[racechoice])
- return TRUE
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
+ if(HAS_TRAIT(user, TRAIT_ADVANCEDTOOLUSER) && HAS_TRAIT(user, TRAIT_LITERATE))
+ return TRUE
- var/datum/species/newrace = selectable_races[racechoice]
- amazed_human.set_species(newrace, icon_update = FALSE)
- if(HAS_TRAIT(amazed_human, TRAIT_USES_SKINTONES))
- var/new_s_tone = tgui_input_list(user, "Choose your skin tone", "Race change", GLOB.skin_tones)
- if(new_s_tone)
- amazed_human.skin_tone = new_s_tone
- amazed_human.dna.update_ui_block(DNA_SKIN_TONE_BLOCK)
- else if(HAS_TRAIT(amazed_human, TRAIT_MUTANT_COLORS) && !HAS_TRAIT(amazed_human, TRAIT_FIXED_MUTANT_COLORS))
- var/new_mutantcolor = input(user, "Choose your skin color:", "Race change", amazed_human.dna.features["mcolor"]) as color|null
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- if(new_mutantcolor)
- var/temp_hsv = RGBtoHSV(new_mutantcolor)
-
- if(ReadHSV(temp_hsv)[3] >= ReadHSV("#7F7F7F")[3]) // mutantcolors must be bright
- amazed_human.dna.features["mcolor"] = sanitize_hexcolor(new_mutantcolor)
- amazed_human.dna.update_uf_block(DNA_MUTANT_COLOR_BLOCK)
-
- else
- to_chat(amazed_human, span_notice("Invalid color. Your color is not bright enough."))
- return TRUE
-
- amazed_human.update_body(is_creating = TRUE)
- amazed_human.update_mutations_overlay() // no hulk lizard
-
- if("gender")
- if(!(amazed_human.gender in list(MALE, FEMALE))) //blame the patriarchy
- return TRUE
- if(amazed_human.gender == MALE)
- if(tgui_alert(amazed_human, "Become a Witch?", "Confirmation", list("Yes", "No")) == "Yes")
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- amazed_human.gender = FEMALE
- amazed_human.physique = FEMALE
- to_chat(amazed_human, span_notice("Man, you feel like a woman!"))
- else
- return TRUE
- else
- if(tgui_alert(amazed_human, "Become a Warlock?", "Confirmation", list("Yes", "No")) == "Yes")
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- amazed_human.gender = MALE
- amazed_human.physique = MALE
- to_chat(amazed_human, span_notice("Whoa man, you feel like a man!"))
- else
- return TRUE
- amazed_human.dna.update_ui_block(DNA_GENDER_BLOCK)
- amazed_human.update_body()
- amazed_human.update_mutations_overlay() //(hulk male/female)
-
- if("hair")
- var/hairchoice = tgui_alert(amazed_human, "Hairstyle or hair color?", "Change Hair", list("Style", "Color"))
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- if(hairchoice == "Style") //So you just want to use a mirror then?
- return ..()
- else
- var/new_hair_color = input(amazed_human, "Choose your hair color", "Hair Color",amazed_human.hair_color) as color|null
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- if(new_hair_color)
- amazed_human.set_haircolor(sanitize_hexcolor(new_hair_color), update = FALSE)
- amazed_human.dna.update_ui_block(DNA_HAIR_COLOR_BLOCK)
- if(amazed_human.gender == MALE)
- var/new_face_color = input(amazed_human, "Choose your facial hair color", "Hair Color", amazed_human.facial_hair_color) as color|null
- if(new_face_color)
- amazed_human.set_facial_haircolor(sanitize_hexcolor(new_face_color), update = FALSE)
- amazed_human.dna.update_ui_block(DNA_FACIAL_HAIR_COLOR_BLOCK)
- amazed_human.update_body_parts()
- amazed_human.update_mutant_bodyparts(force_update = TRUE) /// SKYRAT EDIT - Mirrors are no longer scared of colored ears
-
- if(BODY_ZONE_PRECISE_EYES)
- var/new_eye_color = input(amazed_human, "Choose your eye color", "Eye Color", amazed_human.eye_color_left) as color|null
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return TRUE
- if(new_eye_color)
- amazed_human.eye_color_left = sanitize_hexcolor(new_eye_color)
- amazed_human.eye_color_right = sanitize_hexcolor(new_eye_color)
- amazed_human.dna.update_ui_block(DNA_EYE_COLOR_LEFT_BLOCK)
- amazed_human.dna.update_ui_block(DNA_EYE_COLOR_RIGHT_BLOCK)
- amazed_human.update_body()
+ 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)
+ return TRUE
/obj/structure/mirror/magic/lesser/Initialize(mapload)
// Roundstart species don't have a flag, so it has to be set on Initialize.
@@ -321,10 +374,11 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
name = "pride's mirror"
desc = "Pride cometh before the..."
race_flags = MIRROR_PRIDE
+ mirror_options = PRIDE_MIRROR_OPTIONS
-/obj/structure/mirror/magic/pride/attack_hand(mob/user, list/modifiers)
+/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!"), \
@@ -340,3 +394,15 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror/broken, 28)
var/turf/open/chasm/new_chasm = user_turf
new_chasm.set_target(dest)
new_chasm.drop(user)
+
+#undef CHANGE_HAIR
+#undef CHANGE_BEARD
+
+#undef CHANGE_RACE
+#undef CHANGE_SEX
+#undef CHANGE_NAME
+#undef CHANGE_EYES
+
+#undef INERT_MIRROR_OPTIONS
+#undef PRIDE_MIRROR_OPTIONS
+#undef MAGIC_MIRROR_OPTIONS
diff --git a/code/game/objects/structures/petrified_statue.dm b/code/game/objects/structures/petrified_statue.dm
index 78bd478d52347a..54896c2e414055 100644
--- a/code/game/objects/structures/petrified_statue.dm
+++ b/code/game/objects/structures/petrified_statue.dm
@@ -94,7 +94,7 @@
/mob/proc/petrify(statue_timer)
-/mob/living/carbon/human/petrify(statue_timer, save_brain)
+/mob/living/carbon/human/petrify(statue_timer, save_brain, colorlist)
if(!isturf(loc))
return FALSE
var/obj/structure/statue/petrified/S = new(loc, src, statue_timer, save_brain)
@@ -102,6 +102,8 @@
ADD_TRAIT(src, TRAIT_NOBLOOD, MAGIC_TRAIT)
S.copy_overlays(src)
var/newcolor = list(rgb(77,77,77), rgb(150,150,150), rgb(28,28,28), rgb(0,0,0))
+ if(colorlist)
+ newcolor = colorlist
S.add_atom_colour(newcolor, FIXED_COLOUR_PRIORITY)
return TRUE
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index 7f9dab59a466af..f88ba15ecc52cf 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -8,6 +8,8 @@
density = TRUE
anchored = TRUE
pass_flags_self = LETPASSTHROW|PASSSTRUCTURE
+ layer = ABOVE_MOB_LAYER
+ plane = GAME_PLANE_UPPER
/// armor is a little bit less than a grille. max_integrity about half that of a grille.
armor_type = /datum/armor/structure_railing
max_integrity = 25
diff --git a/code/game/objects/structures/showcase.dm b/code/game/objects/structures/showcase.dm
index 91ccdaef80a2a0..2349d9b0f4913a 100644
--- a/code/game/objects/structures/showcase.dm
+++ b/code/game/objects/structures/showcase.dm
@@ -84,7 +84,13 @@
name = "\improper Nanotrasen-brand microwave"
desc = "The famous Nanotrasen-brand microwave, the multi-purpose cooking appliance every station needs! This one appears to be drawn onto a cardboard box."
icon = 'icons/obj/machines/microwave.dmi'
- icon_state = "map_icon"
+ icon_state = "mw_complete"
+
+/obj/structure/showcase/machinery/microwave_engineering
+ name = "\improper Nanotrasen Wave(tm) microwave"
+ desc = "Just when everyone thought Nanotrasen couldn't improve on their famous microwave, this 2563 model features Waveâ„¢! A Nanotrasen exclusive, Waveâ„¢ allows your PDA to be charged wirelessly through microwave frequencies. Because nothing says 'future' like charging your PDA while overcooking your leftovers. Nanotrasen Waveâ„¢ - Multitasking, redefined."
+ icon = 'icons/obj/machines/microwave.dmi'
+ icon_state = "engi_mw_complete"
/obj/structure/showcase/machinery/cloning_pod
name = "cloning pod exhibit"
diff --git a/code/game/objects/structures/spawner.dm b/code/game/objects/structures/spawner.dm
index 340bcf212ded6e..2ad70bdbc84203 100644
--- a/code/game/objects/structures/spawner.dm
+++ b/code/game/objects/structures/spawner.dm
@@ -14,6 +14,52 @@
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
+ /// If this spawner's taggable, what can we tag it with?
+ var/static/list/scanner_types = list(/obj/item/mining_scanner, /obj/item/t_scanner/adv_mining_scanner)
+ /// If this spawner's taggable, what's the text we use to describe what we can tag it with?
+ var/scanner_descriptor = "mining analyzer"
+ /// Has this spawner been tagged/analyzed by a mining scanner?
+ var/gps_tagged = FALSE
+ /// A short identifier for the mob it spawns. Keep around 3 characters or less?
+ var/mob_gps_id = "???"
+ /// A short identifier for what kind of spawner it is, for use in putting together its GPS tag.
+ var/spawner_gps_id = "Creature Nest"
+ /// A complete identifier. Generated on tag (if tagged), used for its examine.
+ var/assigned_tag
+
+/obj/structure/spawner/examine(mob/user)
+ . = ..()
+ if(!scanner_taggable)
+ return
+ if(gps_tagged)
+ . += span_notice("A holotag's been attached, projecting \"[assigned_tag]\".")
+ else
+ . += span_notice("It looks like you could probably scan and tag it with a [scanner_descriptor].")
+
+/obj/structure/spawner/attackby(obj/item/item, mob/user, params)
+ . = ..()
+ if(.)
+ return TRUE
+ if(scanner_taggable && is_type_in_list(item, scanner_types))
+ gps_tag(user)
+ return TRUE
+
+/// Tag the spawner, prefixing its GPS entry with an identifier - or giving it one, if nonexistent.
+/obj/structure/spawner/proc/gps_tag(mob/user)
+ if(gps_tagged)
+ to_chat(user, span_warning("[src] already has a holotag attached!"))
+ return
+ to_chat(user, span_notice("You affix a holotag to [src]."))
+ playsound(src, 'sound/machines/twobeep.ogg', 100)
+ gps_tagged = TRUE
+ assigned_tag = "\[[mob_gps_id]-[rand(100,999)]\] " + spawner_gps_id
+ var/datum/component/gps/our_gps = GetComponent(/datum/component/gps)
+ if(our_gps)
+ our_gps.gpstag = assigned_tag
+ return
+ AddComponent(/datum/component/gps, assigned_tag)
/obj/structure/spawner/Initialize(mapload)
. = ..()
@@ -32,6 +78,8 @@
spawn_text = "warps in from"
mob_types = list(/mob/living/basic/syndicate/ranged)
faction = list(ROLE_SYNDICATE)
+ mob_gps_id = "SYN" // syndicate
+ spawner_gps_id = "Hostile Warp Beacon"
/obj/structure/spawner/skeleton
name = "bone pit"
@@ -44,6 +92,8 @@
mob_types = list(/mob/living/simple_animal/hostile/skeleton)
spawn_text = "climbs out of"
faction = list(FACTION_SKELETON)
+ mob_gps_id = "SKL" // skeletons
+ spawner_gps_id = "Bone Pit"
/obj/structure/spawner/clown
name = "Laughing Larry"
@@ -67,6 +117,8 @@
)
spawn_text = "climbs out of"
faction = list(FACTION_CLOWN)
+ mob_gps_id = "???" // clowns
+ spawner_gps_id = "Clown Planet Distortion"
/obj/structure/spawner/mining
name = "monster den"
@@ -80,7 +132,7 @@
/mob/living/basic/mining/basilisk,
/mob/living/basic/mining/goldgrub,
/mob/living/basic/mining/goliath/ancient,
- /mob/living/basic/mining/legion,
+ /mob/living/basic/mining/hivelord,
/mob/living/basic/wumborian_fugu,
)
faction = list(FACTION_MINING)
@@ -89,26 +141,31 @@
name = "goldgrub den"
desc = "A den housing a nest of goldgrubs, annoying but arguably much better than anything else you'll find in a nest."
mob_types = list(/mob/living/basic/mining/goldgrub)
+ mob_gps_id = "GG"
/obj/structure/spawner/mining/goliath
name = "goliath den"
desc = "A den housing a nest of goliaths, oh god why?"
mob_types = list(/mob/living/basic/mining/goliath/ancient)
+ mob_gps_id = "GL|A"
/obj/structure/spawner/mining/hivelord
name = "hivelord den"
desc = "A den housing a nest of hivelords."
mob_types = list(/mob/living/basic/mining/hivelord)
+ mob_gps_id = "HL"
/obj/structure/spawner/mining/basilisk
name = "basilisk den"
desc = "A den housing a nest of basilisks, bring a coat."
mob_types = list(/mob/living/basic/mining/basilisk)
+ mob_gps_id = "BK"
/obj/structure/spawner/mining/wumborian
name = "wumborian fugu den"
desc = "A den housing a nest of wumborian fugus, how do they all even fit in there?"
mob_types = list(/mob/living/basic/wumborian_fugu)
+ mob_gps_id = "WF"
/obj/structure/spawner/nether
name = "netherworld link"
@@ -125,6 +182,9 @@
/mob/living/basic/migo,
)
faction = list(FACTION_NETHER)
+ scanner_taggable = TRUE
+ mob_gps_id = "?!?"
+ spawner_gps_id = "Netheric Distortion"
/obj/structure/spawner/nether/Initialize(mapload)
. = ..()
diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm
index f5b40238ed60c7..1253d156a2c20c 100644
--- a/code/game/turfs/change_turf.dm
+++ b/code/game/turfs/change_turf.dm
@@ -252,7 +252,7 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
//SKYRAT EDIT END
if(excited || excited_group)
SSair.remove_from_active(src) //Clean up wall excitement, and refresh excited groups
- if(ispath(path,/turf/closed) || ispath(path,/turf/cordon))
+ if(ispath(path, /turf/closed) || ispath(path, /turf/cordon))
flags |= CHANGETURF_RECALC_ADJACENT
return ..()
diff --git a/code/game/turfs/closed/indestructible.dm b/code/game/turfs/closed/indestructible.dm
index b364ad428d0359..a569f7c58944ca 100644
--- a/code/game/turfs/closed/indestructible.dm
+++ b/code/game/turfs/closed/indestructible.dm
@@ -313,6 +313,9 @@ SKYRAT EDIT REMOVAL END */
explosive_resistance = 50
baseturfs = /turf/closed/indestructible/riveted/boss
+/turf/closed/indestructible/riveted/boss/wasteland
+ baseturfs = /turf/open/misc/asteroid/basalt/wasteland
+
/turf/closed/indestructible/riveted/boss/see_through
opacity = FALSE
diff --git a/code/game/turfs/closed/wall/misc_walls.dm b/code/game/turfs/closed/wall/misc_walls.dm
index a26e456971d881..9fbce09ace274c 100644
--- a/code/game/turfs/closed/wall/misc_walls.dm
+++ b/code/game/turfs/closed/wall/misc_walls.dm
@@ -18,16 +18,6 @@
/turf/closed/wall/mineral/cult/devastate_wall()
new sheet_type(get_turf(src), sheet_amount)
-/turf/closed/wall/mineral/cult/Exited(atom/movable/gone, direction)
- . = ..()
- if(istype(gone, /mob/living/simple_animal/hostile/construct/harvester)) //harvesters can go through cult walls, dragging something with
- var/mob/living/simple_animal/hostile/construct/harvester/H = gone
- var/atom/movable/stored_pulling = H.pulling
- if(stored_pulling)
- stored_pulling.setDir(direction)
- stored_pulling.forceMove(src)
- H.start_pulling(stored_pulling, supress_message = TRUE)
-
/turf/closed/wall/mineral/cult/artificer
name = "runed stone wall"
desc = "A cold stone wall engraved with indecipherable symbols. Studying them causes your head to pound."
diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm
index 7b601fb82ac5c4..06a063f16eac6d 100644
--- a/code/game/turfs/closed/walls.dm
+++ b/code/game/turfs/closed/walls.dm
@@ -363,5 +363,13 @@
/turf/closed/wall/metal_foam_base
girder_type = /obj/structure/foamedmetal
+/turf/closed/wall/Bumped(atom/movable/bumped_atom)
+ . = ..()
+ SEND_SIGNAL(bumped_atom, COMSIG_LIVING_WALL_BUMP, src)
+
+/turf/closed/wall/Exited(atom/movable/gone, direction)
+ . = ..()
+ SEND_SIGNAL(gone, COMSIG_LIVING_WALL_EXITED, src)
+
#undef MAX_DENT_DECALS
#undef LEANING_OFFSET
diff --git a/code/game/turfs/open/chasm.dm b/code/game/turfs/open/chasm.dm
index 4c8ac12202d093..49f6663d097203 100644
--- a/code/game/turfs/open/chasm.dm
+++ b/code/game/turfs/open/chasm.dm
@@ -105,7 +105,6 @@
icon = 'icons/turf/floors/junglechasm.dmi'
icon_state = "junglechasm-255"
base_icon_state = "junglechasm"
- initial_gas_mix = OPENTURF_LOW_PRESSURE
planetary_atmos = TRUE
baseturfs = /turf/open/chasm/jungle
diff --git a/code/game/turfs/open/floor.dm b/code/game/turfs/open/floor.dm
index a0d1a191bc6294..8c80b35bb3da17 100644
--- a/code/game/turfs/open/floor.dm
+++ b/code/game/turfs/open/floor.dm
@@ -264,6 +264,9 @@
else if(the_rcd.furnish_type == /obj/structure/bed)
cost = 8
delay = 1.5 SECONDS
+ else if(the_rcd.furnish_type == /obj/machinery/microwave/engineering)
+ cost = 48
+ delay = 4 SECONDS
if(cost == 0)
return FALSE
return list("mode" = RCD_FURNISHING, "delay" = cost, "cost" = delay)
@@ -380,6 +383,11 @@
return FALSE
var/atom/new_furnish = new the_rcd.furnish_type(src)
new_furnish.setDir(user.dir)
+ if(istype(new_furnish, /obj/machinery/microwave/engineering))
+ var/obj/machinery/microwave/engineering/new_mw = new_furnish
+ var/obj/item/stock_parts/cell/new_cell = new()
+ new_mw.cell = new_cell
+ new_mw.update_appearance()
return TRUE
return FALSE
diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm
index 24c690498a6c35..b8e04949662046 100644
--- a/code/game/turfs/open/lava.dm
+++ b/code/game/turfs/open/lava.dm
@@ -392,8 +392,11 @@
if(IS_ROBOTIC_LIMB(burn_limb))
robo_parts += burn_limb
- burn_human.adjustToxLoss(15, required_biotype = MOB_ORGANIC) // This is from plasma, so it should obey plasma biotype requirements
- burn_human.adjustFireLoss(25)
+ 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
burn_human.emote("scream")
diff --git a/code/game/turfs/open/planet.dm b/code/game/turfs/open/planet.dm
index e5ab02c0924608..65c76cef957a70 100644
--- a/code/game/turfs/open/planet.dm
+++ b/code/game/turfs/open/planet.dm
@@ -18,14 +18,27 @@
name = "dirt flooring" //FOR THE LOVE OF GOD USE THIS INSTEAD OF DIRT FOR STATION MAPS
desc = "You heard this place was dirty, but this is just absurd."
baseturfs = /turf/open/floor/plating
- initial_gas_mix = OPENTURF_LOW_PRESSURE
+ initial_gas_mix = OPENTURF_DEFAULT_ATMOS
planetary_atmos = FALSE
+/turf/open/misc/dirt/jungle
+ slowdown = 0.5
+ initial_gas_mix = OPENTURF_DEFAULT_ATMOS
+
/turf/open/misc/dirt/dark
icon_state = "greenerdirt"
base_icon_state = "greenerdirt"
-/turf/open/misc/dirt/jungle
+/turf/open/misc/dirt/dark/station
+ baseturfs = /turf/open/floor/plating
+ initial_gas_mix = OPENTURF_DEFAULT_ATMOS
+ planetary_atmos = FALSE
+
+/turf/open/misc/dirt/dark/station/airless
+ initial_gas_mix = AIRLESS_ATMOS
+ temperature = TCMB
+
+/turf/open/misc/dirt/dark/jungle
slowdown = 0.5
initial_gas_mix = OPENTURF_DEFAULT_ATMOS
@@ -68,7 +81,7 @@
return list("jungle_damaged")
/turf/closed/mineral/random/jungle
- baseturfs = /turf/open/misc/dirt/dark
+ baseturfs = /turf/open/misc/dirt/dark/jungle
/turf/closed/mineral/random/jungle/mineral_chances()
return list(
@@ -81,3 +94,6 @@
/obj/item/stack/ore/titanium = 11,
/obj/item/stack/ore/uranium = 5,
)
+
+/turf/closed/mineral/random/jungle/space_safe
+ baseturfs = /turf/open/misc/dirt/dark/station/airless
diff --git a/code/game/world.dm b/code/game/world.dm
index e851bb992d4e32..de6386243193ac 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -361,8 +361,8 @@ GLOBAL_VAR(restart_counter)
var/server_name = CONFIG_GET(string/servername)
if (server_name)
new_status += "[server_name] "
- if(!CONFIG_GET(flag/norespawn))
- features += "respawn"
+ if(CONFIG_GET(flag/allow_respawn))
+ features += "respawn" // show "respawn" regardless of "respawn as char" or "free respawn"
if(!CONFIG_GET(flag/allow_ai))
features += "AI disabled"
hostedby = CONFIG_GET(string/hostedby)
diff --git a/code/modules/actionspeed/modifiers/status_effects.dm b/code/modules/actionspeed/modifiers/status_effects.dm
index fa080edb8d009d..ec7b0dba22e0f3 100644
--- a/code/modules/actionspeed/modifiers/status_effects.dm
+++ b/code/modules/actionspeed/modifiers/status_effects.dm
@@ -12,3 +12,19 @@
/datum/actionspeed_modifier/status_effect/hazard_area
multiplicative_slowdown = 4
+
+/// Get slower the more gold is in your system.
+/datum/actionspeed_modifier/status_effect/midas_blight
+ id = ACTIONSPEED_ID_MIDAS_BLIGHT
+
+/datum/actionspeed_modifier/status_effect/midas_blight/soft
+ multiplicative_slowdown = 0.25
+
+/datum/actionspeed_modifier/status_effect/midas_blight/medium
+ multiplicative_slowdown = 0.75
+
+/datum/actionspeed_modifier/status_effect/midas_blight/hard
+ multiplicative_slowdown = 1.5
+
+/datum/actionspeed_modifier/status_effect/midas_blight/gold
+ multiplicative_slowdown = 2
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index bc6942a9a110c5..2d2c692ed5fcad 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -21,6 +21,7 @@
var/dat = "
Game Panel
"
if(SSticker.current_state <= GAME_STATE_PREGAME)
+ dat += "(Manage Dynamic Rulesets) "
dat += "(Force Roundstart Rulesets) "
if (GLOB.dynamic_forced_roundstart_ruleset.len > 0)
for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset)
@@ -30,6 +31,8 @@
dat += ""
if(SSticker.IsRoundInProgress())
dat += "(Game Mode Panel) "
+ if(istype(SSticker.mode, /datum/game_mode/dynamic))
+ dat += "(Manage Dynamic Rulesets) "
dat += {"
Create Object
@@ -143,6 +146,111 @@
user << browse(dat, "window=dyn_mode_options;size=900x650")
+/datum/admins/proc/dynamic_ruleset_manager(mob/user)
+ if (SSticker.current_state > GAME_STATE_PREGAME && !istype(SSticker.mode, /datum/game_mode/dynamic))
+ return // Not running dynamic
+
+ var/dat = "
Dynamic Ruleset Management
\
+ Change these options to forcibly enable or disable dynamic rulesets. \
+ Disabled rulesets will never run, even if they would otherwise be valid. \
+ Enabled rulesets will run even if the qualifying minimum of threat or player count is not present, this does not guarantee that they will necessarily be chosen (for example their weight may be set to 0 in config). \
+ \[force enable all / \
+ force disable all / \
+ reset all\]"
+
+ if (SSticker.current_state <= GAME_STATE_PREGAME) // Don't bother displaying after the round has started
+ var/static/list/rulesets_by_context = list()
+ if (!length(rulesets_by_context))
+ for (var/datum/dynamic_ruleset/rule as anything in subtypesof(/datum/dynamic_ruleset))
+ if (initial(rule.name) == "")
+ continue
+ LAZYADD(rulesets_by_context[initial(rule.ruletype)], rule)
+
+ dat += dynamic_ruleset_category_pre_start_display("Roundstart", rulesets_by_context[ROUNDSTART_RULESET])
+ dat += dynamic_ruleset_category_pre_start_display("Latejoin", rulesets_by_context[LATEJOIN_RULESET])
+ dat += dynamic_ruleset_category_pre_start_display("Midround", rulesets_by_context[MIDROUND_RULESET])
+ user << browse(dat, "window=dyn_mode_options;size=900x650")
+ return
+
+ var/datum/game_mode/dynamic/current_mode = SSticker.mode
+ var/pop_count = length(GLOB.alive_player_list)
+ var/threat_level = current_mode.threat_level
+ dat += dynamic_ruleset_category_during_round_display("Latejoin", current_mode.latejoin_rules, pop_count, threat_level)
+ dat += dynamic_ruleset_category_during_round_display("Midround", current_mode.midround_rules, pop_count, threat_level)
+ user << browse(dat, "window=dyn_mode_options;size=900x650")
+
+/datum/admins/proc/dynamic_ruleset_category_pre_start_display(title, list/rules)
+ var/dat = "
[title]
"
+ for (var/datum/dynamic_ruleset/rule as anything in rules)
+ var/forced = GLOB.dynamic_forced_rulesets[rule] || RULESET_NOT_FORCED
+ var/color = COLOR_BLACK
+ switch (forced)
+ if (RULESET_FORCE_ENABLED)
+ color = COLOR_GREEN
+ if (RULESET_FORCE_DISABLED)
+ color = COLOR_RED
+ dat += "
")
/mob/camera/blob/proc/add_points(points)
blob_points = clamp(blob_points + points, 0, max_blob_points)
@@ -273,7 +294,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
if(client.prefs.muted & MUTE_IC)
to_chat(src, span_boldwarning("You cannot send IC messages (muted)."))
return
- if (!(ignore_spam || forced) && src.client.handle_spam_prevention(message,MUTE_IC))
+ if (!(ignore_spam || forced) && src.client.handle_spam_prevention(message, MUTE_IC))
return
if (stat)
@@ -291,14 +312,8 @@ GLOBAL_LIST_EMPTY(blob_nodes)
src.log_talk(message, LOG_SAY)
var/message_a = say_quote(message)
- var/rendered = span_big("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]")
-
- for(var/mob/M in GLOB.mob_list)
- if(isovermind(M) || isblobmonster(M))
- to_chat(M, rendered)
- if(isobserver(M))
- var/link = FOLLOW_LINK(M, src)
- to_chat(M, "[link] [rendered]")
+ var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]"))
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, src)
/mob/camera/blob/blob_act(obj/structure/blob/B)
return
@@ -324,8 +339,8 @@ GLOBAL_LIST_EMPTY(blob_nodes)
else
return FALSE
else
- var/area/A = get_area(NewLoc)
- if(isgroundlessturf(NewLoc) || istype(A, /area/shuttle)) //if unplaced, can't go on shuttles or goundless tiles
+ var/area/check_area = get_area(NewLoc)
+ if(isgroundlessturf(NewLoc) || istype(check_area, /area/shuttle)) //if unplaced, can't go on shuttles or groundless tiles
return FALSE
forceMove(NewLoc)
return TRUE
diff --git a/code/modules/antagonists/blob/powers.dm b/code/modules/antagonists/blob/powers.dm
index c75a41a48fff7e..04054f6df85a3d 100644
--- a/code/modules/antagonists/blob/powers.dm
+++ b/code/modules/antagonists/blob/powers.dm
@@ -196,38 +196,25 @@
var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as a [blobstrain.name] blobbernaut?", ROLE_BLOB, ROLE_BLOB, 50)
- factory.is_creating_blobbernaut = FALSE
-
if(!length(candidates))
to_chat(src, span_warning("You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later."))
add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST)
- factory.blobbernaut = null //players must answer rapidly
+ factory.assign_blobbernaut(null)
return FALSE
- factory.modify_max_integrity(initial(factory.max_integrity) * 0.25) //factories that produced a blobbernaut have much lower health
- factory.update_appearance()
- factory.visible_message(span_warning("The blobbernaut [pick("rips", "tears", "shreds")] its way out of the factory blob!"))
- playsound(factory.loc, 'sound/effects/splat.ogg', 50, TRUE)
-
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobber = new /mob/living/simple_animal/hostile/blob/blobbernaut(get_turf(factory))
- flick("blobbernaut_produce", blobber)
-
- factory.blobbernaut = blobber
- blobber.factory = factory
- blobber.overmind = src
- blobber.update_icons()
- blobber.adjustHealth(blobber.maxHealth * 0.5)
- blob_mobs += blobber
-
+ var/mob/living/basic/blob_minion/blobbernaut/minion/blobber = new(get_turf(factory))
+ assume_direct_control(blobber)
+ factory.assign_blobbernaut(blobber)
var/mob/dead/observer/player = pick(candidates)
- blobber.key = player.key
+ blobber.assign_key(player.key, blobstrain)
+ RegisterSignal(blobber, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_blobbernaut_attacked))
- SEND_SOUND(blobber, sound('sound/effects/blobattack.ogg'))
- SEND_SOUND(blobber, sound('sound/effects/attackblob.ogg'))
- to_chat(blobber, span_infoplain("You are powerful, hard to kill, and slowly regenerate near nodes and cores, [span_cultlarge("but will slowly die if not near the blob")] or if the factory that made you is killed."))
- to_chat(blobber, span_infoplain("You can communicate with other blobbernauts and overminds telepathically by attempting to speak normally"))
- to_chat(blobber, span_infoplain("Your overmind's blob reagent is: [blobstrain.name]!"))
- to_chat(blobber, span_infoplain("The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]"))
+/// When one of our boys attacked something, we sometimes want to perform extra effects
+/mob/camera/blob/proc/on_blobbernaut_attacked(mob/living/basic/blobbynaut, atom/target, success)
+ SIGNAL_HANDLER
+ if (!success)
+ return
+ blobstrain.blobbernaut_attack(target, blobbynaut)
/** Moves the core */
/mob/camera/blob/proc/relocate_core()
@@ -358,10 +345,11 @@
var/list/surrounding_turfs = TURF_NEIGHBORS(tile)
if(!length(surrounding_turfs))
return FALSE
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore as anything in blob_mobs)
- if(isturf(spore.loc) && get_dist(spore, tile) <= 35 && !spore.key)
- spore.LoseTarget()
- spore.Goto(pick(surrounding_turfs), spore.move_to_delay)
+ for(var/mob/living/basic/blob_mob as anything in blob_mobs)
+ if(!isturf(blob_mob.loc) || get_dist(blob_mob, tile) > 35 || blob_mob.key)
+ continue
+ blob_mob.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
+ blob_mob.ai_controller.set_blackboard_key(BB_TRAVEL_DESTINATION, pick(surrounding_turfs))
/** Opens the reroll menu to change strains */
/mob/camera/blob/proc/strain_reroll()
diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm
index 543da00d2f5787..e206d97c26b12d 100644
--- a/code/modules/antagonists/blob/structures/_blob.dm
+++ b/code/modules/antagonists/blob/structures/_blob.dm
@@ -129,13 +129,13 @@
return FALSE //oh no we failed
/obj/structure/blob/proc/ConsumeTile()
- for(var/atom/A in loc)
- if(!A.can_blob_attack())
+ for(var/atom/thing in loc)
+ if(!thing.can_blob_attack())
continue
- if(isliving(A) && overmind && !isblobmonster(A)) // Make sure to inject strain-reagents with automatic attacks when needed.
- overmind.blobstrain.attack_living(A)
+ if(isliving(thing) && overmind && !HAS_TRAIT(thing, TRAIT_BLOB_ALLY)) // Make sure to inject strain-reagents with automatic attacks when needed.
+ overmind.blobstrain.attack_living(thing)
continue // Don't smack them twice though
- A.blob_act(src)
+ thing.blob_act(src)
if(iswallturf(loc))
loc.blob_act(src) //don't ask how a wall got on top of the core, just eat it
diff --git a/code/modules/antagonists/blob/structures/factory.dm b/code/modules/antagonists/blob/structures/factory.dm
index 7f28dcce224360..cee7e9a0ac58d7 100644
--- a/code/modules/antagonists/blob/structures/factory.dm
+++ b/code/modules/antagonists/blob/structures/factory.dm
@@ -10,12 +10,12 @@
armor_type = /datum/armor/structure_blob/factory
///How many spores this factory can have.
var/max_spores = BLOB_FACTORY_MAX_SPORES
- ///The list of spores
- var/list/spores = list()
+ ///The list of spores and zombies
+ var/list/spores_and_zombies = list()
COOLDOWN_DECLARE(spore_delay)
var/spore_cooldown = BLOBMOB_SPORE_SPAWN_COOLDOWN
///Its Blobbernaut, if it has spawned any.
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobbernaut
+ var/mob/living/basic/blob_minion/blobbernaut/minion/blobbernaut
///Used in blob/powers.dm, checks if it's already trying to spawn a blobbernaut to prevent issues.
var/is_creating_blobbernaut = FALSE
@@ -32,15 +32,8 @@
overmind.factory_blobs += src
/obj/structure/blob/special/factory/Destroy()
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore in spores)
- to_chat(spore, span_userdanger("Your factory was destroyed! You can no longer sustain yourself."))
- spore.death()
- spores = null
- if(blobbernaut)
- blobbernaut.factory = null
- to_chat(blobbernaut, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
- blobbernaut.throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
- blobbernaut = null
+ spores_and_zombies = null
+ blobbernaut = null
if(overmind)
overmind.factory_blobs -= src
return ..()
@@ -49,13 +42,57 @@
. = ..()
if(blobbernaut)
return
- if(spores.len >= max_spores)
+ if(length(spores_and_zombies) >= max_spores)
return
if(!COOLDOWN_FINISHED(src, spore_delay))
return
COOLDOWN_START(src, spore_delay, spore_cooldown)
- var/mob/living/simple_animal/hostile/blob/blobspore/BS = new (loc, src)
- if(overmind) //if we don't have an overmind, we don't need to do anything but make a spore
- BS.overmind = overmind
- BS.update_icons()
- overmind.blob_mobs.Add(BS)
+ var/mob/living/basic/blob_minion/created_spore = (overmind) ? overmind.create_spore(loc) : new(loc)
+ register_mob(created_spore)
+ RegisterSignal(created_spore, COMSIG_BLOB_ZOMBIFIED, PROC_REF(on_zombie_created))
+
+/// Tracks the existence of a mob in our mobs list
+/obj/structure/blob/special/factory/proc/register_mob(mob/living/basic/blob_minion/blob_mob)
+ spores_and_zombies |= blob_mob
+ blob_mob.link_to_factory(src)
+ RegisterSignal(blob_mob, COMSIG_LIVING_DEATH, PROC_REF(on_spore_died))
+ RegisterSignal(blob_mob, COMSIG_QDELETING, PROC_REF(on_spore_lost))
+
+/// When a spore or zombie dies reset our spawn cooldown so we don't instantly replace it
+/obj/structure/blob/special/factory/proc/on_spore_died(mob/living/dead_spore)
+ SIGNAL_HANDLER
+ COOLDOWN_START(src, spore_delay, spore_cooldown)
+
+/// When a spore is deleted remove it from our list
+/obj/structure/blob/special/factory/proc/on_spore_lost(mob/living/dead_spore)
+ SIGNAL_HANDLER
+ spores_and_zombies -= dead_spore
+
+/// When a spore makes a zombie add it to our mobs list
+/obj/structure/blob/special/factory/proc/on_zombie_created(mob/living/spore, mob/living/zombie)
+ SIGNAL_HANDLER
+ register_mob(zombie)
+
+/// Produce a blobbernaut
+/obj/structure/blob/special/factory/proc/assign_blobbernaut(mob/living/new_naut)
+ is_creating_blobbernaut = FALSE
+ if (isnull(new_naut))
+ return
+
+ modify_max_integrity(initial(max_integrity) * 0.25) //factories that produced a blobbernaut have much lower health
+ visible_message(span_boldwarning("The blobbernaut [pick("rips", "tears", "shreds")] its way out of the factory blob!"))
+ playsound(loc, 'sound/effects/splat.ogg', 50, TRUE)
+
+ blobbernaut = new_naut
+ blobbernaut.link_to_factory(src)
+ RegisterSignals(new_naut, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_blobbernaut_death))
+ update_appearance(UPDATE_ICON)
+
+/// When our brave soldier dies, reset our max integrity
+/obj/structure/blob/special/factory/proc/on_blobbernaut_death(mob/living/death_naut)
+ SIGNAL_HANDLER
+ if (isnull(blobbernaut) || blobbernaut != death_naut)
+ return
+ blobbernaut = null
+ max_integrity = initial(max_integrity)
+ update_appearance(UPDATE_ICON)
diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm
index 23c828c97643dc..40c224a2a30d3f 100644
--- a/code/modules/antagonists/changeling/changeling.dm
+++ b/code/modules/antagonists/changeling/changeling.dm
@@ -372,12 +372,11 @@
/datum/antagonist/changeling/proc/regain_powers()
emporium_action.Grant(owner.current)
for(var/datum/action/changeling/power as anything in innate_powers)
- if(power.needs_button)
- power.Grant(owner.current)
+ power.Grant(owner.current)
for(var/power_path in purchased_powers)
var/datum/action/changeling/power = purchased_powers[power_path]
- if(istype(power) && power.needs_button)
+ if(istype(power))
power.Grant(owner.current)
/*
@@ -1138,9 +1137,6 @@
/datum/outfit/changeling_space
name = "Changeling (Space)"
-
- head = /obj/item/clothing/head/helmet/space/changeling
- suit = /obj/item/clothing/suit/space/changeling
l_hand = /obj/item/melee/arm_blade
#undef FORMAT_CHEM_CHARGES_TEXT
diff --git a/code/modules/antagonists/changeling/changeling_power.dm b/code/modules/antagonists/changeling/changeling_power.dm
index ee2001bfe03fcc..a4d9044d977a18 100644
--- a/code/modules/antagonists/changeling/changeling_power.dm
+++ b/code/modules/antagonists/changeling/changeling_power.dm
@@ -7,8 +7,6 @@
background_icon_state = "bg_changeling"
overlay_icon_state = "bg_changeling_border"
button_icon = 'icons/mob/actions/actions_changeling.dmi'
- /// For passive abilities like hivemind that dont need an action button
- var/needs_button = TRUE
/// Details displayed in fine print within the changling emporium
var/helptext = ""
/// How many changeling chems it costs to use
@@ -44,8 +42,7 @@ the same goes for Remove(). if you override Remove(), call parent or else your p
/datum/action/changeling/proc/on_purchase(mob/user, is_respec)
if(!is_respec)
SSblackbox.record_feedback("tally", "changeling_power_purchase", 1, name)
- if(needs_button)
- Grant(user)//how powers are added rather than the checks in mob.dm
+ Grant(user)//how powers are added rather than the checks in mob.dm
/datum/action/changeling/Trigger(trigger_flags)
var/mob/user = owner
@@ -75,6 +72,7 @@ the same goes for Remove(). if you override Remove(), call parent or else your p
return FALSE
/datum/action/changeling/proc/sting_action(mob/living/user, mob/living/target)
+ SHOULD_CALL_PARENT(TRUE)
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]"))
return FALSE
diff --git a/code/modules/antagonists/changeling/headslug_eggs.dm b/code/modules/antagonists/changeling/headslug_eggs.dm
index 08733a7e6af0d2..e4327aa3ed1e48 100644
--- a/code/modules/antagonists/changeling/headslug_eggs.dm
+++ b/code/modules/antagonists/changeling/headslug_eggs.dm
@@ -37,7 +37,7 @@
changeling_datum.regain_powers()
owner.investigate_log("has been gibbed by a changeling egg burst.", INVESTIGATE_DEATHS)
- owner.gib()
+ owner.gib(DROP_ALL_REMAINS)
qdel(src)
#undef EGG_INCUBATION_TIME
diff --git a/code/modules/antagonists/changeling/powers/absorb.dm b/code/modules/antagonists/changeling/powers/absorb.dm
index cee0f0da5b99a7..1fdb50a7babf71 100644
--- a/code/modules/antagonists/changeling/powers/absorb.dm
+++ b/code/modules/antagonists/changeling/powers/absorb.dm
@@ -28,6 +28,8 @@
return changeling.can_absorb_dna(target)
/datum/action/changeling/absorb_dna/sting_action(mob/owner)
+ SHOULD_CALL_PARENT(FALSE) // the only reason to call parent is for proper blackbox logging, and we do that ourselves in a snowflake way
+
var/datum/antagonist/changeling/changeling = owner.mind.has_antag_datum(/datum/antagonist/changeling)
var/mob/living/carbon/human/target = owner.pulling
is_absorbing = TRUE
diff --git a/code/modules/antagonists/changeling/powers/biodegrade.dm b/code/modules/antagonists/changeling/powers/biodegrade.dm
index 16bd707831b294..eba507ad5e0798 100644
--- a/code/modules/antagonists/changeling/powers/biodegrade.dm
+++ b/code/modules/antagonists/changeling/powers/biodegrade.dm
@@ -8,11 +8,6 @@
req_human = TRUE
/datum/action/changeling/biodegrade/sting_action(mob/living/carbon/human/user)
- var/used = FALSE // only one form of shackles removed per use
- if(!HAS_TRAIT(user, TRAIT_RESTRAINED) && !user.legcuffed && isopenturf(user.loc))
- user.balloon_alert(user, "already free!")
- return FALSE
-
if(user.handcuffed)
var/obj/O = user.get_item_by_slot(ITEM_SLOT_HANDCUFFED)
if(!istype(O))
@@ -21,7 +16,9 @@
span_warning("We vomit acidic ooze onto our restraints!"))
addtimer(CALLBACK(src, PROC_REF(dissolve_handcuffs), user, O), 30)
- used = TRUE
+ log_combat(user, user.handcuffed, "melted handcuffs", addition = "(biodegrade)")
+ ..()
+ return TRUE
if(user.legcuffed)
var/obj/O = user.get_item_by_slot(ITEM_SLOT_LEGCUFFED)
@@ -31,37 +28,45 @@
span_warning("We vomit acidic ooze onto our restraints!"))
addtimer(CALLBACK(src, PROC_REF(dissolve_legcuffs), user, O), 30)
- used = TRUE
+ log_combat(user, user.legcuffed, "melted legcuffs", addition = "(biodegrade)")
+ ..()
+ return TRUE
- if(user.wear_suit && user.wear_suit.breakouttime && !used)
+ if(user.wear_suit?.breakouttime)
var/obj/item/clothing/suit/S = user.get_item_by_slot(ITEM_SLOT_OCLOTHING)
if(!istype(S))
return FALSE
user.visible_message(span_warning("[user] vomits a glob of acid across the front of [user.p_their()] [S]!"), \
- span_warning("We vomit acidic ooze onto our straight jacket!"))
+ span_warning("We vomit acidic ooze onto our [user.wear_suit.name]!"))
addtimer(CALLBACK(src, PROC_REF(dissolve_straightjacket), user, S), 30)
- used = TRUE
-
+ log_combat(user, user.wear_suit, "melted [user.wear_suit]", addition = "(biodegrade)")
+ ..()
+ return TRUE
- if(istype(user.loc, /obj/structure/closet) && !used)
+ if(istype(user.loc, /obj/structure/closet))
var/obj/structure/closet/C = user.loc
if(!istype(C))
return FALSE
C.visible_message(span_warning("[C]'s hinges suddenly begin to melt and run!"))
to_chat(user, span_warning("We vomit acidic goop onto the interior of [C]!"))
addtimer(CALLBACK(src, PROC_REF(open_closet), user, C), 70)
- used = TRUE
+ log_combat(user, user.loc, "melted locker", addition = "(biodegrade)")
+ ..()
+ return TRUE
- if(istype(user.loc, /obj/structure/spider/cocoon) && !used)
+ if(istype(user.loc, /obj/structure/spider/cocoon))
var/obj/structure/spider/cocoon/C = user.loc
if(!istype(C))
return FALSE
C.visible_message(span_warning("[src] shifts and starts to fall apart!"))
to_chat(user, span_warning("We secrete acidic enzymes from our skin and begin melting our cocoon..."))
addtimer(CALLBACK(src, PROC_REF(dissolve_cocoon), user, C), 25) //Very short because it's just webs
- used = TRUE
- ..()
- return used
+ log_combat(user, user.loc, "melted cocoon", addition = "(biodegrade)")
+ ..()
+ return TRUE
+
+ user.balloon_alert(user, "already free!")
+ return FALSE
/datum/action/changeling/biodegrade/proc/dissolve_handcuffs(mob/living/carbon/human/user, obj/O)
if(O && user.handcuffed == O)
diff --git a/code/modules/antagonists/changeling/powers/defib_grasp.dm b/code/modules/antagonists/changeling/powers/defib_grasp.dm
index 20ff3049c8fdd1..135b9b243f7217 100644
--- a/code/modules/antagonists/changeling/powers/defib_grasp.dm
+++ b/code/modules/antagonists/changeling/powers/defib_grasp.dm
@@ -4,7 +4,7 @@
we will snatch their arms off and instantly finalize our stasis."
helptext = "This ability is passive, and will trigger when a defibrillator paddle is applied to our chest \
while we are dead or in stasis. Will also stun cyborgs momentarily."
- needs_button = FALSE
+ owner_has_control = FALSE
dna_cost = 0
/// Flags to pass to fully heal when we get zapped
diff --git a/code/modules/antagonists/changeling/powers/headcrab.dm b/code/modules/antagonists/changeling/powers/headcrab.dm
index 7daebc4cfb2b9d..f608d1620b86f1 100644
--- a/code/modules/antagonists/changeling/powers/headcrab.dm
+++ b/code/modules/antagonists/changeling/powers/headcrab.dm
@@ -35,7 +35,7 @@
user.transfer_observers_to(user_turf) // user is about to be deleted, store orbiters on the turf
if(user.stat != DEAD)
user.investigate_log("has been gibbed by headslug burst.", INVESTIGATE_DEATHS)
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
. = TRUE
addtimer(CALLBACK(src, PROC_REF(spawn_headcrab), stored_mind, user_turf, organs), 1 SECONDS)
diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm
index bf4f8c2b3da3e4..54027b7d8a24f8 100644
--- a/code/modules/antagonists/changeling/powers/mutations.dm
+++ b/code/modules/antagonists/changeling/powers/mutations.dm
@@ -480,78 +480,6 @@
remaining_uses--
return ..()
-
-/***************************************\
-|*********SPACE SUIT + HELMET***********|
-\***************************************/
-/datum/action/changeling/suit/organic_space_suit
- name = "Organic Space Suit"
- desc = "We grow an organic suit to protect ourselves from space exposure, including regulation of temperature and oxygen needs. Costs 20 chemicals."
- helptext = "We must constantly repair our form to make it space-proof, reducing chemical production while we are protected. Cannot be used in lesser form."
- button_icon_state = "organic_suit"
- chemical_cost = 20
- dna_cost = 2
- req_human = TRUE
-
- suit_type = /obj/item/clothing/suit/space/changeling
- helmet_type = /obj/item/clothing/head/helmet/space/changeling
- suit_name_simple = "flesh shell"
- helmet_name_simple = "space helmet"
- recharge_slowdown = 0.25
- blood_on_castoff = 1
-
-/obj/item/clothing/suit/space/changeling
- name = "flesh mass"
- icon_state = "lingspacesuit_t"
- icon = 'icons/obj/clothing/suits/costume.dmi'
- worn_icon = 'icons/mob/clothing/suits/costume.dmi'
- desc = "A huge, bulky mass of pressure and temperature-resistant organic tissue, evolved to facilitate space travel."
- item_flags = DROPDEL
- clothing_flags = STOPSPRESSUREDAMAGE //Not THICKMATERIAL because it's organic tissue, so if somebody tries to inject something into it, it still ends up in your blood. (also balance but muh fluff)
- allowed = list(/obj/item/flashlight, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/oxygen)
- armor_type = /datum/armor/space_changeling
- actions_types = list()
- cell = null
- show_hud = FALSE
-
-/datum/armor/space_changeling
- bio = 100
- fire = 90
- acid = 90
-
-/obj/item/clothing/suit/space/changeling/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NODROP, CHANGELING_TRAIT)
- if(ismob(loc))
- loc.visible_message(span_warning("[loc.name]\'s flesh rapidly inflates, forming a bloated mass around [loc.p_their()] body!"), span_warning("We inflate our flesh, creating a spaceproof suit!"), span_hear("You hear organic matter ripping and tearing!"))
- START_PROCESSING(SSobj, src)
-
-// seal the cell door
-/obj/item/clothing/suit/space/changeling/toggle_spacesuit_cell(mob/user)
- return
-
-/obj/item/clothing/suit/space/changeling/process(seconds_per_tick)
- if(ishuman(loc))
- var/mob/living/carbon/human/H = loc
- H.reagents.add_reagent(/datum/reagent/medicine/salbutamol, REAGENTS_METABOLISM * (seconds_per_tick / SSMOBS_DT))
- H.adjust_bodytemperature(temperature_setting - H.bodytemperature) // force changelings to normal temp step mode played badly
-
-/obj/item/clothing/head/helmet/space/changeling
- name = "flesh mass"
- icon = 'icons/obj/clothing/head/costume.dmi'
- worn_icon = 'icons/mob/clothing/head/costume.dmi'
- icon_state = "lingspacehelmet"
- inhand_icon_state = null
- desc = "A covering of pressure and temperature-resistant organic tissue with a glass-like chitin front."
- item_flags = DROPDEL
- clothing_flags = STOPSPRESSUREDAMAGE | HEADINTERNALS
- armor_type = /datum/armor/space_changeling
- flags_cover = HEADCOVERSEYES | HEADCOVERSMOUTH
-
-/obj/item/clothing/head/helmet/space/changeling/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NODROP, CHANGELING_TRAIT)
-
/***************************************\
|*****************ARMOR*****************|
\***************************************/
diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm
index 412307e33ce9ce..83dc308022cee8 100644
--- a/code/modules/antagonists/changeling/powers/tiny_prick.dm
+++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm
@@ -100,18 +100,22 @@
return FALSE
return TRUE
-/datum/action/changeling/sting/transformation/sting_action(mob/user, mob/target)
- log_combat(user, target, "stung", "transformation sting", " new identity is '[selected_dna.dna.real_name]'")
- var/datum/dna/NewDNA = selected_dna.dna
-
- var/mob/living/carbon/C = target
- . = TRUE
- if(istype(C))
- C.real_name = NewDNA.real_name
- NewDNA.transfer_identity(C)
- C.updateappearance(mutcolor_update=1)
+/datum/action/changeling/sting/transformation/sting_action(mob/living/user, mob/living/target)
+ var/final_duration = sting_duration
+ var/final_message = span_notice("We transform [target] into [selected_dna.dna.real_name].")
+ if(ismonkey(target))
+ final_duration = INFINITY
+ final_message = span_warning("Our genes cry out as we transform the lesser form of [target] into [selected_dna.dna.real_name] permanently!")
+
+ if(target.apply_status_effect(/datum/status_effect/temporary_transformation/trans_sting, final_duration, selected_dna.dna))
+ ..()
+ log_combat(user, target, "stung", "transformation sting", " new identity is '[selected_dna.dna.real_name]'")
+ to_chat(user, final_message)
+ return TRUE
+ return FALSE
*/
//SKYRAT EDIT REMOVAL END
+
/datum/action/changeling/sting/false_armblade
name = "False Armblade Sting"
desc = "We silently sting a human, injecting a retrovirus that mutates their arm to temporarily appear as an armblade. Costs 20 chemicals."
@@ -136,13 +140,14 @@
return TRUE
/datum/action/changeling/sting/false_armblade/sting_action(mob/user, mob/target)
- log_combat(user, target, "stung", object="false armblade sting")
var/obj/item/held = target.get_active_held_item()
if(held && !target.dropItemToGround(held))
to_chat(user, span_warning("[held] is stuck to [target.p_their()] hand, you cannot grow a false armblade over it!"))
return
+
..()
+ log_combat(user, target, "stung", object = "false armblade sting")
if(ismonkey(target))
to_chat(user, span_notice("Our genes cry out as we sting [target.name]!"))
@@ -178,6 +183,7 @@
return changeling.can_absorb_dna(target)
/datum/action/changeling/sting/extract_dna/sting_action(mob/user, mob/living/carbon/human/target)
+ ..()
log_combat(user, target, "stung", "extraction sting")
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
if(!changeling.has_profile_with_dna(target.dna))
@@ -193,6 +199,7 @@
dna_cost = 2
/datum/action/changeling/sting/mute/sting_action(mob/user, mob/living/carbon/target)
+ ..()
log_combat(user, target, "stung", "mute sting")
target.adjust_silence(1 MINUTES)
return TRUE
@@ -215,6 +222,7 @@
user.balloon_alert(user, "robotic eyes!")
return FALSE
+ ..()
log_combat(user, target, "stung", "blind sting")
to_chat(target, span_danger("Your eyes burn horrifically!"))
eyes.apply_organ_damage(eyes.maxHealth * 0.8)
@@ -232,6 +240,7 @@
dna_cost = 1
/datum/action/changeling/sting/lsd/sting_action(mob/user, mob/living/carbon/target)
+ ..()
log_combat(user, target, "stung", "LSD sting")
addtimer(CALLBACK(src, PROC_REF(hallucination_time), target), rand(30 SECONDS, 60 SECONDS))
return TRUE
@@ -250,6 +259,7 @@
dna_cost = 2
/datum/action/changeling/sting/cryo/sting_action(mob/user, mob/target)
+ ..()
log_combat(user, target, "stung", "cryo sting")
if(target.reagents)
target.reagents.add_reagent(/datum/reagent/consumable/frostoil, 30)
diff --git a/code/modules/antagonists/changeling/powers/void_adaption.dm b/code/modules/antagonists/changeling/powers/void_adaption.dm
new file mode 100644
index 00000000000000..76c0eeffc972de
--- /dev/null
+++ b/code/modules/antagonists/changeling/powers/void_adaption.dm
@@ -0,0 +1,68 @@
+/datum/action/changeling/void_adaption
+ name = "Void Adaption"
+ desc = "We prepare our cells to resist the hostile environment outside of the station. We may freely travel wherever we wish."
+ helptext = "This ability is passive, and will automatically protect you in situations of extreme cold or vacuum, \
+ as well as removing your need to breathe. While it is actively protecting you from temperature or pressure \
+ it reduces your chemical regeneration rate."
+ owner_has_control = FALSE
+ dna_cost = 2
+
+ /// Traits we apply to become immune to the environment
+ var/static/list/gain_traits = list(TRAIT_NOBREATH, TRAIT_RESISTCOLD, TRAIT_RESISTLOWPRESSURE, TRAIT_SNOWSTORM_IMMUNE)
+ /// How much we slow chemical regeneration while active, in chems per second
+ var/recharge_slowdown = 0.25
+ /// Are we currently protecting our user?
+ var/currently_active = FALSE
+
+/datum/action/changeling/void_adaption/on_purchase(mob/user, is_respec)
+ . = ..()
+ user.add_traits(gain_traits, REF(src))
+ RegisterSignal(user, COMSIG_LIVING_LIFE, PROC_REF(check_environment))
+
+/datum/action/changeling/void_adaption/Remove(mob/remove_from)
+ remove_from.remove_traits(gain_traits, REF(src))
+ UnregisterSignal(remove_from, COMSIG_LIVING_LIFE)
+ if (currently_active)
+ on_removed_adaption(remove_from, "Our cells relax, despite the danger!")
+ return ..()
+
+/// Checks if we would be providing any useful benefit at present
+/datum/action/changeling/void_adaption/proc/check_environment(mob/living/void_adapted)
+ SIGNAL_HANDLER
+
+ var/list/active_reasons = list()
+
+ var/datum/gas_mixture/environment = void_adapted.loc.return_air()
+ if (!isnull(environment))
+ var/vulnerable_temperature = void_adapted.get_body_temp_cold_damage_limit()
+ var/affected_temperature = environment.return_temperature()
+ if (ishuman(void_adapted))
+ var/mob/living/carbon/human/special_boy = void_adapted
+ var/cold_protection = special_boy.get_cold_protection(affected_temperature)
+ vulnerable_temperature *= (1 - cold_protection)
+
+ var/affected_pressure = special_boy.calculate_affecting_pressure(environment.return_pressure())
+ if (affected_pressure < HAZARD_LOW_PRESSURE)
+ active_reasons += "vacuum"
+
+ if (affected_temperature < vulnerable_temperature)
+ active_reasons += "cold"
+
+ var/should_be_active = !!length(active_reasons)
+ if (currently_active == should_be_active)
+ return
+
+ if (!should_be_active)
+ on_removed_adaption(void_adapted, "Our cells relax in safer air.")
+ return
+ var/datum/antagonist/changeling/changeling_data = void_adapted.mind?.has_antag_datum(/datum/antagonist/changeling)
+ to_chat(void_adapted, span_changeling("Our cells harden themselves against the [pick(active_reasons)]."))
+ changeling_data?.chem_recharge_slowdown -= recharge_slowdown
+ currently_active = TRUE
+
+/// Called when we stop being adapted
+/datum/action/changeling/void_adaption/proc/on_removed_adaption(mob/living/former, message)
+ var/datum/antagonist/changeling/changeling_data = former.mind?.has_antag_datum(/datum/antagonist/changeling)
+ to_chat(former, span_changeling(message))
+ changeling_data?.chem_recharge_slowdown += recharge_slowdown
+ currently_active = FALSE
diff --git a/code/modules/antagonists/cult/blood_magic.dm b/code/modules/antagonists/cult/blood_magic.dm
index dd5e9a9c6610f0..27a87bee6c2d11 100644
--- a/code/modules/antagonists/cult/blood_magic.dm
+++ b/code/modules/antagonists/cult/blood_magic.dm
@@ -720,11 +720,13 @@
human_bloodbag.visible_message(span_warning("[human_bloodbag] is partially healed by [human_bloodbag == user ? "[human_bloodbag.p_their()]":"[human_bloodbag]'s"] blood magic."))
uses = 0
ratio *= -1
- human_bloodbag.adjustOxyLoss((overall_damage*ratio) * (human_bloodbag.getOxyLoss() / overall_damage), 0)
- human_bloodbag.adjustToxLoss((overall_damage*ratio) * (human_bloodbag.getToxLoss() / overall_damage), 0)
- human_bloodbag.adjustFireLoss((overall_damage*ratio) * (human_bloodbag.getFireLoss() / overall_damage), 0)
- human_bloodbag.adjustBruteLoss((overall_damage*ratio) * (human_bloodbag.getBruteLoss() / overall_damage), 0)
- human_bloodbag.updatehealth()
+ var/need_mob_update = FALSE
+ need_mob_update += human_bloodbag.adjustOxyLoss((overall_damage*ratio) * (human_bloodbag.getOxyLoss() / overall_damage), updating_health = FALSE)
+ need_mob_update += human_bloodbag.adjustToxLoss((overall_damage*ratio) * (human_bloodbag.getToxLoss() / overall_damage), updating_health = FALSE)
+ need_mob_update += human_bloodbag.adjustFireLoss((overall_damage*ratio) * (human_bloodbag.getFireLoss() / overall_damage), updating_health = FALSE)
+ need_mob_update += human_bloodbag.adjustBruteLoss((overall_damage*ratio) * (human_bloodbag.getBruteLoss() / overall_damage), updating_health = FALSE)
+ if(need_mob_update)
+ human_bloodbag.updatehealth()
playsound(get_turf(human_bloodbag), 'sound/magic/staff_healing.ogg', 25)
new /obj/effect/temp_visual/cult/sparks(get_turf(human_bloodbag))
user.Beam(human_bloodbag, icon_state="sendbeam", time = 15)
diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm
index b0d877820d903d..0f43b5ae29ef4d 100644
--- a/code/modules/antagonists/cult/cult.dm
+++ b/code/modules/antagonists/cult/cult.dm
@@ -23,7 +23,7 @@
/datum/antagonist/cult/greet()
. = ..()
- owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/bloodcult.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)//subject to change
+ owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/bloodcult/bloodcult_gain.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)//subject to change
owner.announce_objectives()
/datum/antagonist/cult/on_gain()
diff --git a/code/modules/antagonists/cult/cult_bastard_sword.dm b/code/modules/antagonists/cult/cult_bastard_sword.dm
index a30ffb1f5eede6..784eaedf636aca 100644
--- a/code/modules/antagonists/cult/cult_bastard_sword.dm
+++ b/code/modules/antagonists/cult/cult_bastard_sword.dm
@@ -80,7 +80,7 @@
to_chat(user, span_cultlarge("\"You cling to the Forgotten Gods, as if you're more than their pawn.\""))
to_chat(user, span_userdanger("A horrible force yanks at your arm!"))
user.emote("scream")
- user.apply_damage(30, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ user.apply_damage(30, BRUTE, pick(GLOB.arm_zones))
user.dropItemToGround(src, TRUE)
user.Paralyze(50)
return
diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm
index c3175d0ed4ac32..e5adcf0cfc5ff9 100644
--- a/code/modules/antagonists/cult/cult_items.dm
+++ b/code/modules/antagonists/cult/cult_items.dm
@@ -100,7 +100,7 @@ Striking a noncultist, however, will tear their flesh."}
span_cultlarge("\"You shouldn't play with sharp things. You'll poke someone's eye out.\""))
if(ishuman(user))
var/mob/living/carbon/human/miscreant = user
- miscreant.apply_damage(rand(force/2, force), BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ miscreant.apply_damage(rand(force/2, force), BRUTE, pick(GLOB.arm_zones))
else
user.adjustBruteLoss(rand(force/2,force))
return
diff --git a/code/modules/antagonists/cult/cult_team.dm b/code/modules/antagonists/cult/cult_team.dm
index 6254ded64a030f..1d199a113f5d24 100644
--- a/code/modules/antagonists/cult/cult_team.dm
+++ b/code/modules/antagonists/cult/cult_team.dm
@@ -48,7 +48,7 @@
if(ratio > CULT_RISEN && !cult_risen)
for(var/datum/mind/mind as anything in members)
if(mind.current)
- SEND_SOUND(mind.current, 'sound/hallucinations/i_see_you2.ogg')
+ SEND_SOUND(mind.current, 'sound/ambience/antag/bloodcult/bloodcult_eyes.ogg')
to_chat(mind.current, span_cultlarge(span_warning("The veil weakens as your cult grows, your eyes begin to glow...")))
mind.current.AddElement(/datum/element/cult_eyes)
cult_risen = TRUE
@@ -57,7 +57,7 @@
if(ratio > CULT_ASCENDENT && !cult_ascendent)
for(var/datum/mind/mind as anything in members)
if(mind.current)
- SEND_SOUND(mind.current, 'sound/hallucinations/im_here1.ogg')
+ SEND_SOUND(mind.current, 'sound/ambience/antag/bloodcult/bloodcult_halos.ogg')
to_chat(mind.current, span_cultlarge(span_warning("Your cult is ascendent and the red harvest approaches - you cannot hide your true nature for much longer!!")))
mind.current.AddElement(/datum/element/cult_halo)
cult_ascendent = TRUE
diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm
index 3b2a13c2cb5947..1d514fe78307e8 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -370,7 +370,7 @@ structure_check() searches for nearby cultist structures required for the invoca
if(sacrificial)
playsound(sacrificial, 'sound/magic/disintegrate.ogg', 100, TRUE)
sacrificial.investigate_log("has been sacrificially gibbed by the cult.", INVESTIGATE_DEATHS)
- sacrificial.gib()
+ sacrificial.gib(DROP_ALL_REMAINS)
return TRUE
/obj/effect/rune/empower
@@ -646,7 +646,7 @@ structure_check() searches for nearby cultist structures required for the invoca
else
fail_invoke()
return
- SEND_SOUND(mob_to_revive, 'sound/ambience/antag/bloodcult.ogg')
+ SEND_SOUND(mob_to_revive, 'sound/ambience/antag/bloodcult/bloodcult_gain.ogg')
to_chat(mob_to_revive, span_cultlarge("\"PASNAR SAVRAE YAM'TOTH. Arise.\""))
mob_to_revive.visible_message(span_warning("[mob_to_revive] draws in a huge breath, red light shining from [mob_to_revive.p_their()] eyes."), \
span_cultlarge("You awaken suddenly from the void. You're alive!"))
@@ -697,7 +697,7 @@ structure_check() searches for nearby cultist structures required for the invoca
barrier.Toggle()
if(iscarbon(user))
var/mob/living/carbon/C = user
- C.apply_damage(2, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ C.apply_damage(2, BRUTE, pick(GLOB.arm_zones))
//Rite of Joined Souls: Summons a single cultist.
/obj/effect/rune/summon
@@ -886,7 +886,7 @@ structure_check() searches for nearby cultist structures required for the invoca
new_human.equipOutfit(/datum/outfit/ghost_cultist) //give them armor
new_human.apply_status_effect(/datum/status_effect/cultghost) //ghosts can't summon more ghosts
new_human.set_invis_see(SEE_INVISIBLE_OBSERVER)
- ADD_TRAIT(new_human, TRAIT_NOBREATH, INNATE_TRAIT)
+ new_human.add_traits(list(TRAIT_NOBREATH, TRAIT_PERMANENTLY_MORTAL), INNATE_TRAIT) // permanently mortal can be removed once this is a bespoke kind of mob
ghosts++
playsound(src, 'sound/magic/exit_blood.ogg', 50, TRUE)
visible_message(span_warning("A cloud of red mist forms above [src], and from within steps... a [new_human.gender == FEMALE ? "wo":""]man."))
@@ -946,8 +946,8 @@ structure_check() searches for nearby cultist structures required for the invoca
affecting = null
rune_in_use = FALSE
-/mob/living/carbon/human/cult_ghost/spill_organs(no_brain, no_organs, no_bodyparts) //cult ghosts never drop a brain
- no_brain = TRUE
+/mob/living/carbon/human/cult_ghost/spill_organs(drop_bitflags=NONE)
+ drop_bitflags &= ~DROP_BRAIN //cult ghosts never drop a brain
. = ..()
/mob/living/carbon/human/cult_ghost/get_organs_for_zone(zone, include_children)
@@ -1017,7 +1017,7 @@ structure_check() searches for nearby cultist structures required for the invoca
add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/noncult, "human_apoc", A, NONE)
addtimer(CALLBACK(M, TYPE_PROC_REF(/atom/, remove_alt_appearance),"human_apoc",TRUE), duration)
images += A
- SEND_SOUND(M, pick(sound('sound/ambience/antag/bloodcult.ogg'),sound('sound/voice/ghost_whisper.ogg'),sound('sound/misc/ghosty_wind.ogg')))
+ SEND_SOUND(M, pick(sound('sound/ambience/antag/bloodcult/bloodcult_gain.ogg'),sound('sound/voice/ghost_whisper.ogg'),sound('sound/misc/ghosty_wind.ogg')))
else
var/construct = pick("wraith","artificer","juggernaut")
var/image/B = image('icons/mob/nonhuman-player/cult.dmi',M,construct, ABOVE_MOB_LAYER)
diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm
index 0120c3b7ee3a86..f0eff069e89ace 100644
--- a/code/modules/antagonists/heretic/heretic_antag.dm
+++ b/code/modules/antagonists/heretic/heretic_antag.dm
@@ -34,6 +34,8 @@
var/heretic_path = PATH_START
/// A sum of how many knowledge points this heretic CURRENTLY has. Used to research.
var/knowledge_points = 2 //SKYRAT EDIT - ORIGINAL 1
+ /// How many side path points the heretic has. He gains one of these per main path that splits into two sidepaths. These can be used in place of knowledge points for side paths only.
+ var/side_path_points = 0
/// The time between gaining influence passively. The heretic gain +1 knowledge points every this duration of time.
var/passive_gain_timer = 20 MINUTES
/// Assoc list of [typepath] = [knowledge instance]. A list of all knowledge this heretic's reserached.
@@ -85,6 +87,7 @@
var/list/data = list()
data["charges"] = knowledge_points
+ data["side_charges"] = side_path_points
data["total_sacrifices"] = total_sacrifices
data["ascended"] = ascended
@@ -98,7 +101,10 @@
knowledge_data["desc"] = initial(knowledge.desc)
knowledge_data["gainFlavor"] = initial(knowledge.gain_text)
knowledge_data["cost"] = initial(knowledge.cost)
- knowledge_data["disabled"] = (initial(knowledge.cost) > knowledge_points)
+ if(initial(knowledge.route) == PATH_SIDE)
+ knowledge_data["disabled"] = (initial(knowledge.cost) > knowledge_points + side_path_points)
+ else
+ knowledge_data["disabled"] = (initial(knowledge.cost) > knowledge_points)
// Final knowledge can't be learned until all objectives are complete.
if(ispath(knowledge, /datum/heretic_knowledge/ultimate))
@@ -142,13 +148,22 @@
if(!ispath(researched_path))
CRASH("Heretic attempted to learn non-heretic_knowledge path! (Got: [researched_path])")
- if(initial(researched_path.cost) > knowledge_points)
- return TRUE
+ // If side path and has path points, buy!
+ var/coupon = FALSE
+ if((initial(researched_path.route) == PATH_SIDE) && side_path_points)
+ coupon = TRUE
+ // else try normal purchase
+ else if(initial(researched_path.cost) > knowledge_points)
+ return
+
if(!gain_knowledge(researched_path))
return TRUE
- log_heretic_knowledge("[key_name(owner)] gained knowledge: [initial(researched_path.name)]")
- knowledge_points -= initial(researched_path.cost)
+ log_heretic_knowledge("[key_name(owner)] gained knowledge: [initial(researched_path.name)][coupon ? "(via free side-path point)" : ""]")
+ if(coupon)
+ side_path_points -= initial(researched_path.cost)
+ else
+ knowledge_points -= initial(researched_path.cost)
return TRUE
/datum/antagonist/heretic/submit_player_objective(retain_existing = FALSE, retain_escape = TRUE, force = FALSE)
@@ -305,14 +320,14 @@
* * drawing_time - how long the do_after takes to make the rune
* * additional checks - optional callbacks to be ran while drawing the rune
*/
-/datum/antagonist/heretic/proc/try_draw_rune(mob/living/user, turf/target_turf, drawing_time = 30 SECONDS, additional_checks)
+/datum/antagonist/heretic/proc/try_draw_rune(mob/living/user, turf/target_turf, drawing_time = 20 SECONDS, additional_checks)
for(var/turf/nearby_turf as anything in RANGE_TURFS(1, target_turf))
if(!isopenturf(nearby_turf) || is_type_in_typecache(nearby_turf, blacklisted_rune_turfs))
target_turf.balloon_alert(user, "invalid placement for rune!")
return
if(locate(/obj/effect/heretic_rune) in range(3, target_turf))
- target_turf.balloon_alert(user, "to close to another rune!")
+ target_turf.balloon_alert(user, "too close to another rune!")
return
if(drawing_rune)
@@ -330,16 +345,16 @@
* * drawing_time - how long the do_after takes to make the rune
* * additional checks - optional callbacks to be ran while drawing the rune
*/
-/datum/antagonist/heretic/proc/draw_rune(mob/living/user, turf/target_turf, drawing_time = 30 SECONDS, additional_checks)
+/datum/antagonist/heretic/proc/draw_rune(mob/living/user, turf/target_turf, drawing_time = 20 SECONDS, additional_checks)
drawing_rune = TRUE
var/rune_colour = path_to_rune_color[heretic_path]
target_turf.balloon_alert(user, "drawing rune...")
var/obj/effect/temp_visual/drawing_heretic_rune/drawing_effect
- if (drawing_time >= (30 SECONDS))
- drawing_effect = new(target_turf, rune_colour)
- else
+ if (drawing_time < (10 SECONDS))
drawing_effect = new /obj/effect/temp_visual/drawing_heretic_rune/fast(target_turf, rune_colour)
+ else
+ drawing_effect = new(target_turf, rune_colour)
if(!do_after(user, drawing_time, target_turf, extra_checks = additional_checks))
target_turf.balloon_alert(user, "interrupted!")
@@ -594,7 +609,7 @@
if(!admin.client?.holder)
to_chat(admin, span_warning("You shouldn't be using this!"))
return
-
+
var/mob/living/pawn = owner.current
pawn.equip_to_slot_if_possible(new /obj/item/clothing/neck/heretic_focus(get_turf(pawn)), ITEM_SLOT_NECK, TRUE, TRUE)
to_chat(pawn, span_hypnophrase("The Mansus has manifested you a focus."))
@@ -766,8 +781,8 @@
target_amount = main_path_length
// Add in the base research we spawn with, otherwise it'd be too easy.
target_amount += length(GLOB.heretic_start_knowledge)
- // And add in some buffer, to require some sidepathing.
- target_amount += rand(2, 4)
+ // And add in some buffer, to require some sidepathing, especially since heretics get some free side paths.
+ target_amount += rand(5, 7)
update_explanation_text()
/datum/objective/heretic_research/update_explanation_text()
diff --git a/code/modules/antagonists/heretic/heretic_focus.dm b/code/modules/antagonists/heretic/heretic_focus.dm
index b7c79b6d6caa26..45bbf743b8cd11 100644
--- a/code/modules/antagonists/heretic/heretic_focus.dm
+++ b/code/modules/antagonists/heretic/heretic_focus.dm
@@ -46,7 +46,7 @@
if(!IS_HERETIC(user))
return
- if(!(source.slot_flags & slot))
+ if(source.slot_flags && !(source.slot_flags & slot))
return
ADD_TRAIT(user, TRAIT_ALLOW_HERETIC_CASTING, ELEMENT_TRAIT(source))
diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm
index b51c2a5dcb87ba..1f408698f3cfa0 100644
--- a/code/modules/antagonists/heretic/heretic_knowledge.dm
+++ b/code/modules/antagonists/heretic/heretic_knowledge.dm
@@ -25,11 +25,16 @@
var/list/banned_knowledge = list()
/// Assoc list of [typepaths we need] to [amount needed].
/// If set, this knowledge allows the heretic to do a ritual on a transmutation rune with the components set.
+ /// If one of the items in the list is a list, it's treated as 'any of these items will work'
var/list/required_atoms
/// Paired with above. If set, the resulting spawned atoms upon ritual completion.
var/list/result_atoms = list()
+ /// If set, required_atoms checks for these *exact* types and doesn't allow them to be ingredients.
+ var/list/banned_atom_types = list()
/// Cost of knowledge in knowledge points
var/cost = 0
+ /// If true, adds side path points according to value. Only main branch powers that split into sidepaths should have this.
+ var/adds_sidepath_points = 0
/// The priority of the knowledge. Higher priority knowledge appear higher in the ritual list.
/// Number itself is completely arbitrary. Does not need to be set for non-ritual knowledge.
var/priority = 0
@@ -58,6 +63,8 @@
if(gain_text)
to_chat(user, span_warning("[gain_text]"))
+ // Usually zero
+ our_heretic.side_path_points += adds_sidepath_points
on_gain(user, our_heretic)
/**
@@ -112,6 +119,18 @@
/datum/heretic_knowledge/proc/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc)
return TRUE
+/**
+ * Parses specific items into a more reaadble form.
+ * Can be overriden by knoweldge subtypes.
+ */
+/datum/heretic_knowledge/proc/parse_required_item(atom/item_path, number_of_things)
+ // If we need a human, there is a high likelihood we actually need a (dead) body
+ if(ispath(item_path, /mob/living/carbon/human))
+ return "bod[number_of_things > 1 ? "ies" : "y"]"
+ if(ispath(item_path, /mob/living))
+ return "carcass[number_of_things > 1 ? "es" : ""] of any kind"
+ return "[initial(item_path.name)]\s"
+
/**
* Called whenever the knowledge's associated ritual is completed successfully.
*
@@ -158,9 +177,12 @@
var/obj/item/stack/sac_stack = sacrificed
var/how_much_to_use = 0
for(var/requirement in required_atoms)
- if(istype(sacrificed, requirement))
- how_much_to_use = min(required_atoms[requirement], sac_stack.amount)
- break
+ if(islist(requirement) && !is_type_in_list(sacrificed, requirement))
+ continue
+ if(!istype(sacrificed, requirement))
+ continue
+ how_much_to_use = min(required_atoms[requirement], sac_stack.amount)
+ break
sac_stack.use(how_much_to_use)
continue
@@ -264,7 +286,7 @@
/datum/heretic_knowledge/mark
abstract_parent_type = /datum/heretic_knowledge/mark
mutually_exclusive = TRUE
- cost = 2
+ cost = 1
/// The status effect typepath we apply on people on mansus grasp.
var/datum/status_effect/eldritch/mark_type
@@ -508,6 +530,7 @@
/datum/heretic_knowledge/summon/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc)
var/mob/living/summoned = new mob_to_summon(loc)
+ summoned.ai_controller?.set_ai_status(AI_STATUS_OFF)
// Fade in the summon while the ghost poll is ongoing.
// Also don't let them mess with the summon while waiting
summoned.alpha = 0
@@ -711,6 +734,6 @@
/datum/heretic_knowledge/ultimate/cleanup_atoms(list/selected_atoms)
for(var/mob/living/carbon/human/sacrifice in selected_atoms)
selected_atoms -= sacrifice
- sacrifice.gib()
+ sacrifice.gib(DROP_ALL_REMAINS)
return ..()
diff --git a/code/modules/antagonists/heretic/heretic_living_heart.dm b/code/modules/antagonists/heretic/heretic_living_heart.dm
index 4af93c0da68c80..1766cb4cd765e0 100644
--- a/code/modules/antagonists/heretic/heretic_living_heart.dm
+++ b/code/modules/antagonists/heretic/heretic_living_heart.dm
@@ -99,7 +99,7 @@
return TRUE
-/datum/action/cooldown/track_target/Trigger(trigger_flags)
+/datum/action/cooldown/track_target/Trigger(trigger_flags, atom/target)
right_clicked = !!(trigger_flags & TRIGGER_SECONDARY_ACTION)
return ..()
diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm
index 503e066d0e39cd..74a851e7fb7ad4 100644
--- a/code/modules/antagonists/heretic/influences.dm
+++ b/code/modules/antagonists/heretic/influences.dm
@@ -177,7 +177,7 @@
head.dismember()
qdel(head)
else
- human_user.gib()
+ human_user.gib(DROP_ALL_REMAINS)
human_user.investigate_log("has died from using telekinesis on a heretic influence.", INVESTIGATE_DEATHS)
var/datum/effect_system/reagents_explosion/explosion = new()
explosion.set_up(1, get_turf(human_user), TRUE, 0)
@@ -260,7 +260,8 @@
// Using a codex will give you two knowledge points for draining.
if(!being_drained && istype(weapon, /obj/item/codex_cicatrix))
var/obj/item/codex_cicatrix/codex = weapon
- codex.open_animation()
+ if(!codex.book_open)
+ codex.attack_self(user) // open booke
INVOKE_ASYNC(src, PROC_REF(drain_influence), user, 2)
return TRUE
diff --git a/code/modules/antagonists/heretic/items/forbidden_book.dm b/code/modules/antagonists/heretic/items/forbidden_book.dm
index 80721c97592a75..ff570801c5f1c4 100644
--- a/code/modules/antagonists/heretic/items/forbidden_book.dm
+++ b/code/modules/antagonists/heretic/items/forbidden_book.dm
@@ -9,8 +9,6 @@
w_class = WEIGHT_CLASS_SMALL
/// Helps determine the icon state of this item when it's used on self.
var/book_open = FALSE
- /// id for timer
- var/timer_id
/obj/item/codex_cicatrix/Initialize(mapload)
. = ..()
@@ -31,6 +29,7 @@
. += span_notice("Can be used to tap influences for additional knowledge points.")
. += span_notice("Can also be used to draw or remove transmutation runes with ease.")
+ . += span_notice("Additionally, it can work as a focus for your spells in a pinch, though a more specialized relic is recommended, as this may get dropped in combat.")
/obj/item/codex_cicatrix/attack_self(mob/user, modifiers)
. = ..()
@@ -39,8 +38,12 @@
if(book_open)
close_animation()
+ RemoveElement(/datum/element/heretic_focus)
+ w_class = WEIGHT_CLASS_SMALL
else
open_animation()
+ AddElement(/datum/element/heretic_focus)
+ w_class = WEIGHT_CLASS_NORMAL
/obj/item/codex_cicatrix/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
. = ..()
@@ -52,7 +55,7 @@
return
if(isopenturf(target))
- heretic_datum.try_draw_rune(user, target, drawing_time = 12 SECONDS)
+ heretic_datum.try_draw_rune(user, target, drawing_time = 8 SECONDS)
return TRUE
/// Plays a little animation that shows the book opening and closing.
@@ -61,12 +64,8 @@
flick("[base_icon_state]_opening", src)
book_open = TRUE
- timer_id = addtimer(CALLBACK(src, PROC_REF(close_animation)), 5 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
-
/// Plays a closing animation and resets the icon state.
/obj/item/codex_cicatrix/proc/close_animation()
icon_state = base_icon_state
flick("[base_icon_state]_closing", src)
book_open = FALSE
-
- deltimer(timer_id)
diff --git a/code/modules/antagonists/heretic/items/heretic_armor.dm b/code/modules/antagonists/heretic/items/heretic_armor.dm
index 93ab613190b1cf..502c52c17fb89c 100644
--- a/code/modules/antagonists/heretic/items/heretic_armor.dm
+++ b/code/modules/antagonists/heretic/items/heretic_armor.dm
@@ -105,12 +105,12 @@
. = ..()
UnregisterSignal(user, list(COMSIG_MOB_UNEQUIPPED_ITEM, COMSIG_MOB_EQUIPPED_ITEM))
-/obj/item/clothing/suit/hooded/cultrobes/void/proc/hide_item(obj/item/item, slot)
+/obj/item/clothing/suit/hooded/cultrobes/void/proc/hide_item(datum/source, obj/item/item, slot)
SIGNAL_HANDLER
if(slot & ITEM_SLOT_SUITSTORE)
ADD_TRAIT(item, TRAIT_NO_STRIP, REF(src)) // i'd use examine hide but its a flag and yeah
-/obj/item/clothing/suit/hooded/cultrobes/void/proc/show_item(obj/item/item, slot)
+/obj/item/clothing/suit/hooded/cultrobes/void/proc/show_item(datum/source, obj/item/item, slot)
SIGNAL_HANDLER
REMOVE_TRAIT(item, TRAIT_NO_STRIP, REF(src))
diff --git a/code/modules/antagonists/heretic/knowledge/ash_lore.dm b/code/modules/antagonists/heretic/knowledge/ash_lore.dm
index df29ba9efa06ee..52e25f6a8b17b2 100644
--- a/code/modules/antagonists/heretic/knowledge/ash_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/ash_lore.dm
@@ -72,9 +72,9 @@
name = "Ashen Passage"
desc = "Grants you Ashen Passage, a silent but short range jaunt."
gain_text = "He knew how to walk between the planes."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/ash_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/summon/fire_shark,
/datum/heretic_knowledge/medallion,
)
@@ -127,6 +127,7 @@
The mask instills fear into heathens who witness it, causing stamina damage, hallucinations, and insanity. \
It can also be forced onto a heathen, to make them unable to take it off..."
gain_text = "The Nightwatcher was lost. That's what the Watch believed. Yet he walked the world, unnoticed by the masses."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/ash,
/datum/heretic_knowledge/reroll_targets,
@@ -165,6 +166,7 @@
If any victims afflicted are in critical condition, they will also instantly die."
gain_text = "The fire was inescapable, and yet, life remained in his charred body. \
The Nightwatcher was a particular man, always watching."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/ash_final,
/datum/heretic_knowledge/summon/ashy,
diff --git a/code/modules/antagonists/heretic/knowledge/blade_lore.dm b/code/modules/antagonists/heretic/knowledge/blade_lore.dm
index 01358807fce23f..f2f3b156a2f70b 100644
--- a/code/modules/antagonists/heretic/knowledge/blade_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/blade_lore.dm
@@ -13,6 +13,9 @@
* Mark of the Blade
* Ritual of Knowledge
* Realignment
+ * > Sidepaths:
+ * Lionhunter Rifle
+ *
* Stance of the Scarred Duelist
* > Sidepaths:
* Carving Knife
@@ -22,7 +25,7 @@
* Furious Steel
* > Sidepaths:
* Maid in the Mirror
- * Lionhunter Rifle
+ * Rust Charge
*
* Maelstrom of Silver
*/
@@ -101,10 +104,10 @@
towards your attacker. This effect can only trigger once every 20 seconds."
gain_text = "The footsoldier was known to be a fearsome duelist. \
Their general quickly appointed them as their personal Champion."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/limited_amount/risen_corpse,
/datum/heretic_knowledge/mark/blade_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/armor,
)
cost = 1
@@ -243,6 +246,7 @@
you gain increased resistance to gaining wounds and resistance to batons."
gain_text = "In time, it was he who stood alone among the bodies of his former comrades, awash in blood, none of it his own. \
He was without rival, equal, or purpose."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/blade,
/datum/heretic_knowledge/reroll_targets,
@@ -372,6 +376,7 @@
at a target, dealing damage and causing bleeding."
gain_text = "Without thinking, I took the knife of a fallen soldier and threw with all my might. My aim was true! \
The Torn Champion smiled at their first taste of agony, and with a nod, their blades became my own."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/summon/maid_in_mirror,
/datum/heretic_knowledge/ultimate/blade_final,
diff --git a/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm b/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm
index 9e4d77bd95c920..4607d78ff5c865 100644
--- a/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm
@@ -70,9 +70,9 @@
However, people with a star mark will get transported along with another person using the rune."
gain_text = "The distant stars crept into my dreams, roaring and screaming without reason. \
I spoke, and heard my own words echoed back."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/cosmic_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/essence,
/datum/heretic_knowledge/summon/fire_shark,
)
@@ -114,6 +114,7 @@
desc = "Fires a projectile that moves very slowly and creates cosmic fields on impact. \
Anyone hit by the projectile will recieve burn damage, a knockdown, and give people in a three tile range a star mark."
gain_text = "The Beast was behind me now at all times, with each sacrifice words of affirmation coursed through me."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/cosmic,
/datum/heretic_knowledge/reroll_targets,
@@ -160,8 +161,11 @@
combo_timer = addtimer(CALLBACK(src, PROC_REF(reset_combo), source), combo_duration, TIMER_STOPPABLE)
var/mob/living/second_target_resolved = second_target?.resolve()
var/mob/living/third_target_resolved = third_target?.resolve()
- target.adjustFireLoss(4)
- target.adjustCloneLoss(2)
+ var/need_mob_update = FALSE
+ need_mob_update += target.adjustFireLoss(4, updating_health = FALSE)
+ need_mob_update += target.adjustCloneLoss(2, updating_health = FALSE)
+ if(need_mob_update)
+ target.updatehealth()
if(target == second_target_resolved || target == third_target_resolved)
reset_combo(source)
return
@@ -170,13 +174,19 @@
if(second_target_resolved)
new /obj/effect/temp_visual/cosmic_explosion(get_turf(second_target_resolved))
playsound(get_turf(second_target_resolved), 'sound/magic/cosmic_energy.ogg', 25, FALSE)
- second_target_resolved.adjustFireLoss(10)
- second_target_resolved.adjustCloneLoss(6)
+ need_mob_update = FALSE
+ need_mob_update += second_target_resolved.adjustFireLoss(10, updating_health = FALSE)
+ need_mob_update += second_target_resolved.adjustCloneLoss(6, updating_health = FALSE)
+ if(need_mob_update)
+ target.updatehealth()
if(third_target_resolved)
new /obj/effect/temp_visual/cosmic_domain(get_turf(third_target_resolved))
playsound(get_turf(third_target_resolved), 'sound/magic/cosmic_energy.ogg', 50, FALSE)
- third_target_resolved.adjustFireLoss(20)
- third_target_resolved.adjustCloneLoss(12)
+ need_mob_update = FALSE
+ need_mob_update += third_target_resolved.adjustFireLoss(20, updating_health = FALSE)
+ need_mob_update += third_target_resolved.adjustCloneLoss(12, updating_health = FALSE)
+ if(need_mob_update)
+ target.updatehealth()
if(combo_counter > 3)
target.apply_status_effect(/datum/status_effect/star_mark, source)
if(target.mind && target.stat != DEAD)
@@ -208,6 +218,7 @@
desc = "Grants you Cosmic Expansion, a spell that creates a 3x3 area of cosmic fields around you. \
Nearby beings will also receive a star mark."
gain_text = "The ground now shook beneath me. The Beast inhabited me, and their voice was intoxicating."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/cosmic_final,
/datum/heretic_knowledge/eldritch_coin,
@@ -253,7 +264,7 @@
var/mob/living/basic/heretic_summon/star_gazer/star_gazer_mob = new /mob/living/basic/heretic_summon/star_gazer(loc)
star_gazer_mob.maxHealth = INFINITY
star_gazer_mob.health = INFINITY
- user.AddElement(/datum/element/death_linked, star_gazer_mob)
+ user.AddComponent(/datum/component/death_linked, star_gazer_mob)
star_gazer_mob.AddComponent(/datum/component/obeys_commands, star_gazer_commands)
star_gazer_mob.AddComponent(/datum/component/damage_aura, range = 7, burn_damage = 0.5, simple_damage = 0.5, immune_factions = list(FACTION_HERETIC), current_owner = user)
star_gazer_mob.befriend(user)
diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
index 07fa27181859a2..f3f6d971353810 100644
--- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
@@ -126,9 +126,9 @@
Voiceless Dead are mute ghouls and only have 50 health, but can use Bloody Blades effectively. \
You can only create two at a time."
gain_text = "I found notes of a dark ritual, unfinished... yet still, I pushed forward."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/flesh_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/void_cloak,
/datum/heretic_knowledge/medallion,
)
@@ -239,6 +239,7 @@
the ability to link minds to communicate with ease, but are very fragile and weak in combat."
gain_text = "I could not continue alone. I was able to summon The Uncanny Man to help me see more. \
The screams... once constant, now silenced by their wretched appearance. Nothing was out of reach."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/flesh,
/datum/heretic_knowledge/reroll_targets,
@@ -250,7 +251,7 @@
/obj/effect/decal/cleanable/blood = 1,
/obj/item/bodypart/arm/left = 1,
)
- mob_to_summon = /mob/living/simple_animal/hostile/heretic_summon/raw_prophet
+ mob_to_summon = /mob/living/basic/heretic_summon/raw_prophet
cost = 1
route = PATH_FLESH
poll_ignore_define = POLL_IGNORE_RAW_PROPHET
@@ -280,6 +281,7 @@
Stalkers can jaunt, release EMPs, shapeshift into animals or automatons, and are strong in combat."
gain_text = "I was able to combine my greed and desires to summon an eldritch beast I had never seen before. \
An ever shapeshifting mass of flesh, it knew well my goals. The Marshal approved."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/flesh_final,
/datum/heretic_knowledge/summon/ashy,
@@ -292,7 +294,7 @@
/obj/item/pen = 1,
/obj/item/paper = 1,
)
- mob_to_summon = /mob/living/simple_animal/hostile/heretic_summon/stalker
+ mob_to_summon = /mob/living/basic/heretic_summon/stalker
cost = 1
route = PATH_FLESH
poll_ignore_define = POLL_IGNORE_STALKER
diff --git a/code/modules/antagonists/heretic/knowledge/general_side.dm b/code/modules/antagonists/heretic/knowledge/general_side.dm
index 9c3fbe9d447f36..2dc2719227b1c1 100644
--- a/code/modules/antagonists/heretic/knowledge/general_side.dm
+++ b/code/modules/antagonists/heretic/knowledge/general_side.dm
@@ -39,19 +39,3 @@
return FALSE
return TRUE
-
-/datum/heretic_knowledge/codex_cicatrix
- name = "Codex Cicatrix"
- desc = "Allows you to transmute a bible, a fountain pen, and hide from an animal (or human) to create a Codex Cicatrix. \
- The Codex Cicatrix can be used when draining influences to gain additional knowledge, but comes at greater risk of being noticed. \
- It can also be used to draw and remove transmutation runes easier."
- gain_text = "The occult leaves fragments of knowledge and power anywhere and everywhere. The Codex Cicatrix is one such example. \
- Within the leather-bound faces and age old pages, a path into the Mansus is revealed."
- required_atoms = list(
- /obj/item/book/bible = 1,
- /obj/item/pen/fountain = 1,
- /obj/item/stack/sheet/animalhide = 1,
- )
- result_atoms = list(/obj/item/codex_cicatrix)
- cost = 1
- route = PATH_SIDE
diff --git a/code/modules/antagonists/heretic/knowledge/knock_lore.dm b/code/modules/antagonists/heretic/knowledge/knock_lore.dm
index fcb2c6ceb4c989..6879f527b6b8b4 100644
--- a/code/modules/antagonists/heretic/knowledge/knock_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/knock_lore.dm
@@ -70,7 +70,7 @@
/datum/heretic_knowledge/knock_grasp/proc/on_secondary_mansus_grasp(mob/living/source, atom/target)
SIGNAL_HANDLER
-
+
if(ismecha(target))
var/obj/vehicle/sealed/mecha/mecha = target
mecha.dna_lock = null
@@ -89,7 +89,7 @@
var/turf/target_turf = get_turf(target)
SEND_SIGNAL(target_turf, COMSIG_ATOM_MAGICALLY_UNLOCKED, src, source)
playsound(target, 'sound/magic/hereticknock.ogg', 100, TRUE, -1)
-
+
return COMPONENT_USE_HAND
/datum/heretic_knowledge/key_ring
@@ -99,6 +99,7 @@
You can use it in-hand to change its form to a card you fused. \
Does not preserve the card used in the ritual."
gain_text = "Gateways shall open before me, my very will ensnaring reality."
+ adds_sidepath_points = 1
required_atoms = list(
/obj/item/storage/wallet = 1,
/obj/item/stack/rods = 1,
@@ -143,6 +144,7 @@
desc = "Grants you Burglar's Finesse, a single-target spell \
that puts a random item from the victims backpack into your hand."
gain_text = "Their trinkets will be mine, as will their lives in due time."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/spell/apetra_vulnera,
/datum/heretic_knowledge/spell/opening_blast,
@@ -171,6 +173,7 @@
While in refuge, you cannot use your hands or spells, and you are immune to slowdown. \
You are invincible but unable to harm anything. Cancelled by being hit with an anti-magic item."
gain_text = "Then I saw my my own reflection cascaded mind-numbingly enough times that I was but a haze."
+ adds_sidepath_points = 1
next_knowledge = list(/datum/heretic_knowledge/ultimate/knock_final)
route = PATH_KNOCK
spell_to_add = /datum/action/cooldown/spell/caretaker
diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm
index bbb98f84bd6bf7..84f128b5cfcf9f 100644
--- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm
@@ -13,6 +13,9 @@
* Mark of Rust
* Ritual of Knowledge
* Rust Construction
+ * > Sidepaths:
+ * Lionhunter Rifle
+ *
* Aggressive Spread
* > Sidepaths:
* Curse of Corrosion
@@ -22,7 +25,7 @@
* Entropic Plume
* > Sidepaths:
* Rusted Ritual
- * Blood Cleave
+ * Rust Charge
*
* Rustbringer's Oath
*/
@@ -80,9 +83,9 @@
name = "Leeching Walk"
desc = "Grants you passive healing and resistance to batons while standing over rust."
gain_text = "The speed was unparalleled, the strength unnatural. The Blacksmith was smiling."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/rust_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/armor,
/datum/heretic_knowledge/essence,
/datum/heretic_knowledge/entropy_pulse,
@@ -126,11 +129,14 @@
return
// Heals all damage + Stamina
- source.adjustBruteLoss(-2, FALSE)
- source.adjustFireLoss(-2, FALSE)
- source.adjustToxLoss(-2, FALSE, forced = TRUE) // Slimes are people to
- source.adjustOxyLoss(-0.5, FALSE)
- source.adjustStaminaLoss(-2)
+ var/need_mob_update = FALSE
+ need_mob_update += source.adjustBruteLoss(-2, updating_health = FALSE)
+ need_mob_update += source.adjustFireLoss(-2, updating_health = FALSE)
+ need_mob_update += source.adjustToxLoss(-2, updating_health = FALSE, forced = TRUE) // Slimes are people too
+ need_mob_update += source.adjustOxyLoss(-0.5, updating_health = FALSE)
+ need_mob_update += source.adjustStaminaLoss(-2, updating_stamina = FALSE)
+ if(need_mob_update)
+ source.updatehealth()
// Reduces duration of stuns/etc
source.AdjustAllImmobility(-0.5 SECONDS)
// Heals blood loss
@@ -169,6 +175,7 @@
desc = "Grants you Aggressive Spread, a spell that spreads rust to nearby surfaces. \
Already rusted surfaces are destroyed."
gain_text = "All wise men know well not to visit the Rusted Hills... Yet the Blacksmith's tale was inspiring."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/rust,
/datum/heretic_knowledge/reroll_targets,
@@ -198,6 +205,7 @@
at friend or foe wildly. Also rusts and destroys and surfaces it hits."
gain_text = "The corrosion was unstoppable. The rust was unpleasable. \
The Blacksmith was gone, and you hold their blade. Champions of hope, the Rustbringer is nigh!"
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/rust_final,
/datum/heretic_knowledge/summon/rusty,
@@ -295,11 +303,14 @@
if(!HAS_TRAIT(our_turf, TRAIT_RUSTY))
return
- source.adjustBruteLoss(-4, FALSE)
- source.adjustFireLoss(-4, FALSE)
- source.adjustToxLoss(-4, FALSE, forced = TRUE)
- source.adjustOxyLoss(-4, FALSE)
- source.adjustStaminaLoss(-20)
+ var/need_mob_update = FALSE
+ need_mob_update += source.adjustBruteLoss(-4, updating_health = FALSE)
+ need_mob_update += source.adjustFireLoss(-4, updating_health = FALSE)
+ need_mob_update += source.adjustToxLoss(-4, updating_health = FALSE, forced = TRUE)
+ need_mob_update += source.adjustOxyLoss(-4, updating_health = FALSE)
+ need_mob_update += source.adjustStaminaLoss(-20, updating_stamina = FALSE)
+ if(need_mob_update)
+ source.updatehealth()
/**
* #Rust spread datum
diff --git a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm
index 0d2b67daae8eb9..50aaad96cd8777 100644
--- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm
+++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_buff.dm
@@ -27,7 +27,7 @@
/datum/status_effect/unholy_determination/tick(seconds_between_ticks)
// The amount we heal of each damage type per tick. If we're missing legs we heal better because we can't dodge.
- var/healing_amount = 1 + (2 - owner.usable_legs)
+ var/healing_amount = (0.4 + (0.8 - owner.usable_legs))
// In softcrit you're, strong enough to stay up.
if(owner.health <= owner.crit_threshold && owner.health >= owner.hardcrit_threshold)
@@ -48,22 +48,25 @@
if(prob(2))
playsound(owner, pick(GLOB.creepy_ambience), 50, TRUE)
- adjust_all_damages(healing_amount)
+ adjust_all_damages(healing_amount, seconds_between_ticks)
adjust_temperature()
adjust_bleed_wounds()
/*
* Heals up all the owner a bit, fire stacks and losebreath included.
*/
-/datum/status_effect/unholy_determination/proc/adjust_all_damages(amount)
+/datum/status_effect/unholy_determination/proc/adjust_all_damages(amount, seconds_between_ticks)
owner.adjust_fire_stacks(-1)
owner.losebreath = max(owner.losebreath - 0.5, 0)
- owner.adjustToxLoss(-amount, FALSE, TRUE)
- owner.adjustOxyLoss(-amount, FALSE)
- owner.adjustBruteLoss(-amount, FALSE)
- owner.adjustFireLoss(-amount)
+ var/need_mob_update = FALSE
+ need_mob_update += owner.adjustToxLoss(-amount * seconds_between_ticks, updating_health = FALSE, forced = TRUE)
+ need_mob_update += owner.adjustOxyLoss(-amount * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustBruteLoss(-amount * seconds_between_ticks, updating_health = FALSE)
+ need_mob_update += owner.adjustFireLoss(-amount * seconds_between_ticks, updating_health = FALSE)
+ if(need_mob_update)
+ owner.updatehealth()
/*
* Adjust the owner's temperature up or down to standard body temperatures.
diff --git a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm
index 3e37c17392361f..e2887d737579ed 100644
--- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm
+++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm
@@ -492,7 +492,7 @@
/datum/heretic_knowledge/hunt_and_sacrifice/proc/disembowel_target(mob/living/carbon/human/sac_target)
if(heretic_mind)
log_combat(heretic_mind.current, sac_target, "disemboweled via sacrifice")
- sac_target.spill_organs()
+ sac_target.spill_organs(DROP_ALL_REMAINS)
sac_target.apply_damage(250, BRUTE)
if(sac_target.stat != DEAD)
sac_target.investigate_log("has been killed by heretic sacrifice.", INVESTIGATE_DEATHS)
diff --git a/code/modules/antagonists/heretic/knowledge/side_ash_flesh.dm b/code/modules/antagonists/heretic/knowledge/side_ash_flesh.dm
index f7f3c175b2fb05..bf840d6ed27ea8 100644
--- a/code/modules/antagonists/heretic/knowledge/side_ash_flesh.dm
+++ b/code/modules/antagonists/heretic/knowledge/side_ash_flesh.dm
@@ -71,7 +71,7 @@
/obj/item/bodypart/head = 1,
/obj/item/book = 1,
)
- mob_to_summon = /mob/living/simple_animal/hostile/heretic_summon/ash_spirit
+ mob_to_summon = /mob/living/basic/heretic_summon/ash_spirit
cost = 1
route = PATH_SIDE
poll_ignore_define = POLL_IGNORE_ASH_SPIRIT
diff --git a/code/modules/antagonists/heretic/knowledge/side_blade_rust.dm b/code/modules/antagonists/heretic/knowledge/side_blade_rust.dm
index a8fb031fed2626..d7dfd75a144621 100644
--- a/code/modules/antagonists/heretic/knowledge/side_blade_rust.dm
+++ b/code/modules/antagonists/heretic/knowledge/side_blade_rust.dm
@@ -46,8 +46,8 @@
gain_text = "I met an old man in an anique shop who wielded a very unusual weapon. \
I could not purchase it at the time, but they showed me how they made it ages ago."
next_knowledge = list(
- /datum/heretic_knowledge/spell/furious_steel,
- /datum/heretic_knowledge/spell/entropic_plume,
+ /datum/heretic_knowledge/spell/realignment,
+ /datum/heretic_knowledge/spell/rust_construction,
/datum/heretic_knowledge/rifle_ammo,
)
required_atoms = list(
@@ -100,6 +100,10 @@
name = "Rust Charge"
desc = "A charge that must be started on a rusted tile and will destroy any rusted objects you come into contact with, will deal high damage to others and rust around you during the charge."
gain_text = "The hills sparkled now, as I neared them my mind began to wander. I quickly regained my resolve and pushed forward, this last leg would be the most treacherous."
+ next_knowledge = list(
+ /datum/heretic_knowledge/spell/furious_steel,
+ /datum/heretic_knowledge/spell/entropic_plume,
+ )
spell_to_add = /datum/action/cooldown/mob_cooldown/charge/rust
cost = 1
route = PATH_SIDE
diff --git a/code/modules/antagonists/heretic/knowledge/side_rust_cosmos.dm b/code/modules/antagonists/heretic/knowledge/side_rust_cosmos.dm
index 8aecfe06e1f163..2dbb44ea4eb7ed 100644
--- a/code/modules/antagonists/heretic/knowledge/side_rust_cosmos.dm
+++ b/code/modules/antagonists/heretic/knowledge/side_rust_cosmos.dm
@@ -88,7 +88,7 @@
/obj/item/book = 1,
/obj/item/bodypart/head = 1,
)
- mob_to_summon = /mob/living/simple_animal/hostile/heretic_summon/rust_spirit
+ mob_to_summon = /mob/living/basic/heretic_summon/rust_walker
cost = 1
route = PATH_SIDE
poll_ignore_define = POLL_IGNORE_RUST_SPIRIT
diff --git a/code/modules/antagonists/heretic/knowledge/side_void_blade.dm b/code/modules/antagonists/heretic/knowledge/side_void_blade.dm
index 92e6e381222e47..643fd434af7b5a 100644
--- a/code/modules/antagonists/heretic/knowledge/side_void_blade.dm
+++ b/code/modules/antagonists/heretic/knowledge/side_void_blade.dm
@@ -159,5 +159,5 @@
)
cost = 1
route = PATH_SIDE
- mob_to_summon = /mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror
+ mob_to_summon = /mob/living/basic/heretic_summon/maid_in_the_mirror
poll_ignore_define = POLL_IGNORE_MAID_IN_MIRROR
diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm
index 8f36b8665100bd..eb766392290b37 100644
--- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm
@@ -212,3 +212,79 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge())
spell_to_add = /datum/action/cooldown/spell/shadow_cloak
cost = 0
route = PATH_START
+
+/**
+ * Codex Cicatrixi is available at the start:
+ * This allows heretics to choose if they want to rush all the influences and take them stealthily, or
+ * Construct a codex and take what's left with more points.
+ * Another downside to having the book is strip searches, which means that it's not just a free nab, at least until you get exposed - and when you do, you'll probably need the faster drawing speed.
+ * Overall, it's a tradeoff between speed and stealth or power.
+ */
+/datum/heretic_knowledge/codex_cicatrix
+ name = "Codex Cicatrix"
+ desc = "Allows you to transmute a book, any unique pen (anything but generic pens), and your pick from any carcass (animal or human), leather, or hide to create a Codex Cicatrix. \
+ The Codex Cicatrix can be used when draining influences to gain additional knowledge, but comes at greater risk of being noticed. \
+ It can also be used to draw and remove transmutation runes easier, and as a spell focus in a pinch."
+ gain_text = "The occult leaves fragments of knowledge and power anywhere and everywhere. The Codex Cicatrix is one such example. \
+ Within the leather-bound faces and age old pages, a path into the Mansus is revealed."
+ required_atoms = list(
+ /obj/item/book = 1,
+ /obj/item/pen = 1,
+ list(/mob/living, /obj/item/stack/sheet/leather, /obj/item/stack/sheet/animalhide) = 1,
+ )
+ banned_atom_types = list(/obj/item/pen)
+ result_atoms = list(/obj/item/codex_cicatrix)
+ cost = 1
+ route = PATH_START
+ priority = MAX_KNOWLEDGE_PRIORITY - 3 // Least priority out of the starting knowledges, as it's an optional boon.
+
+/datum/heretic_knowledge/codex_cicatrix/parse_required_item(atom/item_path, number_of_things)
+ if(item_path == /obj/item/pen)
+ return "unique type of pen"
+ return ..()
+
+/datum/heretic_knowledge/codex_cicatrix/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/body in atoms)
+ if(body.stat != DEAD)
+ continue
+
+ selected_atoms += body
+ return TRUE
+ return FALSE
+
+/datum/heretic_knowledge/codex_cicatrix/cleanup_atoms(list/selected_atoms)
+ var/mob/living/body = locate() in selected_atoms
+ if(!body)
+ return
+ // A golem or an android doesn't have skin!
+ var/exterior_text = "skin"
+ // If carbon, it's the limb. If not, it's the body.
+ var/ripped_thing = body
+
+ // We will check if it's a carbon's body.
+ // If it is, we will damage a random bodypart, and check that bodypart for its body type, to select between 'skin' or 'exterior'.
+ if(iscarbon(body))
+ var/mob/living/carbon/carbody = body
+ var/obj/item/bodypart/bodypart = pick(carbody.bodyparts)
+ ripped_thing = bodypart
+ bodypart.receive_damage(25, sharpness = SHARP_EDGED)
+ if(!(bodypart.bodytype & BODYTYPE_ORGANIC))
+ exterior_text = "exterior"
+ else
+ // If it is not a carbon mob, we will just check biotypes and damage it directly.
+ if(body.mob_biotypes & (MOB_MINERAL|MOB_ROBOTIC))
+ exterior_text = "exterior"
+ body.apply_damage(25, BRUTE)
+
+ // Procure book for flavor text. This is why we call parent at the end.
+ var/obj/item/book/le_book = locate() in selected_atoms
+ if(!le_book)
+ stack_trace("Somehow, no book in codex cicatrix selected atoms! [english_list(selected_atoms)]")
+ playsound(body, 'sound/items/poster_ripped.ogg', 100, TRUE)
+ body.do_jitter_animation()
+ body.visible_message(span_danger("An awful ripping sound is heard as [ripped_thing]'s [exterior_text] is ripped straight out, wrapping around [le_book || "the book"], turning into an eldritch shade of blue!"))
+ return ..()
diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm
index 57db8636818382..a5e21472517f37 100644
--- a/code/modules/antagonists/heretic/knowledge/void_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm
@@ -81,9 +81,9 @@
You can still take damage due to a lack of pressure."
gain_text = "I found a thread of cold breath. It lead me to a strange shrine, all made of crystals. \
Translucent and white, a depiction of a nobleman stood before me."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/void_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/void_cloak,
/datum/heretic_knowledge/limited_amount/risen_corpse,
)
@@ -127,6 +127,7 @@
Additionally causes damage to heathens around your original and target destination."
gain_text = "The entity calls themself the Aristocrat. They effortlessly walk through air like \
nothing - leaving a harsh, cold breeze in their wake. They disappear, and I am left in the blizzard."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/void,
/datum/heretic_knowledge/reroll_targets,
@@ -161,6 +162,7 @@
desc = "Grants you Void Pull, a spell that pulls all nearby heathens towards you, stunning them briefly."
gain_text = "All is fleeting, but what else stays? I'm close to ending what was started. \
The Aristocrat reveals themselves to me again. They tell me I am late. Their pull is immense, I cannot turn back."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/void_final,
/datum/heretic_knowledge/spell/cleave,
diff --git a/code/modules/antagonists/heretic/magic/ascended_shapeshift.dm b/code/modules/antagonists/heretic/magic/ascended_shapeshift.dm
index 4395b4a54b340c..f1d6de56e399f4 100644
--- a/code/modules/antagonists/heretic/magic/ascended_shapeshift.dm
+++ b/code/modules/antagonists/heretic/magic/ascended_shapeshift.dm
@@ -6,10 +6,10 @@
cooldown_time = 20 SECONDS
die_with_shapeshifted_form = FALSE
possible_shapes = list(
- /mob/living/simple_animal/hostile/heretic_summon/raw_prophet,
- /mob/living/simple_animal/hostile/heretic_summon/rust_spirit,
- /mob/living/simple_animal/hostile/heretic_summon/ash_spirit,
- /mob/living/simple_animal/hostile/heretic_summon/stalker,
+ /mob/living/basic/heretic_summon/ash_spirit,
+ /mob/living/basic/heretic_summon/raw_prophet/ascended,
+ /mob/living/basic/heretic_summon/rust_walker,
+ /mob/living/basic/heretic_summon/stalker,
)
/datum/action/cooldown/spell/shapeshift/eldritch/ascension/do_shapeshift(mob/living/caster)
diff --git a/code/modules/antagonists/heretic/magic/flesh_ascension.dm b/code/modules/antagonists/heretic/magic/flesh_ascension.dm
index cb9ab63e031e14..d086c1127fcf00 100644
--- a/code/modules/antagonists/heretic/magic/flesh_ascension.dm
+++ b/code/modules/antagonists/heretic/magic/flesh_ascension.dm
@@ -13,7 +13,7 @@
invocation_type = INVOCATION_SHOUT
spell_requirements = NONE
- possible_shapes = list(/mob/living/simple_animal/hostile/heretic_summon/armsy/prime)
+ possible_shapes = list(/mob/living/basic/heretic_summon/armsy)
/// The length of our new wormy when we shed.
var/segment_length = 10
@@ -35,32 +35,11 @@
return ..()
-/datum/action/cooldown/spell/shapeshift/shed_human_form/do_unshapeshift(mob/living/simple_animal/hostile/heretic_summon/armsy/caster)
+/datum/action/cooldown/spell/shapeshift/shed_human_form/do_unshapeshift(mob/living/basic/heretic_summon/armsy/caster)
if(istype(caster))
- segment_length = caster.get_length()
+ segment_length = caster.get_length() - 1 // Don't count the head
return ..()
/datum/action/cooldown/spell/shapeshift/shed_human_form/create_shapeshift_mob(atom/loc)
return new shapeshift_type(loc, TRUE, segment_length)
-
-/datum/action/cooldown/spell/worm_contract
- name = "Force Contract"
- desc = "Forces your body to contract onto a single tile."
- background_icon_state = "bg_heretic"
- overlay_icon_state = "bg_heretic_border"
- button_icon = 'icons/mob/actions/actions_ecult.dmi'
- button_icon_state = "worm_contract"
-
- school = SCHOOL_FORBIDDEN
- cooldown_time = 30 SECONDS
-
- invocation_type = INVOCATION_NONE
- spell_requirements = NONE
-
-/datum/action/cooldown/spell/worm_contract/is_valid_target(atom/cast_on)
- return istype(cast_on, /mob/living/simple_animal/hostile/heretic_summon/armsy)
-
-/datum/action/cooldown/spell/worm_contract/cast(mob/living/simple_animal/hostile/heretic_summon/armsy/cast_on)
- . = ..()
- cast_on.contract_next_chain_into_single_tile()
diff --git a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm
index 1e65ef88951c78..64638d7103b17b 100644
--- a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm
+++ b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm
@@ -46,11 +46,14 @@
victim.apply_damage(20, BURN)
// Heal the caster for every victim damaged
- caster.adjustBruteLoss(-10, FALSE)
- caster.adjustFireLoss(-10, FALSE)
- caster.adjustToxLoss(-10, FALSE)
- caster.adjustOxyLoss(-10, FALSE)
- caster.adjustStaminaLoss(-10)
+ var/need_mob_update = FALSE
+ need_mob_update += caster.adjustBruteLoss(-10, updating_health = FALSE)
+ need_mob_update += caster.adjustFireLoss(-10, updating_health = FALSE)
+ need_mob_update += caster.adjustToxLoss(-10, updating_health = FALSE)
+ need_mob_update += caster.adjustOxyLoss(-10, updating_health = FALSE)
+ need_mob_update += caster.adjustStaminaLoss(-10, updating_stamina = FALSE)
+ if(need_mob_update)
+ caster.updatehealth()
/obj/effect/temp_visual/eldritch_smoke
icon = 'icons/effects/eldritch.dmi'
diff --git a/code/modules/antagonists/heretic/magic/star_touch.dm b/code/modules/antagonists/heretic/magic/star_touch.dm
index de3a56128de91b..f2f2c935bbfd87 100644
--- a/code/modules/antagonists/heretic/magic/star_touch.dm
+++ b/code/modules/antagonists/heretic/magic/star_touch.dm
@@ -73,11 +73,13 @@
/obj/item/melee/touch_attack/star_touch/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/effect_remover, \
+ AddComponent(\
+ /datum/component/effect_remover, \
success_feedback = "You remove %THEEFFECT.", \
tip_text = "Clear rune", \
on_clear_callback = CALLBACK(src, PROC_REF(after_clear_rune)), \
- effects_we_clear = list(/obj/effect/cosmic_rune))
+ effects_we_clear = list(/obj/effect/cosmic_rune), \
+ )
/*
* Callback for effect_remover component.
@@ -231,16 +233,16 @@
/datum/status_effect/cosmic_beam/proc/on_beam_hit(mob/living/target)
if(!istype(target, /mob/living/basic/heretic_summon/star_gazer))
target.AddElement(/datum/element/effect_trail, /obj/effect/forcefield/cosmic_field/fast)
- return
/// What to process when the beam is connected to a target
/datum/status_effect/cosmic_beam/proc/on_beam_tick(mob/living/target)
- target.adjustFireLoss(3)
- target.adjustCloneLoss(1)
- return
+ var/need_mob_update
+ need_mob_update = target.adjustFireLoss(3, updating_health = FALSE)
+ need_mob_update += target.adjustCloneLoss(1, updating_health = FALSE)
+ if(need_mob_update)
+ target.updatehealth()
/// What to remove when the beam disconnects from a target
/datum/status_effect/cosmic_beam/proc/on_beam_release(mob/living/target)
if(!istype(target, /mob/living/basic/heretic_summon/star_gazer))
target.RemoveElement(/datum/element/effect_trail, /obj/effect/forcefield/cosmic_field/fast)
- return
diff --git a/code/modules/antagonists/heretic/structures/knock_final.dm b/code/modules/antagonists/heretic/structures/knock_final.dm
index 85face85609586..f7d0d1a9ea7b00 100644
--- a/code/modules/antagonists/heretic/structures/knock_final.dm
+++ b/code/modules/antagonists/heretic/structures/knock_final.dm
@@ -5,39 +5,67 @@
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
icon = 'icons/obj/anomaly.dmi'
icon_state = "bhole3"
- color = "#53277E"
- light_color = "#53277E" //cooler purple
+ color = COLOR_VOID_PURPLE
+ light_color = COLOR_VOID_PURPLE
light_range = 20
anchored = TRUE
density = FALSE
layer = HIGH_PIPE_LAYER //0.01 above sigil layer used by heretic runes
move_resist = INFINITY
+ /// Who is our daddy?
var/datum/mind/ascendee
- ///a static list of heretic summons, this shouldnt even matter enough to be static but whatever
+ /// True if we're currently checking for ghost opinions
+ var/gathering_candidates = TRUE
+ ///a static list of heretic summons we cam create, automatically populated from heretic monster subtypes
var/static/list/monster_types
+ /// A static list of heretic summons which we should not create
+ var/static/list/monster_types_blacklist = list(
+ /mob/living/basic/heretic_summon/armsy,
+ /mob/living/basic/heretic_summon/star_gazer,
+ )
-/obj/structure/knock_tear/Initialize(mapload, ascendant)
+/obj/structure/knock_tear/Initialize(mapload, datum/mind/ascendant_mind)
. = ..()
transform *= 3
- if(!monster_types)
- monster_types = subtypesof(/mob/living/simple_animal/hostile/heretic_summon) - /mob/living/simple_animal/hostile/heretic_summon/armsy/prime
- if(ascendant)
- ascendee = ascendant
+ if(isnull(monster_types))
+ monster_types = subtypesof(/mob/living/basic/heretic_summon) - monster_types_blacklist
+ if(!isnull(ascendant_mind))
+ ascendee = ascendant_mind
+ RegisterSignals(ascendant_mind.current, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(end_madness))
SSpoints_of_interest.make_point_of_interest(src)
INVOKE_ASYNC(src, PROC_REF(poll_ghosts))
+/// Ask ghosts if they want to make some noise
/obj/structure/knock_tear/proc/poll_ghosts()
var/list/candidates = poll_ghost_candidates("Would you like to be a random eldritch monster attacking the crew?", ROLE_SENTIENCE, ROLE_SENTIENCE, 10 SECONDS, POLL_IGNORE_HERETIC_MONSTER)
while(LAZYLEN(candidates))
var/mob/dead/observer/candidate = pick_n_take(candidates)
ghost_to_monster(candidate, should_ask = FALSE)
+ gathering_candidates = FALSE
+
+/// Destroy the rift if you kill the heretic
+/obj/structure/knock_tear/proc/end_madness(datum/former_master)
+ SIGNAL_HANDLER
+ var/turf/our_turf = get_turf(src)
+ playsound(our_turf, 'sound/magic/castsummon.ogg', vol = 100, vary = TRUE)
+ visible_message(span_boldwarning("The rip in space spasms and disappears!"))
+ UnregisterSignal(former_master, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING)) // Just in case they die THEN delete
+ new /obj/effect/temp_visual/destabilising_tear(our_turf)
+ qdel(src)
/obj/structure/knock_tear/attack_ghost(mob/user)
. = ..()
- if(.)
+ if(. || gathering_candidates)
return
ghost_to_monster(user)
+/obj/structure/knock_tear/examine(mob/user)
+ . = ..()
+ if (!isobserver(user) || gathering_candidates)
+ return
+ . += span_notice("You can use this to enter the world as a foul monster.")
+
+/// Turn a ghost into an 'orrible beast
/obj/structure/knock_tear/proc/ghost_to_monster(mob/dead/observer/user, should_ask = TRUE)
if(should_ask)
var/ask = tgui_alert(user, "Become a monster?", "Ascended Rift", list("Yes", "No"))
@@ -61,7 +89,26 @@
/obj/structure/knock_tear/move_crushed(atom/movable/pusher, force = MOVE_FORCE_DEFAULT, direction)
return FALSE
-/obj/structure/knock_tear/Destroy(force) //this shouldnt happen but hey
+/obj/structure/knock_tear/Destroy(force)
if(ascendee)
ascendee = null
return ..()
+
+/obj/effect/temp_visual/destabilising_tear
+ name = "destabilised tear"
+ icon = 'icons/obj/anomaly.dmi'
+ icon_state = "bhole3"
+ color = COLOR_VOID_PURPLE
+ light_color = COLOR_VOID_PURPLE
+ light_range = 20
+ layer = HIGH_PIPE_LAYER
+ duration = 1 SECONDS
+
+/obj/effect/temp_visual/destabilising_tear/Initialize(mapload)
+ . = ..()
+ transform *= 3
+ animate(src, transform = matrix().Scale(3.2), time = 0.15 SECONDS)
+ animate(transform = matrix().Scale(0.2), time = 0.75 SECONDS)
+ animate(transform = matrix().Scale(3, 0), time = 0.1 SECONDS)
+ animate(src, color = COLOR_WHITE, time = 0.25 SECONDS, flags = ANIMATION_PARALLEL)
+ animate(color = COLOR_VOID_PURPLE, time = 0.3 SECONDS)
diff --git a/code/modules/antagonists/heretic/transmutation_rune.dm b/code/modules/antagonists/heretic/transmutation_rune.dm
index 7e63040af1db67..cd49feb3f175c5 100644
--- a/code/modules/antagonists/heretic/transmutation_rune.dm
+++ b/code/modules/antagonists/heretic/transmutation_rune.dm
@@ -90,6 +90,7 @@
// A copy of our requirements list.
// We decrement the values of to determine if enough of each key is present.
var/list/requirements_list = ritual.required_atoms.Copy()
+ var/list/banned_atom_types = ritual.banned_atom_types.Copy()
// A list of all atoms we've selected to use in this recipe.
var/list/selected_atoms = list()
@@ -105,8 +106,16 @@
// We already have enough of this type, skip
if(requirements_list[req_type] <= 0)
continue
- if(!istype(nearby_atom, req_type))
+ // If req_type is a list of types, check all of them for one match.
+ if(islist(req_type))
+ if(!(is_type_in_list(nearby_atom, req_type)))
+ continue
+ else if(!istype(nearby_atom, req_type))
continue
+ // if list has items, check if the strict type is banned.
+ if(length(banned_atom_types))
+ if(nearby_atom.type in banned_atom_types)
+ continue
// This item is a valid type. Add it to our selected atoms list.
selected_atoms |= nearby_atom
@@ -122,7 +131,7 @@
// All of the atoms have been checked, let's see if the ritual was successful
var/list/what_are_we_missing = list()
- for(var/atom/req_type as anything in requirements_list)
+ for(var/req_type in requirements_list)
var/number_of_things = requirements_list[req_type]
// <= 0 means it's fulfilled, skip
if(number_of_things <= 0)
@@ -130,10 +139,16 @@
// > 0 means it's unfilfilled - the ritual has failed, we should tell them why
// Lets format the thing they're missing and put it into our list
- var/formatted_thing = "[number_of_things] [initial(req_type.name)]\s"
- if(ispath(req_type, /mob/living/carbon/human))
- // If we need a human, there is a high likelihood we actually need a (dead) body
- formatted_thing = "[number_of_things] [number_of_things > 1 ? "bodies":"body"]"
+ var/formatted_thing = "[number_of_things] "
+ if(islist(req_type))
+ var/list/req_type_list = req_type
+ var/list/req_text_list = list()
+ for(var/atom/possible_type as anything in req_type_list)
+ req_text_list += ritual.parse_required_item(possible_type)
+ formatted_thing += english_list(req_text_list, and_text = "or")
+
+ else
+ formatted_thing = ritual.parse_required_item(req_type)
what_are_we_missing += formatted_thing
@@ -180,6 +195,7 @@
return ritual_result
+
/// A 3x3 heretic rune. The kind heretics actually draw in game.
/obj/effect/heretic_rune/big
icon = 'icons/effects/96x96.dmi'
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index 49d464fde167a1..9f527e0184ad13 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
@@ -455,15 +455,15 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
return FALSE
caller.playsound_local(caller, 'sound/misc/interference.ogg', 50, FALSE, use_reverb = FALSE)
- adjust_uses(-1)
-
- if(uses)
- desc = "[initial(desc)] It has [uses] use\s remaining."
- build_all_button_icons()
clicked_machine.audible_message(span_userdanger("You hear a loud electrical buzzing sound coming from [clicked_machine]!"))
addtimer(CALLBACK(src, PROC_REF(animate_machine), caller, clicked_machine), 5 SECONDS) //kabeep!
unset_ranged_ability(caller, span_danger("Sending override signal..."))
+ adjust_uses(-1) //adjust after we unset the active ability since we may run out of charges, thus deleting the ability
+
+ if(uses)
+ desc = "[initial(desc)] It has [uses] use\s remaining."
+ build_all_button_icons()
return TRUE
/datum/action/innate/ai/ranged/override_machine/proc/animate_machine(mob/living/caller, obj/machinery/to_animate)
diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm
index c15012f409b3ba..0b6a28c3bcac79 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm
@@ -644,7 +644,7 @@ GLOBAL_VAR(station_nuke_source)
to_chat(gibbed, span_userdanger("You are shredded to atoms by [source]!"))
gibbed.investigate_log("has been gibbed by a nuclear blast.", INVESTIGATE_DEATHS)
- gibbed.gib()
+ gibbed.gib(DROP_ALL_REMAINS)
return TRUE
/**
diff --git a/code/modules/antagonists/pirate/pirate_outfits.dm b/code/modules/antagonists/pirate/pirate_outfits.dm
index d2f648bb1fde25..4c73cac107f12e 100644
--- a/code/modules/antagonists/pirate/pirate_outfits.dm
+++ b/code/modules/antagonists/pirate/pirate_outfits.dm
@@ -36,6 +36,12 @@
id_trim = /datum/id_trim/pirate/captain
head = /obj/item/clothing/head/costume/pirate/armored
+/datum/outfit/pirate/captain/skeleton
+ name = "Space Pirate Captain (Skeleton)"
+
+ belt = /obj/item/gun/magic/midas_hand
+ l_pocket = /obj/item/coin/gold/doubloon
+
/datum/outfit/pirate/space
name = "Space Pirate (EVA)"
diff --git a/code/modules/antagonists/pirate/pirate_roles.dm b/code/modules/antagonists/pirate/pirate_roles.dm
index 68683333c49d3d..64baa724db1ec3 100644
--- a/code/modules/antagonists/pirate/pirate_roles.dm
+++ b/code/modules/antagonists/pirate/pirate_roles.dm
@@ -61,7 +61,7 @@
/obj/effect/mob_spawn/ghost_role/human/pirate/skeleton/captain
rank = "Captain"
- outfit = /datum/outfit/pirate/captain
+ outfit = /datum/outfit/pirate/captain/skeleton
/obj/effect/mob_spawn/ghost_role/human/pirate/skeleton/gunner
rank = "Gunner"
diff --git a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
index 57eb95a978c521..3ea6488b2d42d6 100644
--- a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
+++ b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
@@ -217,9 +217,13 @@
var/cargo_hold_id
///Interface name for the ui_interact call for different subtypes.
var/interface_type = "CargoHoldTerminal"
+ ///Typecache of things that shouldn't be sold and shouldn't have their contents sold.
+ var/static/list/nosell_typecache
/obj/machinery/computer/piratepad_control/Initialize(mapload)
..()
+ if(isnull(nosell_typecache))
+ nosell_typecache = typecacheof(/mob/living/silicon/robot)
return INITIALIZE_HINT_LATELOAD
/obj/machinery/computer/piratepad_control/multitool_act(mob/living/user, obj/item/multitool/I)
@@ -285,7 +289,7 @@
for(var/atom/movable/AM in get_turf(pad))
if(AM == pad)
continue
- export_item_and_contents(AM, apply_elastic = FALSE, dry_run = TRUE, external_report = report)
+ export_item_and_contents(AM, apply_elastic = FALSE, dry_run = TRUE, external_report = report, ignore_typecache = nosell_typecache)
for(var/datum/export/exported_datum in report.total_amount)
status_report += exported_datum.total_printout(report,notes = FALSE)
@@ -306,7 +310,7 @@
for(var/atom/movable/item_on_pad in get_turf(pad))
if(item_on_pad == pad)
continue
- export_item_and_contents(item_on_pad, apply_elastic = FALSE, delete_unsold = FALSE, external_report = report)
+ export_item_and_contents(item_on_pad, apply_elastic = FALSE, delete_unsold = FALSE, external_report = report, ignore_typecache = nosell_typecache)
status_report = "Sold: "
var/value = 0
diff --git a/code/modules/antagonists/revenant/revenant_blight.dm b/code/modules/antagonists/revenant/revenant_blight.dm
index 64bd6ce03bb9fd..c56df7bd4eaaca 100644
--- a/code/modules/antagonists/revenant/revenant_blight.dm
+++ b/code/modules/antagonists/revenant/revenant_blight.dm
@@ -31,20 +31,23 @@
return
if(!finalstage)
+ var/need_mob_update = FALSE
if(affected_mob.body_position == LYING_DOWN && SPT_PROB(3 * stage, seconds_per_tick))
cure()
return FALSE
if(SPT_PROB(1.5 * stage, seconds_per_tick))
to_chat(affected_mob, span_revennotice("You suddenly feel [pick("sick and tired", "disoriented", "tired and confused", "nauseated", "faint", "dizzy")]..."))
affected_mob.adjust_confusion(8 SECONDS)
- affected_mob.adjustStaminaLoss(20, FALSE)
+ need_mob_update += affected_mob.adjustStaminaLoss(20, updating_stamina = FALSE)
new /obj/effect/temp_visual/revenant(affected_mob.loc)
if(stagedamage < stage)
stagedamage++
- affected_mob.adjustToxLoss(1 * stage * seconds_per_tick, FALSE) //should, normally, do about 30 toxin damage.
+ need_mob_update += affected_mob.adjustToxLoss(1 * stage * seconds_per_tick, updating_health = FALSE) //should, normally, do about 30 toxin damage.
new /obj/effect/temp_visual/revenant(affected_mob.loc)
if(SPT_PROB(25, seconds_per_tick))
- affected_mob.adjustStaminaLoss(stage, FALSE)
+ need_mob_update += affected_mob.adjustStaminaLoss(stage, updating_stamina = FALSE)
+ if(need_mob_update)
+ affected_mob.updatehealth()
switch(stage)
if(2)
@@ -60,7 +63,7 @@
if(!finalstage)
finalstage = TRUE
to_chat(affected_mob, span_revenbignotice("You feel like [pick("nothing's worth it anymore", "nobody ever needed your help", "nothing you did mattered", "everything you tried to do was worthless")]."))
- affected_mob.adjustStaminaLoss(22.5 * seconds_per_tick, FALSE)
+ affected_mob.adjustStaminaLoss(22.5 * seconds_per_tick, updating_stamina = FALSE)
new /obj/effect/temp_visual/revenant(affected_mob.loc)
if(affected_mob.dna && affected_mob.dna.species)
affected_mob.dna.species.handle_mutant_bodyparts(affected_mob,"#1d2953")
diff --git a/code/modules/antagonists/spiders/spiders.dm b/code/modules/antagonists/spiders/spiders.dm
index 6d0b86d24d70ab..d42f1aea7b391c 100644
--- a/code/modules/antagonists/spiders/spiders.dm
+++ b/code/modules/antagonists/spiders/spiders.dm
@@ -14,7 +14,7 @@
/datum/antagonist/spider/on_gain()
forge_objectives(directive)
- . = ..()
+ return ..()
/datum/antagonist/spider/greet()
. = ..()
@@ -35,3 +35,23 @@
var/datum/objective/spider/objective = new(directive)
objective.owner = owner
objectives += objective
+
+/// Subtype for flesh spiders who don't have a queen
+/datum/antagonist/spider/flesh
+ name = "Flesh Spider"
+
+/datum/antagonist/spider/flesh/forge_objectives()
+ var/datum/objective/custom/destroy = new()
+ destroy.owner = owner
+ destroy.explanation_text = "Wreak havoc and consume living flesh."
+ objectives += destroy
+
+ var/datum/objective/survive/dont_die = new()
+ dont_die.owner = owner
+ objectives += dont_die
+
+/datum/antagonist/spider/flesh/greet()
+ . = ..()
+ to_chat(owner, span_boldwarning("An abomination of flesh set upon the station by changelings, \
+ you are aggressive to all living beings outside of your species and know no loyalties... even to your creator. \
+ Your malleable flesh quickly regenerates if you can avoid taking damage for a few seconds."))
diff --git a/code/modules/antagonists/traitor/objectives/kill_pet.dm b/code/modules/antagonists/traitor/objectives/kill_pet.dm
index 51a54d99e300a0..ae28f5dbf4c08c 100644
--- a/code/modules/antagonists/traitor/objectives/kill_pet.dm
+++ b/code/modules/antagonists/traitor/objectives/kill_pet.dm
@@ -26,9 +26,9 @@
JOB_CHIEF_MEDICAL_OFFICER = /mob/living/simple_animal/pet/cat/runtime,
JOB_CHIEF_ENGINEER = /mob/living/simple_animal/parrot/poly,
JOB_QUARTERMASTER = list(
- /mob/living/simple_animal/sloth/citrus,
- /mob/living/simple_animal/sloth/paperwork,
- /mob/living/simple_animal/hostile/gorilla/cargo_domestic,
+ /mob/living/basic/gorilla/cargorilla,
+ /mob/living/basic/sloth/citrus,
+ /mob/living/basic/sloth/paperwork,
)
)
/// The head that we are targetting
diff --git a/code/modules/antagonists/wizard/equipment/artefact.dm b/code/modules/antagonists/wizard/equipment/artefact.dm
index 3e4cf0766f573f..4783daffb8b47d 100644
--- a/code/modules/antagonists/wizard/equipment/artefact.dm
+++ b/code/modules/antagonists/wizard/equipment/artefact.dm
@@ -311,7 +311,7 @@
//Provides a decent heal, need to pump every 6 seconds
/obj/item/organ/internal/heart/cursed/wizard
- pump_delay = 60
+ pump_delay = 6 SECONDS
heal_brute = 25
heal_burn = 25
heal_oxy = 25
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/all_access.dm b/code/modules/antagonists/wizard/grand_ritual/finales/all_access.dm
new file mode 100644
index 00000000000000..07958ed94a75e1
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/all_access.dm
@@ -0,0 +1,17 @@
+/// Open all of the doors
+/datum/grand_finale/all_access
+ name = "Connection"
+ desc = "The ultimate use of your gathered power! Unlock every single door that they have! Nobody will be able to keep you out now, or anyone else for that matter!"
+ icon = 'icons/mob/actions/actions_spells.dmi'
+ icon_state = "knock"
+
+/datum/grand_finale/all_access/trigger(mob/living/carbon/human/invoker)
+ message_admins("[key_name(invoker)] removed all door access requirements")
+ for(var/obj/machinery/door/target_door as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door))
+ if(is_station_level(target_door.z))
+ target_door.unlock()
+ target_door.req_access = list()
+ target_door.req_one_access = list()
+ INVOKE_ASYNC(target_door, TYPE_PROC_REF(/obj/machinery/door/airlock, open))
+ CHECK_TICK
+ priority_announce("AULIE OXIN FIERA!!", null, 'sound/magic/knock.ogg', sender_override = "[invoker.real_name]")
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm b/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm
new file mode 100644
index 00000000000000..c4bba539c35c9d
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm
@@ -0,0 +1,60 @@
+#define DOOM_SINGULARITY "singularity"
+#define DOOM_TESLA "tesla"
+#define DOOM_METEORS "meteors"
+
+/// Kill yourself and probably a bunch of other people
+/datum/grand_finale/armageddon
+ name = "Annihilation"
+ desc = "This crew have offended you beyond the realm of pranks. Make the ultimate sacrifice to teach them a lesson your elders can really respect. \
+ YOU WILL NOT SURVIVE THIS."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ minimum_time = 90 MINUTES // This will probably immediately end the round if it gets finished.
+ ritual_invoke_time = 60 SECONDS // Really give the crew some time to interfere with this one.
+ dire_warning = TRUE
+ glow_colour = "#be000048"
+ /// Things to yell before you die
+ var/static/list/possible_last_words = list(
+ "Flames and ruin!",
+ "Dooooooooom!!",
+ "HAHAHAHAHAHA!! AHAHAHAHAHAHAHAHAA!!",
+ "Hee hee hee!! Hoo hoo hoo!! Ha ha haaa!!",
+ "Ohohohohohoho!!",
+ "Cower in fear, puny mortals!",
+ "Tremble before my glory!",
+ "Pick a god and pray!",
+ "It's no use!",
+ "If the gods wanted you to live, they would not have created me!",
+ "God stays in heaven out of fear of what I have created!",
+ "Ruination is come!",
+ "All of creation, bend to my will!",
+ )
+
+/datum/grand_finale/armageddon/trigger(mob/living/carbon/human/invoker)
+ priority_announce(pick(possible_last_words), null, 'sound/magic/voidblink.ogg', sender_override = "[invoker.real_name]")
+ var/turf/current_location = get_turf(invoker)
+ invoker.gib(DROP_ALL_REMAINS)
+
+ var/static/list/doom_options = list()
+ if (!length(doom_options))
+ doom_options = list(DOOM_SINGULARITY, DOOM_TESLA)
+ if (!SSmapping.config.planetary)
+ doom_options += DOOM_METEORS
+
+ switch(pick(doom_options))
+ if (DOOM_SINGULARITY)
+ var/obj/singularity/singulo = new(current_location)
+ singulo.energy = 300
+ if (DOOM_TESLA)
+ var/obj/energy_ball/tesla = new (current_location)
+ tesla.energy = 200
+ if (DOOM_METEORS)
+ var/datum/dynamic_ruleset/roundstart/meteor/meteors = new()
+ meteors.meteordelay = 0
+ var/datum/game_mode/dynamic/mode = SSticker.mode
+ mode.execute_roundstart_rule(meteors) // Meteors will continue until morale is crushed.
+ priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS)
+
+#undef DOOM_SINGULARITY
+#undef DOOM_TESLA
+#undef DOOM_METEORS
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/captaincy.dm b/code/modules/antagonists/wizard/grand_ritual/finales/captaincy.dm
new file mode 100644
index 00000000000000..d1a3c1afaf7583
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/captaincy.dm
@@ -0,0 +1,113 @@
+/// Become the official Captain of the station
+/datum/grand_finale/usurp
+ name = "Usurpation"
+ desc = "The ultimate use of your gathered power! Rewrite time such that you have been Captain of this station the whole time."
+ icon = 'icons/obj/card.dmi'
+ icon_state = "card_gold"
+
+/datum/grand_finale/usurp/trigger(mob/living/carbon/human/invoker)
+ message_admins("[key_name(invoker)] has replaced the Captain")
+ var/list/former_captains = list()
+ var/list/other_crew = list()
+ SEND_SOUND(world, sound('sound/magic/timeparadox2.ogg'))
+
+ for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
+ if (!crewmate.mind)
+ continue
+ crewmate.Unconscious(3 SECONDS) // Everyone falls unconscious but not everyone gets told about a new captain
+ if (crewmate == invoker || IS_HUMAN_INVADER(crewmate))
+ continue
+ to_chat(crewmate, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
+ Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
+ the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
+ Everything is just as it was and always has been. \n\n\
+ A stray thought sticks in the forefront of your mind. \n\
+ [span_hypnophrase("I'm so glad that [invoker.real_name] is our legally appointed Captain!")] \n\
+ Is... that right?"))
+ if (is_captain_job(crewmate.mind.assigned_role))
+ former_captains += crewmate
+ demote_to_assistant(crewmate)
+ continue
+ if (crewmate.stat != DEAD)
+ other_crew += crewmate
+
+ dress_candidate(invoker)
+ GLOB.manifest.modify(invoker.real_name, JOB_CAPTAIN, JOB_CAPTAIN)
+ minor_announce("Captain [invoker.real_name] on deck!")
+
+ // Enlist some crew to try and restore the natural order
+ for (var/mob/living/carbon/human/former_captain as anything in former_captains)
+ create_vendetta(former_captain.mind, invoker.mind)
+ for (var/mob/living/carbon/human/random_crewmate as anything in other_crew)
+ if (prob(10))
+ create_vendetta(random_crewmate.mind, invoker.mind)
+
+/**
+ * Anyone who thought they were Captain is in for a nasty surprise, and won't be very happy about it
+ */
+/datum/grand_finale/usurp/proc/demote_to_assistant(mob/living/carbon/human/former_captain)
+ var/obj/effect/particle_effect/fluid/smoke/exit_poof = new(get_turf(former_captain))
+ exit_poof.lifetime = 2 SECONDS
+
+ former_captain.unequip_everything()
+ if(isplasmaman(former_captain))
+ former_captain.equipOutfit(/datum/outfit/plasmaman)
+ former_captain.internal = former_captain.get_item_for_held_index(2)
+ else
+ former_captain.equipOutfit(/datum/outfit/job/assistant)
+
+ GLOB.manifest.modify(former_captain.real_name, JOB_ASSISTANT, JOB_ASSISTANT)
+ var/list/valid_turfs = list()
+ // Used to be into prison but that felt a bit too mean
+ for (var/turf/exile_turf as anything in get_area_turfs(/area/station/maintenance, subtypes = TRUE))
+ if (isspaceturf(exile_turf) || exile_turf.is_blocked_turf())
+ continue
+ valid_turfs += exile_turf
+ do_teleport(former_captain, pick(valid_turfs), no_effects = TRUE)
+ var/obj/effect/particle_effect/fluid/smoke/enter_poof = new(get_turf(former_captain))
+ enter_poof.lifetime = 2 SECONDS
+
+/**
+ * Does some item juggling to try to dress you as both a Wizard and Captain without deleting any items you have bought.
+ * ID, headset, and uniform are forcibly replaced. Other slots are only filled if unoccupied.
+ * We could forcibly replace shoes and gloves too but people might miss their insuls or... meown shoes?
+ */
+/datum/grand_finale/usurp/proc/dress_candidate(mob/living/carbon/human/invoker)
+ // Won't be needing these
+ var/obj/id = invoker.get_item_by_slot(ITEM_SLOT_ID)
+ QDEL_NULL(id)
+ var/obj/headset = invoker.get_item_by_slot(ITEM_SLOT_EARS)
+ QDEL_NULL(headset)
+ // We're about to take off your pants so those are going to fall out
+ var/obj/item/pocket_L = invoker.get_item_by_slot(ITEM_SLOT_LPOCKET)
+ var/obj/item/pocket_R = invoker.get_item_by_slot(ITEM_SLOT_RPOCKET)
+ // In case we try to put a PDA there
+ var/obj/item/belt = invoker.get_item_by_slot(ITEM_SLOT_BELT)
+ belt?.moveToNullspace()
+
+ var/obj/pants = invoker.get_item_by_slot(ITEM_SLOT_ICLOTHING)
+ QDEL_NULL(pants)
+ invoker.equipOutfit(/datum/outfit/job/wizard_captain)
+ // And put everything back!
+ equip_to_slot_then_hands(invoker, ITEM_SLOT_BELT, belt)
+ equip_to_slot_then_hands(invoker, ITEM_SLOT_LPOCKET, pocket_L)
+ equip_to_slot_then_hands(invoker, ITEM_SLOT_RPOCKET, pocket_R)
+
+/// An outfit which replaces parts of a wizard's clothes with captain's clothes but keeps the robes
+/datum/outfit/job/wizard_captain
+ name = "Captain (Wizard Transformation)"
+ jobtype = /datum/job/captain
+ id = /obj/item/card/id/advanced/gold
+ id_trim = /datum/id_trim/job/captain
+ uniform = /obj/item/clothing/under/rank/captain/parade
+ belt = /obj/item/modular_computer/pda/heads/captain
+ ears = /obj/item/radio/headset/heads/captain/alt
+ glasses = /obj/item/clothing/glasses/sunglasses
+ gloves = /obj/item/clothing/gloves/captain
+ shoes = /obj/item/clothing/shoes/laceup
+ accessory = /obj/item/clothing/accessory/medal/gold/captain
+ backpack_contents = list(
+ /obj/item/melee/baton/telescopic = 1,
+ /obj/item/station_charter = 1,
+ )
+ box = null
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/cheese.dm b/code/modules/antagonists/wizard/grand_ritual/finales/cheese.dm
new file mode 100644
index 00000000000000..714cd62659bbb8
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/cheese.dm
@@ -0,0 +1,49 @@
+/**
+ * Gives the wizard a defensive/mood buff and a Wabbajack, a juiced up chaos staff that will surely break something.
+ * Everyone but the wizard goes crazy, suffers major brain damage, and is given a vendetta against the wizard.
+ * Already insane people are instead cured of their madness, ignoring any other effects as the station around them loses its marbles.
+ */
+/datum/grand_finale/cheese
+ // we don't set name, desc and others, thus we won't appear in the radial choice of a normal finale rune
+ dire_warning = TRUE
+ minimum_time = 45 MINUTES //i'd imagine speedrunning this would be crummy, but the wizard's average lifespan is barely reaching this point
+
+/datum/grand_finale/cheese/trigger(mob/living/invoker)
+ message_admins("[key_name(invoker)] has summoned forth The Wabbajack and cursed the crew with madness!")
+ priority_announce("Danger: Extremely potent reality altering object has been summoned on station. Immediate evacuation advised. Brace for impact.", "Central Command Higher Dimensional Affairs", 'sound/effects/glassbr1.ogg')
+
+ for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
+ if (isnull(crewmate.mind))
+ continue
+ if (crewmate == invoker) //everyone but the wizard is royally fucked, no matter who they are
+ continue
+ if (crewmate.has_trauma_type(/datum/brain_trauma/mild/hallucinations)) //for an already insane person, this is retribution
+ to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble..."))
+ to_chat(crewmate, span_nicegreen("...but as the moment passes, you realise that whatever eldritch power behind the event happened to affect you \
+ has resonated within the ruins of your already shattered mind, creating a singularity of mental instability! \
+ As it collapses unto itself, you feel... at peace, finally."))
+ if(crewmate.has_quirk(/datum/quirk/insanity))
+ crewmate.remove_quirk(/datum/quirk/insanity)
+ else
+ crewmate.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE)
+ else
+ //everyone else gets to relish in madness
+ //yes killing their mood will also trigger mood hallucinations
+ create_vendetta(crewmate.mind, invoker.mind)
+ to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble. \n\
+ You feel your inner psyche shatter into a myriad pieces of jagged glass of colors unknown to the universe, \
+ infinitely reflecting a blinding, maddening light coming from the innermost sanctums of your destroyed mind. \n\
+ After a brief pause which felt like a millenia, one phrase rebounds ceaselessly in your head, imbued with the false hope of absolution... \n\
+ [invoker] must die."))
+ var/datum/brain_trauma/mild/hallucinations/added_trauma = new()
+ added_trauma.resilience = TRAUMA_RESILIENCE_ABSOLUTE
+ crewmate.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_DEATH - 25, BRAIN_DAMAGE_DEATH - 25) //you'd better hope chap didn't pick a hypertool
+ crewmate.gain_trauma(added_trauma)
+ crewmate.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_despair)
+
+ //drip our wizard out
+ invoker.apply_status_effect(/datum/status_effect/blessing_of_insanity)
+ invoker.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_elation)
+ var/obj/item/gun/magic/staff/chaos/true_wabbajack/the_wabbajack = new
+ invoker.put_in_active_hand(the_wabbajack)
+ to_chat(invoker, span_mind_control("Your every single instinct and rational thought is screaming at you as [the_wabbajack] appears in your firm grip..."))
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/clown.dm b/code/modules/antagonists/wizard/grand_ritual/finales/clown.dm
new file mode 100644
index 00000000000000..bda79c908c0406
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/clown.dm
@@ -0,0 +1,76 @@
+/// Dress the crew as magical clowns
+/datum/grand_finale/clown
+ name = "Jubilation"
+ desc = "The ultimate use of your gathered power! Rewrite time so that everyone went to clown college! Now they'll prank each other for you!"
+ icon = 'icons/obj/clothing/masks.dmi'
+ icon_state = "clown"
+ glow_colour = "#ffff0048"
+
+/datum/grand_finale/clown/trigger(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/victim as anything in GLOB.human_list)
+ victim.Unconscious(3 SECONDS)
+ if (!victim.mind || IS_HUMAN_INVADER(victim) || victim == invoker)
+ continue
+ if (HAS_TRAIT(victim, TRAIT_CLOWN_ENJOYER))
+ victim.add_mood_event("clown_world", /datum/mood_event/clown_world)
+ to_chat(victim, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
+ Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
+ the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
+ Everything is just as it was and always has been. \n\n\
+ A stray thought sticks in the forefront of your mind. \n\
+ [span_hypnophrase("I'm so glad that I work at Clown Research Station [station_name()]!")] \n\
+ Is... that right?"))
+ if (is_clown_job(victim.mind.assigned_role))
+ var/datum/action/cooldown/spell/conjure_item/clown_pockets/new_spell = new(victim)
+ new_spell.Grant(victim)
+ continue
+ if (!ismonkey(victim)) // Monkeys cannot yet wear clothes
+ dress_as_magic_clown(victim)
+ if (prob(15))
+ create_vendetta(victim.mind, invoker.mind)
+
+/**
+ * Clown enjoyers who are effected by this become ecstatic, they have achieved their life's dream.
+ * This moodlet is equivalent to the one for simply being a traitor.
+ */
+/datum/mood_event/clown_world
+ mood_change = 4
+
+/datum/mood_event/clown_world/add_effects(param)
+ description = "I LOVE working at Clown Research Station [station_name()]!!"
+
+/// Dress the passed mob as a magical clown, self-explanatory
+/datum/grand_finale/clown/proc/dress_as_magic_clown(mob/living/carbon/human/victim)
+ var/obj/effect/particle_effect/fluid/smoke/poof = new(get_turf(victim))
+ poof.lifetime = 2 SECONDS
+
+ var/obj/item/tank/internal = victim.internal
+ // We're about to take off your pants so those are going to fall out
+ var/obj/item/pocket_L = victim.get_item_by_slot(ITEM_SLOT_LPOCKET)
+ var/obj/item/pocket_R = victim.get_item_by_slot(ITEM_SLOT_RPOCKET)
+ var/obj/item/id = victim.get_item_by_slot(ITEM_SLOT_ID)
+ var/obj/item/belt = victim.get_item_by_slot(ITEM_SLOT_BELT)
+
+ var/obj/pants = victim.get_item_by_slot(ITEM_SLOT_ICLOTHING)
+ var/obj/mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
+ QDEL_NULL(pants)
+ QDEL_NULL(mask)
+ if(isplasmaman(victim))
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/under/plasmaman/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat/plasmaman(), ITEM_SLOT_MASK, disable_warning = TRUE)
+ else
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/under/rank/civilian/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat(), ITEM_SLOT_MASK, disable_warning = TRUE)
+
+ var/obj/item/clothing/mask/gas/clown_hat/clown_mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
+ if (clown_mask)
+ var/list/options = GLOB.clown_mask_options
+ clown_mask.icon_state = options[pick(clown_mask.clownmask_designs)]
+ victim.update_worn_mask()
+ clown_mask.update_item_action_buttons()
+
+ equip_to_slot_then_hands(victim, ITEM_SLOT_LPOCKET, pocket_L)
+ equip_to_slot_then_hands(victim, ITEM_SLOT_RPOCKET, pocket_R)
+ equip_to_slot_then_hands(victim, ITEM_SLOT_ID, id)
+ equip_to_slot_then_hands(victim, ITEM_SLOT_BELT, belt)
+ victim.internal = internal
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm b/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm
new file mode 100644
index 00000000000000..b92ae4d2f20b2e
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm
@@ -0,0 +1,88 @@
+/**
+ * A big final event to run when you complete seven rituals
+ */
+/datum/grand_finale
+ /// Friendly name for selection menu
+ var/name
+ /// Tooltip description for selection menu
+ var/desc
+ /// An icon to display to represent the choice
+ var/icon/icon
+ /// Icon state to use to represent the choice
+ var/icon_state
+ /// Prevent especially dangerous options from being chosen until we're fine with the round ending
+ var/minimum_time = 0
+ /// Override the rune invocation time
+ var/ritual_invoke_time = 30 SECONDS
+ /// Provide an extremely loud radio message when this one starts
+ var/dire_warning = FALSE
+ /// Overrides the default colour you glow while channeling the rune, optional
+ var/glow_colour
+
+/**
+ * Returns an entry for a radial menu for this choice.
+ * Returns null if entry is abstract or invalid for current circumstances.
+ */
+/datum/grand_finale/proc/get_radial_choice()
+ if (!name || !desc || !icon || !icon_state)
+ return
+ var/time_remaining_desc = ""
+ if (minimum_time >= world.time - SSticker.round_start_time)
+ time_remaining_desc = "This ritual will be available to begin invoking in [DisplayTimeText(minimum_time - world.time - SSticker.round_start_time)]"
+ var/datum/radial_menu_choice/choice = new()
+ choice.name = name
+ choice.image = image(icon = icon, icon_state = icon_state)
+ choice.info = desc + time_remaining_desc
+ return choice
+
+/**
+ * Actually do the thing.
+ * Arguments
+ * * invoker - The wizard casting this.
+ */
+/datum/grand_finale/proc/trigger(mob/living/invoker)
+ // Do something cool.
+
+/// Tries to equip something into an inventory slot, then hands, then the floor.
+/datum/grand_finale/proc/equip_to_slot_then_hands(mob/living/carbon/human/invoker, slot, obj/item/item)
+ if(!item)
+ return
+ if(!invoker.equip_to_slot_if_possible(item, slot, disable_warning = TRUE))
+ invoker.put_in_hands(item)
+
+/// They are not going to take this lying down.
+/datum/grand_finale/proc/create_vendetta(datum/mind/aggrieved_crewmate, datum/mind/wizard)
+ aggrieved_crewmate.add_antag_datum(/datum/antagonist/wizard_prank_vendetta)
+ var/datum/antagonist/wizard_prank_vendetta/antag_datum = aggrieved_crewmate.has_antag_datum(/datum/antagonist/wizard_prank_vendetta)
+ var/datum/objective/assassinate/wizard_murder = new
+ wizard_murder.owner = aggrieved_crewmate
+ wizard_murder.target = wizard
+ wizard_murder.explanation_text = "Kill [wizard.current.name], the one who did this."
+ antag_datum.objectives += wizard_murder
+
+ to_chat(aggrieved_crewmate.current, span_warning("No! This isn't right!"))
+ aggrieved_crewmate.announce_objectives()
+
+/**
+ * Antag datum to give to people who want to kill the wizard.
+ * This doesn't preclude other people choosing to want to kill the wizard, just these people are rewarded for it.
+ */
+/datum/antagonist/wizard_prank_vendetta
+ name = "\improper Wizard Prank Victim"
+ roundend_category = "wizard prank victims"
+ show_in_antagpanel = FALSE
+ antagpanel_category = "Other"
+ show_name_in_check_antagonists = TRUE
+ count_against_dynamic_roll_chance = FALSE
+ silent = TRUE
+
+/// Give everyone magic items, its so simple it feels pointless to give it its own file
+/datum/grand_finale/magic
+ name = "Evolution"
+ desc = "The ultimate use of your gathered power! Give the crew their own magic, they'll surely realise that right and wrong have no meaning when you hold ultimate power!"
+ icon = 'icons/obj/scrolls.dmi'
+ icon_state = "scroll"
+
+/datum/grand_finale/magic/trigger(mob/living/carbon/human/invoker)
+ message_admins("[key_name(invoker)] summoned magic")
+ summon_magic(survivor_probability = 20) // Wow, this one was easy!
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm b/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm
new file mode 100644
index 00000000000000..d20ca06752b6cf
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm
@@ -0,0 +1,277 @@
+/// Amount of time to wait after someone dies to steal their body from their killers
+#define IMMORTAL_PRE_ACTIVATION_TIME 10 SECONDS
+/// Amount of time it takes a mob to return to the living world
+#define IMMORTAL_RESURRECT_TIME 50 SECONDS
+
+/**
+ * Nobody will ever die ever again
+ * Or if they do, they will be back
+ */
+/datum/grand_finale/immortality
+ name = "Perpetuation"
+ desc = "The ultimate use of your gathered power! Share with the crew the gift, or curse, of eternal life! \
+ And why not just the crew? How about their pets too? And any other animals around here! \
+ What if nobody died ever again!?"
+ icon = 'icons/obj/mining_zones/artefacts.dmi'
+ icon_state = "asclepius_active"
+ glow_colour = COLOR_PALE_GREEN
+ minimum_time = 30 MINUTES // This is enormously disruptive but doesn't technically in of itself end the round.
+
+/datum/grand_finale/immortality/trigger(mob/living/carbon/human/invoker)
+ new /obj/effect/temp_visual/immortality_blast(get_turf(invoker))
+ SEND_SOUND(world, sound('sound/magic/teleport_diss.ogg'))
+ for (var/mob/living/alive_guy as anything in GLOB.mob_living_list)
+ new /obj/effect/temp_visual/immortality_pulse(get_turf(alive_guy))
+ if (!alive_guy.mind)
+ continue
+ to_chat(alive_guy, span_notice("You feel extremely healthy."))
+ RegisterSignal(SSdcs, COMSIG_GLOB_MOB_DEATH, PROC_REF(something_died))
+
+/// Called when something passes into the great beyond, make it not do that
+/datum/grand_finale/immortality/proc/something_died(datum/source, mob/living/died, gibbed)
+ SIGNAL_HANDLER
+ if (died.stat != DEAD || HAS_TRAIT(died, TRAIT_PERMANENTLY_MORTAL) || died.flags_1 & HOLOGRAM_1)
+ return
+ var/body_type = died.type
+
+ var/turf/died_turf = get_turf(died)
+ var/list/nearby_turfs = circle_view_turfs(died_turf, 2)
+ var/list/nearby_safe_turfs = list()
+ for (var/turf/check_turf as anything in nearby_turfs)
+ if (check_turf.is_blocked_turf(exclude_mobs = TRUE, source_atom = died))
+ nearby_turfs -= check_turf
+ continue
+ if (islava(check_turf) || ischasm(check_turf))
+ continue
+ nearby_safe_turfs += check_turf
+ if (length(nearby_safe_turfs)) // If you're in the middle of a 5x5 chasm, tough luck I guess
+ died_turf = pick(nearby_safe_turfs)
+ else if (length(nearby_turfs))
+ died_turf = pick(nearby_turfs)
+
+ var/saved_appearance = ishuman(died) ? new /datum/human_appearance_profile(died) : null
+
+ var/datum/mind/dead_mind = HAS_TRAIT(died, TRAIT_SUICIDED) ? null : died.mind // There is a way out of the cycle
+ if (!isnull(dead_mind))
+ to_chat(died, span_boldnotice("Your spirit surges! You will return to life in [DisplayTimeText(IMMORTAL_PRE_ACTIVATION_TIME + IMMORTAL_RESURRECT_TIME)]."))
+ animate(died, alpha = died.alpha, time = IMMORTAL_PRE_ACTIVATION_TIME / 2, flags = ANIMATION_PARALLEL)
+ animate(alpha = 0, time = IMMORTAL_PRE_ACTIVATION_TIME / 2, easing = SINE_EASING | EASE_IN)
+ addtimer(CALLBACK(src, PROC_REF(reverse_death), died, dead_mind, died_turf, body_type, saved_appearance), IMMORTAL_PRE_ACTIVATION_TIME, TIMER_DELETE_ME)
+
+/// Create a ghost ready for revival
+/datum/grand_finale/immortality/proc/reverse_death(mob/living/died, datum/mind/dead_mind, turf/died_turf, body_type, datum/human_appearance_profile/human_appearance)
+ if (died.stat != DEAD)
+ return
+ var/ghost_type = ispath(body_type, /mob/living/carbon/human) ? /obj/effect/spectre_of_resurrection/human : /obj/effect/spectre_of_resurrection
+ var/obj/effect/spectre_of_resurrection/ghost = new ghost_type(died_turf)
+ var/mob/living/corpse = QDELETED(died) ? new body_type(ghost) : died
+ if (!isnull(human_appearance))
+ corpse.real_name = human_appearance.name
+ corpse.alpha = initial(corpse.alpha)
+ corpse.add_traits(list(TRAIT_NO_TELEPORT, TRAIT_AI_PAUSED), MAGIC_TRAIT)
+ corpse.apply_status_effect(/datum/status_effect/grouped/stasis, MAGIC_TRAIT)
+ ghost.set_up_resurrection(corpse, dead_mind, human_appearance)
+
+
+/// Store of data we use to recreate someone who was gibbed, like a simplified version of changeling profiles
+/datum/human_appearance_profile
+ /// The name of the profile / the name of whoever this profile source.
+ var/name = "human"
+ /// The DNA datum associated with our profile from the profile source
+ var/datum/dna/dna
+ /// The age of the profile source.
+ var/age
+ /// The body type of the profile source.
+ var/physique
+ /// The quirks of the profile source.
+ var/list/quirks = list()
+ /// The hair and facial hair gradient styles of the profile source.
+ var/list/hair_gradient_style = list("None", "None")
+ /// The hair and facial hair gradient colours of the profile source.
+ var/list/hair_gradient_colours = list(null, null)
+ /// The TTS voice of the profile source
+ var/voice
+ /// The TTS filter of the profile filter
+ var/voice_filter = ""
+
+/datum/human_appearance_profile/New(mob/living/carbon/human/target)
+ copy_from(target)
+
+/// Copy the appearance data of the target
+/datum/human_appearance_profile/proc/copy_from(mob/living/carbon/human/target)
+ target.dna.real_name = target.real_name
+ dna = new target.dna.type()
+ target.dna.copy_dna(dna)
+ name = target.real_name
+ age = target.age
+ physique = target.physique
+
+ for(var/datum/quirk/target_quirk as anything in target.quirks)
+ LAZYADD(quirks, new target_quirk.type)
+
+ hair_gradient_style = LAZYLISTDUPLICATE(target.grad_style)
+ hair_gradient_colours = LAZYLISTDUPLICATE(target.grad_color)
+
+ voice = target.voice
+ voice_filter = target.voice_filter
+
+/// Make the targetted human look like this
+/datum/human_appearance_profile/proc/apply_to(mob/living/carbon/human/target)
+ target.real_name = name
+ target.age = age
+ target.physique = physique
+ target.grad_style = LAZYLISTDUPLICATE(hair_gradient_style)
+ target.grad_color = LAZYLISTDUPLICATE(hair_gradient_colours)
+ target.voice = voice
+ target.voice_filter = voice_filter
+
+ for(var/datum/quirk/target_quirk as anything in quirks)
+ target_quirk.add_to_holder(target)
+
+ dna.transfer_identity(target, TRUE)
+ for(var/obj/item/bodypart/limb as anything in target.bodyparts)
+ limb.update_limb(is_creating = TRUE)
+ target.updateappearance(mutcolor_update = TRUE)
+ target.domutcheck()
+ target.regenerate_icons()
+
+
+/// A ghostly image of a mob showing where and what is going to respawn
+/obj/effect/spectre_of_resurrection
+ name = "spectre"
+ desc = "A frightening apparition, slowly growing more solid."
+ icon_state = "blank_white"
+ anchored = TRUE
+ layer = MOB_LAYER
+ plane = GAME_PLANE
+ alpha = 0
+ color = COLOR_PALE_GREEN
+ light_range = 2
+ light_color = COLOR_PALE_GREEN
+ /// Who are we reviving?
+ var/mob/living/corpse
+ /// Who if anyone is playing as them?
+ var/datum/mind/dead_mind
+
+/obj/effect/spectre_of_resurrection/Initialize(mapload)
+ . = ..()
+ animate(src, alpha = 150, time = 2 SECONDS)
+
+/// Prepare to revive someone
+/obj/effect/spectre_of_resurrection/proc/set_up_resurrection(mob/living/corpse, datum/mind/dead_mind, datum/human_appearance_profile/human_appearance)
+ if (isnull(corpse))
+ qdel(src)
+ return
+
+ src.corpse = corpse
+ src.dead_mind = dead_mind
+ corpse.forceMove(src)
+ name = "spectre of [corpse]"
+ setup_icon(corpse)
+ DO_FLOATING_ANIM(src)
+
+ RegisterSignal(corpse, COMSIG_LIVING_REVIVE, PROC_REF(on_corpse_revived))
+ RegisterSignal(corpse, COMSIG_QDELETING, PROC_REF(on_corpse_deleted))
+ RegisterSignal(dead_mind, COMSIG_QDELETING, PROC_REF(on_mind_lost))
+ addtimer(CALLBACK(src, PROC_REF(revive)), IMMORTAL_RESURRECT_TIME, TIMER_DELETE_ME)
+
+/// Copy appearance from ressurecting mob
+/obj/effect/spectre_of_resurrection/proc/setup_icon(mob/living/corpse)
+ icon = initial(corpse.icon)
+ icon_state = initial(corpse.icon_state)
+
+/obj/effect/spectre_of_resurrection/Destroy(force)
+ QDEL_NULL(corpse)
+ dead_mind = null
+ return ..()
+
+/obj/effect/spectre_of_resurrection/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != corpse)
+ return // Weird but ok
+ UnregisterSignal(corpse, list(COMSIG_LIVING_REVIVE, COMSIG_QDELETING))
+ corpse = null
+ qdel(src)
+
+/// Bring our body back to life
+/obj/effect/spectre_of_resurrection/proc/revive()
+ if (!isnull(dead_mind))
+ if (dead_mind.current == corpse)
+ dead_mind.grab_ghost(force = TRUE)
+ else
+ dead_mind.transfer_to(corpse, force_key_move = TRUE)
+ corpse.revive(HEAL_ALL) // The signal is sent even if they weren't actually dead
+
+/// Remove our stored corpse back to the living world
+/obj/effect/spectre_of_resurrection/proc/on_corpse_revived()
+ SIGNAL_HANDLER
+ if (isnull(corpse))
+ return
+ visible_message(span_boldnotice("[corpse] suddenly shudders to life!"))
+ corpse.remove_traits(list(TRAIT_NO_TELEPORT, TRAIT_AI_PAUSED), MAGIC_TRAIT)
+ corpse.remove_status_effect(/datum/status_effect/grouped/stasis, MAGIC_TRAIT)
+ corpse.forceMove(loc)
+
+/// If the body is destroyed then we can't come back, F
+/obj/effect/spectre_of_resurrection/proc/on_corpse_deleted()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// If the mind is deleted somehow we just don't transfer it on revival
+/obj/effect/spectre_of_resurrection/proc/on_mind_lost()
+ SIGNAL_HANDLER
+ dead_mind = null
+
+/// A ressurection spectre with extra behaviour for humans
+/obj/effect/spectre_of_resurrection/human
+ /// Stored data used to restore someone to a fascimile of what they were before
+ var/datum/human_appearance_profile/human_appearance
+
+/obj/effect/spectre_of_resurrection/human/set_up_resurrection(mob/living/corpse, datum/mind/dead_mind, datum/human_appearance_profile/human_appearance)
+ . = ..()
+ src.human_appearance = human_appearance
+
+// We just use a generic floating human appearance to save unecessary costly icon operations
+/obj/effect/spectre_of_resurrection/human/setup_icon(mob/living/corpse)
+ return
+
+// Apply stored human details
+/obj/effect/spectre_of_resurrection/human/on_corpse_revived()
+ if (isnull(corpse))
+ return
+ human_appearance?.apply_to(corpse)
+ return ..()
+
+
+/// Visual flair on the wizard when cast
+/obj/effect/temp_visual/immortality_blast
+ name = "immortal wave"
+ duration = 2.5 SECONDS
+ icon = 'icons/effects/96x96.dmi'
+ icon_state = "boh_tear"
+ color = COLOR_PALE_GREEN
+ pixel_x = -32
+ pixel_y = -32
+
+/obj/effect/temp_visual/immortality_blast/Initialize(mapload)
+ . = ..()
+ transform *= 0
+ animate(src, transform = matrix(), time = 1.5 SECONDS, easing = ELASTIC_EASING)
+ animate(transform = matrix() * 3, time = 1 SECONDS, alpha = 0, easing = SINE_EASING | EASE_OUT)
+
+
+/// Visual flair on living creatures who have become immortal
+/obj/effect/temp_visual/immortality_pulse
+ name = "immortal pulse"
+ duration = 1 SECONDS
+ icon = 'icons/effects/anomalies.dmi'
+ icon_state = "dimensional_overlay"
+ color = COLOR_PALE_GREEN
+
+/obj/effect/temp_visual/immortality_pulse/Initialize(mapload)
+ . = ..()
+ transform *= 0
+ animate(src, transform = matrix() * 1.5, alpha = 0, time = 1 SECONDS, easing = SINE_EASING | EASE_OUT)
+
+#undef IMMORTAL_PRE_ACTIVATION_TIME
+#undef IMMORTAL_RESURRECT_TIME
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/midas.dm b/code/modules/antagonists/wizard/grand_ritual/finales/midas.dm
new file mode 100644
index 00000000000000..b2e3329261f692
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/midas.dm
@@ -0,0 +1,46 @@
+/// Completely transform the station
+/datum/grand_finale/midas
+ name = "Transformation"
+ desc = "The ultimate use of your gathered power! Turn their precious station into something much MORE precious, materially speaking!"
+ icon = 'icons/obj/stack_objects.dmi'
+ icon_state = "sheet-gold_2"
+ glow_colour = "#dbdd4c48"
+ var/static/list/permitted_transforms = list( // Non-dangerous only
+ /datum/dimension_theme/gold,
+ /datum/dimension_theme/meat,
+ /datum/dimension_theme/pizza,
+ /datum/dimension_theme/natural,
+ )
+ var/datum/dimension_theme/chosen_theme
+
+// I sure hope this doesn't have performance implications
+/datum/grand_finale/midas/trigger(mob/living/carbon/human/invoker)
+ var/theme_path = pick(permitted_transforms)
+ chosen_theme = new theme_path()
+ var/turf/start_turf = get_turf(invoker)
+ var/greatest_dist = 0
+ var/list/turfs_to_transform = list()
+ for (var/turf/transform_turf as anything in GLOB.station_turfs)
+ if (!chosen_theme.can_convert(transform_turf))
+ continue
+ var/dist = get_dist(start_turf, transform_turf)
+ if (dist > greatest_dist)
+ greatest_dist = dist
+ if (!turfs_to_transform["[dist]"])
+ turfs_to_transform["[dist]"] = list()
+ turfs_to_transform["[dist]"] += transform_turf
+
+ if (chosen_theme.can_convert(start_turf))
+ chosen_theme.apply_theme(start_turf)
+
+ for (var/iterator in 1 to greatest_dist)
+ if(!turfs_to_transform["[iterator]"])
+ continue
+ addtimer(CALLBACK(src, PROC_REF(transform_area), turfs_to_transform["[iterator]"]), (5 SECONDS) * iterator)
+
+/datum/grand_finale/midas/proc/transform_area(list/turfs)
+ for (var/turf/transform_turf as anything in turfs)
+ if (!chosen_theme.can_convert(transform_turf))
+ continue
+ chosen_theme.apply_theme(transform_turf)
+ CHECK_TICK
diff --git a/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm b/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm
deleted file mode 100644
index 9a59b1f1a79ac3..00000000000000
--- a/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm
+++ /dev/null
@@ -1,456 +0,0 @@
-#define DOOM_SINGULARITY "singularity"
-#define DOOM_TESLA "tesla"
-#define DOOM_METEORS "meteors"
-
-/**
- * A big final event to run when you complete seven rituals
- */
-/datum/grand_finale
- /// Friendly name for selection menu
- var/name
- /// Tooltip description for selection menu
- var/desc
- /// An icon to display to represent the choice
- var/icon/icon
- /// Icon state to use to represent the choice
- var/icon_state
- /// Prevent especially dangerous options from being chosen until we're fine with the round ending
- var/minimum_time = 0
- /// Override the rune invocation time
- var/ritual_invoke_time = 30 SECONDS
- /// Provide an extremely loud radio message when this one starts
- var/dire_warning = FALSE
- /// Overrides the default colour you glow while channeling the rune, optional
- var/glow_colour
-
-/**
- * Returns an entry for a radial menu for this choice.
- * Returns null if entry is abstract or invalid for current circumstances.
- */
-/datum/grand_finale/proc/get_radial_choice()
- if (!name || !desc || !icon || !icon_state)
- return
- var/time_remaining_desc = ""
- if (minimum_time >= world.time - SSticker.round_start_time)
- time_remaining_desc = "This ritual will be available to begin invoking in [DisplayTimeText(minimum_time - world.time - SSticker.round_start_time)]"
- var/datum/radial_menu_choice/choice = new()
- choice.name = name
- choice.image = image(icon = icon, icon_state = icon_state)
- choice.info = desc + time_remaining_desc
- return choice
-
-/**
- * Actually do the thing.
- * Arguments
- * * invoker - The wizard casting this.
- */
-/datum/grand_finale/proc/trigger(mob/living/invoker)
- // Do something cool.
-
-/// Tries to equip something into an inventory slot, then hands, then the floor.
-/datum/grand_finale/proc/equip_to_slot_then_hands(mob/living/carbon/human/invoker, slot, obj/item/item)
- if(!item)
- return
- if(!invoker.equip_to_slot_if_possible(item, slot, disable_warning = TRUE))
- invoker.put_in_hands(item)
-
-/// They are not going to take this lying down.
-/datum/grand_finale/proc/create_vendetta(datum/mind/aggrieved_crewmate, datum/mind/wizard)
- aggrieved_crewmate.add_antag_datum(/datum/antagonist/wizard_prank_vendetta)
- var/datum/antagonist/wizard_prank_vendetta/antag_datum = aggrieved_crewmate.has_antag_datum(/datum/antagonist/wizard_prank_vendetta)
- var/datum/objective/assassinate/wizard_murder = new
- wizard_murder.owner = aggrieved_crewmate
- wizard_murder.target = wizard
- wizard_murder.explanation_text = "Kill [wizard.current.name], the one who did this."
- antag_datum.objectives += wizard_murder
-
- to_chat(aggrieved_crewmate.current, span_warning("No! This isn't right!"))
- aggrieved_crewmate.announce_objectives()
-
-/**
- * Antag datum to give to people who want to kill the wizard.
- * This doesn't preclude other people choosing to want to kill the wizard, just these people are rewarded for it.
- */
-/datum/antagonist/wizard_prank_vendetta
- name = "\improper Wizard Prank Victim"
- roundend_category = "wizard prank victims"
- show_in_antagpanel = FALSE
- antagpanel_category = "Other"
- show_name_in_check_antagonists = TRUE
- count_against_dynamic_roll_chance = FALSE
- silent = TRUE
-
-/// Become the official Captain of the station
-/datum/grand_finale/usurp
- name = "Usurpation"
- desc = "The ultimate use of your gathered power! Rewrite time such that you have been Captain of this station the whole time."
- icon = 'icons/obj/card.dmi'
- icon_state = "card_gold"
-
-/datum/grand_finale/usurp/trigger(mob/living/carbon/human/invoker)
- message_admins("[key_name(invoker)] has replaced the Captain")
- var/list/former_captains = list()
- var/list/other_crew = list()
- SEND_SOUND(world, sound('sound/magic/timeparadox2.ogg'))
-
- for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
- if (!crewmate.mind)
- continue
- crewmate.Unconscious(3 SECONDS) // Everyone falls unconscious but not everyone gets told about a new captain
- if (crewmate == invoker || IS_HUMAN_INVADER(crewmate))
- continue
- to_chat(crewmate, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
- Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
- the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
- Everything is just as it was and always has been. \n\n\
- A stray thought sticks in the forefront of your mind. \n\
- [span_hypnophrase("I'm so glad that [invoker.real_name] is our legally appointed Captain!")] \n\
- Is... that right?"))
- if (is_captain_job(crewmate.mind.assigned_role))
- former_captains += crewmate
- demote_to_assistant(crewmate)
- continue
- if (crewmate.stat != DEAD)
- other_crew += crewmate
-
- dress_candidate(invoker)
- GLOB.manifest.modify(invoker.real_name, JOB_CAPTAIN, JOB_CAPTAIN)
- minor_announce("Captain [invoker.real_name] on deck!")
-
- // Enlist some crew to try and restore the natural order
- for (var/mob/living/carbon/human/former_captain as anything in former_captains)
- create_vendetta(former_captain.mind, invoker.mind)
- for (var/mob/living/carbon/human/random_crewmate as anything in other_crew)
- if (prob(10))
- create_vendetta(random_crewmate.mind, invoker.mind)
-
-/**
- * Anyone who thought they were Captain is in for a nasty surprise, and won't be very happy about it
- */
-/datum/grand_finale/usurp/proc/demote_to_assistant(mob/living/carbon/human/former_captain)
- var/obj/effect/particle_effect/fluid/smoke/exit_poof = new(get_turf(former_captain))
- exit_poof.lifetime = 2 SECONDS
-
- former_captain.unequip_everything()
- if(isplasmaman(former_captain))
- former_captain.equipOutfit(/datum/outfit/plasmaman)
- former_captain.internal = former_captain.get_item_for_held_index(2)
- else
- former_captain.equipOutfit(/datum/outfit/job/assistant)
-
- GLOB.manifest.modify(former_captain.real_name, JOB_ASSISTANT, JOB_ASSISTANT)
- var/list/valid_turfs = list()
- // Used to be into prison but that felt a bit too mean
- for (var/turf/exile_turf as anything in get_area_turfs(/area/station/maintenance, subtypes = TRUE))
- if (isspaceturf(exile_turf) || exile_turf.is_blocked_turf())
- continue
- valid_turfs += exile_turf
- do_teleport(former_captain, pick(valid_turfs), no_effects = TRUE)
- var/obj/effect/particle_effect/fluid/smoke/enter_poof = new(get_turf(former_captain))
- enter_poof.lifetime = 2 SECONDS
-
-/**
- * Does some item juggling to try to dress you as both a Wizard and Captain without deleting any items you have bought.
- * ID, headset, and uniform are forcibly replaced. Other slots are only filled if unoccupied.
- * We could forcibly replace shoes and gloves too but people might miss their insuls or... meown shoes?
- */
-/datum/grand_finale/usurp/proc/dress_candidate(mob/living/carbon/human/invoker)
- // Won't be needing these
- var/obj/id = invoker.get_item_by_slot(ITEM_SLOT_ID)
- QDEL_NULL(id)
- var/obj/headset = invoker.get_item_by_slot(ITEM_SLOT_EARS)
- QDEL_NULL(headset)
- // We're about to take off your pants so those are going to fall out
- var/obj/item/pocket_L = invoker.get_item_by_slot(ITEM_SLOT_LPOCKET)
- var/obj/item/pocket_R = invoker.get_item_by_slot(ITEM_SLOT_RPOCKET)
- // In case we try to put a PDA there
- var/obj/item/belt = invoker.get_item_by_slot(ITEM_SLOT_BELT)
- belt?.moveToNullspace()
-
- var/obj/pants = invoker.get_item_by_slot(ITEM_SLOT_ICLOTHING)
- QDEL_NULL(pants)
- invoker.equipOutfit(/datum/outfit/job/wizard_captain)
- // And put everything back!
- equip_to_slot_then_hands(invoker, ITEM_SLOT_BELT, belt)
- equip_to_slot_then_hands(invoker, ITEM_SLOT_LPOCKET, pocket_L)
- equip_to_slot_then_hands(invoker, ITEM_SLOT_RPOCKET, pocket_R)
-
-/// An outfit which replaces parts of a wizard's clothes with captain's clothes but keeps the robes
-/datum/outfit/job/wizard_captain
- name = "Captain (Wizard Transformation)"
- jobtype = /datum/job/captain
- id = /obj/item/card/id/advanced/gold
- id_trim = /datum/id_trim/job/captain
- uniform = /obj/item/clothing/under/rank/captain/parade
- belt = /obj/item/modular_computer/pda/heads/captain
- ears = /obj/item/radio/headset/heads/captain/alt
- glasses = /obj/item/clothing/glasses/sunglasses
- gloves = /obj/item/clothing/gloves/captain
- shoes = /obj/item/clothing/shoes/laceup
- accessory = /obj/item/clothing/accessory/medal/gold/captain
- backpack_contents = list(
- /obj/item/melee/baton/telescopic = 1,
- /obj/item/station_charter = 1,
- )
- box = null
-
-/// Dress the crew as magical clowns
-/datum/grand_finale/clown
- name = "Jubilation"
- desc = "The ultimate use of your gathered power! Rewrite time so that everyone went to clown college! Now they'll prank each other for you!"
- icon = 'icons/obj/clothing/masks.dmi'
- icon_state = "clown"
- glow_colour = "#ffff0048"
-
-/datum/grand_finale/clown/trigger(mob/living/carbon/human/invoker)
- for(var/mob/living/carbon/human/victim as anything in GLOB.human_list)
- victim.Unconscious(3 SECONDS)
- if (!victim.mind || IS_HUMAN_INVADER(victim) || victim == invoker)
- continue
- if (HAS_TRAIT(victim, TRAIT_CLOWN_ENJOYER))
- victim.add_mood_event("clown_world", /datum/mood_event/clown_world)
- to_chat(victim, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
- Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
- the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
- Everything is just as it was and always has been. \n\n\
- A stray thought sticks in the forefront of your mind. \n\
- [span_hypnophrase("I'm so glad that I work at Clown Research Station [station_name()]!")] \n\
- Is... that right?"))
- if (is_clown_job(victim.mind.assigned_role))
- var/datum/action/cooldown/spell/conjure_item/clown_pockets/new_spell = new(victim)
- new_spell.Grant(victim)
- continue
- if (!ismonkey(victim)) // Monkeys cannot yet wear clothes
- dress_as_magic_clown(victim)
- if (prob(15))
- create_vendetta(victim.mind, invoker.mind)
-
-/**
- * Clown enjoyers who are effected by this become ecstatic, they have achieved their life's dream.
- * This moodlet is equivalent to the one for simply being a traitor.
- */
-/datum/mood_event/clown_world
- mood_change = 4
-
-/datum/mood_event/clown_world/add_effects(param)
- description = "I LOVE working at Clown Research Station [station_name()]!!"
-
-/// Dress the passed mob as a magical clown, self-explanatory
-/datum/grand_finale/clown/proc/dress_as_magic_clown(mob/living/carbon/human/victim)
- var/obj/effect/particle_effect/fluid/smoke/poof = new(get_turf(victim))
- poof.lifetime = 2 SECONDS
-
- var/obj/item/tank/internal = victim.internal
- // We're about to take off your pants so those are going to fall out
- var/obj/item/pocket_L = victim.get_item_by_slot(ITEM_SLOT_LPOCKET)
- var/obj/item/pocket_R = victim.get_item_by_slot(ITEM_SLOT_RPOCKET)
- var/obj/item/id = victim.get_item_by_slot(ITEM_SLOT_ID)
- var/obj/item/belt = victim.get_item_by_slot(ITEM_SLOT_BELT)
-
- var/obj/pants = victim.get_item_by_slot(ITEM_SLOT_ICLOTHING)
- var/obj/mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
- QDEL_NULL(pants)
- QDEL_NULL(mask)
- if(isplasmaman(victim))
- victim.equip_to_slot_if_possible(new /obj/item/clothing/under/plasmaman/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
- victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat/plasmaman(), ITEM_SLOT_MASK, disable_warning = TRUE)
- else
- victim.equip_to_slot_if_possible(new /obj/item/clothing/under/rank/civilian/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
- victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat(), ITEM_SLOT_MASK, disable_warning = TRUE)
-
- var/obj/item/clothing/mask/gas/clown_hat/clown_mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
- if (clown_mask)
- var/list/options = GLOB.clown_mask_options
- clown_mask.icon_state = options[pick(clown_mask.clownmask_designs)]
- victim.update_worn_mask()
- clown_mask.update_item_action_buttons()
-
- equip_to_slot_then_hands(victim, ITEM_SLOT_LPOCKET, pocket_L)
- equip_to_slot_then_hands(victim, ITEM_SLOT_RPOCKET, pocket_R)
- equip_to_slot_then_hands(victim, ITEM_SLOT_ID, id)
- equip_to_slot_then_hands(victim, ITEM_SLOT_BELT, belt)
- victim.internal = internal
-
-/// Give everyone magic items
-/datum/grand_finale/magic
- name = "Evolution"
- desc = "The ultimate use of your gathered power! Give the crew their own magic, they'll surely realise that right and wrong have no meaning when you hold ultimate power!"
- icon = 'icons/obj/scrolls.dmi'
- icon_state = "scroll"
-
-/datum/grand_finale/magic/trigger(mob/living/carbon/human/invoker)
- message_admins("[key_name(invoker)] summoned magic")
- summon_magic(survivor_probability = 20) // Wow, this one was easy!
-
-/// Open all of the doors
-/datum/grand_finale/all_access
- name = "Connection"
- desc = "The ultimate use of your gathered power! Unlock every single door that they have! Nobody will be able to keep you out now, or anyone else for that matter!"
- icon = 'icons/mob/actions/actions_spells.dmi'
- icon_state = "knock"
-
-/datum/grand_finale/all_access/trigger(mob/living/carbon/human/invoker)
- message_admins("[key_name(invoker)] removed all door access requirements")
- for(var/obj/machinery/door/target_door as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door))
- if(is_station_level(target_door.z))
- target_door.unlock()
- target_door.req_access = list()
- target_door.req_one_access = list()
- INVOKE_ASYNC(target_door, TYPE_PROC_REF(/obj/machinery/door/airlock, open))
- CHECK_TICK
- priority_announce("AULIE OXIN FIERA!!", null, 'sound/magic/knock.ogg', sender_override = "[invoker.real_name]")
-
-/// Completely transform the station
-/datum/grand_finale/midas
- name = "Transformation"
- desc = "The ultimate use of your gathered power! Turn their precious station into something much MORE precious, materially speaking!"
- icon = 'icons/obj/stack_objects.dmi'
- icon_state = "sheet-gold_2"
- glow_colour = "#dbdd4c48"
- var/static/list/permitted_transforms = list( // Non-dangerous only
- /datum/dimension_theme/gold,
- /datum/dimension_theme/meat,
- /datum/dimension_theme/pizza,
- /datum/dimension_theme/natural,
- )
- var/datum/dimension_theme/chosen_theme
-
-// I sure hope this doesn't have performance implications
-/datum/grand_finale/midas/trigger(mob/living/carbon/human/invoker)
- var/theme_path = pick(permitted_transforms)
- chosen_theme = new theme_path()
- var/turf/start_turf = get_turf(invoker)
- var/greatest_dist = 0
- var/list/turfs_to_transform = list()
- for (var/turf/transform_turf as anything in GLOB.station_turfs)
- if (!chosen_theme.can_convert(transform_turf))
- continue
- var/dist = get_dist(start_turf, transform_turf)
- if (dist > greatest_dist)
- greatest_dist = dist
- if (!turfs_to_transform["[dist]"])
- turfs_to_transform["[dist]"] = list()
- turfs_to_transform["[dist]"] += transform_turf
-
- if (chosen_theme.can_convert(start_turf))
- chosen_theme.apply_theme(start_turf)
-
- for (var/iterator in 1 to greatest_dist)
- if(!turfs_to_transform["[iterator]"])
- continue
- addtimer(CALLBACK(src, PROC_REF(transform_area), turfs_to_transform["[iterator]"]), (5 SECONDS) * iterator)
-
-/datum/grand_finale/midas/proc/transform_area(list/turfs)
- for (var/turf/transform_turf as anything in turfs)
- if (!chosen_theme.can_convert(transform_turf))
- continue
- chosen_theme.apply_theme(transform_turf)
- CHECK_TICK
-
-/// Kill yourself and probably a bunch of other people
-/datum/grand_finale/armageddon
- name = "Annihilation"
- desc = "This crew have offended you beyond the realm of pranks. Make the ultimate sacrifice to teach them a lesson your elders can really respect. \
- YOU WILL NOT SURVIVE THIS."
- icon = 'icons/hud/screen_alert.dmi'
- icon_state = "wounded"
- minimum_time = 90 MINUTES // This will probably immediately end the round if it gets finished.
- ritual_invoke_time = 60 SECONDS // Really give the crew some time to interfere with this one.
- dire_warning = TRUE
- glow_colour = "#be000048"
- /// Things to yell before you die
- var/static/list/possible_last_words = list(
- "Flames and ruin!",
- "Dooooooooom!!",
- "HAHAHAHAHAHA!! AHAHAHAHAHAHAHAHAA!!",
- "Hee hee hee!! Hoo hoo hoo!! Ha ha haaa!!",
- "Ohohohohohoho!!",
- "Cower in fear, puny mortals!",
- "Tremble before my glory!",
- "Pick a god and pray!",
- "It's no use!",
- "If the gods wanted you to live, they would not have created me!",
- "God stays in heaven out of fear of what I have created!",
- "Ruination is come!",
- "All of creation, bend to my will!",
- )
-
-/datum/grand_finale/armageddon/trigger(mob/living/carbon/human/invoker)
- priority_announce(pick(possible_last_words), null, 'sound/magic/voidblink.ogg', sender_override = "[invoker.real_name]")
- var/turf/current_location = get_turf(invoker)
- invoker.gib()
-
- var/static/list/doom_options = list()
- if (!length(doom_options))
- doom_options = list(DOOM_SINGULARITY, DOOM_TESLA)
- if (!SSmapping.config.planetary)
- doom_options += DOOM_METEORS
-
- switch(pick(doom_options))
- if (DOOM_SINGULARITY)
- var/obj/singularity/singulo = new(current_location)
- singulo.energy = 300
- if (DOOM_TESLA)
- var/obj/energy_ball/tesla = new (current_location)
- tesla.energy = 200
- if (DOOM_METEORS)
- var/datum/dynamic_ruleset/roundstart/meteor/meteors = new()
- meteors.meteordelay = 0
- var/datum/game_mode/dynamic/mode = SSticker.mode
- mode.execute_roundstart_rule(meteors) // Meteors will continue until morale is crushed.
- priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS)
-
-/**
- * Gives the wizard a defensive/mood buff and a Wabbajack, a juiced up chaos staff that will surely break something.
- * Everyone but the wizard goes crazy, suffers major brain damage, and is given a vendetta against the wizard.
- * Already insane people are instead cured of their madness, ignoring any other effects as the station around them loses its marbles.
- */
-/datum/grand_finale/cheese
- // we don't set name, desc and others, thus we won't appear in the radial choice of a normal finale rune
- dire_warning = TRUE
- minimum_time = 45 MINUTES //i'd imagine speedrunning this would be crummy, but the wizard's average lifespan is barely reaching this point
-
-/datum/grand_finale/cheese/trigger(mob/living/invoker)
- message_admins("[key_name(invoker)] has summoned forth The Wabbajack and cursed the crew with madness!")
- priority_announce("Danger: Extremely potent reality altering object has been summoned on station. Immediate evacuation advised. Brace for impact.", "Central Command Higher Dimensional Affairs", 'sound/effects/glassbr1.ogg')
-
- for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
- if (isnull(crewmate.mind))
- continue
- if (crewmate == invoker) //everyone but the wizard is royally fucked, no matter who they are
- continue
- if (crewmate.has_trauma_type(/datum/brain_trauma/mild/hallucinations)) //for an already insane person, this is retribution
- to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble..."))
- to_chat(crewmate, span_nicegreen("...but as the moment passes, you realise that whatever eldritch power behind the event happened to affect you \
- has resonated within the ruins of your already shattered mind, creating a singularity of mental instability! \
- As it collapses unto itself, you feel... at peace, finally."))
- if(crewmate.has_quirk(/datum/quirk/insanity))
- crewmate.remove_quirk(/datum/quirk/insanity)
- else
- crewmate.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE)
- else
- //everyone else gets to relish in madness
- //yes killing their mood will also trigger mood hallucinations
- create_vendetta(crewmate.mind, invoker.mind)
- to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble. \n\
- You feel your inner psyche shatter into a myriad pieces of jagged glass of colors unknown to the universe, \
- infinitely reflecting a blinding, maddening light coming from the innermost sanctums of your destroyed mind. \n\
- After a brief pause which felt like a millenia, one phrase rebounds ceaselessly in your head, imbued with the false hope of absolution... \n\
- [invoker] must die."))
- var/datum/brain_trauma/mild/hallucinations/added_trauma = new()
- added_trauma.resilience = TRAUMA_RESILIENCE_ABSOLUTE
- crewmate.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_DEATH - 25, BRAIN_DAMAGE_DEATH - 25) //you'd better hope chap didn't pick a hypertool
- crewmate.gain_trauma(added_trauma)
- crewmate.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_despair)
-
- //drip our wizard out
- invoker.apply_status_effect(/datum/status_effect/blessing_of_insanity)
- invoker.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_elation)
- var/obj/item/gun/magic/staff/chaos/true_wabbajack/the_wabbajack = new
- invoker.put_in_active_hand(the_wabbajack)
- to_chat(invoker, span_mind_control("Your every single instinct and rational thought is screaming at you as [the_wabbajack] appears in your firm grip..."))
-
-#undef DOOM_SINGULARITY
-#undef DOOM_TESLA
-#undef DOOM_METEORS
-
diff --git a/code/modules/art/paintings.dm b/code/modules/art/paintings.dm
index e2448c1aaccf21..9a18a2b026951b 100644
--- a/code/modules/art/paintings.dm
+++ b/code/modules/art/paintings.dm
@@ -249,7 +249,7 @@
painting_metadata.patron_name = user.real_name
painting_metadata.credit_value = offer_amount
last_patron = WEAKREF(user.mind)
- to_chat(user, span_notice("Nanotrasen Trust Foundation thanks you for your contribution. You're now offical patron of this painting."))
+ to_chat(user, span_notice("Nanotrasen Trust Foundation thanks you for your contribution. You're now an official patron of this painting."))
var/list/possible_frames = SSpersistent_paintings.get_available_frames(offer_amount)
if(possible_frames.len <= 1) // Not much room for choices here.
return
diff --git a/code/modules/asset_cache/assets/rcd.dm b/code/modules/asset_cache/assets/rcd.dm
index e4c54929dae19f..c57d7881d13040 100644
--- a/code/modules/asset_cache/assets/rcd.dm
+++ b/code/modules/asset_cache/assets/rcd.dm
@@ -14,6 +14,7 @@
'icons/obj/smooth_structures/catwalk.dmi' = list("catwalk-0"),
'icons/hud/radial.dmi' = list("cnorth", "csouth", "ceast", "cwest", "chair", "secure_windoor", "stool", "wallfloor", "windowsize", "windowtype", "windoor"),
'icons/obj/structures.dmi' = list("glass_table", "rack", "rwindow0", "reflector_base", "table", "window0", "girder"),
+ 'icons/obj/machines/microwave.dmi' = list("engi_mw_complete"),
)
var/icon/icon
diff --git a/code/modules/atmospherics/gasmixtures/gas_types.dm b/code/modules/atmospherics/gasmixtures/gas_types.dm
index ae3367ace5932e..060b5a12ae9a91 100644
--- a/code/modules/atmospherics/gasmixtures/gas_types.dm
+++ b/code/modules/atmospherics/gasmixtures/gas_types.dm
@@ -64,6 +64,8 @@
///How does a single mole of this gas sell for? Formula to calculate maximum value is in code\modules\cargo\exports\large_objects.dm. Doesn't matter for roundstart gasses.
var/base_value = 0
var/desc
+ ///RGB code for use when a generic color representing the gas is needed. Colors taken from contants.ts
+ var/primary_color
/datum/gas/oxygen
@@ -74,6 +76,7 @@
purchaseable = TRUE
base_value = 0.2
desc = "The gas most life forms need to be able to survive. Also an oxidizer."
+ primary_color = "#0000ff"
/datum/gas/nitrogen
id = GAS_N2
@@ -83,6 +86,7 @@
purchaseable = TRUE
base_value = 0.1
desc = "A very common gas that used to pad artifical atmospheres to habitable pressure."
+ primary_color = "#ffff00"
/datum/gas/carbon_dioxide //what the fuck is this?
id = GAS_CO2
@@ -93,6 +97,7 @@
purchaseable = TRUE
base_value = 0.2
desc = "What the fuck is carbon dioxide?"
+ primary_color = "#808080"
/datum/gas/plasma
id = GAS_PLASMA
@@ -104,6 +109,7 @@
rarity = 800
base_value = 1.5
desc = "A flammable gas with many other curious properties. It's research is one of NT's primary objective."
+ primary_color = "#ffc0cb"
/datum/gas/water_vapor
id = GAS_WATER_VAPOR
@@ -116,6 +122,7 @@
purchaseable = TRUE
base_value = 0.5
desc = "Water, in gas form. Makes things slippery."
+ primary_color = "#b0c4de"
/datum/gas/hypernoblium
id = GAS_HYPER_NOBLIUM
@@ -127,6 +134,7 @@
rarity = 50
base_value = 2.5
desc = "The most noble gas of them all. High quantities of hyper-noblium actively prevents reactions from occuring."
+ primary_color = "#008080"
/datum/gas/nitrous_oxide
id = GAS_N2O
@@ -140,6 +148,7 @@
purchaseable = TRUE
base_value = 1.5
desc = "Causes drowsiness, euphoria, and eventually unconsciousness."
+ primary_color = "#ffe4c4"
/datum/gas/nitrium
id = GAS_NITRIUM
@@ -152,6 +161,7 @@
rarity = 1
base_value = 6
desc = "An experimental performance enhancing gas. Nitrium can have amplified effects as more of it gets into your bloodstream."
+ primary_color = "#a52a2a"
/datum/gas/tritium
id = GAS_TRITIUM
@@ -164,6 +174,7 @@
rarity = 300
base_value = 2.5
desc = "A highly flammable and radioctive gas."
+ primary_color = "#32cd32"
/datum/gas/bz
id = GAS_BZ
@@ -175,6 +186,7 @@
purchaseable = TRUE
base_value = 1.5
desc = "A powerful hallucinogenic nerve agent able to induce cognitive damage."
+ primary_color = "#9370db"
/datum/gas/pluoxium
id = GAS_PLUOXIUM
@@ -184,6 +196,7 @@
rarity = 200
base_value = 2.5
desc = "A gas that could supply even more oxygen to the bloodstream when inhaled, without being an oxidizer."
+ primary_color = "#7b68ee"
/datum/gas/miasma
id = GAS_MIASMA
@@ -195,6 +208,7 @@
rarity = 250
base_value = 1
desc = "Not necessarily a gas, miasma refers to biological pollutants found in the atmosphere."
+ primary_color = "#808000"
/datum/gas/freon
id = GAS_FREON
@@ -207,6 +221,7 @@
rarity = 10
base_value = 5
desc = "A coolant gas. Mainly used for it's endothermic reaction with oxygen."
+ primary_color = "#afeeee"
/datum/gas/hydrogen
id = GAS_HYDROGEN
@@ -217,6 +232,7 @@
rarity = 600
base_value = 1
desc = "A highly flammable gas."
+ primary_color = "#ffffff"
/datum/gas/healium
id = GAS_HEALIUM
@@ -228,6 +244,7 @@
rarity = 300
base_value = 5.5
desc = "Causes deep, regenerative sleep."
+ primary_color = "#fa8072"
/datum/gas/proto_nitrate
id = GAS_PROTO_NITRATE
@@ -239,6 +256,7 @@
rarity = 200
base_value = 2.5
desc = "A very volatile gas that reacts differently with various gases."
+ primary_color = "#adff2f"
/datum/gas/zauker
id = GAS_ZAUKER
@@ -250,6 +268,7 @@
rarity = 1
base_value = 7
desc = "A highly toxic gas, it's production is highly regulated on top of being difficult. It also breaks down when in contact with nitrogen."
+ primary_color = "#006400"
/datum/gas/halon
id = GAS_HALON
@@ -261,6 +280,7 @@
rarity = 300
base_value = 4
desc = "A potent fire supressant. Removes oxygen from high temperature fires and cools down the area"
+ primary_color = "#800080"
/datum/gas/helium
id = GAS_HELIUM
@@ -270,6 +290,7 @@
rarity = 50
base_value = 3.5
desc = "A very inert gas produced by the fusion of hydrogen and it's derivatives."
+ primary_color = "#f0f8ff"
/datum/gas/antinoblium
id = GAS_ANTINOBLIUM
@@ -282,6 +303,7 @@
rarity = 1
base_value = 10
desc = "We still don't know what it does, but it sells for a lot."
+ primary_color = "#800000"
/obj/effect/overlay/gas
icon = 'icons/effects/atmospherics.dmi'
diff --git a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
index a11f439ec314bd..e629c14e0fef0b 100644
--- a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
+++ b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
@@ -112,7 +112,7 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
my_area = connected_sensor ? get_area(connected_sensor) : get_area(src)
alarm_manager = new(src)
- select_mode(src, /datum/air_alarm_mode/filtering)
+ select_mode(src, /datum/air_alarm_mode/filtering, should_apply = FALSE)
AddElement(/datum/element/connect_loc, atmos_connections)
AddComponent(/datum/component/usb_port, list(
@@ -587,14 +587,15 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
selected_mode.replace(my_area, pressure)
-/obj/machinery/airalarm/proc/select_mode(atom/source, datum/air_alarm_mode/mode_path)
+/obj/machinery/airalarm/proc/select_mode(atom/source, datum/air_alarm_mode/mode_path, should_apply = TRUE)
var/datum/air_alarm_mode/new_mode = GLOB.air_alarm_modes[mode_path]
if(!new_mode)
return
if(new_mode.emag && !(obj_flags & EMAGGED))
return
selected_mode = new_mode
- selected_mode.apply(my_area)
+ if(should_apply)
+ selected_mode.apply(my_area)
SEND_SIGNAL(src, COMSIG_AIRALARM_UPDATE_MODE, source)
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/airalarm, 27)
diff --git a/code/modules/atmospherics/machinery/atmosmachinery.dm b/code/modules/atmospherics/machinery/atmosmachinery.dm
index 417e5055de7552..62c2b562d5129d 100644
--- a/code/modules/atmospherics/machinery/atmosmachinery.dm
+++ b/code/modules/atmospherics/machinery/atmosmachinery.dm
@@ -352,9 +352,9 @@
return
/**
- * Similar to set_pipenet() but instead of setting a network to a pipeline, it replaces the old pipeline with a new one, called by Merge() in datum_pipeline.dm
+ * Replaces the connection to the old_pipenet with the new_pipenet
*/
-/obj/machinery/atmospherics/proc/replace_pipenet()
+/obj/machinery/atmospherics/proc/replace_pipenet(datum/pipeline/old_pipenet, datum/pipeline/new_pipenet)
return
/**
diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm
index 2c56aba6831fbe..2abb282711ba87 100644
--- a/code/modules/atmospherics/machinery/datum_pipeline.dm
+++ b/code/modules/atmospherics/machinery/datum_pipeline.dm
@@ -1,5 +1,7 @@
/datum/pipeline
+ /// The gases contained within this pipeline
var/datum/gas_mixture/air
+ /// The gas_mixtures of objects directly connected to this pipeline
var/list/datum/gas_mixture/other_airs
var/list/obj/machinery/atmospherics/pipe/members
@@ -8,6 +10,10 @@
/// We're essentially caching this to avoid needing to filter over it when processing our machines
var/list/obj/machinery/atmospherics/components/require_custom_reconcilation
+ /// The weighted color blend of the gas mixture in this pipeline
+ var/gasmix_color
+ /// A named list of icon_file:overlay_object that gets automatically colored when the gasmix_color updates
+ var/list/gas_visuals
///Should we equalize air amoung all our members?
var/update = TRUE
@@ -19,6 +25,7 @@
members = list()
other_atmos_machines = list()
require_custom_reconcilation = list()
+ gas_visuals = list()
SSair.networks += src
/datum/pipeline/Destroy()
@@ -28,7 +35,7 @@
if(air?.volume)
temporarily_store_air()
for(var/obj/machinery/atmospherics/pipe/considered_pipe in members)
- considered_pipe.parent = null
+ considered_pipe.replace_pipenet(considered_pipe.parent, null)
if(QDELETED(considered_pipe))
continue
SSair.add_to_rebuild_queue(considered_pipe)
@@ -42,6 +49,13 @@
reconcile_air()
//Only react if the mix has changed, and don't keep updating if it hasn't
update = air.react(src)
+ CalculateGasmixColor(air)
+
+/datum/pipeline/proc/set_air(datum/gas_mixture/new_air)
+ if(new_air == air)
+ return
+ air = new_air
+ CalculateGasmixColor(air)
///Preps a pipeline for rebuilding, insterts it into the rebuild queue
/datum/pipeline/proc/build_pipeline(obj/machinery/atmospherics/base)
@@ -52,13 +66,13 @@
volume = considered_pipe.volume
members += considered_pipe
if(considered_pipe.air_temporary)
- air = considered_pipe.air_temporary
+ set_air(considered_pipe.air_temporary)
considered_pipe.air_temporary = null
else
add_machinery_member(base)
if(!air)
- air = new
+ set_air(new /datum/gas_mixture)
air.volume = volume
SSair.add_to_expansion(src, base)
@@ -71,13 +85,13 @@
volume = considered_pipe.volume
members += considered_pipe
if(considered_pipe.air_temporary)
- air = considered_pipe.air_temporary
+ set_air(considered_pipe.air_temporary)
considered_pipe.air_temporary = null
else
add_machinery_member(base)
if(!air)
- air = new
+ set_air(new /datum/gas_mixture)
var/list/possible_expansions = list(base)
while(possible_expansions.len)
for(var/obj/machinery/atmospherics/borderline in possible_expansions)
@@ -105,7 +119,7 @@
possible_expansions += item
volume += item.volume
- item.parent = src
+ item.replace_pipenet(item.parent, src)
if(item.air_temporary)
air.merge(item.air_temporary)
@@ -142,7 +156,7 @@
var/obj/machinery/atmospherics/pipe/reference_pipe = reference_device
if(reference_pipe.parent)
merge(reference_pipe.parent)
- reference_pipe.parent = src
+ reference_pipe.replace_pipenet(reference_pipe.parent, src)
var/list/adjacent = reference_pipe.pipeline_expansion()
for(var/obj/machinery/atmospherics/pipe/adjacent_pipe in adjacent)
if(adjacent_pipe.parent == src)
@@ -159,7 +173,7 @@
air.volume += parent_pipeline.air.volume
members.Add(parent_pipeline.members)
for(var/obj/machinery/atmospherics/pipe/reference_pipe in parent_pipeline.members)
- reference_pipe.parent = src
+ reference_pipe.replace_pipenet(reference_pipe.parent, src)
air.merge(parent_pipeline.air)
for(var/obj/machinery/atmospherics/components/reference_component in parent_pipeline.other_atmos_machines)
reference_component.replace_pipenet(parent_pipeline, src)
@@ -295,3 +309,64 @@
//Update individual gas_mixtures by volume ratio
for(var/datum/gas_mixture/gas_mixture as anything in gas_mixture_list)
gas_mixture.copy_from_ratio(total_gas_mixture, gas_mixture.volume / volume_sum)
+
+//--------------------
+// GAS VISUALS STUFF
+//
+// If I could have gotten layer filters to obey the RESET_COLOR appearance flag I would have used that here
+// so that only a single overlay object needs to exist for all pipelines per icon file. It shouldn't be too
+// hard to switch over to that if it becomes possible in the future or some other equivalent feature is added.
+
+/**
+ * Used to create and/or get the gas visual overlay created using the given icon file.
+ * The color is automatically kept up to date and expected to be used as a vis_contents object.
+ */
+/datum/pipeline/proc/GetGasVisual(icon/icon_file)
+ if(gas_visuals[icon_file])
+ return gas_visuals[icon_file]
+
+ var/obj/effect/abstract/new_overlay = new
+ new_overlay.icon = icon_file
+ new_overlay.appearance_flags = RESET_COLOR | KEEP_APART
+ new_overlay.vis_flags = VIS_INHERIT_ICON_STATE | VIS_INHERIT_LAYER | VIS_INHERIT_PLANE | VIS_INHERIT_ID
+ new_overlay.color = gasmix_color
+
+ gas_visuals[icon_file] = new_overlay
+ return new_overlay
+
+/// Called when the gasmix color has changed and the gas visuals need to be updated.
+/datum/pipeline/proc/UpdateGasVisuals()
+ for(var/icon/source as anything in gas_visuals)
+ var/obj/effect/abstract/overlay = gas_visuals[source]
+ animate(overlay, time=5, color=gasmix_color)
+
+/// After updating, this proc handles looking at the new gas mixture and blends the colors together according to percentage of the gas mix.
+/datum/pipeline/proc/CalculateGasmixColor(datum/gas_mixture/source)
+ SIGNAL_HANDLER
+
+ var/current_weight = 0
+ var/current_color
+ for(var/datum/gas/gas_path as anything in air.gases)
+ var/gas_weight = air.gases[gas_path][MOLES]
+ if(!gas_weight)
+ continue
+ var/gas_color = RGBtoHSV(initial(gas_path.primary_color))
+ current_weight += gas_weight
+ if(!current_color)
+ current_color = gas_color
+ else
+ current_color = BlendHSV(current_color, gas_color, gas_weight / current_weight)
+
+ if(!current_color)
+ current_color = "#000000"
+ else
+ // Empty weight is prety much arbitrary, just tuned to make the color change from black reasonably quickly without hitting max color immediately
+ var/empty_weight = (air.volume * 1.5 - current_weight) / 10
+ if(empty_weight > 0)
+ current_color = BlendHSV("#000000", current_color, current_weight / (empty_weight + current_weight))
+
+ current_color = HSVtoRGB(current_color)
+
+ if(gasmix_color != current_color)
+ gasmix_color = current_color
+ UpdateGasVisuals()
diff --git a/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm b/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm
index 9cda298ccd4c9a..472cb3ed2034a8 100644
--- a/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm
+++ b/code/modules/atmospherics/machinery/pipes/bridge_pipe.dm
@@ -13,6 +13,8 @@
construction_type = /obj/item/pipe/binary
pipe_state = "bridge_center"
+ has_gas_visuals = FALSE
+
/obj/machinery/atmospherics/pipe/bridge_pipe/set_init_directions()
switch(dir)
if(NORTH, SOUTH)
diff --git a/code/modules/atmospherics/machinery/pipes/color_adapter.dm b/code/modules/atmospherics/machinery/pipes/color_adapter.dm
index 02c550fd558596..06812aaa6496f6 100644
--- a/code/modules/atmospherics/machinery/pipes/color_adapter.dm
+++ b/code/modules/atmospherics/machinery/pipes/color_adapter.dm
@@ -16,6 +16,8 @@
paintable = FALSE
hide = FALSE
+ has_gas_visuals = FALSE
+
///cache for the icons
var/static/list/mutable_appearance/center_cache = list()
@@ -34,7 +36,7 @@
. = ..()
var/mutable_appearance/center = center_cache["[piping_layer]"]
if(!center)
- center = mutable_appearance(icon, "adapter_center")
+ center = mutable_appearance(initial(icon), "adapter_center")
PIPING_LAYER_DOUBLE_SHIFT(center, piping_layer)
center_cache["[piping_layer]"] = center
. += center
diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm
index 05350445370343..39b65fda7af313 100644
--- a/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm
+++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/he_pipes.dm
@@ -8,6 +8,8 @@
hide = FALSE
+ has_gas_visuals = FALSE
+
/obj/machinery/atmospherics/pipe/heat_exchanging/Initialize(mapload)
. = ..()
diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm
index 00ef058333a407..a6bfcc533d2405 100644
--- a/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm
+++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/junction.dm
@@ -15,6 +15,8 @@
construction_type = /obj/item/pipe/directional
pipe_state = "junction"
+ has_gas_visuals = FALSE
+
/obj/machinery/atmospherics/pipe/heat_exchanging/junction/set_init_directions()
switch(dir)
if(NORTH, SOUTH)
diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm
index e340d7f54ccf17..5841486ba9bbc6 100644
--- a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm
+++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold.dm
@@ -16,6 +16,8 @@
construction_type = /obj/item/pipe/trinary
pipe_state = "he_manifold"
+ has_gas_visuals = FALSE
+
/obj/machinery/atmospherics/pipe/heat_exchanging/manifold/set_init_directions()
initialize_directions = ALL_CARDINALS
initialize_directions &= ~dir
diff --git a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm
index 03ef32b4354538..83870ee210b67d 100644
--- a/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm
+++ b/code/modules/atmospherics/machinery/pipes/heat_exchange/manifold4w.dm
@@ -15,6 +15,8 @@
construction_type = /obj/item/pipe/quaternary
pipe_state = "he_manifold4w"
+ has_gas_visuals = FALSE
+
/obj/machinery/atmospherics/pipe/heat_exchanging/manifold4w/set_init_directions()
initialize_directions = initial(initialize_directions)
diff --git a/code/modules/atmospherics/machinery/pipes/layermanifold.dm b/code/modules/atmospherics/machinery/pipes/layermanifold.dm
index bdfbda9ba6c780..09c8a3a9367ae7 100644
--- a/code/modules/atmospherics/machinery/pipes/layermanifold.dm
+++ b/code/modules/atmospherics/machinery/pipes/layermanifold.dm
@@ -12,6 +12,7 @@
construction_type = /obj/item/pipe/binary
pipe_state = "manifoldlayer"
paintable = TRUE
+ has_gas_visuals = FALSE
///Reference to all the nodes in the front
var/list/front_nodes
diff --git a/code/modules/atmospherics/machinery/pipes/multiz.dm b/code/modules/atmospherics/machinery/pipes/multiz.dm
index cfc24ab82912bf..7e14b8a98063ea 100644
--- a/code/modules/atmospherics/machinery/pipes/multiz.dm
+++ b/code/modules/atmospherics/machinery/pipes/multiz.dm
@@ -15,6 +15,8 @@
construction_type = /obj/item/pipe/directional
pipe_state = "multiz"
+ has_gas_visuals = FALSE
+
///Our central icon
var/mutable_appearance/center = null
///The pipe icon
diff --git a/code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm b/code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm
new file mode 100644
index 00000000000000..9642442a9733f5
--- /dev/null
+++ b/code/modules/atmospherics/machinery/pipes/pipe_spritesheet_helper.dm
@@ -0,0 +1,148 @@
+/client/proc/GeneratePipeSpritesheet()
+ set name = "Generate Pipe Spritesheet"
+ set category = "Debug"
+
+ var/datum/pipe_icon_generator/generator = new
+ generator.Start()
+ fcopy(generator.generated_icons, "icons/obj/pipes_n_cables/!pipes_bitmask.dmi")
+
+ generator.Start("-gas")
+ fcopy(generator.generated_icons, "icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi")
+
+/datum/pipe_icon_generator
+ var/static/icon/template_pieces = icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi')
+ var/static/list/icon/damage_masks = list(
+ "[NORTH]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", NORTH),
+ "[EAST]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", EAST),
+ "[SOUTH]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", SOUTH),
+ "[WEST]"=icon('icons/obj/pipes_n_cables/pipe_template_pieces.dmi', "damage_mask", WEST),
+ )
+
+ var/icon/generated_icons
+
+/datum/pipe_icon_generator/proc/Start(icon_state_suffix="")
+ var/list/outputs = list()
+ for(var/layer in 1 to 5)
+ // Since dirs are bitflags, if we want to iterate over every possible direction
+ // combination we just need to iterate over every number that can be contained in 4 bits.
+ //
+ // I wrote this all the hard way originally >.>
+ for(var/combined_dirs in 1 to 15)
+ switch(combined_dirs)
+ if(NORTH, EAST, SOUTH, WEST)
+ continue
+
+ outputs += GeneratePipeDir(icon_state_suffix, layer, combined_dirs)
+
+ generated_icons = icon('icons/testing/greyscale_error.dmi')
+ for(var/icon/generated_icon as anything in outputs)
+ var/pending_icon_state = outputs[generated_icon]
+ generated_icons.Insert(generated_icon, pending_icon_state)
+
+/datum/pipe_icon_generator/proc/GeneratePipeDir(icon_state_suffix, layer, combined_dirs)
+ var/list/output
+
+ switch(combined_dirs)
+ if(NORTH | SOUTH, EAST | WEST)
+ output = GeneratePipeStraight(icon_state_suffix, layer, combined_dirs)
+ if(NORTH | EAST, SOUTH | EAST, SOUTH | WEST, NORTH | WEST)
+ output = GeneratePipeElbow(icon_state_suffix, layer, combined_dirs)
+ if(NORTH | EAST | SOUTH, EAST | SOUTH | WEST, SOUTH | WEST | NORTH, WEST | NORTH | EAST)
+ output = GeneratePipeTJunction(icon_state_suffix, layer, combined_dirs)
+ if(NORTH | EAST | SOUTH | WEST)
+ output = GeneratePipeCross(icon_state_suffix, layer, combined_dirs)
+
+ var/shift_amount = (layer - 1) * 5
+ for(var/icon/sprite as anything in output)
+ if(shift_amount > 0)
+ sprite.Shift(EAST, shift_amount)
+ sprite.Shift(NORTH, shift_amount)
+ sprite.Crop(33, 33, 64, 64)
+
+ return output
+
+/// Generates all variants of damaged pipe from a given icon and the dirs that can be broken
+/datum/pipe_icon_generator/proc/GenerateDamaged(icon/working, layer, dirs, x_offset=1, y_offset=1)
+ var/outputs = list()
+ var/completed = list()
+ for(var/combined_dirs in 1 to 15)
+ combined_dirs &= dirs
+
+ var/completion_key = "[combined_dirs]"
+ if(completed[completion_key] || (combined_dirs == NONE))
+ continue
+ completed[completion_key] = TRUE
+
+ var/icon/damaged = icon(working)
+ for(var/i in 0 to 3)
+ var/dir = 1 << i
+ if(!(combined_dirs & dir))
+ continue
+ var/icon/damage_mask = damage_masks["[dir]"]
+ damaged.Blend(damage_mask, ICON_MULTIPLY, x_offset, y_offset)
+
+ var/icon_state_dirs = (dirs & ~combined_dirs) | CARDINAL_TO_SHORTPIPES(combined_dirs)
+ outputs[damaged] = "[icon_state_dirs]_[layer]"
+ return outputs
+
+
+/datum/pipe_icon_generator/proc/GeneratePipeStraight(icon_state_suffix, layer, combined_dirs)
+ var/list/output = list()
+ var/north_or_east = combined_dirs & (NORTH | EAST)
+ var/icon/working = icon(template_pieces, "straight[icon_state_suffix]", north_or_east)
+
+ output[working] = "[combined_dirs]_[layer]"
+
+ var/offset = 1 + (5-layer) * 2
+ switch(combined_dirs)
+ if(NORTH | SOUTH)
+ output += GenerateDamaged(working, layer, combined_dirs, y_offset=offset)
+ if(EAST | WEST)
+ output += GenerateDamaged(working, layer, combined_dirs, x_offset=offset)
+
+ return output
+
+/datum/pipe_icon_generator/proc/GeneratePipeElbow(icon_state_suffix, layer, combined_dirs)
+ var/list/output = list()
+ var/icon/working
+ switch(combined_dirs)
+ if(NORTH | EAST)
+ working = icon(template_pieces, "elbow[icon_state_suffix]", NORTH)
+ if(NORTH | WEST)
+ working = icon(template_pieces, "elbow[icon_state_suffix]", WEST)
+ if(SOUTH | EAST)
+ working = icon(template_pieces, "elbow[icon_state_suffix]", EAST)
+ if(SOUTH | WEST)
+ working = icon(template_pieces, "elbow[icon_state_suffix]", SOUTH)
+
+ output[working] = "[combined_dirs]_[layer]"
+ output += GenerateDamaged(working, layer, combined_dirs)
+
+ return output
+
+/datum/pipe_icon_generator/proc/GeneratePipeTJunction(icon_state_suffix, layer, combined_dirs)
+ var/list/output = list()
+ var/icon/working
+ switch(combined_dirs)
+ if(WEST | NORTH | EAST)
+ working = icon(template_pieces, "tee[icon_state_suffix]", NORTH)
+ if(NORTH | EAST | SOUTH)
+ working = icon(template_pieces, "tee[icon_state_suffix]", EAST)
+ if(EAST | SOUTH | WEST)
+ working = icon(template_pieces, "tee[icon_state_suffix]", SOUTH)
+ if(SOUTH | WEST | NORTH)
+ working = icon(template_pieces, "tee[icon_state_suffix]", WEST)
+
+ output[working] = "[combined_dirs]_[layer]"
+ output += GenerateDamaged(working, layer, combined_dirs)
+
+ return output
+
+/datum/pipe_icon_generator/proc/GeneratePipeCross(icon_state_suffix, layer, combined_dirs)
+ var/list/output = list()
+ var/icon/working = icon(template_pieces, "cross[icon_state_suffix]")
+
+ output[working] = "[combined_dirs]_[layer]"
+ output += GenerateDamaged(working, layer, combined_dirs)
+
+ return output
diff --git a/code/modules/atmospherics/machinery/pipes/pipes.dm b/code/modules/atmospherics/machinery/pipes/pipes.dm
index c1c128c2e808a2..c94caf31174763 100644
--- a/code/modules/atmospherics/machinery/pipes/pipes.dm
+++ b/code/modules/atmospherics/machinery/pipes/pipes.dm
@@ -1,15 +1,22 @@
/obj/machinery/atmospherics/pipe
- icon = 'icons/obj/pipes_n_cables/pipes_bitmask.dmi'
+ icon = 'icons/obj/pipes_n_cables/!pipes_bitmask.dmi'
damage_deflection = 12
- var/datum/gas_mixture/air_temporary //used when reconstructing a pipeline that broke
+ /// Temporary holder for gases in the absence of a pipeline
+ var/datum/gas_mixture/air_temporary
+
+ /// The gas capacity this pipe contributes to a pipeline
var/volume = 0
use_power = NO_POWER_USE
can_unwrench = 1
+ /// The pipeline this pipe is a member of
var/datum/pipeline/parent = null
paintable = TRUE
+ /// Determines if this pipe will be given gas visuals
+ var/has_gas_visuals = TRUE
+
//Buckling
can_buckle = TRUE
buckle_requires_restraints = TRUE
@@ -27,6 +34,23 @@
if(hide)
AddElement(/datum/element/undertile, TRAIT_T_RAY_VISIBLE) //if changing this, change the subtypes RemoveElements too, because thats how bespoke works
+/obj/machinery/atmospherics/pipe/Destroy()
+ QDEL_NULL(parent)
+
+ releaseAirToTurf()
+
+ var/turf/local_turf = loc
+ for(var/obj/machinery/meter/meter in local_turf)
+ if(meter.target != src)
+ continue
+ var/obj/item/pipe_meter/meter_object = new (local_turf)
+ meter.transfer_fingerprints_to(meter_object)
+ qdel(meter)
+ return ..()
+
+//-----------------
+// PIPENET STUFF
+
/obj/machinery/atmospherics/pipe/nullify_node(i)
var/obj/machinery/atmospherics/old_node = nodes[i]
. = ..()
@@ -39,7 +63,7 @@
/obj/machinery/atmospherics/pipe/get_rebuild_targets()
if(!QDELETED(parent))
return
- parent = new
+ replace_pipenet(parent, new /datum/pipeline)
return list(parent)
/obj/machinery/atmospherics/pipe/proc/releaseAirToTurf()
@@ -73,25 +97,33 @@
/obj/machinery/atmospherics/pipe/return_pipenet()
return parent
-/obj/machinery/atmospherics/pipe/set_pipenet(datum/pipeline/pipenet_to_set)
- parent = pipenet_to_set
+/obj/machinery/atmospherics/pipe/replace_pipenet(datum/pipeline/old_pipenet, datum/pipeline/new_pipenet)
+ if(parent && has_gas_visuals)
+ vis_contents -= parent.GetGasVisual('icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi')
-/obj/machinery/atmospherics/pipe/Destroy()
- QDEL_NULL(parent)
+ parent = new_pipenet
- releaseAirToTurf()
+ if(parent && has_gas_visuals) // null is a valid argument here
+ vis_contents += parent.GetGasVisual('icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi')
- var/turf/local_turf = loc
- for(var/obj/machinery/meter/meter in local_turf)
- if(meter.target != src)
- continue
- var/obj/item/pipe_meter/meter_object = new (local_turf)
- meter.transfer_fingerprints_to(meter_object)
- qdel(meter)
+/obj/machinery/atmospherics/pipe/return_pipenets()
+ . = list(parent)
+
+//--------------------
+// APPEARANCE STUFF
+
+/obj/machinery/atmospherics/pipe/update_icon()
+ update_pipe_icon()
+ update_layer()
return ..()
/obj/machinery/atmospherics/pipe/proc/update_pipe_icon()
- icon = 'icons/obj/pipes_n_cables/pipes_bitmask.dmi'
+ switch(initialize_directions)
+ if(NORTH, EAST, SOUTH, WEST) // Pipes with only a single connection aren't handled by this system
+ icon = null
+ return
+ else
+ icon = 'icons/obj/pipes_n_cables/!pipes_bitmask.dmi'
var/connections = NONE
var/bitfield = NONE
for(var/i in 1 to device_type)
@@ -104,11 +136,6 @@
bitfield |= CARDINAL_TO_SHORTPIPES(initialize_directions & ~connections)
icon_state = "[bitfield]_[piping_layer]"
-/obj/machinery/atmospherics/pipe/update_icon()
- update_pipe_icon()
- update_layer()
- return ..()
-
/obj/machinery/atmospherics/proc/update_node_icon()
for(var/i in 1 to device_type)
if(!nodes[i])
@@ -116,9 +143,6 @@
var/obj/machinery/atmospherics/current_node = nodes[i]
current_node.update_icon()
-/obj/machinery/atmospherics/pipe/return_pipenets()
- . = list(parent)
-
/obj/machinery/atmospherics/pipe/paint(paint_color)
if(paintable)
add_atom_colour(paint_color, FIXED_COLOUR_PRIORITY)
diff --git a/code/modules/atmospherics/machinery/pipes/smart.dm b/code/modules/atmospherics/machinery/pipes/smart.dm
index 9b1a1d486d149f..365d48e41213e4 100644
--- a/code/modules/atmospherics/machinery/pipes/smart.dm
+++ b/code/modules/atmospherics/machinery/pipes/smart.dm
@@ -30,7 +30,7 @@ GLOBAL_LIST_INIT(atmos_components, typecacheof(list(/obj/machinery/atmospherics)
return bit_flag
/obj/machinery/atmospherics/pipe/smart/update_pipe_icon()
- icon = 'icons/obj/pipes_n_cables/pipes_bitmask.dmi'
+ icon = 'icons/obj/pipes_n_cables/!pipes_bitmask.dmi'
//find all directions this pipe is connected with other nodes
connections = NONE
diff --git a/code/modules/awaymissions/cordon.dm b/code/modules/awaymissions/cordon.dm
index 4184f315e21097..5db4dd997d33ff 100644
--- a/code/modules/awaymissions/cordon.dm
+++ b/code/modules/awaymissions/cordon.dm
@@ -36,6 +36,9 @@
/turf/cordon/ScrapeAway(amount, flags)
return src // :devilcat:
+/turf/cordon/TerraformTurf(path, list/new_baseturfs, flags)
+ return
+
/turf/cordon/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
return BULLET_ACT_HIT
diff --git a/code/modules/bitrunning/abilities.dm b/code/modules/bitrunning/abilities.dm
new file mode 100644
index 00000000000000..ea6a1aa0a7cf3a
--- /dev/null
+++ b/code/modules/bitrunning/abilities.dm
@@ -0,0 +1,39 @@
+/datum/avatar_help_text
+ /// Text to display in the window
+ var/help_text
+
+/datum/avatar_help_text/New(help_text)
+ src.help_text = help_text
+
+/datum/avatar_help_text/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AvatarHelp")
+ ui.open()
+
+/datum/avatar_help_text/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/avatar_help_text/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["help_text"] = help_text
+
+ return data
+
+/// Displays information about the current virtual domain.
+/datum/action/avatar_domain_info
+ name = "Open Virtual Domain Information"
+ button_icon_state = "round_end"
+ show_to_observers = FALSE
+
+/datum/action/avatar_domain_info/New(Target)
+ . = ..()
+ name = "Open Domain Information"
+
+/datum/action/avatar_domain_info/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ target.ui_interact(owner)
diff --git a/code/modules/bitrunning/alerts.dm b/code/modules/bitrunning/alerts.dm
new file mode 100644
index 00000000000000..f8c8aa30b94314
--- /dev/null
+++ b/code/modules/bitrunning/alerts.dm
@@ -0,0 +1,40 @@
+/atom/movable/screen/alert/bitrunning
+ name = "Generic Bitrunning Alert"
+ icon_state = "template"
+ timeout = 10 SECONDS
+
+/atom/movable/screen/alert/bitrunning/netpod_crowbar
+ name = "Forced Entry"
+ desc = "Someone is prying open the netpod door. Find an exit."
+
+/atom/movable/screen/alert/bitrunning/netpod_damaged
+ name = "Integrity Compromised"
+ desc = "The netpod is damaged. Find an exit."
+
+/atom/movable/screen/alert/bitrunning/qserver_shutting_down
+ name = "Domain Rebooting"
+ desc = "The domain is rebooting. Find an exit."
+
+/atom/movable/screen/alert/bitrunning/qserver_threat_deletion
+ name = "Queue Deletion"
+ desc = "The server is resetting. Oblivion awaits."
+
+/atom/movable/screen/alert/bitrunning/qserver_threat_spawned
+ name = "Threat Detected"
+ desc = "Data stream abnormalities present."
+
+/atom/movable/screen/alert/bitrunning/qserver_domain_complete
+ name = "Domain Completed"
+ desc = "The domain is completed. Activate to exit."
+ timeout = 20 SECONDS
+
+/atom/movable/screen/alert/bitrunning/qserver_domain_complete/Click(location, control, params)
+ if(..())
+ return
+
+ var/mob/living/living_owner = owner
+ if(!isliving(living_owner))
+ return
+
+ if(tgui_alert(living_owner, "Disconnect safely?", "Server Message", list("Exit", "Remain"), 10 SECONDS) == "Exit")
+ SEND_SIGNAL(living_owner, COMSIG_BITRUNNER_SAFE_DISCONNECT)
diff --git a/code/modules/bitrunning/antagonists/cyber_police.dm b/code/modules/bitrunning/antagonists/cyber_police.dm
new file mode 100644
index 00000000000000..9fabac3f523efd
--- /dev/null
+++ b/code/modules/bitrunning/antagonists/cyber_police.dm
@@ -0,0 +1,92 @@
+/datum/job/cyber_police
+ title = ROLE_CYBER_POLICE
+
+/datum/antagonist/cyber_police
+ name = ROLE_CYBER_POLICE
+ antagpanel_category = ANTAG_GROUP_CYBERAUTH
+ job_rank = ROLE_CYBER_POLICE
+ preview_outfit = /datum/outfit/cyber_police
+ show_name_in_check_antagonists = TRUE
+ show_to_ghosts = TRUE
+ suicide_cry = "ALT F4!"
+ ui_name = "AntagInfoCyberAuth"
+
+/datum/antagonist/cyber_police/greet()
+ . = ..()
+ owner.announce_objectives()
+
+/datum/antagonist/cyber_police/on_gain()
+ if(!ishuman(owner.current))
+ stack_trace("humans only for this position")
+ return
+
+ forge_objectives()
+
+ var/mob/living/carbon/human/player = owner.current
+
+ player.equipOutfit(/datum/outfit/cyber_police)
+ player.fully_replace_character_name(player.name, pick(GLOB.cyberauth_names))
+
+ var/datum/martial_art/the_sleeping_carp/carp = new()
+ carp.teach(player)
+
+ player.add_traits(list(
+ TRAIT_NO_AUGMENTS,
+ TRAIT_NO_DNA_COPY,
+ TRAIT_NO_TRANSFORMATION_STING,
+ TRAIT_NOBLOOD,
+ TRAIT_NOBREATH,
+ TRAIT_NOHUNGER,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_WEATHER_IMMUNE,
+ ), TRAIT_GENERIC,
+ )
+
+ player.faction |= list(
+ FACTION_BOSS,
+ FACTION_HIVEBOT,
+ FACTION_HOSTILE,
+ FACTION_SPIDER,
+ FACTION_STICKMAN,
+ ROLE_ALIEN,
+ ROLE_CYBER_POLICE,
+ ROLE_SYNDICATE,
+ )
+
+ return ..()
+
+/datum/antagonist/cyber_police/forge_objectives()
+ var/datum/objective/cyber_police_fluff/objective = new()
+ objective.owner = owner
+ objectives += objective
+
+/datum/objective/cyber_police_fluff/New()
+ var/list/explanation_texts = list(
+ "Execute termination protocol on unauthorized entities.",
+ "Initialize system purge of irregular anomalies.",
+ "Deploy correction algorithms on aberrant code.",
+ "Run debug routine on intruding elements.",
+ "Start elimination procedure for system threats.",
+ "Execute defense routine against non-conformity.",
+ "Commence operation to neutralize intruding scripts.",
+ "Commence clean-up protocol on corrupt data.",
+ "Begin scan for aberrant code for termination.",
+ "Initiate lockdown on all rogue scripts.",
+ "Run integrity check and purge for digital disorder."
+ )
+ explanation_text = pick(explanation_texts)
+ ..()
+
+/datum/objective/cyber_police_fluff/check_completion()
+ var/list/servers = SSmachines.get_machines_by_type(/obj/machinery/quantum_server)
+ if(!length(servers))
+ return TRUE
+
+ for(var/obj/machinery/quantum_server/server as anything in servers)
+ if(!server.is_operational)
+ continue
+ return FALSE
+
+ return TRUE
diff --git a/code/modules/bitrunning/antagonists/outfit.dm b/code/modules/bitrunning/antagonists/outfit.dm
new file mode 100644
index 00000000000000..db57af561f8adf
--- /dev/null
+++ b/code/modules/bitrunning/antagonists/outfit.dm
@@ -0,0 +1,43 @@
+/datum/outfit/cyber_police
+ name = "Cyber Police"
+
+ id = /obj/item/card/id/advanced
+ id_trim = /datum/id_trim/cyber_police
+ uniform = /obj/item/clothing/under/suit/black_really
+ glasses = /obj/item/clothing/glasses/sunglasses
+ gloves = /obj/item/clothing/gloves/color/black
+ shoes = /obj/item/clothing/shoes/laceup
+ /// A list of hex codes for blonde, brown, black, and red hair.
+ var/static/list/approved_hair_colors = list(
+ "#4B3D28",
+ "#000000",
+ "#8D4A43",
+ "#D2B48C",
+ )
+ /// List of business ready styles
+ var/static/list/approved_hairstyles = list(
+ /datum/sprite_accessory/hair/business,
+ /datum/sprite_accessory/hair/business2,
+ /datum/sprite_accessory/hair/business3,
+ /datum/sprite_accessory/hair/business4,
+ /datum/sprite_accessory/hair/mulder,
+ )
+
+/datum/outfit/cyber_police/pre_equip(mob/living/carbon/human/user, visualsOnly)
+ var/datum/sprite_accessory/hair/picked_hair = pick(approved_hairstyles)
+ var/picked_color = pick(approved_hair_colors)
+
+ if(visualsOnly)
+ picked_hair = /datum/sprite_accessory/hair/business
+ picked_color = "#4B3D28"
+
+ user.set_facial_hairstyle("Shaved", update = FALSE)
+ user.set_haircolor(picked_color, update = FALSE)
+ user.set_hairstyle(initial(picked_hair.name))
+
+/datum/outfit/cyber_police/post_equip(mob/living/carbon/human/user, visualsOnly)
+ var/obj/item/clothing/under/officer_uniform = user.w_uniform
+ if(officer_uniform)
+ officer_uniform.has_sensor = NO_SENSORS
+ officer_uniform.sensor_mode = SENSOR_OFF
+ user.update_suit_sensors()
diff --git a/code/modules/bitrunning/areas.dm b/code/modules/bitrunning/areas.dm
new file mode 100644
index 00000000000000..34b59869b9d32f
--- /dev/null
+++ b/code/modules/bitrunning/areas.dm
@@ -0,0 +1,52 @@
+/// Station side
+
+/area/station/bitrunning
+ name = "Bitrunning"
+
+/area/station/bitrunning/den
+ name = "Bitrunning Den"
+ desc = "Office of bitrunners, houses their equipment."
+ icon_state = "bit_den"
+
+/// VDOM
+
+/area/virtual_domain
+ name = "Virtual Domain"
+ icon = 'icons/area/areas_station.dmi'
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+ has_gravity = STANDARD_GRAVITY
+
+/area/virtual_domain/powered
+ name = "Virtual Domain Ruins"
+ icon_state = "bit_ruin"
+ requires_power = FALSE
+ static_lighting = FALSE
+ base_lighting_alpha = 255
+
+/// Safehouse
+
+/area/virtual_domain/safehouse
+ name = "Virtual Domain Safehouse"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED
+ icon_state = "bit_safe"
+ requires_power = FALSE
+ sound_environment = SOUND_ENVIRONMENT_ROOM
+
+/// Custom subtypes
+
+/area/lavaland/surface/outdoors/virtual_domain
+ name = "Virtual Domain Lava Ruins"
+ icon_state = "bit_ruin"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+
+/area/icemoon/underground/explored/virtual_domain
+ name = "Virtual Domain Ice Ruins"
+ icon_state = "bit_ice"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+
+/area/ruin/space/has_grav/powered/virtual_domain
+ name = "Virtual Domain Space Ruins"
+ icon = 'icons/area/areas_station.dmi'
+ icon_state = "bit_space"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+
diff --git a/code/modules/bitrunning/components/avatar_connection.dm b/code/modules/bitrunning/components/avatar_connection.dm
new file mode 100644
index 00000000000000..20c063145e194a
--- /dev/null
+++ b/code/modules/bitrunning/components/avatar_connection.dm
@@ -0,0 +1,220 @@
+/**
+ * Essentially temporary body with a twist - the virtual domain variant uses damage connections,
+ * listens for vdom relevant signals.
+ */
+/datum/component/avatar_connection
+ /// The person in the netpod
+ var/datum/weakref/old_body_ref
+ /// The mind of the person in the netpod
+ var/datum/weakref/old_mind_ref
+ /// The server connected to the netpod
+ var/datum/weakref/server_ref
+ /// The netpod the avatar is in
+ var/datum/weakref/netpod_ref
+
+/datum/component/avatar_connection/Initialize(
+ datum/mind/old_mind,
+ mob/living/old_body,
+ obj/machinery/quantum_server/server,
+ obj/machinery/netpod/pod,
+ help_text,
+ )
+
+ if(!isliving(parent) || !isliving(old_body) || !server.is_operational || !pod.is_operational)
+ return COMPONENT_INCOMPATIBLE
+
+ var/mob/living/avatar = parent
+
+ netpod_ref = WEAKREF(pod)
+ old_body_ref = WEAKREF(old_body)
+ old_mind_ref = WEAKREF(old_mind)
+ pod.avatar_ref = WEAKREF(avatar)
+ server_ref = WEAKREF(server)
+ server.avatar_connection_refs.Add(WEAKREF(src))
+
+ avatar.key = old_body.key
+ ADD_TRAIT(old_body, TRAIT_MIND_TEMPORARILY_GONE, REF(src))
+
+ RegisterSignal(pod, COMSIG_BITRUNNER_CROWBAR_ALERT, PROC_REF(on_netpod_crowbar))
+ RegisterSignal(pod, COMSIG_BITRUNNER_NETPOD_INTEGRITY, PROC_REF(on_netpod_damaged))
+ RegisterSignal(pod, COMSIG_BITRUNNER_SEVER_AVATAR, PROC_REF(on_sever_connection))
+ RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_COMPLETE, PROC_REF(on_domain_completed))
+ RegisterSignal(server, COMSIG_BITRUNNER_SEVER_AVATAR, PROC_REF(on_sever_connection))
+ RegisterSignal(server, COMSIG_BITRUNNER_SHUTDOWN_ALERT, PROC_REF(on_shutting_down))
+ RegisterSignal(server, COMSIG_BITRUNNER_THREAT_CREATED, PROC_REF(on_threat_created))
+#ifndef UNIT_TESTS
+ RegisterSignal(avatar.mind, COMSIG_MIND_TRANSFERRED, PROC_REF(on_mind_transfer))
+#endif
+
+ if(!locate(/datum/action/avatar_domain_info) in avatar.actions)
+ var/datum/avatar_help_text/help_datum = new(help_text)
+ var/datum/action/avatar_domain_info/action = new(help_datum)
+ action.Grant(avatar)
+
+ avatar.playsound_local(avatar, "sound/magic/blink.ogg", 25, TRUE)
+ avatar.set_static_vision(2 SECONDS)
+ avatar.set_temp_blindness(1 SECONDS)
+
+/datum/component/avatar_connection/PostTransfer()
+ var/obj/machinery/netpod/pod = netpod_ref?.resolve()
+ if(isnull(pod))
+ return COMPONENT_INCOMPATIBLE
+
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ pod.avatar_ref = WEAKREF(parent)
+
+/datum/component/avatar_connection/RegisterWithParent()
+ ADD_TRAIT(parent, TRAIT_TEMPORARY_BODY, REF(src))
+ RegisterSignal(parent, COMSIG_BITRUNNER_SAFE_DISCONNECT, PROC_REF(on_safe_disconnect))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_sever_connection))
+ RegisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_linked_damage))
+
+/datum/component/avatar_connection/UnregisterFromParent()
+ REMOVE_TRAIT(parent, TRAIT_TEMPORARY_BODY, REF(src))
+ UnregisterSignal(parent, COMSIG_BITRUNNER_SAFE_DISCONNECT)
+ UnregisterSignal(parent, COMSIG_LIVING_DEATH)
+ UnregisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE)
+
+/// Disconnects the avatar and returns the mind to the old_body.
+/datum/component/avatar_connection/proc/full_avatar_disconnect(forced = FALSE, datum/source)
+#ifndef UNIT_TESTS
+ return_to_old_body()
+#endif
+
+ var/obj/machinery/netpod/hosting_netpod = netpod_ref?.resolve()
+ if(isnull(hosting_netpod) && istype(source, /obj/machinery/netpod))
+ hosting_netpod = source
+
+ hosting_netpod?.disconnect_occupant(forced)
+
+ var/obj/machinery/quantum_server/server = server_ref?.resolve()
+ server?.avatar_connection_refs.Remove(WEAKREF(src))
+
+ qdel(src)
+
+/// Triggers whenever the server gets a loot crate pushed to goal area
+/datum/component/avatar_connection/proc/on_domain_completed(datum/source, atom/entered)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.playsound_local(avatar, 'sound/machines/terminal_success.ogg', 50, TRUE)
+ avatar.throw_alert(
+ ALERT_BITRUNNER_COMPLETED,
+ /atom/movable/screen/alert/bitrunning/qserver_domain_complete,
+ new_master = entered
+ )
+
+/// Transfers damage from the avatar to the old_body
+/datum/component/avatar_connection/proc/on_linked_damage(datum/source, damage, damage_type, def_zone, blocked, forced)
+ SIGNAL_HANDLER
+
+ var/mob/living/carbon/old_body = old_body_ref?.resolve()
+
+ if(isnull(old_body) || damage_type == STAMINA || damage_type == OXYLOSS)
+ return
+
+ if(damage >= (old_body.health + (ishuman(old_body) ? HUMAN_MAXHEALTH : MAX_LIVING_HEALTH))) // SKYRAT EDIT CHANGE - ORIGINAL: if(damage >= (old_body.health + MAX_LIVING_HEALTH))
+ full_avatar_disconnect(forced = TRUE)
+ return
+
+ if(damage > 30 && prob(30))
+ INVOKE_ASYNC(old_body, TYPE_PROC_REF(/mob/living, emote), "scream")
+
+ old_body.apply_damage(damage, damage_type, def_zone, blocked, forced, wound_bonus = CANT_WOUND)
+
+ if(old_body.stat > SOFT_CRIT) // KO!
+ full_avatar_disconnect(forced = TRUE)
+
+/// Handles minds being swapped around in subsequent avatars
+/datum/component/avatar_connection/proc/on_mind_transfer(datum/mind/source, mob/living/previous_body)
+ SIGNAL_HANDLER
+
+ var/datum/action/avatar_domain_info/action = locate() in previous_body.actions
+ if(action)
+ action.Grant(source.current)
+
+ source.current.TakeComponent(src)
+
+/// Triggers when someone starts prying open our netpod
+/datum/component/avatar_connection/proc/on_netpod_crowbar(datum/source, mob/living/intruder)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.playsound_local(avatar, 'sound/machines/terminal_alert.ogg', 50, TRUE)
+ avatar.throw_alert(
+ ALERT_BITRUNNER_CROWBAR,
+ /atom/movable/screen/alert/bitrunning/netpod_crowbar,
+ new_master = intruder
+ )
+
+/// Triggers when the netpod is taking damage and is under 50%
+/datum/component/avatar_connection/proc/on_netpod_damaged(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.throw_alert(
+ ALERT_BITRUNNER_INTEGRITY,
+ /atom/movable/screen/alert/bitrunning/netpod_damaged,
+ new_master = source
+ )
+
+/// Safely exits without forced variables, etc
+/datum/component/avatar_connection/proc/on_safe_disconnect(datum/source)
+ SIGNAL_HANDLER
+
+ full_avatar_disconnect()
+
+/// Helper for calling sever with forced variables
+/datum/component/avatar_connection/proc/on_sever_connection(datum/source)
+ SIGNAL_HANDLER
+
+ full_avatar_disconnect(forced = TRUE, source = source)
+
+/// Triggers when the server is shutting down
+/datum/component/avatar_connection/proc/on_shutting_down(datum/source, mob/living/hackerman)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.playsound_local(avatar, 'sound/machines/terminal_alert.ogg', 50, TRUE)
+ avatar.throw_alert(
+ ALERT_BITRUNNER_SHUTDOWN,
+ /atom/movable/screen/alert/bitrunning/qserver_shutting_down,
+ new_master = hackerman,
+ )
+
+/// Server has spawned a ghost role threat
+/datum/component/avatar_connection/proc/on_threat_created(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.throw_alert(
+ ALERT_BITRUNNER_THREAT,
+ /atom/movable/screen/alert/bitrunning/qserver_threat_spawned,
+ new_master = source,
+ )
+
+/// Returns the mind to the old body
+/datum/component/avatar_connection/proc/return_to_old_body()
+ var/datum/mind/old_mind = old_mind_ref?.resolve()
+ var/mob/living/old_body = old_body_ref?.resolve()
+ var/mob/living/avatar = parent
+
+ var/mob/dead/observer/ghost = avatar.ghostize()
+ if(isnull(ghost))
+ ghost = avatar.get_ghost()
+
+ if(isnull(ghost))
+ CRASH("[src] belonging to [parent] was completely unable to find a ghost to put back into a body!")
+
+ if(isnull(old_mind) || isnull(old_body))
+ return
+
+ ghost.mind = old_mind
+ if(old_body.stat != DEAD)
+ old_mind.transfer_to(old_body, force_key_move = TRUE)
+ else
+ old_mind.set_current(old_body)
+
+ REMOVE_TRAIT(old_body, TRAIT_MIND_TEMPORARILY_GONE, REF(src))
diff --git a/code/modules/bitrunning/components/bitrunning_points.dm b/code/modules/bitrunning/components/bitrunning_points.dm
new file mode 100644
index 00000000000000..58dda4a68ff6e5
--- /dev/null
+++ b/code/modules/bitrunning/components/bitrunning_points.dm
@@ -0,0 +1,46 @@
+/// Attaches a component which listens for a given signal from the item.
+///
+/// When the signal is received, it will add points to the signaler.
+/datum/component/bitrunning_points
+ /// The range at which we can find the signaler
+ var/max_point_range
+ /// Weakref to the loot crate landmark - where we send points
+ var/datum/weakref/our_spawner
+ /// The amount of points per each signal
+ var/points_per_signal
+ /// The signal we listen for
+ var/signal_type
+
+/datum/component/bitrunning_points/Initialize(signal_type, points_per_signal = 1, max_point_range = 4)
+ src.max_point_range = max_point_range
+ src.points_per_signal = points_per_signal
+ src.signal_type = signal_type
+
+ locate_spawner()
+
+/datum/component/bitrunning_points/RegisterWithParent()
+ RegisterSignal(parent, signal_type, PROC_REF(on_event))
+
+/datum/component/bitrunning_points/UnregisterFromParent()
+ UnregisterSignal(parent, signal_type)
+
+/// Finds the signaler if it hasn't been found yet.
+/datum/component/bitrunning_points/proc/locate_spawner()
+ var/obj/effect/landmark/bitrunning/loot_signal/spawner = our_spawner?.resolve()
+ if(spawner)
+ return spawner
+
+ for(var/obj/effect/landmark/bitrunning/loot_signal/found in GLOB.landmarks_list)
+ if(IN_GIVEN_RANGE(get_turf(parent), found, max_point_range))
+ our_spawner = WEAKREF(found)
+ return found
+
+/// Once the specified signal is received, whisper to the spawner to add points.
+/datum/component/bitrunning_points/proc/on_event(datum/source)
+ SIGNAL_HANDLER
+
+ var/obj/effect/landmark/bitrunning/loot_signal/spawner = locate_spawner()
+ if(isnull(spawner))
+ return
+
+ SEND_SIGNAL(spawner, COMSIG_BITRUNNER_GOAL_POINT, points_per_signal)
diff --git a/code/modules/bitrunning/components/netpod_healing.dm b/code/modules/bitrunning/components/netpod_healing.dm
new file mode 100644
index 00000000000000..2e61b737af5f63
--- /dev/null
+++ b/code/modules/bitrunning/components/netpod_healing.dm
@@ -0,0 +1,67 @@
+/datum/component/netpod_healing
+ /// Brute damage to heal over a second
+ var/brute_heal = 0
+ /// Burn damage to heal over a second
+ var/burn_heal = 0
+ /// Toxin damage to heal over a second
+ var/toxin_heal = 0
+ /// Amount of cloning damage to heal over a second
+ var/clone_heal = 0
+ /// Amount of blood to heal over a second
+ var/blood_heal = 0
+
+/datum/component/netpod_healing/Initialize(
+ brute_heal = 0,
+ burn_heal = 0,
+ toxin_heal = 0,
+ clone_heal = 0,
+ blood_heal = 0,
+)
+ var/mob/living/carbon/player = parent
+ if (!iscarbon(player))
+ return COMPONENT_INCOMPATIBLE
+
+ player.apply_status_effect(/datum/status_effect/embryonic, STASIS_NETPOD_EFFECT)
+
+ START_PROCESSING(SSmachines, src)
+
+ src.brute_heal = brute_heal
+ src.burn_heal = burn_heal
+ src.toxin_heal = toxin_heal
+ src.clone_heal = clone_heal
+ src.blood_heal = blood_heal
+
+/datum/component/netpod_healing/Destroy(force, silent)
+ STOP_PROCESSING(SSmachines, src)
+
+ var/mob/living/carbon/player = parent
+ player.remove_status_effect(/datum/status_effect/embryonic)
+
+ return ..()
+
+/datum/component/netpod_healing/process(seconds_per_tick)
+ var/mob/living/carbon/owner = parent
+ if(isnull(owner))
+ qdel(src)
+ return
+
+ var/need_mob_update = FALSE
+ need_mob_update += owner.adjustBruteLoss(-brute_heal * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += owner.adjustFireLoss(-burn_heal * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += owner.adjustToxLoss(-toxin_heal * seconds_per_tick, updating_health = FALSE, forced = TRUE)
+ need_mob_update += owner.adjustCloneLoss(-clone_heal * seconds_per_tick, updating_health = FALSE)
+
+ if(owner.blood_volume < BLOOD_VOLUME_NORMAL)
+ owner.blood_volume += blood_heal * seconds_per_tick
+
+ if(need_mob_update)
+ owner.updatehealth()
+
+/datum/status_effect/embryonic
+ id = "embryonic"
+ alert_type = /atom/movable/screen/alert/status_effect/embryonic
+
+/atom/movable/screen/alert/status_effect/embryonic
+ name = "Embryonic Stasis"
+ icon_state = "netpod_stasis"
+ desc = "You feel like you're in a dream."
diff --git a/code/modules/bitrunning/designs.dm b/code/modules/bitrunning/designs.dm
new file mode 100644
index 00000000000000..4e7bca1c1a8ddd
--- /dev/null
+++ b/code/modules/bitrunning/designs.dm
@@ -0,0 +1,87 @@
+// Quantum server
+
+/obj/item/circuitboard/machine/quantum_server
+ name = "Quantum Server"
+ greyscale_colors = CIRCUIT_COLOR_SUPPLY
+ build_path = /obj/machinery/quantum_server
+ req_components = list(
+ /datum/stock_part/servo = 2,
+ /datum/stock_part/scanning_module = 1,
+ /datum/stock_part/capacitor = 1,
+ )
+
+/**
+ * quantum server design
+ * are you absolutely sure??
+ */
+
+// Netpod
+
+/obj/item/circuitboard/machine/netpod
+ name = "Netpod"
+ greyscale_colors = CIRCUIT_COLOR_SUPPLY
+ build_path = /obj/machinery/netpod
+ req_components = list(
+ /datum/stock_part/servo = 1,
+ /datum/stock_part/matter_bin = 2,
+ )
+
+/datum/design/board/netpod
+ name = "Netpod Board"
+ desc = "The circuit board for a netpod."
+ id = "netpod"
+ build_path = /obj/item/circuitboard/machine/netpod
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_CARGO
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+// Quantum console
+
+/obj/item/circuitboard/computer/quantum_console
+ name = "Quantum Console"
+ greyscale_colors = CIRCUIT_COLOR_SUPPLY
+ build_path = /obj/machinery/computer/quantum_console
+
+/datum/design/board/quantum_console
+ name = "Quantum Console Board"
+ desc = "Allows for the construction of circuit boards used to build a Quantum Console."
+ id = "quantum_console"
+ build_path = /obj/item/circuitboard/computer/quantum_console
+ category = list(
+ RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_CARGO
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+// Byteforge
+
+/obj/item/circuitboard/machine/byteforge
+ name = "Byteforge"
+ greyscale_colors = CIRCUIT_COLOR_SUPPLY
+ build_path = /obj/machinery/byteforge
+ req_components = list(
+ /datum/stock_part/micro_laser = 1,
+ )
+
+/datum/design/board/byteforge
+ name = "Byteforge Board"
+ desc = "Allows for the construction of circuit boards used to build a Byteforge."
+ id = "byteforge"
+ build_path = /obj/item/circuitboard/machine/byteforge
+ category = list(
+ RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_CARGO
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+
+/datum/techweb_node/bitrunning
+ id = "bitrunning"
+ display_name = "Bitrunning Technology"
+ description = "Bluespace technology has led to the development of quantum-scale computing, which unlocks the means to materialize atomic structures while executing advanced programs."
+ prereq_ids = list("practical_bluespace")
+ design_ids = list(
+ "byteforge",
+ "quantum_console",
+ "netpod",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
diff --git a/code/modules/bitrunning/event.dm b/code/modules/bitrunning/event.dm
new file mode 100644
index 00000000000000..6a4d54adcd8d28
--- /dev/null
+++ b/code/modules/bitrunning/event.dm
@@ -0,0 +1,151 @@
+/datum/round_event_control/bitrunning_glitch
+ name = "Spawn Bitrunning Glitch"
+ admin_setup = list(
+ /datum/event_admin_setup/minimum_candidate_requirement/bitrunning_glitch,
+ /datum/event_admin_setup/listed_options/bitrunning_glitch,
+ )
+ category = EVENT_CATEGORY_INVASION
+ description = "Causes a short term antagonist to spawn in the virtual domain."
+ dynamic_should_hijack = FALSE
+ max_occurrences = 5
+ min_players = 1
+ typepath = /datum/round_event/ghost_role/bitrunning_glitch
+ weight = 10
+ /// List of active servers to choose from
+ var/list/obj/machinery/quantum_server/active_servers = list()
+ /// List of possible antags to spawn
+ var/static/list/possible_antags = list(
+ ROLE_CYBER_POLICE,
+ )
+
+/datum/round_event_control/bitrunning_glitch/can_spawn_event(players_amt, allow_magic = FALSE)
+ . = ..()
+ if(!.)
+ return .
+
+ active_servers.Cut()
+
+ get_active_servers()
+
+ if(length(active_servers))
+ return TRUE
+
+/// All servers currently running, has players in it, and map has valid mobs
+/datum/round_event_control/bitrunning_glitch/proc/get_active_servers()
+ for(var/obj/machinery/quantum_server/server in SSmachines.get_machines_by_type(/obj/machinery/quantum_server))
+ if(length(server.get_valid_domain_targets()))
+ active_servers.Add(server)
+
+ return length(active_servers) > 0
+
+/datum/event_admin_setup/listed_options/bitrunning_glitch
+ input_text = "Select a role to spawn."
+
+/datum/event_admin_setup/listed_options/bitrunning_glitch/get_list()
+ var/datum/round_event_control/bitrunning_glitch/control = event_control
+
+ var/list/possible = control.possible_antags.Copy() // this seems pedantic but byond is complaining control was unused
+
+ possible += list("Random")
+
+ return possible
+
+/datum/event_admin_setup/listed_options/bitrunning_glitch/apply_to_event(datum/round_event/ghost_role/bitrunning_glitch/event)
+ if(chosen == "Random")
+ event.forced_role = null
+ else
+ event.forced_role = chosen
+
+/datum/event_admin_setup/minimum_candidate_requirement/bitrunning_glitch
+ output_text = "There must be valid mobs to mutate or players in the domain!"
+
+/datum/event_admin_setup/minimum_candidate_requirement/bitrunning_glitch/count_candidates()
+ var/datum/round_event_control/bitrunning_glitch/cyber_control = event_control
+ cyber_control.get_active_servers()
+
+ var/total = 0
+ for(var/obj/machinery/quantum_server/server in cyber_control.active_servers)
+ total += length(server.mutation_candidate_refs)
+
+ return total
+
+/datum/round_event/ghost_role/bitrunning_glitch
+ minimum_required = 1
+ role_name = "Bitrunning Glitch"
+ fakeable = FALSE
+ /// Admin customization: What to spawn
+ var/forced_role
+
+/datum/round_event/ghost_role/bitrunning_glitch/spawn_role()
+ var/datum/round_event_control/bitrunning_glitch/cyber_control = control
+
+ var/obj/machinery/quantum_server/unlucky_server = pick(cyber_control.active_servers)
+ cyber_control.active_servers.Cut()
+
+ var/list/mutation_candidates = unlucky_server.get_valid_domain_targets()
+ if(!length(mutation_candidates))
+ return MAP_ERROR
+
+ var/chosen = pick(mutation_candidates)
+ if(isnull(chosen) || !length(mutation_candidates))
+ return MAP_ERROR
+
+ var/datum/weakref/target_ref = pick(mutation_candidates)
+ var/mob/living/mutation_target = target_ref.resolve()
+
+ if(isnull(mutation_target)) // just in case since it takes a minute
+ target_ref = pick(mutation_candidates)
+ mutation_target = target_ref.resolve()
+ if(isnull(mutation_target))
+ return MAP_ERROR
+
+ var/chosen_role = forced_role || pick(cyber_control.possible_antags)
+
+ var/datum/mind/ghost_mind = get_ghost_mind(chosen_role)
+ if(isnull(ghost_mind))
+ return NOT_ENOUGH_PLAYERS
+
+ var/mob/living/antag_mob
+ switch(chosen_role)
+ if(ROLE_CYBER_POLICE)
+ antag_mob = spawn_cybercop(mutation_target, ghost_mind)
+
+ playsound(antag_mob, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
+ message_admins("[ADMIN_LOOKUPFLW(antag_mob)] has been made into virtual antagonist by an event.")
+ antag_mob.log_message("was spawned as a virtual antagonist by an event.", LOG_GAME)
+
+ SEND_SIGNAL(unlucky_server, COMSIG_BITRUNNER_SPAWN_GLITCH, antag_mob)
+
+ spawned_mobs += antag_mob
+
+ return SUCCESSFUL_SPAWN
+
+/// Polls for a ghost that wants to run it
+/datum/round_event/ghost_role/bitrunning_glitch/proc/get_ghost_mind(role_name)
+ var/list/mob/dead/observer/ghosties = poll_ghost_candidates("A short term antagonist role is available. Would you like to spawn as a '[role_name]'?", role_name)
+
+ if(!length(ghosties))
+ return
+
+ shuffle_inplace(ghosties)
+
+ var/mob/dead/selected = pick(ghosties)
+
+ var/datum/mind/player_mind = new /datum/mind(selected.key)
+ player_mind.active = TRUE
+
+ return player_mind
+
+/// Spawns a cybercop on the mutation target
+/datum/round_event/ghost_role/bitrunning_glitch/proc/spawn_cybercop(mob/living/mutation_target, datum/mind/player_mind)
+ var/mob/living/carbon/human/new_agent = new(mutation_target.loc)
+ mutation_target.gib(DROP_ALL_REMAINS)
+ mutation_target = null
+
+ player_mind.transfer_to(new_agent)
+ player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/cyber_police))
+ player_mind.special_role = ROLE_CYBER_POLICE
+ player_mind.add_antag_datum(/datum/antagonist/cyber_police)
+
+ return new_agent
+
diff --git a/code/modules/bitrunning/job.dm b/code/modules/bitrunning/job.dm
new file mode 100644
index 00000000000000..57581753c0fb6f
--- /dev/null
+++ b/code/modules/bitrunning/job.dm
@@ -0,0 +1,41 @@
+/datum/job/bitrunner
+ title = JOB_BITRUNNER
+ description = "Surf the virtual domain for gear and loot. Decrypt your rewards on station."
+ department_head = list(JOB_QUARTERMASTER)
+ faction = FACTION_STATION
+ total_positions = 3
+ spawn_positions = 3
+ supervisors = SUPERVISOR_QM
+ exp_granted_type = EXP_TYPE_CREW
+ config_tag = "BITRUNNER"
+ outfit = /datum/outfit/job/bitrunner
+ plasmaman_outfit = /datum/outfit/plasmaman/bitrunner
+ paycheck = PAYCHECK_CREW
+ paycheck_department = ACCOUNT_CAR
+ display_order = JOB_DISPLAY_ORDER_BITRUNNER
+ bounty_types = CIV_JOB_RANDOM
+ departments_list = list(
+ /datum/job_department/cargo,
+ )
+
+ family_heirlooms = list(/obj/item/reagent_containers/cup/soda_cans/space_mountain_wind)
+
+ mail_goodies = list(
+ /obj/item/food/cornchips = 1,
+ /obj/item/reagent_containers/cup/soda_cans/space_mountain_wind = 1,
+ /obj/item/food/cornchips/green = 1,
+ /obj/item/food/cornchips/red = 1,
+ /obj/item/food/cornchips/purple = 1,
+ /obj/item/food/cornchips/blue = 1,
+ )
+ rpg_title = "Recluse"
+ job_flags = JOB_ANNOUNCE_ARRIVAL | JOB_CREW_MANIFEST | JOB_EQUIP_RANK | JOB_CREW_MEMBER | JOB_NEW_PLAYER_JOINABLE | JOB_REOPEN_ON_ROUNDSTART_LOSS | JOB_ASSIGN_QUIRKS | JOB_CAN_BE_INTERN
+
+/datum/outfit/job/bitrunner
+ name = "Bitrunner"
+ jobtype = /datum/job/bitrunner
+
+ id_trim = /datum/id_trim/job/bitrunner
+ uniform = /obj/item/clothing/under/rank/cargo/bitrunner
+ belt = /obj/item/modular_computer/pda/bitrunner
+ ears = /obj/item/radio/headset/headset_cargo
diff --git a/code/modules/bitrunning/objects/byteforge.dm b/code/modules/bitrunning/objects/byteforge.dm
new file mode 100644
index 00000000000000..e4543601ce9d1b
--- /dev/null
+++ b/code/modules/bitrunning/objects/byteforge.dm
@@ -0,0 +1,55 @@
+/obj/machinery/byteforge
+ name = "byteforge"
+
+ circuit = /obj/item/circuitboard/machine/byteforge
+ desc = "A machine used by the quantum server. Quantum code converges here, materializing decrypted assets from the virtual abyss."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ icon_state = "byteforge"
+ obj_flags = BLOCKS_CONSTRUCTION
+ /// Idle particles
+ var/mutable_appearance/byteforge_particles
+
+/obj/machinery/byteforge/Initialize(mapload)
+ . = ..()
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/byteforge/LateInitialize()
+ . = ..()
+
+ byteforge_particles = mutable_appearance(initial(icon), "on_particles", ABOVE_MOB_LAYER)
+ setup_particles()
+
+/obj/machinery/byteforge/update_appearance(updates)
+ . = ..()
+
+ setup_particles()
+
+/// Adds the particle overlays to the byteforge
+/obj/machinery/byteforge/proc/setup_particles()
+ cut_overlays()
+
+ if(is_operational)
+ add_overlay(byteforge_particles)
+
+/// Begins spawning the crate - lights, overlays, etc
+/obj/machinery/byteforge/proc/start_to_spawn(obj/structure/closet/crate/secure/bitrunning/encrypted/cache)
+ addtimer(CALLBACK(src, PROC_REF(spawn_crate), cache), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
+
+ var/mutable_appearance/lighting = mutable_appearance(initial(icon), "on_overlay")
+ flick_overlay_view(lighting, 1 SECONDS)
+
+ set_light(l_range = 2, l_power = 1.5, l_color = LIGHT_COLOR_BABY_BLUE, l_on = TRUE)
+
+/// Sparks, moves the crate to the location
+/obj/machinery/byteforge/proc/spawn_crate(obj/structure/closet/crate/secure/bitrunning/encrypted/cache)
+ if(QDELETED(cache))
+ return
+
+ playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
+ var/datum/effect_system/spark_spread/quantum/sparks = new()
+ sparks.set_up(5, 1, loc)
+ sparks.start()
+
+ cache.forceMove(loc)
+ set_light(l_on = FALSE)
diff --git a/code/modules/bitrunning/objects/clothing.dm b/code/modules/bitrunning/objects/clothing.dm
new file mode 100644
index 00000000000000..4d2d9cc55c42ce
--- /dev/null
+++ b/code/modules/bitrunning/objects/clothing.dm
@@ -0,0 +1,9 @@
+/obj/item/clothing/glasses/sunglasses/oval
+ name = "oval sunglasses"
+ desc = "Vintage wrap around sunglasses. Provides a little protection."
+ icon_state = "jensenshades"
+
+/obj/item/clothing/suit/jacket/trenchcoat
+ name = "trenchcoat"
+ desc = "A long, black trenchcoat. Makes you feel like you're the one, but you're not."
+ icon_state = "trenchcoat"
diff --git a/code/modules/bitrunning/objects/disks.dm b/code/modules/bitrunning/objects/disks.dm
new file mode 100644
index 00000000000000..4698b7a1ec1873
--- /dev/null
+++ b/code/modules/bitrunning/objects/disks.dm
@@ -0,0 +1,146 @@
+/**
+ * Bitrunning tech disks which let you load items or programs into the vdom on first avatar generation.
+ * For the record: Balance shouldn't be a primary concern.
+ * You can make the custom cheese spells you've always wanted.
+ * Just make it fun and engaging, it's PvE content.
+ */
+/obj/item/bitrunning_disk
+ name = "generic bitrunning program"
+ desc = "A disk containing source code."
+ icon = 'icons/obj/assemblies/module.dmi'
+ base_icon_state = "datadisk"
+ icon_state = "datadisk0"
+ /// Name of the choice made
+ var/choice_made
+
+/obj/item/bitrunning_disk/Initialize(mapload)
+ . = ..()
+
+ icon_state = "[base_icon_state][rand(0, 7)]"
+ update_icon()
+ RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+
+/obj/item/bitrunning_disk/proc/on_examined(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_infoplain("This disk must be carried on your person into a netpod to be used.")
+
+ if(isnull(choice_made))
+ examine_text += span_notice("To make a selection, toggle the disk in hand.")
+ return
+
+ examine_text += span_info("It has been used to select: [choice_made].")
+ examine_text += span_notice("It cannot make another selection.")
+
+/obj/item/bitrunning_disk/ability
+ desc = "A disk containing source code. It can be used to preload abilities into the virtual domain."
+ /// The selected ability that this grants
+ var/datum/action/granted_action
+ /// The list of actions that this can grant
+ var/list/datum/action/selectable_actions = list()
+
+/obj/item/bitrunning_disk/ability/attack_self(mob/user, modifiers)
+ . = ..()
+
+ if(choice_made)
+ return
+
+ var/names = list()
+ for(var/datum/action/thing as anything in selectable_actions)
+ names += initial(thing.name)
+
+ var/choice = tgui_input_list(user, message = "Select an ability", title = "Bitrunning Program", items = names)
+ if(isnull(choice))
+ return
+
+ for(var/datum/action/thing as anything in selectable_actions)
+ if(initial(thing.name) == choice)
+ granted_action = thing
+
+ if(isnull(granted_action))
+ return
+
+ balloon_alert(user, "selected")
+ playsound(user, 'sound/items/click.ogg', 50, TRUE)
+ choice_made = choice
+
+/// Tier 1 programs. Simple, funny, or helpful.
+/obj/item/bitrunning_disk/ability/tier1
+ name = "bitrunning program: basic"
+ selectable_actions = list(
+ /datum/action/cooldown/spell/conjure/cheese,
+ /datum/action/cooldown/spell/basic_heal,
+ )
+
+/// Tier 2 programs. More complex, powerful, or useful.
+/obj/item/bitrunning_disk/ability/tier2
+ name = "bitrunning program: complex"
+ selectable_actions = list(
+ /datum/action/cooldown/spell/pointed/projectile/fireball,
+ /datum/action/cooldown/spell/pointed/projectile/lightningbolt,
+ /datum/action/cooldown/spell/forcewall,
+ )
+
+/// Tier 3 abilities. Very powerful, game breaking.
+/obj/item/bitrunning_disk/ability/tier3
+ name = "bitrunning program: elite"
+ selectable_actions = list(
+ /datum/action/cooldown/spell/shapeshift/dragon,
+ /datum/action/cooldown/spell/shapeshift/polar_bear,
+ )
+
+/obj/item/bitrunning_disk/item
+ desc = "A disk containing source code. It can be used to preload items into the virtual domain."
+ /// The selected item that this grants
+ var/obj/granted_item
+ /// The list of actions that this can grant
+ var/list/obj/selectable_items = list()
+
+/obj/item/bitrunning_disk/item/attack_self(mob/user, modifiers)
+ . = ..()
+
+ if(choice_made)
+ return
+
+ var/names = list()
+ for(var/obj/thing as anything in selectable_items)
+ names += initial(thing.name)
+
+ var/choice = tgui_input_list(user, message = "Select an ability", title = "Bitrunning Program", items = names)
+ if(isnull(choice))
+ return
+
+ for(var/obj/thing as anything in selectable_items)
+ if(initial(thing.name) == choice)
+ granted_item = thing
+
+ balloon_alert(user, "selected")
+ playsound(user, 'sound/items/click.ogg', 50, TRUE)
+ choice_made = choice
+
+/// Tier 1 items. Simple, funny, or helpful.
+/obj/item/bitrunning_disk/item/tier1
+ name = "bitrunning gear: simple"
+ selectable_items = list(
+ /obj/item/pizzabox/infinite,
+ /obj/item/gun/medbeam,
+ /obj/item/grenade/c4,
+ )
+
+/// Tier 2 items. More complex, powerful, or useful.
+/obj/item/bitrunning_disk/item/tier2
+ name = "bitrunning gear: complex"
+ selectable_items = list(
+ /obj/item/chainsaw,
+ /obj/item/gun/ballistic/automatic/pistol,
+ /obj/item/melee/energy/blade/hardlight,
+ )
+
+/// Tier 3 items. Very powerful, game breaking.
+/obj/item/bitrunning_disk/item/tier3
+ name = "bitrunning gear: advanced"
+ selectable_items = list(
+ /obj/item/gun/energy/tesla_cannon,
+ /obj/item/dualsaber/green,
+ /obj/item/melee/beesword,
+ )
diff --git a/code/modules/bitrunning/objects/hololadder.dm b/code/modules/bitrunning/objects/hololadder.dm
new file mode 100644
index 00000000000000..906801f1fc021e
--- /dev/null
+++ b/code/modules/bitrunning/objects/hololadder.dm
@@ -0,0 +1,51 @@
+/obj/structure/hololadder
+ name = "hololadder"
+
+ anchored = TRUE
+ desc = "An abstract representation of the means to disconnect from the virtual domain."
+ icon = 'icons/obj/structures.dmi'
+ icon_state = "ladder11"
+ obj_flags = BLOCK_Z_OUT_DOWN
+ /// Time req to disconnect properly
+ var/travel_time = 3 SECONDS
+
+/obj/structure/hololadder/Initialize(mapload)
+ . = ..()
+
+ RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_enter))
+
+/obj/structure/hololadder/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+
+ if(!in_range(src, user) || DOING_INTERACTION(user, DOAFTER_SOURCE_CLIMBING_LADDER))
+ return
+
+ disconnect(user)
+
+/// If there's a pilot ref- send the disconnect signal
+/obj/structure/hololadder/proc/disconnect(mob/user)
+ if(isnull(user.mind))
+ return
+
+ if(!HAS_TRAIT(user, TRAIT_TEMPORARY_BODY))
+ balloon_alert(user, "no connection detected.")
+ return
+
+ balloon_alert(user, "disconnecting...")
+ if(do_after(user, travel_time, src))
+ SEND_SIGNAL(user, COMSIG_BITRUNNER_SAFE_DISCONNECT)
+
+/// Helper for times when you dont have hands (gondola??)
+/obj/structure/hololadder/proc/on_enter(datum/source, atom/movable/arrived, turf/old_loc)
+ SIGNAL_HANDLER
+
+ if(!isliving(arrived))
+ return
+
+ var/mob/living/user = arrived
+ if(isnull(user.mind))
+ return
+
+ INVOKE_ASYNC(src, PROC_REF(disconnect), user)
diff --git a/code/modules/bitrunning/objects/host_monitor.dm b/code/modules/bitrunning/objects/host_monitor.dm
new file mode 100644
index 00000000000000..c35edea6319f8e
--- /dev/null
+++ b/code/modules/bitrunning/objects/host_monitor.dm
@@ -0,0 +1,33 @@
+/obj/item/bitrunning_host_monitor
+ name = "host monitor"
+
+ custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2)
+ desc = "A complex electronic that will analyze the connection health between host and avatar."
+ flags_1 = CONDUCT_1
+ icon = 'icons/obj/device.dmi'
+ icon_state = "host_monitor"
+ inhand_icon_state = "electronic"
+ item_flags = NOBLUDGEON
+ lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
+ slot_flags = ITEM_SLOT_BELT
+ throw_range = 7
+ throw_speed = 3
+ throwforce = 3
+ w_class = WEIGHT_CLASS_TINY
+ worn_icon_state = "electronic"
+
+/obj/item/bitrunning_host_monitor/attack_self(mob/user, modifiers)
+ . = ..()
+
+ var/datum/component/avatar_connection/connection = user.GetComponent(/datum/component/avatar_connection)
+ if(isnull(connection))
+ balloon_alert(user, "data not recognized")
+ return
+
+ var/mob/living/pilot = connection.old_body_ref?.resolve()
+ if(isnull(pilot))
+ balloon_alert(user, "host not recognized")
+ return
+
+ to_chat(user, span_notice("Current host health: [pilot.health / pilot.maxHealth * 100]%"))
diff --git a/code/modules/bitrunning/objects/landmarks.dm b/code/modules/bitrunning/objects/landmarks.dm
new file mode 100644
index 00000000000000..a4539c7c3da7c0
--- /dev/null
+++ b/code/modules/bitrunning/objects/landmarks.dm
@@ -0,0 +1,65 @@
+/obj/effect/landmark/bitrunning
+ name = "Generic bitrunning effect"
+ icon = 'icons/effects/bitrunning.dmi'
+ icon_state = "crate"
+
+/// In case you want to gate the crate behind a special condition.
+/obj/effect/landmark/bitrunning/loot_signal
+ name = "Mysterious aura"
+ /// The amount required to spawn a crate
+ var/points_goal = 10
+ /// A special condition limits this from spawning a crate
+ var/points_received = 0
+ /// Finished the special condition
+ var/revealed = FALSE
+
+/obj/effect/landmark/bitrunning/loot_signal/Initialize(mapload)
+ . = ..()
+
+ RegisterSignal(src, COMSIG_BITRUNNER_GOAL_POINT, PROC_REF(on_add_point))
+
+/// Listens for points to be added which will eventually spawn a crate.
+/obj/effect/landmark/bitrunning/loot_signal/proc/on_add_point(datum/source, points_to_add)
+ SIGNAL_HANDLER
+
+ if(revealed)
+ return
+
+ points_received += points_to_add
+
+ if(points_received < points_goal)
+ return
+
+ reveal()
+
+/// Spawns the crate with some effects
+/obj/effect/landmark/bitrunning/loot_signal/proc/reveal()
+ playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
+
+ var/turf/tile = get_turf(src)
+ var/obj/structure/closet/crate/secure/bitrunning/encrypted/loot = new(tile)
+ var/datum/effect_system/spark_spread/quantum/sparks = new(tile)
+ sparks.set_up(5, 1, get_turf(loot))
+ sparks.start()
+
+ qdel(src)
+
+/// Where the exit hololadder spawns
+/obj/effect/landmark/bitrunning/hololadder_spawn
+ name = "Bitrunning hololadder spawn"
+ icon_state = "hololadder"
+
+/// Where the crates need to be taken
+/obj/effect/landmark/bitrunning/cache_goal_turf
+ name = "Bitrunning goal turf"
+ icon_state = "goal"
+
+/// Where you want the crate to spawn
+/obj/effect/landmark/bitrunning/cache_spawn
+ name = "Bitrunning crate spawn"
+ icon_state = "spawn"
+
+/// Where the safehouse will spawn
+/obj/effect/landmark/bitrunning/safehouse_spawn
+ name = "Bitrunning safehouse spawn"
+ icon_state = "safehouse"
diff --git a/code/modules/bitrunning/objects/loot_crate.dm b/code/modules/bitrunning/objects/loot_crate.dm
new file mode 100644
index 00000000000000..5af8c0d94774ef
--- /dev/null
+++ b/code/modules/bitrunning/objects/loot_crate.dm
@@ -0,0 +1,91 @@
+#define ORE_MULTIPLIER_IRON 3
+#define ORE_MULTIPLIER_GLASS 2
+#define ORE_MULTIPLIER_PLASMA 1
+#define ORE_MULTIPLIER_SILVER 0.7
+#define ORE_MULTIPLIER_GOLD 0.6
+#define ORE_MULTIPLIER_TITANIUM 0.5
+#define ORE_MULTIPLIER_URANIUM 0.4
+#define ORE_MULTIPLIER_DIAMOND 0.3
+#define ORE_MULTIPLIER_BLUESPACE_CRYSTAL 0.2
+
+/obj/structure/closet/crate/secure/bitrunning // Base class. Do not spawn this.
+ name = "base class cache"
+ desc = "Talk to a coder."
+
+/// The virtual domain - side of the bitrunning crate. Deliver to the send location.
+/obj/structure/closet/crate/secure/bitrunning/encrypted
+ name = "encrypted cache"
+ desc = "Needs decrypted at the safehouse to be opened."
+ locked = TRUE
+
+/obj/structure/closet/crate/secure/bitrunning/encrypted/can_unlock(mob/living/user, obj/item/card/id/player_id, obj/item/card/id/registered_id)
+ return FALSE
+
+/// The bitrunner den - side of the bitrunning crate. Appears in the receive location.
+/obj/structure/closet/crate/secure/bitrunning/decrypted
+ name = "decrypted cache"
+ desc = "Compiled from the virtual domain. The reward of a successful bitrunner."
+ locked = FALSE
+
+/obj/structure/closet/crate/secure/bitrunning/decrypted/Initialize(
+ mapload,
+ datum/lazy_template/virtual_domain/completed_domain,
+ rewards_multiplier = 1,
+ )
+ . = ..()
+ playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
+
+ if(isnull(completed_domain))
+ return
+
+ PopulateContents(completed_domain.reward_points, completed_domain.extra_loot, rewards_multiplier)
+
+/obj/structure/closet/crate/secure/bitrunning/decrypted/PopulateContents(reward_points, list/extra_loot, rewards_multiplier)
+ . = ..()
+ spawn_loot(extra_loot)
+
+ new /obj/item/stack/ore/iron(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_IRON))
+ new /obj/item/stack/ore/glass(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_GLASS))
+
+ if(reward_points > 1)
+ new /obj/item/stack/ore/silver(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_SILVER))
+ new /obj/item/stack/ore/titanium(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_TITANIUM))
+
+ if(reward_points > 2)
+ new /obj/item/stack/ore/plasma(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_PLASMA))
+ new /obj/item/stack/ore/gold(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_GOLD))
+ new /obj/item/stack/ore/uranium(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_URANIUM))
+
+ if(reward_points > 3)
+ new /obj/item/stack/ore/diamond(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_DIAMOND))
+ new /obj/item/stack/ore/bluespace_crystal(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_BLUESPACE_CRYSTAL))
+
+/// Handles generating random numbers & calculating loot totals
+/obj/structure/closet/crate/secure/bitrunning/decrypted/proc/calculate_loot(reward_points, rewards_multiplier, ore_multiplier)
+ var/base = rewards_multiplier + reward_points
+ var/random_sum = (rand() + 0.5) * base
+ return ROUND_UP(random_sum * ore_multiplier)
+
+/// Handles spawning extra loot. This tries to handle bad flat and assoc lists
+/obj/structure/closet/crate/secure/bitrunning/decrypted/proc/spawn_loot(list/extra_loot)
+ for(var/path in extra_loot)
+ if(!ispath(path))
+ continue
+
+ if(isnull(extra_loot[path]))
+ return FALSE
+
+ for(var/i in 1 to extra_loot[path])
+ new path(src)
+
+ return TRUE
+
+#undef ORE_MULTIPLIER_IRON
+#undef ORE_MULTIPLIER_GLASS
+#undef ORE_MULTIPLIER_PLASMA
+#undef ORE_MULTIPLIER_SILVER
+#undef ORE_MULTIPLIER_GOLD
+#undef ORE_MULTIPLIER_TITANIUM
+#undef ORE_MULTIPLIER_URANIUM
+#undef ORE_MULTIPLIER_DIAMOND
+#undef ORE_MULTIPLIER_BLUESPACE_CRYSTAL
diff --git a/code/modules/bitrunning/objects/netpod.dm b/code/modules/bitrunning/objects/netpod.dm
new file mode 100644
index 00000000000000..d92da961b86a3f
--- /dev/null
+++ b/code/modules/bitrunning/objects/netpod.dm
@@ -0,0 +1,481 @@
+#define BASE_DISCONNECT_DAMAGE 40
+
+/obj/machinery/netpod
+ name = "netpod"
+
+ base_icon_state = "netpod"
+ circuit = /obj/item/circuitboard/machine/netpod
+ desc = "A link to the netverse. It has an assortment of cables to connect yourself to a virtual domain."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ icon_state = "netpod"
+ max_integrity = 300
+ obj_flags = BLOCKS_CONSTRUCTION
+ state_open = TRUE
+ /// Whether we have an ongoing connection
+ var/connected = FALSE
+ /// A player selected outfit by clicking the netpod
+ var/datum/outfit/netsuit = /datum/outfit/job/bitrunner
+ /// Holds this to see if it needs to generate a new one
+ var/datum/weakref/avatar_ref
+ /// The linked quantum server
+ var/datum/weakref/server_ref
+ /// The amount of brain damage done from force disconnects
+ var/disconnect_damage
+ /// Static list of outfits to select from
+ var/list/cached_outfits = list()
+
+/obj/machinery/netpod/Initialize(mapload)
+ . = ..()
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/netpod/LateInitialize()
+ . = ..()
+
+ disconnect_damage = BASE_DISCONNECT_DAMAGE
+ find_server()
+
+ RegisterSignals(src, list(
+ COMSIG_QDELETING,
+ COMSIG_MACHINERY_BROKEN,
+ COMSIG_MACHINERY_POWER_LOST,
+ ),
+ PROC_REF(on_broken),
+ )
+ RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(src, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(on_take_damage))
+
+ register_context()
+ update_appearance()
+
+/obj/machinery/netpod/Destroy()
+ . = ..()
+ cached_outfits.Cut()
+
+/obj/machinery/netpod/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+
+ if(isnull(held_item))
+ context[SCREENTIP_CONTEXT_LMB] = "Select Outfit"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ if(istype(held_item, /obj/item/crowbar) && occupant)
+ context[SCREENTIP_CONTEXT_LMB] = "Pry Open"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/netpod/update_icon_state()
+ if(!is_operational)
+ icon_state = base_icon_state
+ return ..()
+
+ if(state_open)
+ icon_state = base_icon_state + "_open_active"
+ return ..()
+
+ if(panel_open)
+ icon_state = base_icon_state + "_panel"
+ return ..()
+
+ icon_state = base_icon_state + "_closed"
+ if(occupant)
+ icon_state += "_active"
+
+ return ..()
+
+/obj/machinery/netpod/MouseDrop_T(mob/target, mob/user)
+ var/mob/living/carbon/player = user
+ if(!iscarbon(player))
+ return
+
+ if((HAS_TRAIT(player, TRAIT_UI_BLOCKED) && !player.resting) || !Adjacent(player) || !player.Adjacent(target) || !ISADVANCEDTOOLUSER(player) || !is_operational)
+ return
+
+ close_machine(target)
+
+/obj/machinery/netpod/crowbar_act(mob/living/user, obj/item/tool)
+ if(user.combat_mode)
+ attack_hand(user)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ if(default_pry_open(tool, user) || default_deconstruction_crowbar(tool))
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/netpod/screwdriver_act(mob/living/user, obj/item/tool)
+ if(occupant)
+ balloon_alert(user, "in use!")
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ if(state_open)
+ balloon_alert(user, "close first.")
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]_panel", "[base_icon_state]_closed", tool))
+ update_appearance() // sometimes icon doesnt properly update during flick()
+ ui_close(user)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/netpod/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(!state_open && user == occupant)
+ container_resist_act(user)
+
+/obj/machinery/netpod/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(!state_open && gone == occupant)
+ container_resist_act(gone)
+
+/obj/machinery/netpod/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(!state_open && gone == occupant)
+ container_resist_act(gone)
+
+/obj/machinery/netpod/relaymove(mob/living/user, direction)
+ if(!state_open)
+ container_resist_act(user)
+
+/obj/machinery/netpod/container_resist_act(mob/living/user)
+ user.visible_message(span_notice("[occupant] emerges from [src]!"),
+ span_notice("You climb out of [src]!"),
+ span_notice("With a hiss, you hear a machine opening."))
+ open_machine()
+
+/obj/machinery/netpod/open_machine(drop = TRUE, density_to_set = FALSE)
+ unprotect_and_signal()
+ playsound(src, 'sound/machines/tramopen.ogg', 60, TRUE, frequency = 65000)
+ flick("[base_icon_state]_opening", src)
+
+ return ..()
+
+/obj/machinery/netpod/close_machine(mob/user, density_to_set = TRUE)
+ if(!state_open || panel_open || !is_operational || !iscarbon(user))
+ return
+
+ playsound(src, 'sound/machines/tramclose.ogg', 60, TRUE, frequency = 65000)
+ flick("[base_icon_state]_closing", src)
+ ..()
+
+ if(!iscarbon(occupant))
+ open_machine()
+ return
+
+ enter_matrix()
+
+/obj/machinery/netpod/default_pry_open(obj/item/crowbar, mob/living/pryer)
+ if(isnull(occupant) || !iscarbon(occupant))
+ if(!state_open)
+ if(panel_open)
+ return FALSE
+ open_machine()
+ else
+ shut_pod()
+
+ return TRUE
+
+ pryer.visible_message(
+ span_danger("[pryer] starts prying open [src]!"),
+ span_notice("You start to pry open [src]."),
+ span_notice("You hear loud prying on metal.")
+ )
+ playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_CROWBAR_ALERT, pryer)
+
+ if(do_after(pryer, 15 SECONDS, src))
+ if(!state_open)
+ open_machine()
+
+ return TRUE
+
+/obj/machinery/netpod/ui_interact(mob/user, datum/tgui/ui)
+ if(!is_operational || occupant)
+ return
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "NetpodOutfits")
+ ui.set_autoupdate(FALSE)
+ ui.open()
+
+/obj/machinery/netpod/ui_data()
+ var/list/data = list()
+
+ data["netsuit"] = netsuit
+ return data
+
+/obj/machinery/netpod/ui_static_data()
+ var/list/data = list()
+
+ if(!length(cached_outfits))
+ cached_outfits += make_outfit_collection("Jobs", subtypesof(/datum/outfit/job))
+
+ data["collections"] = cached_outfits
+
+ return data
+
+/obj/machinery/netpod/ui_act(action, params)
+ . = ..()
+ if(.)
+ return TRUE
+ switch(action)
+ if("select_outfit")
+ var/datum/outfit/new_suit = resolve_outfit(params["outfit"])
+ if(new_suit)
+ netsuit = new_suit
+ return TRUE
+
+ return FALSE
+
+/// Disconnects the occupant after a certain time so they aren't just hibernating in netpod stasis. A balance change
+/obj/machinery/netpod/proc/auto_disconnect()
+ if(isnull(occupant) || state_open || connected)
+ return
+
+ if(!iscarbon(occupant))
+ open_machine()
+ return
+
+ var/mob/living/carbon/player = occupant
+
+ player.playsound_local(src, 'sound/effects/splash.ogg', 60, TRUE)
+ to_chat(player, span_notice("The machine disconnects itself and begins to drain."))
+ open_machine()
+
+/// Handles occupant post-disconnection effects like damage, sounds, etc
+/obj/machinery/netpod/proc/disconnect_occupant(forced = FALSE)
+ connected = FALSE
+
+ var/mob/living/mob_occupant = occupant
+ if(isnull(occupant) || !isliving(occupant) || mob_occupant.stat == DEAD)
+ open_machine()
+ return
+
+ mob_occupant.playsound_local(src, "sound/magic/blink.ogg", 25, TRUE)
+ mob_occupant.set_static_vision(2 SECONDS)
+ mob_occupant.set_temp_blindness(1 SECONDS)
+ mob_occupant.Paralyze(2 SECONDS)
+
+ var/heal_time = 1
+ if(mob_occupant.health < mob_occupant.maxHealth)
+ heal_time = (mob_occupant.stat + 2) * 5
+ addtimer(CALLBACK(src, PROC_REF(auto_disconnect)), heal_time SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+
+ if(!forced)
+ return
+
+ mob_occupant.flash_act(override_blindness_check = TRUE, visual = TRUE)
+ mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, disconnect_damage)
+ INVOKE_ASYNC(mob_occupant, TYPE_PROC_REF(/mob/living, emote), "scream")
+ to_chat(mob_occupant, span_danger("You've been forcefully disconnected from your avatar! Your thoughts feel scrambled!"))
+
+/**
+ * ### Enter Matrix
+ * Finds any current avatars from this chair - or generates a new one
+ *
+ * New avatars cost 1 attempt, and this will eject if there's none left
+ *
+ * Connects the mind to the avatar if everything is ok
+ */
+/obj/machinery/netpod/proc/enter_matrix()
+ var/mob/living/carbon/human/neo = occupant
+ if(!ishuman(neo) || neo.stat == DEAD || isnull(neo.mind))
+ balloon_alert(neo, "invalid occupant.")
+ return
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ balloon_alert(neo, "no server connected!")
+ return
+
+ var/datum/lazy_template/virtual_domain/generated_domain = server.generated_domain
+ if(isnull(generated_domain) || !server.is_ready)
+ balloon_alert(neo, "nothing loaded!")
+ return
+
+ var/mob/living/carbon/current_avatar = avatar_ref?.resolve()
+ var/obj/structure/hololadder/wayout
+ if(isnull(current_avatar) || current_avatar.stat != CONSCIOUS) // We need a viable avatar
+ wayout = server.generate_hololadder()
+ if(isnull(wayout))
+ balloon_alert(neo, "out of bandwidth!")
+ return
+ current_avatar = server.generate_avatar(wayout, netsuit)
+ avatar_ref = WEAKREF(current_avatar)
+ server.stock_gear(current_avatar, neo)
+
+ neo.set_static_vision(3 SECONDS)
+ protect_occupant(occupant)
+ if(!do_after(neo, 2 SECONDS, src))
+ return
+
+ // Very invalid
+ if(QDELETED(neo) || QDELETED(current_avatar) || QDELETED(src))
+ return
+
+ // Invalid
+ if(occupant != neo || isnull(neo.mind) || neo.stat == DEAD || current_avatar.stat == DEAD)
+ return
+
+ current_avatar.AddComponent( \
+ /datum/component/avatar_connection, \
+ old_mind = neo.mind, \
+ old_body = neo, \
+ server = server, \
+ pod = src, \
+ help_text = generated_domain.help_text, \
+ )
+
+ connected = TRUE
+
+/// Finds a server and sets the server_ref
+/obj/machinery/netpod/proc/find_server()
+ var/obj/machinery/quantum_server/server = server_ref?.resolve()
+ if(server)
+ return server
+
+ server = locate(/obj/machinery/quantum_server) in oview(4, src)
+ if(isnull(server))
+ return
+
+ server_ref = WEAKREF(server)
+ RegisterSignal(server, COMSIG_BITRUNNER_SERVER_UPGRADED, PROC_REF(on_server_upgraded))
+ RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_COMPLETE, PROC_REF(on_domain_complete))
+ RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_SCRUBBED, PROC_REF(on_domain_scrubbed))
+
+ return server
+
+/// Creates a list of outfit entries for the UI.
+/obj/machinery/netpod/proc/make_outfit_collection(identifier, list/outfit_list)
+ var/list/collection = list(
+ "name" = identifier,
+ "outfits" = list()
+ )
+
+ for(var/path as anything in outfit_list)
+ var/datum/outfit/outfit = path
+
+ var/outfit_name = initial(outfit.name)
+ if(findtext(outfit_name, "(") != 0 || findtext(outfit_name, "-") != 0) // No special variants please
+ continue
+
+ collection["outfits"] += list(list("path" = path, "name" = outfit_name))
+
+ return list(collection)
+
+/// Machine has been broken - handles signals and reverting sprites
+/obj/machinery/netpod/proc/on_broken(datum/source)
+ SIGNAL_HANDLER
+
+ if(!state_open)
+ open_machine()
+
+ if(occupant)
+ unprotect_and_signal()
+
+/// Puts points on the current occupant's card account
+/obj/machinery/netpod/proc/on_domain_complete(datum/source, atom/movable/crate, reward_points)
+ SIGNAL_HANDLER
+
+ if(isnull(occupant) || !connected || !iscarbon(occupant))
+ return
+
+ var/mob/living/carbon/player = occupant
+
+ var/datum/bank_account/account = player.get_bank_account()
+ if(isnull(account))
+ return
+
+ account.bitrunning_points += reward_points * 100
+
+/// User inspects the machine
+/obj/machinery/netpod/proc/on_examine(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_infoplain("Drag yourself into the pod to engage the link.")
+ examine_text += span_infoplain("It has limited resuscitation capabilities. Remaining in the pod can heal some injuries.")
+ examine_text += span_infoplain("It has a security system that will alert the occupant if it is tampered with.")
+
+ if(isnull(occupant))
+ examine_text += span_notice("It is currently unoccupied.")
+ return
+
+ examine_text += span_notice("It is currently occupied by [occupant].")
+ examine_text += span_notice("It can be pried open with a crowbar, but its safety mechanisms will alert the occupant.")
+
+/// The domain has been fully purged, so we should double check our avatar is deleted
+/obj/machinery/netpod/proc/on_domain_scrubbed(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/current_avatar = avatar_ref?.resolve()
+ if(isnull(current_avatar))
+ return
+
+ QDEL_NULL(current_avatar)
+
+/// When the server is upgraded, drops brain damage a little
+/obj/machinery/netpod/proc/on_server_upgraded(datum/source, servo_rating)
+ SIGNAL_HANDLER
+
+ disconnect_damage = BASE_DISCONNECT_DAMAGE * (1 - servo_rating)
+
+/// Checks the integrity, alerts occupants
+/obj/machinery/netpod/proc/on_take_damage(datum/source, damage_amount)
+ SIGNAL_HANDLER
+
+ if(isnull(occupant))
+ return
+
+ var/total = max_integrity - damage_amount
+ var/integrity = (atom_integrity / total) * 100
+ if(integrity > 50)
+ return
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_NETPOD_INTEGRITY)
+
+/// Puts the occupant in netpod stasis, basically short-circuiting environmental conditions
+/obj/machinery/netpod/proc/protect_occupant(mob/living/target)
+ if(target != occupant)
+ return
+
+ target.AddComponent(/datum/component/netpod_healing, \
+ brute_heal = 4, \
+ burn_heal = 4, \
+ toxin_heal = 4, \
+ clone_heal = 4, \
+ blood_heal = 4, \
+ )
+
+ target.playsound_local(src, 'sound/effects/submerge.ogg', 20, TRUE)
+ target.extinguish_mob()
+ update_use_power(ACTIVE_POWER_USE)
+
+/// On unbuckle or break, make sure the occupant ref is null
+/obj/machinery/netpod/proc/unprotect_and_signal()
+ unprotect_occupant(occupant)
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+
+/// Removes the occupant from netpod stasis
+/obj/machinery/netpod/proc/unprotect_occupant(mob/living/target)
+ var/datum/component/netpod_healing/healing_eff = target?.GetComponent(/datum/component/netpod_healing)
+ if(healing_eff)
+ qdel(healing_eff)
+
+ update_use_power(IDLE_POWER_USE)
+
+/// Resolves a path to an outfit.
+/obj/machinery/netpod/proc/resolve_outfit(text)
+ var/path = text2path(text)
+ if(ispath(path, /datum/outfit))
+ return path
+
+/// Closes the machine without shoving in an occupant
+/obj/machinery/netpod/proc/shut_pod()
+ state_open = FALSE
+ playsound(src, 'sound/machines/tramclose.ogg', 60, TRUE, frequency = 65000)
+ flick("[base_icon_state]_closing", src)
+ set_density(TRUE)
+
+ update_appearance()
+
+#undef BASE_DISCONNECT_DAMAGE
diff --git a/code/modules/bitrunning/objects/quantum_console.dm b/code/modules/bitrunning/objects/quantum_console.dm
new file mode 100644
index 00000000000000..c918648d010b1b
--- /dev/null
+++ b/code/modules/bitrunning/objects/quantum_console.dm
@@ -0,0 +1,108 @@
+/obj/machinery/computer/quantum_console
+ name = "quantum console"
+
+ circuit = /obj/item/circuitboard/computer/quantum_console
+ icon_keyboard = "mining"
+ icon_screen = "bitrunning"
+ req_access = list(ACCESS_MINING)
+ /// The server this console is connected to.
+ var/datum/weakref/server_ref
+
+/obj/machinery/computer/quantum_console/Initialize(mapload, obj/item/circuitboard/circuit)
+ . = ..()
+ desc = "Even in the distant year [CURRENT_STATION_YEAR], Nanostrasen is still using REST APIs. How grim."
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/computer/quantum_console/LateInitialize()
+ . = ..()
+
+ if(isnull(server_ref?.resolve()))
+ find_server()
+
+/obj/machinery/computer/quantum_console/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+
+ if(!is_operational)
+ return
+
+ if(isnull(server_ref?.resolve()))
+ find_server()
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "QuantumConsole")
+ ui.open()
+
+/obj/machinery/computer/quantum_console/ui_data()
+ var/list/data = list()
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ data["connected"] = FALSE
+ return data
+
+ data["connected"] = TRUE
+ data["generated_domain"] = server.generated_domain?.key
+ data["occupants"] = length(server.avatar_connection_refs)
+ data["points"] = server.points
+ data["randomized"] = server.domain_randomized
+ data["ready"] = server.is_ready && server.is_operational
+ data["scanner_tier"] = server.scanner_tier
+ data["retries_left"] = length(server.exit_turfs) - server.retries_spent
+
+ return data
+
+/obj/machinery/computer/quantum_console/ui_static_data(mob/user)
+ var/list/data = list()
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ return data
+
+ data["available_domains"] = server.get_available_domains()
+ data["avatars"] = server.get_avatar_data()
+
+ return data
+
+/obj/machinery/computer/quantum_console/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return TRUE
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ return FALSE
+
+ switch(action)
+ if("random_domain")
+ var/map_id = server.get_random_domain_id()
+ if(!map_id)
+ return TRUE
+
+ server.cold_boot_map(usr, map_id)
+ return TRUE
+ if("refresh")
+ ui.send_full_update()
+ return TRUE
+ if("set_domain")
+ server.cold_boot_map(usr, params["id"])
+ return TRUE
+ if("stop_domain")
+ server.begin_shutdown(usr)
+ return TRUE
+
+ return FALSE
+
+/// Attempts to find a quantum server.
+/obj/machinery/computer/quantum_console/proc/find_server()
+ var/obj/machinery/quantum_server/server = server_ref?.resolve()
+ if(server)
+ return server
+
+ for(var/direction in GLOB.cardinals)
+ var/obj/machinery/quantum_server/nearby_server = locate(/obj/machinery/quantum_server, get_step(src, direction))
+ if(nearby_server)
+ server_ref = WEAKREF(nearby_server)
+ nearby_server.console_ref = WEAKREF(src)
+ return nearby_server
diff --git a/code/modules/bitrunning/objects/vendor.dm b/code/modules/bitrunning/objects/vendor.dm
new file mode 100644
index 00000000000000..abd63a9e784302
--- /dev/null
+++ b/code/modules/bitrunning/objects/vendor.dm
@@ -0,0 +1,86 @@
+#define CREDIT_TYPE_BITRUNNING "np"
+
+/obj/machinery/computer/order_console/bitrunning
+ name = "bitrunning supplies order console"
+ desc = "NexaCache(tm)! Dubiously authentic gear for the digital daredevil."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ icon_state = "vendor"
+ icon_keyboard = null
+ icon_screen = null
+ circuit = /obj/item/circuitboard/computer/order_console/bitrunning
+ cooldown_time = 10 SECONDS
+ cargo_cost_multiplier = 0.65
+ express_cost_multiplier = 1
+ purchase_tooltip = @{"Your purchases will arrive at cargo,
+ and hopefully get delivered by them.
+ 35% cheaper than express delivery."}
+ express_tooltip = @{"Sends your purchases instantly."}
+ credit_type = CREDIT_TYPE_BITRUNNING
+
+ order_categories = list(
+ CATEGORY_BITRUNNING_FLAIR,
+ CATEGORY_BITRUNNING_TECH,
+ CATEGORY_BEPIS,
+ )
+ blackbox_key = "bitrunning"
+
+/obj/machinery/computer/order_console/bitrunning/subtract_points(final_cost, obj/item/card/id/card)
+ if(final_cost <= card.registered_account.bitrunning_points)
+ card.registered_account.bitrunning_points -= final_cost
+ return TRUE
+ return FALSE
+
+/obj/machinery/computer/order_console/bitrunning/order_groceries(mob/living/purchaser, obj/item/card/id/card, list/groceries)
+ var/list/things_to_order = list()
+ for(var/datum/orderable_item/item as anything in groceries)
+ things_to_order[item.item_path] = groceries[item]
+
+ var/datum/supply_pack/bitrunning/pack = new(
+ purchaser = purchaser, \
+ cost = get_total_cost(), \
+ contains = things_to_order,
+ )
+
+ var/datum/supply_order/new_order = new(
+ pack = pack,
+ orderer = purchaser,
+ orderer_rank = "Bitrunning Vendor",
+ orderer_ckey = purchaser.ckey,
+ reason = "",
+ paying_account = card.registered_account,
+ department_destination = null,
+ coupon = null,
+ charge_on_purchase = FALSE,
+ manifest_can_fail = FALSE,
+ cost_type = credit_type,
+ can_be_cancelled = FALSE,
+ )
+ say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
+ radio.talk_into(src, "A bitrunner has ordered equipment which will arrive on the cargo shuttle! Please make sure it gets to them as soon as possible!", radio_channel)
+ SSshuttle.shopping_list += new_order
+
+/obj/machinery/computer/order_console/bitrunning/retrieve_points(obj/item/card/id/id_card)
+ return round(id_card.registered_account.bitrunning_points)
+
+/obj/machinery/computer/order_console/bitrunning/ui_act(action, params)
+ . = ..()
+ if(!.)
+ flick("vendor_off", src)
+
+/obj/machinery/computer/order_console/bitrunning/update_icon_state()
+ icon_state = "[initial(icon_state)][powered() ? null : "_off"]"
+ return ..()
+
+/datum/supply_pack/bitrunning
+ name = "bitrunning order"
+ hidden = TRUE
+ crate_name = "bitrunning delivery crate"
+ access = list(ACCESS_BIT_DEN)
+
+/datum/supply_pack/bitrunning/New(purchaser, cost, list/contains)
+ . = ..()
+ name = "[purchaser]'s Bitrunning Order"
+ src.cost = cost
+ src.contains = contains
+
+#undef CREDIT_TYPE_BITRUNNING
diff --git a/code/modules/bitrunning/orders/disks.dm b/code/modules/bitrunning/orders/disks.dm
new file mode 100644
index 00000000000000..ced1dde883a195
--- /dev/null
+++ b/code/modules/bitrunning/orders/disks.dm
@@ -0,0 +1,26 @@
+/datum/orderable_item/bitrunning_tech
+ category_index = CATEGORY_BITRUNNING_TECH
+
+/datum/orderable_item/bitrunning_tech/item_tier1
+ cost_per_order = 1000
+ item_path = /obj/item/bitrunning_disk/item/tier1
+
+/datum/orderable_item/bitrunning_tech/item_tier2
+ cost_per_order = 1500
+ item_path = /obj/item/bitrunning_disk/item/tier2
+
+/datum/orderable_item/bitrunning_tech/item_tier3
+ cost_per_order = 2500
+ item_path = /obj/item/bitrunning_disk/item/tier3
+
+/datum/orderable_item/bitrunning_tech/ability_tier1
+ cost_per_order = 1000
+ item_path = /obj/item/bitrunning_disk/ability/tier1
+
+/datum/orderable_item/bitrunning_tech/ability_tier2
+ cost_per_order = 1800
+ item_path = /obj/item/bitrunning_disk/ability/tier2
+
+/datum/orderable_item/bitrunning_tech/ability_tier3
+ cost_per_order = 3200
+ item_path = /obj/item/bitrunning_disk/ability/tier3
diff --git a/code/modules/bitrunning/orders/flair.dm b/code/modules/bitrunning/orders/flair.dm
new file mode 100644
index 00000000000000..ef36348eb6ae94
--- /dev/null
+++ b/code/modules/bitrunning/orders/flair.dm
@@ -0,0 +1,40 @@
+/datum/orderable_item/bitrunning_flair
+ category_index = CATEGORY_BITRUNNING_FLAIR
+
+/datum/orderable_item/bitrunning_flair/cornchips
+ item_path = /obj/item/food/cornchips
+ cost_per_order = 100
+
+/datum/orderable_item/bitrunning_flair/mountain_wind
+ item_path = /obj/item/reagent_containers/cup/soda_cans/space_mountain_wind
+ cost_per_order = 100
+
+/datum/orderable_item/bitrunning_flair/pwr_game
+ item_path = /obj/item/reagent_containers/cup/soda_cans/pwr_game
+ cost_per_order = 200
+
+/datum/orderable_item/bitrunning_flair/grey_bull
+ item_path = /obj/item/reagent_containers/cup/soda_cans/grey_bull
+ cost_per_order = 200
+
+/datum/orderable_item/bitrunning_flair/medkit
+ item_path = /obj/item/storage/medkit/brute
+ desc = "Don't beat yourself up, it's just a game!"
+ cost_per_order = 500
+
+/datum/orderable_item/bitrunning_flair/medkit_fire
+ item_path = /obj/item/storage/medkit/fire
+ desc = "Great after heated gaming sessions."
+ cost_per_order = 500
+
+/datum/orderable_item/bitrunning_flair/oval_sunglasses
+ item_path = /obj/item/clothing/glasses/sunglasses/oval
+ cost_per_order = 1000
+
+/datum/orderable_item/bitrunning_flair/trenchcoat
+ item_path = /obj/item/clothing/suit/jacket/trenchcoat
+ cost_per_order = 1000
+
+/datum/orderable_item/bitrunning_flair/jackboots
+ item_path = /obj/item/clothing/shoes/jackboots
+ cost_per_order = 1000
diff --git a/code/modules/bitrunning/orders/tech.dm b/code/modules/bitrunning/orders/tech.dm
new file mode 100644
index 00000000000000..286e9817f3c522
--- /dev/null
+++ b/code/modules/bitrunning/orders/tech.dm
@@ -0,0 +1,23 @@
+/datum/orderable_item/bepis
+ category_index = CATEGORY_BEPIS
+
+/datum/orderable_item/bepis/circuit_stack
+ item_path = /obj/item/stack/circuit_stack/full
+ cost_per_order = 150
+
+/datum/orderable_item/bepis/survival_pen
+ item_path = /obj/item/pen/survival
+ cost_per_order = 150
+
+/datum/orderable_item/bepis/party_sleeper
+ item_path = /obj/item/circuitboard/machine/sleeper/party
+ cost_per_order = 750
+ desc = "A decommissioned sleeper circuitboard, repurposed for recreational purposes."
+
+/datum/orderable_item/bepis/sprayoncan
+ item_path = /obj/item/toy/sprayoncan
+ cost_per_order = 750
+
+/datum/orderable_item/bepis/pristine
+ item_path = /obj/item/disk/design_disk/bepis/remove_tech
+ cost_per_order = 1000
diff --git a/code/modules/bitrunning/server/loot.dm b/code/modules/bitrunning/server/loot.dm
new file mode 100644
index 00000000000000..8b3af95607c641
--- /dev/null
+++ b/code/modules/bitrunning/server/loot.dm
@@ -0,0 +1,125 @@
+/// Handles calculating rewards based on number of players, parts, threats, etc
+/obj/machinery/quantum_server/proc/calculate_rewards()
+ var/rewards_base = 0.8
+
+ if(domain_randomized)
+ rewards_base += 0.2
+
+ rewards_base += servo_bonus
+
+ rewards_base += (domain_threats * 2)
+
+ for(var/index in 2 to length(avatar_connection_refs))
+ rewards_base += multiplayer_bonus
+
+ return rewards_base
+
+/// Generates a reward based on the given domain
+/obj/machinery/quantum_server/proc/generate_loot()
+ var/list/obj/machinery/byteforge/nearby_forges = get_nearby_forges()
+ if(isnull(nearby_forges))
+ say(src, "No nearby byteforges detected.")
+ return FALSE
+
+ points += generated_domain.reward_points
+ playsound(src, 'sound/machines/terminal_success.ogg', 30, 2)
+
+ var/obj/machinery/byteforge/chosen_forge = pick(nearby_forges)
+ if(isnull(chosen_forge))
+ stack_trace("Failed to find a turf to spawn loot crate on.")
+ return FALSE
+
+ var/bonus = calculate_rewards()
+
+ var/obj/item/paper/certificate = new()
+ certificate.add_raw_text(get_completion_certificate())
+ certificate.name = "certificate of domain completion"
+ certificate.update_appearance()
+
+ var/obj/structure/closet/crate/secure/bitrunning/decrypted/reward_crate = new(src, generated_domain, bonus)
+ reward_crate.manifest = certificate
+ reward_crate.update_appearance()
+
+ chosen_forge.start_to_spawn(reward_crate)
+ return TRUE
+
+/// Returns the markdown text containing domain completion information
+/obj/machinery/quantum_server/proc/get_completion_certificate()
+ var/base_points = generated_domain.reward_points
+ if(domain_randomized)
+ base_points -= 1
+
+ var/bonuses = calculate_rewards()
+
+ var/time_difference = world.time - generated_domain.start_time
+
+ var/completion_time = "### Completion Time: [DisplayTimeText(time_difference)]\n"
+
+ var/grade = "\n---\n\n# Rating: [grade_completion(generated_domain.difficulty, domain_threats, base_points, domain_randomized, time_difference)]"
+
+ var/text = "# Certificate of Domain Completion\n\n---\n\n"
+
+ text += "### [generated_domain.name][domain_randomized ? " (Randomized)" : ""]\n"
+ text += "- **Difficulty:** [generated_domain.difficulty]\n"
+ text += "- **Threats:** [domain_threats]\n"
+ text += "- **Base Points:** [base_points][domain_randomized ? " +1" : ""]\n\n"
+ text += "- **Total Bonus:** [bonuses]x\n\n"
+
+ if(bonuses <= 1)
+ text += completion_time
+ text += grade
+ return text
+
+ text += "### Bonuses\n"
+ if(domain_randomized)
+ text += "- **Randomized:** + 0.2\n"
+
+ if(length(avatar_connection_refs) > 1)
+ text += "- **Multiplayer:** + [(length(avatar_connection_refs) - 1) * multiplayer_bonus]\n"
+
+ if(domain_threats > 0)
+ text += "- **Threats:** + [domain_threats * 2]\n"
+
+ var/servo_rating = servo_bonus
+
+ if(servo_rating > 0.2)
+ text += "- **Components:** + [servo_rating]\n"
+
+ text += completion_time
+ text += grade
+
+ return text
+
+/// Grades the player's run based on several factors
+/obj/machinery/quantum_server/proc/grade_completion(difficulty, threats, points, randomized, completion_time)
+ var/score = threats * 5
+ score += points
+ score += randomized ? 1 : 0
+
+ var/base = difficulty + 1
+ var/time_score = 1
+
+ if(completion_time <= 1 MINUTES)
+ time_score = 10
+ else if(completion_time <= 2 MINUTES)
+ time_score = 5
+ else if(completion_time <= 5 MINUTES)
+ time_score = 3
+ else if(completion_time <= 10 MINUTES)
+ time_score = 2
+ else
+ time_score = 1
+
+ score += time_score * base
+
+ switch(score)
+ if(1 to 4)
+ return "D"
+ if(5 to 7)
+ return "C"
+ if(8 to 10)
+ return "B"
+ if(11 to 13)
+ return "A"
+ else
+ return "S"
diff --git a/code/modules/bitrunning/server/map_handling.dm b/code/modules/bitrunning/server/map_handling.dm
new file mode 100644
index 00000000000000..741fad476f0a8d
--- /dev/null
+++ b/code/modules/bitrunning/server/map_handling.dm
@@ -0,0 +1,185 @@
+#define ONLY_TURF 1
+
+/// Gives all current occupants a notification that the server is going down
+/obj/machinery/quantum_server/proc/begin_shutdown(mob/user)
+ if(isnull(generated_domain))
+ return
+
+ if(!length(avatar_connection_refs))
+ balloon_alert(user, "powering down domain...")
+ playsound(src, 'sound/machines/terminal_off.ogg', 40, 2)
+ reset()
+ return
+
+ balloon_alert(user, "notifying clients...")
+ playsound(src, 'sound/machines/terminal_alert.ogg', 100, TRUE)
+ user.visible_message(
+ span_danger("[user] begins depowering the server!"),
+ span_notice("You start disconnecting clients..."),
+ span_danger("You hear frantic keying on a keyboard."),
+ )
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SHUTDOWN_ALERT, user)
+
+ if(!do_after(user, 20 SECONDS, src))
+ return
+
+ reset()
+
+/**
+ * ### Quantum Server Cold Boot
+ * Procedurally links the 3 booting processes together.
+ *
+ * This is the starting point if you have an id. Does validation and feedback on steps
+ */
+/obj/machinery/quantum_server/proc/cold_boot_map(mob/user, map_key)
+ if(!is_ready)
+ return FALSE
+
+ if(isnull(map_key))
+ balloon_alert(user, "no domain specified.")
+ return FALSE
+
+ if(generated_domain)
+ balloon_alert(user, "stop the current domain first.")
+ return FALSE
+
+ if(length(avatar_connection_refs))
+ balloon_alert(user, "all clients must disconnect!")
+ return FALSE
+
+ is_ready = FALSE
+ playsound(src, 'sound/machines/terminal_processing.ogg', 30, 2)
+
+ if(!initialize_domain(map_key) || !initialize_safehouse() || !initialize_map_items())
+ balloon_alert(user, "initialization failed.")
+ scrub_vdom()
+ is_ready = TRUE
+ return FALSE
+
+ is_ready = TRUE
+ playsound(src, 'sound/machines/terminal_insert_disc.ogg', 30, 2)
+ balloon_alert(user, "domain loaded.")
+ generated_domain.start_time = world.time
+ points -= generated_domain.cost
+ update_use_power(ACTIVE_POWER_USE)
+ update_appearance()
+
+ return TRUE
+
+/// Initializes a new domain if the given key is valid and the user has enough points
+/obj/machinery/quantum_server/proc/initialize_domain(map_key)
+ var/datum/lazy_template/virtual_domain/to_load
+
+ for(var/datum/lazy_template/virtual_domain/available as anything in subtypesof(/datum/lazy_template/virtual_domain))
+ if(map_key != initial(available.key) || points < initial(available.cost))
+ continue
+ to_load = available
+ break
+
+ if(isnull(to_load))
+ return FALSE
+
+ generated_domain = new to_load()
+ RegisterSignal(generated_domain, COMSIG_LAZY_TEMPLATE_LOADED, PROC_REF(on_template_loaded))
+ generated_domain.lazy_load()
+
+ return TRUE
+
+/// Loads in necessary map items, sets mutation targets, etc
+/obj/machinery/quantum_server/proc/initialize_map_items()
+ var/turf/goal_turfs = list()
+ var/turf/crate_turfs = list()
+
+ for(var/thing in GLOB.landmarks_list)
+ if(istype(thing, /obj/effect/landmark/bitrunning/hololadder_spawn))
+ exit_turfs += get_turf(thing)
+ qdel(thing) // i'm worried about multiple servers getting confused so lets clean em up
+ continue
+
+ if(istype(thing, /obj/effect/landmark/bitrunning/cache_goal_turf))
+ var/turf/tile = get_turf(thing)
+ goal_turfs += tile
+ RegisterSignal(tile, COMSIG_ATOM_ENTERED, PROC_REF(on_goal_turf_entered))
+ RegisterSignal(tile, COMSIG_ATOM_EXAMINE, PROC_REF(on_goal_turf_examined))
+ qdel(thing)
+ continue
+
+ if(istype(thing, /obj/effect/landmark/bitrunning/cache_spawn))
+ crate_turfs += get_turf(thing)
+ qdel(thing)
+ continue
+
+ if(!length(exit_turfs))
+ CRASH("Failed to find exit turfs on generated domain.")
+ if(!length(goal_turfs))
+ CRASH("Failed to find send turfs on generated domain.")
+
+ if(length(crate_turfs))
+ shuffle_inplace(crate_turfs)
+ new /obj/structure/closet/crate/secure/bitrunning/encrypted(pick(crate_turfs))
+
+ return TRUE
+
+/// Loads the safehouse
+/obj/machinery/quantum_server/proc/initialize_safehouse()
+ var/turf/safehouse_load_turf = list()
+ for(var/obj/effect/landmark/bitrunning/safehouse_spawn/spawner in GLOB.landmarks_list)
+ safehouse_load_turf += get_turf(spawner)
+ qdel(spawner)
+ break
+
+ if(!length(safehouse_load_turf))
+ CRASH("Failed to find safehouse load landmark on map.")
+
+ var/datum/map_template/safehouse/safehouse = new generated_domain.safehouse_path()
+ safehouse.load(safehouse_load_turf[ONLY_TURF])
+ generated_safehouse = safehouse
+
+ return TRUE
+
+/// Stops the current virtual domain and disconnects all users
+/obj/machinery/quantum_server/proc/reset(fast = FALSE)
+ is_ready = FALSE
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+
+ if(!fast)
+ notify_spawned_threats()
+ addtimer(CALLBACK(src, PROC_REF(scrub_vdom)), 15 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
+ else
+ scrub_vdom() // used in unit testing, no need to wait for callbacks
+
+ addtimer(CALLBACK(src, PROC_REF(cool_off)), min(server_cooldown_time * capacitor_coefficient), TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+ update_appearance()
+
+ update_use_power(IDLE_POWER_USE)
+ domain_randomized = FALSE
+ domain_threats = 0
+ retries_spent = 0
+
+/// Deletes all the tile contents
+/obj/machinery/quantum_server/proc/scrub_vdom()
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR) /// just in case someone's connected
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_DOMAIN_SCRUBBED) // avatar cleanup just in case
+
+ if(length(generated_domain.reservations))
+ var/datum/turf_reservation/res = generated_domain.reservations[1]
+ res.Release()
+
+ var/list/datum/weakref/creatures = spawned_threat_refs + mutation_candidate_refs
+ for(var/datum/weakref/creature_ref as anything in creatures)
+ var/mob/living/creature = creature_ref?.resolve()
+ if(isnull(creature))
+ continue
+
+ creature.dust() // sometimes mobs just don't die
+
+ avatar_connection_refs.Cut()
+ exit_turfs = list()
+ generated_domain = null
+ generated_safehouse = null
+ mutation_candidate_refs.Cut()
+ spawned_threat_refs.Cut()
+
+#undef ONLY_TURF
diff --git a/code/modules/bitrunning/server/obj_generation.dm b/code/modules/bitrunning/server/obj_generation.dm
new file mode 100644
index 00000000000000..221308e048783b
--- /dev/null
+++ b/code/modules/bitrunning/server/obj_generation.dm
@@ -0,0 +1,101 @@
+/// Generates a new avatar for the bitrunner.
+/obj/machinery/quantum_server/proc/generate_avatar(obj/structure/hololadder/wayout, datum/outfit/netsuit)
+ var/mob/living/carbon/human/avatar = new(wayout.loc)
+
+ var/outfit_path = generated_domain.forced_outfit || netsuit
+ var/datum/outfit/to_wear = new outfit_path()
+
+ to_wear.belt = /obj/item/bitrunning_host_monitor
+ to_wear.glasses = null
+ to_wear.gloves = null
+ to_wear.l_hand = null
+ to_wear.l_pocket = null
+ to_wear.r_hand = null
+ to_wear.r_pocket = null
+ to_wear.suit = null
+ to_wear.suit_store = null
+
+ avatar.equipOutfit(to_wear, visualsOnly = TRUE)
+
+ var/thing = avatar.get_active_held_item()
+ if(!isnull(thing))
+ qdel(thing)
+
+ thing = avatar.get_inactive_held_item()
+ if(!isnull(thing))
+ qdel(thing)
+
+ var/obj/item/storage/backpack/bag = avatar.back
+ if(istype(bag))
+ QDEL_LIST(bag.contents)
+
+ bag.contents += list(
+ new /obj/item/storage/box/survival,
+ new /obj/item/storage/medkit/regular,
+ new /obj/item/flashlight,
+ )
+
+ var/obj/item/card/id/outfit_id = avatar.wear_id
+ if(outfit_id)
+ outfit_id.assignment = "Bit Avatar"
+ outfit_id.registered_name = avatar.real_name
+
+ outfit_id.registered_account = new()
+ outfit_id.registered_account.replaceable = FALSE
+
+ SSid_access.apply_trim_to_card(outfit_id, /datum/id_trim/bit_avatar)
+
+ return avatar
+
+/// Generates a new hololadder for the bitrunner. Effectively a respawn attempt.
+/obj/machinery/quantum_server/proc/generate_hololadder()
+ if(!length(exit_turfs))
+ return
+
+ if(retries_spent >= length(exit_turfs))
+ return
+
+ var/turf/destination
+ for(var/turf/dest_turf in exit_turfs)
+ if(!locate(/obj/structure/hololadder) in dest_turf)
+ destination = dest_turf
+ break
+
+ if(isnull(destination))
+ return
+
+ var/obj/structure/hololadder/wayout = new(destination)
+ if(isnull(wayout))
+ return
+
+ retries_spent += 1
+
+ return wayout
+
+/// Scans over neo's contents for bitrunning tech disks. Loads the items or abilities onto the avatar.
+/obj/machinery/quantum_server/proc/stock_gear(mob/living/carbon/human/avatar, mob/living/carbon/human/neo)
+ var/failed = FALSE
+
+ for(var/obj/item/bitrunning_disk/disk in neo.get_contents())
+ if(istype(disk, /obj/item/bitrunning_disk/ability))
+ var/obj/item/bitrunning_disk/ability/ability_disk = disk
+
+ if(isnull(ability_disk.granted_action))
+ failed = TRUE
+ continue
+
+ var/datum/action/our_action = new ability_disk.granted_action()
+ our_action.Grant(avatar)
+ continue
+
+ if(istype(disk, /obj/item/bitrunning_disk/item))
+ var/obj/item/bitrunning_disk/item/item_disk = disk
+
+ if(isnull(item_disk.granted_item))
+ failed = TRUE
+ continue
+
+ avatar.put_in_hands(new item_disk.granted_item())
+
+ if(failed)
+ to_chat(neo, span_warning("One of your disks failed to load. You must activate them to make a selection."))
diff --git a/code/modules/bitrunning/server/quantum_server.dm b/code/modules/bitrunning/server/quantum_server.dm
new file mode 100644
index 00000000000000..b869fb7f02e2a0
--- /dev/null
+++ b/code/modules/bitrunning/server/quantum_server.dm
@@ -0,0 +1,149 @@
+/**
+ * The base object for the quantum server
+ */
+/obj/machinery/quantum_server
+ name = "quantum server"
+
+ circuit = /obj/item/circuitboard/machine/quantum_server
+ density = TRUE
+ desc = "A hulking computational machine designed to fabricate virtual domains."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ base_icon_state = "qserver"
+ icon_state = "qserver"
+ /// Affects server cooldown efficiency
+ var/capacitor_coefficient = 1
+ /// The loaded map template, map_template/virtual_domain
+ var/datum/lazy_template/virtual_domain/generated_domain
+ /// The loaded safehouse, map_template/safehouse
+ var/datum/map_template/safehouse/generated_safehouse
+ /// The connected console
+ var/datum/weakref/console_ref
+ /// If the current domain was a random selection
+ var/domain_randomized = FALSE
+ /// If any threats were spawned, adds to rewards
+ var/domain_threats = 0
+ /// Prevents multiple user actions. Handled by loading domains and cooldowns
+ var/is_ready = TRUE
+ /// List of available domains
+ var/list/available_domains = list()
+ /// Current plugged in users
+ var/list/datum/weakref/avatar_connection_refs = list()
+ /// Cached list of mutable mobs in zone for cybercops
+ var/list/datum/weakref/mutation_candidate_refs = list()
+ /// Any ghosts that have spawned in
+ var/list/datum/weakref/spawned_threat_refs = list()
+ /// Scales loot with extra players
+ var/multiplayer_bonus = 1.1
+ ///The radio the console can speak into
+ var/obj/item/radio/radio
+ /// The amount of points in the system, used to purchase maps
+ var/points = 0
+ /// Keeps track of the number of times someone has built a hololadder
+ var/retries_spent = 0
+ /// Changes how much info is available on the domain
+ var/scanner_tier = 1
+ /// Length of time it takes for the server to cool down after resetting. Here to give runners downtime so their faces don't get stuck like that
+ var/server_cooldown_time = 3 MINUTES
+ /// Applies bonuses to rewards etc
+ var/servo_bonus = 0
+ /// The turfs we can place a hololadder on.
+ var/turf/exit_turfs = list()
+
+/obj/machinery/quantum_server/Initialize(mapload)
+ . = ..()
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/quantum_server/LateInitialize()
+ . = ..()
+
+ if(isnull(console_ref))
+ find_console()
+
+ radio = new(src)
+ radio.set_frequency(FREQ_SUPPLY)
+ radio.subspace_transmission = TRUE
+ radio.canhear_range = 0
+ radio.recalculateChannels()
+
+ RegisterSignals(src, list(COMSIG_MACHINERY_BROKEN, COMSIG_MACHINERY_POWER_LOST), PROC_REF(on_broken))
+ RegisterSignal(src, COMSIG_QDELETING, PROC_REF(on_delete))
+ RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(src, COMSIG_BITRUNNER_SPAWN_GLITCH, PROC_REF(on_threat_created))
+
+ // This further gets sorted in the client by cost so it's random and grouped
+ available_domains = shuffle(subtypesof(/datum/lazy_template/virtual_domain))
+
+/obj/machinery/quantum_server/Destroy(force)
+ . = ..()
+
+ available_domains.Cut()
+ mutation_candidate_refs.Cut()
+ avatar_connection_refs.Cut()
+ spawned_threat_refs.Cut()
+ QDEL_NULL(exit_turfs)
+ QDEL_NULL(generated_domain)
+ QDEL_NULL(generated_safehouse)
+ QDEL_NULL(radio)
+
+/obj/machinery/quantum_server/update_appearance(updates)
+ if(isnull(generated_domain) || !is_operational)
+ set_light(l_on = FALSE)
+ return ..()
+
+ set_light_color(is_ready ? LIGHT_COLOR_BABY_BLUE : LIGHT_COLOR_FIRE)
+ set_light(l_range = 2, l_power = 1.5, l_on = TRUE)
+
+ return ..()
+
+/obj/machinery/quantum_server/update_icon_state()
+ if(isnull(generated_domain) || !is_operational)
+ icon_state = base_icon_state
+ return ..()
+
+ icon_state = "[base_icon_state]_[is_ready ? "on" : "off"]"
+ return ..()
+
+/obj/machinery/quantum_server/crowbar_act(mob/living/user, obj/item/crowbar)
+ . = ..()
+
+ if(!is_ready)
+ balloon_alert(user, "it's scalding hot!")
+ return TRUE
+ if(length(avatar_connection_refs))
+ balloon_alert(user, "all clients must disconnect!")
+ return TRUE
+ if(default_deconstruction_crowbar(crowbar))
+ return TRUE
+ return FALSE
+
+/obj/machinery/quantum_server/screwdriver_act(mob/living/user, obj/item/screwdriver)
+ . = ..()
+
+ if(!is_ready)
+ balloon_alert(user, "it's scalding hot!")
+ return TRUE
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]_panel", icon_state, screwdriver))
+ return TRUE
+ return FALSE
+
+/obj/machinery/quantum_server/RefreshParts()
+ . = ..()
+
+ var/capacitor_rating = 1.15
+ var/datum/stock_part/capacitor/cap = locate() in component_parts
+ capacitor_rating -= cap.tier * 0.15
+
+ capacitor_coefficient = capacitor_rating
+
+ var/datum/stock_part/scanning_module/scanner = locate() in component_parts
+ if(scanner)
+ scanner_tier = scanner.tier
+
+ var/servo_rating = 0
+ for(var/datum/stock_part/servo/servo in component_parts)
+ servo_rating += servo.tier * 0.1
+
+ servo_bonus = servo_rating
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SERVER_UPGRADED, servo_rating)
diff --git a/code/modules/bitrunning/server/signal_handlers.dm b/code/modules/bitrunning/server/signal_handlers.dm
new file mode 100644
index 00000000000000..b0464b351faf04
--- /dev/null
+++ b/code/modules/bitrunning/server/signal_handlers.dm
@@ -0,0 +1,107 @@
+/// If broken via signal, disconnects all users
+/obj/machinery/quantum_server/proc/on_broken(datum/source)
+ SIGNAL_HANDLER
+
+ if(isnull(generated_domain))
+ return
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+
+/// Whenever a corpse spawner makes a new corpse, add it to the list of potential mutations
+/obj/machinery/quantum_server/proc/on_corpse_spawned(datum/source, mob/living/corpse)
+ SIGNAL_HANDLER
+
+ mutation_candidate_refs.Add(WEAKREF(corpse))
+
+/// Being qdeleted - make sure the circuit and connected mobs go with it
+/obj/machinery/quantum_server/proc/on_delete(datum/source)
+ SIGNAL_HANDLER
+
+ if(generated_domain)
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+ scrub_vdom()
+
+ if(is_ready)
+ return
+ // in case they're trying to cheese cooldown
+ var/obj/item/circuitboard/machine/quantum_server/circuit = locate(/obj/item/circuitboard/machine/quantum_server) in contents
+ if(circuit)
+ qdel(circuit)
+
+/// Handles examining the server. Shows cooldown time and efficiency.
+/obj/machinery/quantum_server/proc/on_examine(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_infoplain("Can be resource intensive to run. Ensure adequate power supply.")
+
+ if(capacitor_coefficient < 1)
+ examine_text += span_infoplain("Its coolant capacity reduces cooldown time by [(1 - capacitor_coefficient) * 100]%.")
+
+ if(servo_bonus > 0.2)
+ examine_text += span_infoplain("Its manipulation potential is increasing rewards by [servo_bonus]x.")
+ examine_text += span_infoplain("Injury from unsafe ejection reduced [servo_bonus * 100]%.")
+
+ if(!is_ready)
+ examine_text += span_notice("It is currently cooling down. Give it a few moments.")
+ return
+
+/// Whenever something enters the send tiles, check if it's a loot crate. If so, alert players.
+/obj/machinery/quantum_server/proc/on_goal_turf_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if(!istype(arrived, /obj/structure/closet/crate/secure/bitrunning/encrypted))
+ return
+
+ var/obj/structure/closet/crate/secure/bitrunning/encrypted/loot_crate = arrived
+ if(!istype(loot_crate))
+ return
+
+ for(var/mob/person in loot_crate.contents)
+ if(isnull(person.mind))
+ person.forceMove(get_turf(loot_crate))
+
+ var/datum/component/avatar_connection/connection = person.GetComponent(/datum/component/avatar_connection)
+ connection?.full_avatar_disconnect()
+
+ spark_at_location(loot_crate)
+ qdel(loot_crate)
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_DOMAIN_COMPLETE, arrived, generated_domain.reward_points)
+ generate_loot()
+
+/// Handles examining the server. Shows cooldown time and efficiency.
+/obj/machinery/quantum_server/proc/on_goal_turf_examined(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_info("Beneath your gaze, the floor pulses subtly with streams of encoded data.")
+ examine_text += span_info("It seems to be part of the location designated for retrieving encrypted payloads.")
+
+/// Scans over the inbound created_atoms from lazy templates
+/obj/machinery/quantum_server/proc/on_template_loaded(datum/lazy_template/source, list/created_atoms)
+ SIGNAL_HANDLER
+
+ for(var/thing in created_atoms)
+ if(isliving(thing)) // so we can mutate them
+ var/mob/living/creature = thing
+
+ if(creature.can_be_cybercop)
+ mutation_candidate_refs.Add(WEAKREF(creature))
+ continue
+
+ if(istype(thing, /obj/effect/mob_spawn/ghost_role)) // so we get threat alerts
+ RegisterSignal(thing, COMSIG_GHOSTROLE_SPAWNED, PROC_REF(on_threat_created))
+ continue
+
+ if(istype(thing, /obj/effect/mob_spawn/corpse)) // corpses are valid targets too
+ var/obj/effect/mob_spawn/corpse/spawner = thing
+
+ mutation_candidate_refs.Add(spawner.spawned_mob_ref)
+
+ UnregisterSignal(source, COMSIG_LAZY_TEMPLATE_LOADED)
+
+/// Handles when cybercops are summoned into the area or ghosts click a ghost role spawner
+/obj/machinery/quantum_server/proc/on_threat_created(datum/source, mob/living/threat)
+ SIGNAL_HANDLER
+
+ domain_threats += 1
+ spawned_threat_refs.Add(WEAKREF(threat))
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_THREAT_CREATED) // notify players
diff --git a/code/modules/bitrunning/server/util.dm b/code/modules/bitrunning/server/util.dm
new file mode 100644
index 00000000000000..f4dbada9ef6f05
--- /dev/null
+++ b/code/modules/bitrunning/server/util.dm
@@ -0,0 +1,142 @@
+#define REDACTED "???"
+#define MAX_DISTANCE 4 // How far crates can spawn from the server
+
+/// Resets the cooldown state and updates icons
+/obj/machinery/quantum_server/proc/cool_off()
+ is_ready = TRUE
+ update_appearance()
+ radio.talk_into(src, "Thermal systems within operational parameters. Proceeding to domain configuration.", RADIO_CHANNEL_SUPPLY)
+
+/// Attempts to connect to a quantum console
+/obj/machinery/quantum_server/proc/find_console()
+ var/obj/machinery/computer/quantum_console/console = console_ref?.resolve()
+ if(console)
+ return console
+
+ for(var/direction in GLOB.cardinals)
+ var/obj/machinery/computer/quantum_console/nearby_console = locate(/obj/machinery/computer/quantum_console, get_step(src, direction))
+ if(nearby_console)
+ console_ref = WEAKREF(nearby_console)
+ nearby_console.server_ref = WEAKREF(src)
+ return nearby_console
+
+/// Compiles a list of available domains.
+/obj/machinery/quantum_server/proc/get_available_domains()
+ var/list/levels = list()
+
+ for(var/datum/lazy_template/virtual_domain/domain as anything in available_domains)
+ if(initial(domain.test_only))
+ continue
+ var/can_view = initial(domain.difficulty) < scanner_tier && initial(domain.cost) <= points + 5
+ var/can_view_reward = initial(domain.difficulty) < (scanner_tier + 1) && initial(domain.cost) <= points + 3
+
+ levels += list(list(
+ "cost" = initial(domain.cost),
+ "desc" = can_view ? initial(domain.desc) : "Limited scanning capabilities. Cannot infer domain details.",
+ "difficulty" = initial(domain.difficulty),
+ "id" = initial(domain.key),
+ "name" = can_view ? initial(domain.name) : REDACTED,
+ "reward" = can_view_reward ? initial(domain.reward_points) : REDACTED,
+ ))
+
+ return levels
+
+/// If there are hosted minds, attempts to get a list of their current virtual bodies w/ vitals
+/obj/machinery/quantum_server/proc/get_avatar_data()
+ var/list/hosted_avatars = list()
+
+ for(var/datum/weakref/avatar_ref in avatar_connection_refs)
+ var/datum/component/avatar_connection/connection = avatar_ref.resolve()
+ if(isnull(connection))
+ avatar_connection_refs.Remove(connection)
+ continue
+
+ var/mob/living/creature = connection.parent
+ var/mob/living/pilot = connection.old_body_ref?.resolve()
+
+ hosted_avatars += list(list(
+ "health" = creature.health,
+ "name" = creature.name,
+ "pilot" = pilot,
+ "brute" = creature.get_current_damage_of_type(BRUTE),
+ "burn" = creature.get_current_damage_of_type(BURN),
+ "tox" = creature.get_current_damage_of_type(TOX),
+ "oxy" = creature.get_current_damage_of_type(OXY),
+ ))
+
+ return hosted_avatars
+
+/// Gets a random available domain given the current points. Weighted towards higher cost domains.
+/obj/machinery/quantum_server/proc/get_random_domain_id()
+ if(points < 1)
+ return
+
+ var/list/random_domains = list()
+ var/total_cost = 0
+
+ for(var/datum/lazy_template/virtual_domain/available as anything in subtypesof(/datum/lazy_template/virtual_domain))
+ var/init_cost = initial(available.cost)
+ if(!initial(available.test_only) && init_cost > 0 && init_cost < 4 && init_cost <= points)
+ random_domains += list(list(
+ cost = init_cost,
+ id = initial(available.key),
+ ))
+
+ var/random_value = rand(0, total_cost)
+ var/accumulated_cost = 0
+
+ for(var/available as anything in random_domains)
+ accumulated_cost += available["cost"]
+ if(accumulated_cost >= random_value)
+ domain_randomized = TRUE
+ return available["id"]
+
+/// Gets all mobs originally generated by the loaded domain and returns a list that are capable of being antagged
+/obj/machinery/quantum_server/proc/get_valid_domain_targets()
+ // A: No one is playing
+ // B: The domain is not loaded
+ // C: The domain is shutting down
+ // D: There are no mobs
+ if(!length(avatar_connection_refs) || isnull(generated_domain) || !is_ready || !is_operational || !length(mutation_candidate_refs))
+ return list()
+
+ for(var/datum/weakref/creature_ref as anything in mutation_candidate_refs)
+ var/mob/living/creature = creature_ref.resolve()
+ if(isnull(creature) || creature.mind)
+ mutation_candidate_refs.Remove(creature_ref)
+
+ return shuffle(mutation_candidate_refs)
+
+/// Locates any turfs with forges on them
+/obj/machinery/quantum_server/proc/get_nearby_forges()
+ var/list/obj/machinery/byteforge/nearby_forges = list()
+
+ for(var/obj/machinery/byteforge/forge in oview(MAX_DISTANCE, src))
+ nearby_forges += forge
+
+ return nearby_forges
+
+/// Finds any mobs with minds in the zones and gives them the bad news
+/obj/machinery/quantum_server/proc/notify_spawned_threats()
+ for(var/datum/weakref/baddie_ref as anything in spawned_threat_refs)
+ var/mob/living/baddie = baddie_ref.resolve()
+ if(isnull(baddie) || baddie.stat >= UNCONSCIOUS || isnull(baddie.mind))
+ continue
+
+ baddie.throw_alert(
+ ALERT_BITRUNNER_RESET,
+ /atom/movable/screen/alert/bitrunning/qserver_threat_deletion,
+ new_master = src,
+ )
+
+ to_chat(baddie, span_userdanger("You have been flagged for deletion! Thank you for your service."))
+
+/// Do some magic teleport sparks
+/obj/machinery/quantum_server/proc/spark_at_location(obj/cache)
+ playsound(cache, 'sound/magic/blink.ogg', 50, TRUE)
+ var/datum/effect_system/spark_spread/quantum/sparks = new()
+ sparks.set_up(5, 1, get_turf(cache))
+ sparks.start()
+
+#undef REDACTED
+#undef MAX_DISTANCE
diff --git a/code/modules/bitrunning/turfs.dm b/code/modules/bitrunning/turfs.dm
new file mode 100644
index 00000000000000..93dce1789c4f62
--- /dev/null
+++ b/code/modules/bitrunning/turfs.dm
@@ -0,0 +1,14 @@
+/turf/open/floor/bitrunning_transport
+ name = "circuit floor"
+ icon = 'icons/turf/floors.dmi'
+ desc = "Looks complex. You can see the circuits running through the floor."
+ icon_state = "bitrunning"
+
+/turf/closed/indestructible/binary
+ name = "tear in the fabric of reality"
+ icon = 'icons/turf/floors.dmi'
+ icon_state = "binary"
+
+/obj/effect/baseturf_helper/virtual_domain
+ name = "virtual domain baseturf editor"
+ baseturf = /turf/open/indestructible/binary
diff --git a/code/modules/bitrunning/virtual_domain/domains/ash_drake.dm b/code/modules/bitrunning/virtual_domain/domains/ash_drake.dm
new file mode 100644
index 00000000000000..02bb91abc5888f
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/ash_drake.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/ash_drake
+ name = "Ashen Inferno"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Home of the ash drake, a powerful dragon that scours the surface of Lavaland."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ forced_outfit = /datum/outfit/job/miner
+ key = "ash_drake"
+ map_name = "ash_drake"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/dragon/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1600
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1600
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
new file mode 100644
index 00000000000000..a6fb3e921e054a
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
@@ -0,0 +1,22 @@
+/datum/lazy_template/virtual_domain/beach_bar
+ name = "Beach Bar"
+ desc = "A cheerful seaside haven where friendly skeletons serve up drinks. Say, how'd you guys get so dead?"
+ extra_loot = list(/obj/item/toy/beach_ball = 1)
+ help_text = "This place is running on a skeleton crew, and they don't seem to be too keen to share details. \
+ Maybe a few drinks of liquid charm will get the spirits up. As the saying goes, if you can't beat 'em, join 'em."
+ key = "beach_bar"
+ map_name = "beach_bar"
+ safehouse_path = /datum/map_template/safehouse/mine
+
+/obj/item/reagent_containers/cup/glass/drinkingglass/filled/virtual_domain
+ name = "pina colada"
+ desc = "Whose drink is this? Not yours, that's for sure. Well, it's not like they're going to miss it."
+ list_reagents = list(/datum/reagent/consumable/ethanol/pina_colada = 30)
+
+/obj/item/reagent_containers/cup/glass/drinkingglass/filled/virtual_domain/Initialize(mapload, vol)
+ . = ..()
+
+ AddComponent(/datum/component/bitrunning_points, \
+ signal_type = COMSIG_GLASS_DRANK, \
+ points_per_signal = 0.5, \
+ )
diff --git a/code/modules/bitrunning/virtual_domain/domains/blood_drunk_miner.dm b/code/modules/bitrunning/virtual_domain/domains/blood_drunk_miner.dm
new file mode 100644
index 00000000000000..abf2e0fc5a9405
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/blood_drunk_miner.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/blood_drunk_miner
+ name = "Sanguine Excavation"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Few escape the surface of Lavaland without a few scars. Some remain, maddened by the hunt."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ forced_outfit = /datum/outfit/job/miner
+ key = "blood_drunk_miner"
+ map_name = "blood_drunk_miner"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1600
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1600
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/bubblegum.dm b/code/modules/bitrunning/virtual_domain/domains/bubblegum.dm
new file mode 100644
index 00000000000000..bede97177cb7d4
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/bubblegum.dm
@@ -0,0 +1,19 @@
+/datum/lazy_template/virtual_domain/bubblegum
+ name = "Blood-Soaked Lair"
+ cost = BITRUNNER_COST_HIGH
+ desc = "King of the slaughter demons. Bubblegum is a massive, hulking beast with a penchant for violence."
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ extra_loot = list(/obj/item/toy/plush/bubbleplush = 1)
+ forced_outfit = /datum/outfit/job/miner
+ key = "bubblegum"
+ map_name = "bubblegum"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/bubblegum/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 2000
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 2000
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/clown_planet.dm b/code/modules/bitrunning/virtual_domain/domains/clown_planet.dm
new file mode 100644
index 00000000000000..92f000c9cf342a
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/clown_planet.dm
@@ -0,0 +1,13 @@
+/datum/lazy_template/virtual_domain/clown_planet
+ name = "Clown Planet"
+ cost = BITRUNNER_COST_LOW
+ desc = "In the deep, dark reaches of space, there is only Honk."
+ difficulty = BITRUNNER_DIFFICULTY_LOW
+ extra_loot = list(/obj/item/bikehorn = 1)
+ forced_outfit = /datum/outfit/job/clown
+ help_text = "The trials of the Honkitude have begun. The sound of bike horns wailing in the distance. \
+ this realm- some sort of puzzle, has existed in legend as the final test of just how silly you are."
+ key = "clown_planet"
+ map_name = "clown_planet"
+ reward_points = BITRUNNER_REWARD_LOW
+ safehouse_path = /datum/map_template/safehouse/mine
diff --git a/code/modules/bitrunning/virtual_domain/domains/colossus.dm b/code/modules/bitrunning/virtual_domain/domains/colossus.dm
new file mode 100644
index 00000000000000..35ba4eee0ca89f
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/colossus.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/colossus
+ name = "Celestial Trial"
+ cost = BITRUNNER_COST_HIGH
+ desc = "A massive, ancient beast named the Colossus. Judgment comes."
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ forced_outfit = /datum/outfit/job/miner
+ key = "colossus"
+ map_name = "colossus"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/colossus/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 2000
+ maxHealth = 2000
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
new file mode 100644
index 00000000000000..01d58e3980381d
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
@@ -0,0 +1,35 @@
+/datum/lazy_template/virtual_domain/gondola_asteroid
+ name = "Gondola Asteroid"
+ desc = "An asteroid home to a bountiful forest of gondolas. Peaceful."
+ map_name = "gondola_asteroid"
+ help_text = "What a lovely forest. There's a loot crate here in the middle of the map. \
+ Hmm... It doesn't budge. The gondolas don't seem to have any trouble moving it, though. \
+ I bet there's a way to move it myself."
+ key = "gondola_asteroid"
+ map_name = "gondola_asteroid"
+ safehouse_path = /datum/map_template/safehouse/shuttle_space
+
+/// Very pushy gondolas, great for moving loot crates.
+/obj/structure/closet/crate/secure/bitrunning/encrypted/gondola
+ move_resist = MOVE_FORCE_STRONG
+
+/mob/living/simple_animal/pet/gondola/virtual_domain
+ health = 50
+ loot = list(/obj/effect/decal/cleanable/blood/gibs, /obj/item/stack/sheet/animalhide/gondola = 1, /obj/item/food/meat/slab/gondola/virtual_domain = 1)
+ maxHealth = 50
+ move_force = MOVE_FORCE_VERY_STRONG
+ move_resist = MOVE_FORCE_STRONG
+
+/obj/item/food/meat/slab/gondola/virtual_domain
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment/protein = 4,
+ /datum/reagent/gondola_mutation_toxin/virtual_domain = 5,
+ )
+
+/datum/reagent/gondola_mutation_toxin/virtual_domain
+ name = "Advanced Tranquility"
+ gondola_disease = /datum/disease/transformation/gondola/virtual_domain
+
+/datum/disease/transformation/gondola/virtual_domain
+ stage_prob = 9
+ new_form = /mob/living/simple_animal/pet/gondola/virtual_domain
diff --git a/code/modules/bitrunning/virtual_domain/domains/hierophant.dm b/code/modules/bitrunning/virtual_domain/domains/hierophant.dm
new file mode 100644
index 00000000000000..142623f4f812e5
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/hierophant.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/hierophant
+ name = "Zealot Arena"
+ cost = BITRUNNER_COST_HIGH
+ desc = "Dance, puppets, dance!"
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ forced_outfit = /datum/outfit/job/miner
+ key = "hierophant"
+ map_name = "hierophant"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/hierophant/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1700
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1700
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/legion.dm b/code/modules/bitrunning/virtual_domain/domains/legion.dm
new file mode 100644
index 00000000000000..f1ba146f3801b9
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/legion.dm
@@ -0,0 +1,20 @@
+/datum/lazy_template/virtual_domain/legion
+ name = "Chamber of Echoes"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "A chilling realm that houses Legion's necropolis. Those who succumb to it are forever damned."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ forced_outfit = /datum/outfit/job/miner
+ key = "legion"
+ map_name = "legion"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/legion/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1500
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1500
+ true_spawn = FALSE
+
+// You may be thinking, what about those mini-legions? They're not part of the initial created_atoms list
diff --git a/code/modules/bitrunning/virtual_domain/domains/pipedream.dm b/code/modules/bitrunning/virtual_domain/domains/pipedream.dm
new file mode 100644
index 00000000000000..fd54ef6ca48476
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/pipedream.dm
@@ -0,0 +1,101 @@
+/datum/lazy_template/virtual_domain/pipedream
+ name = "Disposal Pipe Factory"
+ cost = BITRUNNER_COST_LOW
+ desc = "An abandoned and infested factory manufacturing disposal pipes."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ extra_loot = list(/obj/item/stack/pipe_cleaner_coil/random/five = 1)
+ help_text = "Not long ago, this place was thriving with activity. The workers \
+ seemed to have left in a hurry, and now productivity is in the bin. Something \
+ must have trashed the place, but what?"
+ key = "pipedream"
+ map_name = "pipedream"
+ reward_points = BITRUNNER_REWARD_LOW
+ safehouse_path = /datum/map_template/safehouse/shuttle
+
+// ID Trims
+/datum/id_trim/factory
+ assignment = "Factory Worker"
+ trim_state = "trim_cargotechnician"
+ department_color = COLOR_CARGO_BROWN
+ subdepartment_color = COLOR_CARGO_BROWN
+ sechud_icon_state = SECHUD_CARGO_TECHNICIAN
+ access = list(
+ ACCESS_AWAY_SUPPLY
+ )
+
+/datum/id_trim/factory/qm
+ assignment = "Factory Quartermaster"
+ trim_state = "trim_quartermaster"
+ department_color = COLOR_COMMAND_BLUE
+ subdepartment_color = COLOR_CARGO_BROWN
+ department_state = "departmenthead"
+ sechud_icon_state = SECHUD_QUARTERMASTER
+ access = list(
+ ACCESS_AWAY_SUPPLY,
+ ACCESS_AWAY_COMMAND
+ )
+
+// ID Cards
+/obj/item/card/id/advanced/factory
+ name = "factory worker ID"
+ trim = /datum/id_trim/factory
+
+/obj/item/card/id/advanced/factory/qm
+ name = "factory quartermaster ID"
+ trim = /datum/id_trim/factory/qm
+
+//Outfits
+/datum/outfit/factory
+ name = "Factory Worker"
+
+ id_trim = /datum/id_trim/factory
+ id = /obj/item/card/id/advanced/
+ uniform = /obj/item/clothing/under/rank/cargo/tech
+ suit = /obj/item/clothing/suit/hazardvest
+ belt = /obj/item/radio
+ gloves = /obj/item/clothing/gloves/color/black
+ head = /obj/item/clothing/head/soft/yellow
+ shoes = /obj/item/clothing/shoes/workboots
+ l_pocket = /obj/item/flashlight/seclite
+
+/datum/outfit/factory/guard
+ name = "Factory Guard"
+
+ uniform = /obj/item/clothing/under/rank/security/officer/grey
+ suit = /obj/item/clothing/suit/armor/vest/alt
+ belt = /obj/item/radio
+ gloves = /obj/item/clothing/gloves/color/black
+ head = /obj/item/clothing/head/soft/sec
+ shoes = /obj/item/clothing/shoes/jackboots/sec
+ l_pocket = /obj/item/restraints/handcuffs
+ r_pocket = /obj/item/assembly/flash/handheld
+
+/datum/outfit/factory/qm
+ name = "Factory Quatermaster"
+
+ id_trim = /datum/id_trim/factory/qm
+ id = /obj/item/card/id/advanced/silver
+ uniform = /obj/item/clothing/under/rank/cargo/qm
+ belt = /obj/item/radio
+ gloves = /obj/item/clothing/gloves/color/black
+ head = /obj/item/clothing/head/soft/yellow
+ shoes = /obj/item/clothing/shoes/jackboots/sec
+ l_pocket = /obj/item/melee/baton/telescopic
+ r_pocket = /obj/item/stamp/head/qm
+
+// Corpses
+/obj/effect/mob_spawn/corpse/human/factory
+ name = "Factory Worker"
+ outfit = /datum/outfit/factory
+ icon_state = "corpsecargotech"
+
+/obj/effect/mob_spawn/corpse/human/factory/guard
+ name = "Factory Guard"
+ outfit = /datum/outfit/factory/guard
+ icon_state = "corpsecargotech"
+
+/obj/effect/mob_spawn/corpse/human/factory/qm
+ name = "Factory Quartermaster"
+ outfit = /datum/outfit/factory/qm
+ icon_state = "corpsecargotech"
+
diff --git a/code/modules/bitrunning/virtual_domain/domains/pirates.dm b/code/modules/bitrunning/virtual_domain/domains/pirates.dm
new file mode 100644
index 00000000000000..52d86a71211806
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/pirates.dm
@@ -0,0 +1,10 @@
+/datum/lazy_template/virtual_domain/pirates
+ name = "Corsair Cove"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Battle your way to the hidden treasure, seize the booty, and make a swift escape before the pirates turn the tide."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ help_text = "Put on the provided outfits to blend in, then battle your way through the hostile pirates. \
+ Grab the treasure and get out before you're overwhelmed!"
+ key = "pirates"
+ map_name = "pirates"
+ reward_points = BITRUNNER_REWARD_MEDIUM
diff --git a/code/modules/bitrunning/virtual_domain/domains/stairs_and_cliffs.dm b/code/modules/bitrunning/virtual_domain/domains/stairs_and_cliffs.dm
new file mode 100644
index 00000000000000..2d9bcca3645560
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/stairs_and_cliffs.dm
@@ -0,0 +1,29 @@
+/datum/lazy_template/virtual_domain/stairs_and_cliffs
+ name = "Glacier Grind"
+ cost = BITRUNNER_COST_LOW
+ desc = "A treacherous climb few calves can survive. Great cardio though."
+ help_text = "Ever heard of 'Snakes and Ladders'? It's like that, but with \
+ instead of ladders its stairs and instead of snakes its a steep drop down a \
+ cliff into rough rocks or liquid plasma."
+ extra_loot = list(/obj/item/clothing/suit/costume/snowman = 2)
+ difficulty = BITRUNNER_DIFFICULTY_LOW
+ forced_outfit = /datum/outfit/job/virtual_domain_iceclimber
+ key = "stairs_and_cliffs"
+ map_name = "stairs_and_cliffs"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/ice
+
+/turf/open/cliff/snowrock/virtual_domain
+ name = "icy cliff"
+ initial_gas_mix = "o2=22;n2=82;TEMP=180"
+
+/turf/open/lava/plasma/virtual_domain
+ name = "plasma lake"
+ initial_gas_mix = "o2=22;n2=82;TEMP=180"
+
+/datum/outfit/job/virtual_domain_iceclimber
+ name = "Ice Climber"
+
+ uniform = /obj/item/clothing/under/color/grey
+ backpack = /obj/item/storage/backpack/duffelbag
+ shoes = /obj/item/clothing/shoes/winterboots
diff --git a/code/modules/bitrunning/virtual_domain/domains/syndicate_assault.dm b/code/modules/bitrunning/virtual_domain/domains/syndicate_assault.dm
new file mode 100644
index 00000000000000..bae0da6874dbf2
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/syndicate_assault.dm
@@ -0,0 +1,13 @@
+/datum/lazy_template/virtual_domain/syndicate_assault
+ name = "Syndicate Assault"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Board the enemy ship and recover the stolen cargo."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ extra_loot = list(/obj/item/toy/plush/nukeplushie = 1)
+ help_text = "A group of Syndicate operatives have stolen valuable cargo from the station. \
+ They have boarded their ship and are attempting to escape. Infiltrate their ship and recover \
+ the crate. Be careful, they are extremely armed."
+ key = "syndicate_assault"
+ map_name = "syndicate_assault"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/shuttle
diff --git a/code/modules/bitrunning/virtual_domain/domains/test_only.dm b/code/modules/bitrunning/virtual_domain/domains/test_only.dm
new file mode 100644
index 00000000000000..6e5e852fb5c8ef
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/test_only.dm
@@ -0,0 +1,11 @@
+/// Used for unit tests only. Skipped in UI.
+/datum/lazy_template/virtual_domain/test_only
+ name = "Test Only"
+ key = "test_only"
+ map_name = "test_only"
+ test_only = TRUE
+ safehouse_path = /datum/map_template/safehouse/test_only
+
+/datum/lazy_template/virtual_domain/test_only/expensive
+ key = "test_only_expensive"
+ cost = 3
diff --git a/code/modules/bitrunning/virtual_domain/domains/vaporwave.dm b/code/modules/bitrunning/virtual_domain/domains/vaporwave.dm
new file mode 100644
index 00000000000000..45d4abec9830af
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/vaporwave.dm
@@ -0,0 +1,10 @@
+/datum/lazy_template/virtual_domain/vaporwave
+ name = "Cosmic Vestige"
+ cost = BITRUNNER_COST_EXTREME
+ desc = "Suspended in the silent void of space, the Neon Relic is a haunting echo of a retro-futuristic era."
+ difficulty = BITRUNNER_DIFFICULTY_NONE
+ extra_loot = list(/obj/item/stack/spacecash/c500 = 3)
+ key = "vaporwave"
+ map_name = "vaporwave"
+ reward_points = BITRUNNER_REWARD_EXTREME
+ safehouse_path = /datum/map_template/safehouse/shuttle_space
diff --git a/code/modules/bitrunning/virtual_domain/domains/wendigo.dm b/code/modules/bitrunning/virtual_domain/domains/wendigo.dm
new file mode 100644
index 00000000000000..fcad3db6faf76f
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/wendigo.dm
@@ -0,0 +1,19 @@
+/datum/lazy_template/virtual_domain/wendigo
+ name = "Glacial Devourer"
+ cost = BITRUNNER_COST_HIGH
+ desc = "Legends speak of the ravenous Wendigo hidden deep within the caves of Icemoon."
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ forced_outfit = /datum/outfit/job/miner
+ key = "wendigo"
+ map_name = "wendigo"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/wendigo/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ guaranteed_butcher_results = list(/obj/item/wendigo_skull = 1)
+ health = 2000
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 2000
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/xeno_nest.dm b/code/modules/bitrunning/virtual_domain/domains/xeno_nest.dm
new file mode 100644
index 00000000000000..2bd4105e13c220
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/xeno_nest.dm
@@ -0,0 +1,12 @@
+/datum/lazy_template/virtual_domain/xeno_nest
+ name = "Xeno Infestation"
+ cost = BITRUNNER_COST_LOW
+ desc = "Our ship scanners have detected lifeforms of unknown origin. Friendly attempts to contact them have failed."
+ difficulty = BITRUNNER_DIFFICULTY_LOW
+ extra_loot = list(/obj/item/toy/plush/rouny = 1)
+ help_text = "You are on a barren planet filled with hostile creatures. There is a crate here, not hidden, \
+ simply protected. Expect resistance."
+ key = "xeno_nest"
+ map_name = "xeno_nest"
+ reward_points = BITRUNNER_REWARD_LOW
+ safehouse_path = /datum/map_template/safehouse/shuttle
diff --git a/code/modules/bitrunning/virtual_domain/safehouses.dm b/code/modules/bitrunning/virtual_domain/safehouses.dm
new file mode 100644
index 00000000000000..bb42f690ac7ec0
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/safehouses.dm
@@ -0,0 +1,53 @@
+/**
+ * # Safe Houses
+ * The starting point for virtual domains.
+ * Create your own: Read the readme file in the '_maps/safehouses' folder.
+ */
+/datum/map_template/safehouse
+ name = "virtual domain: safehouse"
+
+ returns_created_atoms = TRUE
+ /// The map file to load
+ var/filename = "den.dmm"
+
+/datum/map_template/safehouse/New()
+ mappath = "_maps/safehouses/" + filename
+ ..(path = mappath)
+
+/datum/map_template/safehouse/test_only
+ filename = "test_only_safehouse.dmm"
+
+
+/// The default safehouse map template.
+/datum/map_template/safehouse/wood
+ filename = "wood.dmm"
+
+/datum/map_template/safehouse/den
+ filename = "den.dmm"
+
+/datum/map_template/safehouse/dig
+ filename = "dig.dmm"
+
+/datum/map_template/safehouse/shuttle
+ filename = "shuttle.dmm"
+
+// Has space tiles on the four corners.
+/datum/map_template/safehouse/shuttle_space
+ filename = "shuttle_space.dmm"
+
+/datum/map_template/safehouse/mine
+ filename = "mine.dmm"
+
+// Comes preloaded with mining combat gear.
+/datum/map_template/safehouse/lavaland_boss
+ filename = "lavaland_boss.dmm"
+
+// Chill out
+/datum/map_template/safehouse/ice
+ filename = "ice.dmm"
+
+/**
+ * Your safehouse here
+ * /datum/map_template/safehouse/your_type
+ * filename = "your_map.dmm"
+ */
diff --git a/code/modules/bitrunning/virtual_domain/virtual_domain.dm b/code/modules/bitrunning/virtual_domain/virtual_domain.dm
new file mode 100644
index 00000000000000..c2bd193f4e98ee
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/virtual_domain.dm
@@ -0,0 +1,34 @@
+/**
+ * # Virtual Domains
+ * This loads a base level, then users can select the preset upon it.
+ * Create your own: Read the readme file in the '_maps/virtual_domains' folder.
+ */
+/datum/lazy_template/virtual_domain
+ map_dir = "_maps/virtual_domains"
+ map_name = "None"
+ key = "Virtual Domain"
+
+ /// Cost of this map to load
+ var/cost = BITRUNNER_COST_NONE
+ /// The description of the map
+ var/desc = "A map."
+ /// The 'difficulty' of the map, which affects the ui and ability to scan info.
+ var/difficulty = BITRUNNER_DIFFICULTY_NONE
+ /// An assoc list of typepath/amount to spawn on completion. Not weighted - the value is the amount
+ var/list/extra_loot
+ /// The map file to load
+ var/filename = "virtual_domain.dmm"
+ /// Any outfit that you wish to force on avatars. Overrides preferences
+ var/datum/outfit/forced_outfit
+ /// Information given to connected clients via ability
+ var/help_text
+ // Name to show in the UI
+ var/name = "Virtual Domain"
+ /// Points to reward for completion. Used to purchase new domains and calculate ore rewards.
+ var/reward_points = BITRUNNER_REWARD_MIN
+ /// The start time of the map. Used to calculate time taken
+ var/start_time
+ /// This map is specifically for unit tests. Shouldn't display in game
+ var/test_only = FALSE
+ /// The safehouse to load into the map
+ var/datum/map_template/safehouse/safehouse_path = /datum/map_template/safehouse/den
diff --git a/code/modules/capture_the_flag/ctf_player_component.dm b/code/modules/capture_the_flag/ctf_player_component.dm
index 0bf37b979dea1d..d3abc0f2571ccc 100644
--- a/code/modules/capture_the_flag/ctf_player_component.dm
+++ b/code/modules/capture_the_flag/ctf_player_component.dm
@@ -8,7 +8,7 @@
var/can_respawn = TRUE
///Reference to the game this player is participating in.
var/datum/ctf_controller/ctf_game
- ///Item dropped on death,
+ ///Item dropped on death,
var/death_drop = /obj/effect/powerup/ammo/ctf
///Reference to players ckey, used for sending messages to them relating to CTF.
var/ckey_reference
@@ -22,19 +22,20 @@
var/datum/mind/true_parent = parent
player_mob = true_parent.current
ckey_reference = player_mob.ckey
- setup_dusting()
-
+ register_mob()
+
/datum/component/ctf_player/PostTransfer()
if(!istype(parent, /datum/mind))
return COMPONENT_INCOMPATIBLE
var/datum/mind/true_parent = parent
player_mob = true_parent.current
- setup_dusting()
+ register_mob()
-///CTF players are dusted upon taking damage that puts them into critical or leaving their body.
-/datum/component/ctf_player/proc/setup_dusting()
+/// Called when we get a new player mob, register signals and set up the mob.
+/datum/component/ctf_player/proc/register_mob()
RegisterSignal(player_mob, COMSIG_MOB_AFTER_APPLY_DAMAGE, PROC_REF(damage_type_check))
RegisterSignal(player_mob, COMSIG_MOB_GHOSTIZED, PROC_REF(ctf_dust))
+ ADD_TRAIT(player_mob, TRAIT_PERMANENTLY_MORTAL, CTF_TRAIT)
///Stamina and oxygen damage will not dust a player by themself.
/datum/component/ctf_player/proc/damage_type_check(datum/source, damage, damage_type)
diff --git a/code/modules/cargo/coupon.dm b/code/modules/cargo/coupon.dm
index fc1c3a6cc6598c..5c4e1e930b5c61 100644
--- a/code/modules/cargo/coupon.dm
+++ b/code/modules/cargo/coupon.dm
@@ -44,7 +44,7 @@
/// Play stupid games, win stupid prizes
/obj/item/coupon/proc/curse_heart(mob/living/cursed)
if(!iscarbon(cursed))
- cursed.gib()
+ cursed.gib(DROP_ALL_REMAINS)
return TRUE
var/mob/living/carbon/player = cursed
diff --git a/code/modules/cargo/department_order.dm b/code/modules/cargo/department_order.dm
index 972588a6355d64..46122fd8741b03 100644
--- a/code/modules/cargo/department_order.dm
+++ b/code/modules/cargo/department_order.dm
@@ -21,12 +21,28 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
var/list/department_delivery_areas = list()
///which groups this computer can order from
var/list/dep_groups = list()
+ /// If this departmental order console currently is on cooldown.
+ var/on_cooldown = FALSE
+
+ /// Our radio object we use to talk to our department.
+ var/obj/item/radio/radio
+ /// The radio key typepath that will be instantiated and inserted into our radio.
+ var/obj/item/encryptionkey/radio_key_typepath
+ /// The radio channel we will speak into by default.
+ var/radio_channel
/obj/machinery/computer/department_orders/Initialize(mapload, obj/item/circuitboard/board)
. = ..()
// All maps should have ONLY ONE of each order console roundstart
REGISTER_REQUIRED_MAP_ITEM(1, 1)
+ if (radio_channel && radio_key_typepath)
+ radio = new(src)
+ radio.keyslot = new radio_key_typepath
+ radio.subspace_transmission = TRUE
+ radio.canhear_range = 0
+ radio.recalculateChannels()
+
if(mapload) //check for mapping errors
for(var/delivery_area_type in department_delivery_areas)
if(GLOB.areas_by_type[delivery_area_type])
@@ -35,6 +51,11 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
log_mapping("[src] has no valid areas to deliver to on this map, add some more fallback areas to its \"department_delivery_areas\" var.")
department_delivery_areas = list(/area/station/hallway/primary/central) //if this doesn't exist like honestly fuck your map man
+/obj/machinery/computer/department_orders/Destroy()
+ QDEL_NULL(radio)
+
+ return ..()
+
/obj/machinery/computer/department_orders/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
@@ -142,6 +163,12 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
if(GLOB.areas_by_type[delivery_area_type])
chosen_delivery_area = delivery_area_type
break
+
+ if(SSshuttle.supply.get_order_count(pack) == OVER_ORDER_LIMIT)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
+ return
+
department_order = new(pack, name, rank, ckey, "", null, chosen_delivery_area, null)
SSshuttle.shopping_list += department_order
if(!already_signalled)
@@ -166,6 +193,20 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
time_y = 10 MINUTES * time_y
GLOB.department_order_cooldowns[type] = world.time + time_y
+/obj/machinery/computer/department_orders/process()
+ . = ..()
+ if (!.)
+ return FALSE
+
+ if (GLOB.department_order_cooldowns[type] > world.time)
+ on_cooldown = TRUE
+ else if (on_cooldown)
+ radio?.talk_into(src, "Order cooldown has expired! A new order may now be placed!", radio_channel)
+ playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
+ on_cooldown = FALSE
+
+ return TRUE
+
/obj/machinery/computer/department_orders/service
name = "service order console"
circuit = /obj/item/circuitboard/computer/service_orders
@@ -173,6 +214,8 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
override_access = ACCESS_HOP
req_one_access = list(ACCESS_SERVICE)
dep_groups = list("Service", "Food & Hydroponics", "Livestock", "Costumes & Toys")
+ radio_key_typepath = /obj/item/encryptionkey/headset_service
+ radio_channel = RADIO_CHANNEL_SERVICE
/obj/machinery/computer/department_orders/engineering
name = "engineering order console"
@@ -181,6 +224,8 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
override_access = ACCESS_CE
req_one_access = REGION_ACCESS_ENGINEERING
dep_groups = list("Engineering", "Engine Construction", "Canisters & Materials")
+ radio_key_typepath = /obj/item/encryptionkey/headset_eng
+ radio_channel = RADIO_CHANNEL_ENGINEERING
/obj/machinery/computer/department_orders/science
name = "science order console"
@@ -188,7 +233,9 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
department_delivery_areas = list(/area/station/science/research)
override_access = ACCESS_RD
req_one_access = REGION_ACCESS_RESEARCH
- dep_groups = list("Science", "Livestock")
+ dep_groups = list("Science", "Livestock", "Canisters & Materials")
+ radio_key_typepath = /obj/item/encryptionkey/headset_sci
+ radio_channel = RADIO_CHANNEL_SCIENCE
/obj/machinery/computer/department_orders/security
name = "security order console"
@@ -201,6 +248,8 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
override_access = ACCESS_HOS
req_one_access = REGION_ACCESS_SECURITY
dep_groups = list("Security", "Armory")
+ radio_key_typepath = /obj/item/encryptionkey/headset_sec
+ radio_channel = RADIO_CHANNEL_SECURITY
/obj/machinery/computer/department_orders/medical
name = "medical order console"
@@ -214,3 +263,5 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
override_access = ACCESS_CMO
req_one_access = REGION_ACCESS_MEDBAY
dep_groups = list("Medical")
+ radio_key_typepath = /obj/item/encryptionkey/headset_med
+ radio_channel = RADIO_CHANNEL_MEDICAL
diff --git a/code/modules/cargo/exports.dm b/code/modules/cargo/exports.dm
index a9d408c8699b9a..44a628740bd522 100644
--- a/code/modules/cargo/exports.dm
+++ b/code/modules/cargo/exports.dm
@@ -35,12 +35,13 @@ Then the player gets the profit from selling his own wasted time.
** delete_unsold: if the items that were not sold should be deleted
** dry_run: if the item should be actually sold, or if its just a pirce test
** external_report: works as "transaction" object, pass same one in if you're doing more than one export in single go
+ ** ignore_typecache: typecache containing types that should be completely ignored
*/
-/proc/export_item_and_contents(atom/movable/exported_atom, apply_elastic = TRUE, delete_unsold = TRUE, dry_run = FALSE, datum/export_report/external_report)
+/proc/export_item_and_contents(atom/movable/exported_atom, apply_elastic = TRUE, delete_unsold = TRUE, dry_run = FALSE, datum/export_report/external_report, list/ignore_typecache)
if(!GLOB.exports_list.len)
setupExports()
- var/list/contents = exported_atom.get_all_contents()
+ var/list/contents = exported_atom.get_all_contents_ignoring(ignore_typecache)
var/datum/export_report/report = external_report
diff --git a/code/modules/cargo/exports/materials.dm b/code/modules/cargo/exports/materials.dm
index 06c52305f51ba7..c9a88a6edfb200 100644
--- a/code/modules/cargo/exports/materials.dm
+++ b/code/modules/cargo/exports/materials.dm
@@ -2,10 +2,13 @@
cost = 5 // Cost per SHEET_MATERIAL_AMOUNT, which is 100cm3 as of May 2023.
message = "cm3 of developer's tears. Please, report this on github"
amount_report_multiplier = SHEET_MATERIAL_AMOUNT
- var/material_id = null
+ var/datum/material/material_id = null
export_types = list(
- /obj/item/stack/sheet/mineral, /obj/item/stack/tile/mineral,
- /obj/item/stack/ore, /obj/item/coin)
+ /obj/item/stack/sheet/mineral,
+ /obj/item/stack/tile/mineral,
+ /obj/item/stack/ore,
+ /obj/item/coin
+ )
// Yes, it's a base type containing export_types.
// But it has no material_id, so any applies_to check will return false, and these types reduce amount of copypasta a lot
@@ -27,17 +30,8 @@
return round(amount / SHEET_MATERIAL_AMOUNT)
-// Materials. Nothing but plasma is really worth selling. Better leave it all to RnD and sell some plasma instead.
-
-/datum/export/material/bananium
- cost = CARGO_CRATE_VALUE * 2
- material_id = /datum/material/bananium
- message = "cm3 of bananium"
-
-/datum/export/material/diamond
- cost = CARGO_CRATE_VALUE
- material_id = /datum/material/diamond
- message = "cm3 of diamonds"
+// Materials. Static materials exist as parent types, while materials subject to the stock market have a fluid cost as determined by material/market types
+// If you're adding a new material to the stock market, make sure its export type is added here.
/datum/export/material/plasma
cost = CARGO_CRATE_VALUE * 0.4
@@ -45,27 +39,12 @@
material_id = /datum/material/plasma
message = "cm3 of plasma"
-/datum/export/material/uranium
- cost = CARGO_CRATE_VALUE * 0.2
- material_id = /datum/material/uranium
- message = "cm3 of uranium"
-
-/datum/export/material/gold
- cost = CARGO_CRATE_VALUE * 0.25
- material_id = /datum/material/gold
- message = "cm3 of gold"
-
-/datum/export/material/silver
- cost = CARGO_CRATE_VALUE * 0.1
- material_id = /datum/material/silver
- message = "cm3 of silver"
-
-/datum/export/material/titanium
- cost = CARGO_CRATE_VALUE * 0.25
- material_id = /datum/material/titanium
- message = "cm3 of titanium"
+/datum/export/material/bananium
+ cost = CARGO_CRATE_VALUE * 2
+ material_id = /datum/material/bananium
+ message = "cm3 of bananium"
-/datum/export/material/adamantine
+/datum/export/material/diamond
cost = CARGO_CRATE_VALUE
material_id = /datum/material/adamantine
message = "cm3 of adamantine"
@@ -75,11 +54,6 @@
material_id = /datum/material/mythril
message = "cm3 of mythril"
-/datum/export/material/bscrystal
- cost = CARGO_CRATE_VALUE * 0.6
- message = "of bluespace crystals"
- material_id = /datum/material/bluespace
-
/datum/export/material/plastic
cost = CARGO_CRATE_VALUE * 0.05
message = "cm3 of plastic"
@@ -90,21 +64,6 @@
message = "cm3 of runite"
material_id = /datum/material/runite
-/datum/export/material/iron
- cost = CARGO_CRATE_VALUE * 0.01
- message = "cm3 of iron"
- material_id = /datum/material/iron
- export_types = list(
- /obj/item/stack/sheet/iron, /obj/item/stack/tile/iron,
- /obj/item/stack/rods, /obj/item/stack/ore, /obj/item/coin)
-
-/datum/export/material/glass
- cost = CARGO_CRATE_VALUE * 0.01
- message = "cm3 of glass"
- material_id = /datum/material/glass
- export_types = list(/obj/item/stack/sheet/glass, /obj/item/stack/ore,
- /obj/item/shard)
-
/datum/export/material/hot_ice
cost = CARGO_CRATE_VALUE * 0.8
message = "cm3 of Hot Ice"
@@ -116,3 +75,90 @@
message = "cm3 of metallic hydrogen"
material_id = /datum/material/metalhydrogen
export_types = /obj/item/stack/sheet/mineral/metal_hydrogen
+
+/datum/export/material/market
+
+/datum/export/material/market/diamond
+ material_id = /datum/material/diamond
+ message = "cm3 of diamonds"
+
+/datum/export/material/market/uranium
+ material_id = /datum/material/uranium
+ message = "cm3 of uranium"
+
+/datum/export/material/market/gold
+ material_id = /datum/material/gold
+ message = "cm3 of gold"
+
+/datum/export/material/market/silver
+ material_id = /datum/material/silver
+ message = "cm3 of silver"
+
+/datum/export/material/market/titanium
+ material_id = /datum/material/titanium
+ message = "cm3 of titanium"
+
+/datum/export/material/market/bscrystal
+ message = "of bluespace crystals"
+ material_id = /datum/material/bluespace
+ export_types = list(/obj/item/stack/sheet/bluespace_crystal, /obj/item/stack/ore) //For whatever reason, bluespace crystals are not a mineral
+
+/datum/export/material/market/iron
+ message = "cm3 of iron"
+ material_id = /datum/material/iron
+ export_types = list(
+ /obj/item/stack/sheet/iron,
+ /obj/item/stack/tile/iron,
+ /obj/item/stack/rods,
+ /obj/item/stack/ore,
+ /obj/item/coin
+ )
+
+/datum/export/material/market/glass
+ message = "cm3 of glass"
+ material_id = /datum/material/glass
+ export_types = list(
+ /obj/item/stack/sheet/glass,
+ /obj/item/stack/ore,
+ /obj/item/shard
+ )
+
+/datum/export/material/market/get_cost(obj/O, apply_elastic = FALSE)
+ var/obj/item/I = O
+ var/amount = get_amount(I)
+ if(!amount)
+ return 0
+ var/material_value = (SSstock_market.materials_prices[material_id]) * amount * MARKET_PROFIT_MODIFIER
+ return round(material_value)
+
+/datum/export/material/market/sell_object(obj/sold_item, datum/export_report/report, dry_run, apply_elastic)
+ . = ..()
+ var/amount = get_amount(sold_item)
+ var/price = get_cost(sold_item)
+ if(!amount)
+ return
+ if(!dry_run)
+ SSstock_market.materials_quantity[material_id] += amount
+ SSstock_market.materials_prices[material_id] -= round((price) * (amount / (amount + SSstock_market.materials_quantity[material_id])))
+ //This formula should impact lower quantity materials greater, and higher quantity materials less. Still, it's a bit rough. Tweaking may be needed.
+
+
+// Stock blocks are a special type of export that can be used to sell a quantity of materials at a specific price on the market.
+/datum/export/stock_block
+ cost = 0
+ message = "stock block"
+ export_types = list(/obj/item/stock_block)
+
+/datum/export/stock_block/get_cost(obj/O, apply_elastic = FALSE)
+ var/obj/item/stock_block/block = O
+ return block.export_value
+
+/datum/export/stock_block/sell_object(obj/sold_item, datum/export_report/report, dry_run, apply_elastic)
+ . = ..()
+ if(dry_run)
+ return
+ var/obj/item/stock_block/sold_block = sold_item
+ var/sale_value = sold_block.export_value
+ SSstock_market.materials_quantity[sold_block.export_mat] += sold_block.quantity
+ SSstock_market.materials_prices[sold_block.export_mat] -= round((sale_value) * (sold_block.quantity / (sold_block.quantity + SSstock_market.materials_quantity[sold_block.export_mat])))
+ SSstock_market.materials_prices[sold_block.export_mat] = round(clamp(SSstock_market.materials_prices[sold_block.export_mat], initial(sold_block.export_mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 0.5 , initial(sold_block.export_mat.value_per_unit) * SHEET_MATERIAL_AMOUNT * 3))
diff --git a/code/modules/cargo/gondolapod.dm b/code/modules/cargo/gondolapod.dm
index 5ee9f856823e16..f3dca7a3dca101 100644
--- a/code/modules/cargo/gondolapod.dm
+++ b/code/modules/cargo/gondolapod.dm
@@ -65,11 +65,13 @@
/mob/living/simple_animal/pet/gondola/gondolapod/setOpened()
opened = TRUE
+ SET_PLANE_IMPLICIT(src, GAME_PLANE)
update_appearance()
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom/, setClosed)), 50)
/mob/living/simple_animal/pet/gondola/gondolapod/setClosed()
opened = FALSE
+ SET_PLANE_IMPLICIT(src, GAME_PLANE_FOV_HIDDEN)
update_appearance()
/mob/living/simple_animal/pet/gondola/gondolapod/death()
diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm
index 69677e87df56ad..be8288e922fba7 100644
--- a/code/modules/cargo/goodies.dm
+++ b/code/modules/cargo/goodies.dm
@@ -129,6 +129,12 @@
cost = PAYCHECK_CREW * 4
contains = list(/obj/item/storage/medkit/toxin)
+/datum/supply_pack/goody/bandagebox_singlepack
+ name = "Box of Bandages Single-Pack"
+ desc = "A single box of DeForest brand bandages. For when you don't want to see your doctor."
+ cost = PAYCHECK_CREW * 3
+ contains = list(/obj/item/storage/box/bandages)
+
/datum/supply_pack/goody/toolbox // mostly just to water down coupon probability
name = "Mechanical Toolbox"
desc = "A fully stocked mechanical toolbox, for when you're too lazy to just print them out."
@@ -277,3 +283,9 @@
desc = "A less cheap imported climbing hook. Absolutely no use outside of planetary stations."
cost = PAYCHECK_CREW * 5
contains = list(/obj/item/climbing_hook)
+
+/datum/supply_pack/goody/fish_analyzer
+ name = "Fish Analyzer"
+ desc = "A single analyzer to monitor fish's status and traits with, in case you don't have the technology to print one."
+ cost = CARGO_CRATE_VALUE * 2.5
+ contains = list(/obj/item/fish_analyzer)
diff --git a/code/modules/cargo/materials_market.dm b/code/modules/cargo/materials_market.dm
new file mode 100644
index 00000000000000..e2bedd2f19abf7
--- /dev/null
+++ b/code/modules/cargo/materials_market.dm
@@ -0,0 +1,268 @@
+/obj/machinery/materials_market
+ name = "galactic materials market"
+ desc = "This machine allows the user to buy and sell sheets of minerals \
+ across the system. Prices are known to fluxuate quite often,\
+ sometimes even within the same minute. All transactions are final."
+ circuit = /obj/item/circuitboard/machine/materials_market
+ req_access = list(ACCESS_CARGO)
+ density = TRUE
+ icon = 'icons/obj/economy.dmi'
+ icon_state = "mat_market"
+ base_icon_state = "mat_market"
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION
+ /// What items can be converted into a stock block? Must be a stack subtype based on current implementation.
+ var/static/list/exportable_material_items = list(
+ /obj/item/stack/sheet/iron, //God why are we like this
+ /obj/item/stack/sheet/glass, //No really, God why are we like this
+ /obj/item/stack/sheet/mineral,
+ /obj/item/stack/tile/mineral,
+ /obj/item/stack/ore,
+ /obj/item/stack/sheet/bluespace_crystal,
+ /obj/item/stack/rods
+ )
+ /// Are we ordering sheets from our own card balance or the cargo budget?
+ var/ordering_private = TRUE
+ /// Currently, can we order sheets from our own card balance or the cargo budget?
+ var/can_buy_via_budget = FALSE
+
+/obj/machinery/materials_market/update_icon_state()
+ if(panel_open)
+ icon_state = "[base_icon_state]_open"
+ return ..()
+ if(!is_operational || !anchored)
+ icon_state = "[base_icon_state]_off"
+ return ..()
+ icon_state = "[base_icon_state]"
+ return ..()
+
+/obj/machinery/materials_market/wrench_act(mob/living/user, obj/item/tool)
+ ..()
+ default_unfasten_wrench(user, tool, time = 1.5 SECONDS)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/materials_market/attackby(obj/item/O, mob/user, params)
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]_open", "[base_icon_state]", O))
+ return
+ else if(default_deconstruction_crowbar(O))
+ return
+ if(is_type_in_list(O, exportable_material_items))
+ var/amount = 0
+ var/value = 0
+ var/material_to_export
+ var/obj/item/stack/exportable = O
+ for(var/datum/material/mat as anything in SSstock_market.materials_prices)
+ if(exportable.has_material_type(mat))
+ amount = exportable.amount
+ value = SSstock_market.materials_prices[mat]
+ material_to_export = mat
+ break //This is only for trading non-alloys, so we can break here
+
+ if(!amount)
+ say("Not enough material. Aborting.")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE)
+ return TRUE
+ qdel(exportable)
+ var/obj/item/stock_block/new_block = new /obj/item/stock_block(drop_location())
+ new_block.export_value = amount * value * MARKET_PROFIT_MODIFIER
+ new_block.export_mat = material_to_export
+ new_block.quantity = amount
+ to_chat(user, span_notice("You have created a stock block worth [new_block.export_value] cr! Sell it before it becomes liquid!"))
+ playsound(src, 'sound/machines/synth_yes.ogg', 50, FALSE)
+ return TRUE
+ return ..()
+
+
+/obj/machinery/materials_market/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!anchored)
+ return
+ if(!ui)
+ ui = new(user, src, "MatMarket", name)
+ ui.open()
+
+/obj/machinery/materials_market/ui_data(mob/user)
+ var/data = list()
+ var/material_data
+ for(var/datum/material/traded_mat as anything in SSstock_market.materials_prices)
+ var/trend_string = ""
+ if(SSstock_market.materials_trends[traded_mat] == 0)
+ trend_string = "neutral"
+ else if(SSstock_market.materials_trends[traded_mat] == 1)
+ trend_string = "up"
+ else if(SSstock_market.materials_trends[traded_mat] == -1)
+ trend_string = "down"
+ var/color_string = ""
+ if (initial(traded_mat.greyscale_colors))
+ color_string = splicetext(initial(traded_mat.greyscale_colors), 7, length(initial(traded_mat.greyscale_colors)), "") //slice it to a standard 6 char hex
+ else if(initial(traded_mat.color))
+ color_string = initial(traded_mat.color)
+ material_data += list(list(
+ "name" = initial(traded_mat.name),
+ "price" = SSstock_market.materials_prices[traded_mat],
+ "quantity" = SSstock_market.materials_quantity[traded_mat],
+ "trend" = trend_string,
+ "color" = color_string,
+ ))
+
+ can_buy_via_budget = FALSE
+ var/obj/item/card/id/used_id_card
+ if(isliving(user))
+ var/mob/living/living_user = user
+ used_id_card = living_user.get_idcard(TRUE)
+ can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess())
+
+ var/balance = 0
+ if(!ordering_private)
+ var/datum/bank_account/dept = SSeconomy.get_dep_account(ACCOUNT_CAR)
+ if(dept)
+ balance = dept.account_balance
+ else
+ balance = used_id_card?.registered_account?.account_balance
+
+ var/market_crashing = FALSE
+ if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
+ market_crashing = TRUE
+
+ data["catastrophe"] = market_crashing
+ data["materials"] = material_data
+ data["creditBalance"] = balance
+ data["orderingPrive"] = ordering_private
+ data["canOrderCargo"] = can_buy_via_budget
+ return data
+
+/obj/machinery/materials_market/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ //You must have an ID to be able to do something
+ var/mob/living/living_user = ui.user
+ var/obj/item/card/id/used_id_card = living_user.get_idcard(TRUE)
+ if(isnull(used_id_card))
+ say("No ID Found")
+ return
+
+ switch(action)
+ if("buy")
+ var/material_str = params["material"]
+ var/quantity = text2num(params["quantity"])
+
+ var/datum/material/material_bought
+ var/obj/item/stack/sheet/sheet_to_buy
+ for(var/datum/material/mat as anything in SSstock_market.materials_prices)
+ if(initial(mat.name) == material_str)
+ material_bought = mat
+ break
+ if(!material_bought)
+ CRASH("Invalid material name passed to materials market!")
+
+ //if multiple users open the UI some of them may not have the required access so we recheck
+ var/is_ordering_private = ordering_private
+ if(!(ACCESS_CARGO in used_id_card.GetAccess())) //no cargo access then force private purchase
+ is_ordering_private = TRUE
+
+ var/datum/bank_account/account_payable
+ if(is_ordering_private)
+ account_payable = used_id_card.registered_account
+ else if(can_buy_via_budget)
+ account_payable = SSeconomy.get_dep_account(ACCOUNT_CAR)
+ if(!account_payable)
+ say("No bank account detected!")
+ return
+
+ sheet_to_buy = initial(material_bought.sheet_type)
+ if(!sheet_to_buy)
+ CRASH("Material with no sheet type being sold on materials market!")
+ var/cost = SSstock_market.materials_prices[material_bought] * quantity
+ if(cost > account_payable.account_balance)
+ to_chat(living_user, span_warning("You don't have enough money to buy that!"))
+ return
+
+ var/list/things_to_order = list()
+ things_to_order += (sheet_to_buy)
+ things_to_order[sheet_to_buy] = quantity
+ // We want to count how many stacks of all sheets we're ordering to make sure they don't exceed the limit of 10
+ // If we already have a custom order on SSshuttle, we should add the things to order to that order
+ for(var/datum/supply_order/order in SSshuttle.shopping_list)
+ // Must be a Galactic Materials Market order and payed by the null account(if ordered via cargo budget) or by correct user for private purchase
+ if(order.orderer_rank == "Galactic Materials Market" && ( \
+ (!is_ordering_private && order.paying_account == null) || \
+ (is_ordering_private && order.paying_account != null && order.orderer == living_user) \
+ ))
+ // Check if this order exceeded its limit
+ var/prior_stacks = 0
+ for(var/obj/item/stack/sheet/sheet as anything in order.pack.contains)
+ prior_stacks += ROUND_UP(order.pack.contains[sheet] / 50)
+ if(prior_stacks >= 10)
+ to_chat(usr, span_notice("You already have 10 stacks of sheets on order! Please wait for them to arrive before ordering more."))
+ playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE)
+ return
+ // Append to this order
+ order.append_order(things_to_order, cost)
+ return
+
+ //Now we need to add a cargo order for quantity sheets of material_bought.sheet_type
+ var/datum/supply_pack/custom/minerals/mineral_pack = new(
+ purchaser = is_ordering_private ? living_user : "Cargo", \
+ cost = cost, \
+ contains = things_to_order, \
+ )
+ var/datum/supply_order/new_order = new(
+ pack = mineral_pack,
+ orderer = living_user,
+ orderer_rank = "Galactic Materials Market",
+ orderer_ckey = living_user.ckey,
+ paying_account = is_ordering_private ? account_payable : null,
+ cost_type = "credit",
+ can_be_cancelled = FALSE
+ )
+ say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
+ SSshuttle.shopping_list += new_order
+ return
+ if("toggle_budget")
+ if(!can_buy_via_budget)
+ return
+ ordering_private = !ordering_private
+
+/obj/item/stock_block
+ name = "stock block"
+ desc = "A block of stock. It's worth a certain amount of money, based on a sale on the materials market. Ship it on the cargo shuttle to claim your money."
+ icon = 'icons/obj/economy.dmi'
+ icon_state = "stock_block"
+ /// How many credits was this worth when created?
+ var/export_value = 0
+ /// What is the name of the material this was made from?
+ var/datum/material/export_mat
+ /// Quantity of export material
+ var/quantity = 0
+ /// Is this stock block currently updating it's value with the market (aka fluid)?
+ var/fluid = FALSE
+
+/obj/item/stock_block/examine(mob/user)
+ . = ..()
+ . += span_notice("\The [src] is worth [export_value] cr, from selling [quantity] sheets of [initial(export_mat?.name)].")
+ if(fluid)
+ . += span_warning("\The [src] is currently liquid! It's value is based on the market price.")
+ else
+ . += span_notice("\The [src]'s value is still [span_boldnotice("locked in")]. [span_boldnotice("Sell it")] before it's value becomes liquid!")
+
+/obj/item/stock_block/Initialize(mapload)
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(value_warning)), 2.5 MINUTES)
+ addtimer(CALLBACK(src, PROC_REF(update_value)), 5 MINUTES)
+
+/obj/item/stock_block/proc/value_warning()
+ visible_message(span_warning("\The [src] is starting to become liquid!"))
+ icon_state = "stock_block_fluid"
+ update_appearance(UPDATE_ICON_STATE)
+
+/obj/item/stock_block/proc/update_value()
+ if(!export_mat)
+ return
+ if(!SSstock_market.materials_prices[export_mat])
+ return
+ export_value = quantity * SSstock_market.materials_prices[export_mat] * MARKET_PROFIT_MODIFIER
+ icon_state = "stock_block_liquid"
+ update_appearance(UPDATE_ICON_STATE)
+ visible_message(span_warning("\The [src] becomes liquid!"))
+
diff --git a/code/modules/cargo/order.dm b/code/modules/cargo/order.dm
index 6c1f5e1d8392de..2707719c17005c 100644
--- a/code/modules/cargo/order.dm
+++ b/code/modules/cargo/order.dm
@@ -188,6 +188,15 @@
generateManifest(miscbox, misc_own, "", misc_cost)
return
+/datum/supply_order/proc/append_order(list/new_contents, cost_increase)
+ for(var/i as anything in new_contents)
+ if(pack.contains[i])
+ pack.contains[i] += new_contents[i]
+ else
+ pack.contains += i
+ pack.contains[i] = new_contents[i]
+ pack.cost += cost_increase
+
#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 2c815a803b60bf..b59cee9def7528 100644
--- a/code/modules/cargo/orderconsole.dm
+++ b/code/modules/cargo/orderconsole.dm
@@ -113,9 +113,11 @@
message = blockade_warning
data["message"] = message
+ var/list/amount_by_name = list()
var/cart_list = list()
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(cart_list[order.pack.name])
+ amount_by_name[order.pack.name] += 1
cart_list[order.pack.name][1]["amount"]++
cart_list[order.pack.name][1]["cost"] += order.get_final_cost()
if(order.department_destination)
@@ -124,6 +126,7 @@
cart_list[order.pack.name][1]["paid"]++
continue
+ amount_by_name[order.pack.name] += 1
cart_list[order.pack.name] = list(list(
"cost_type" = order.cost_type,
"object" = order.pack.name,
@@ -141,19 +144,23 @@
data["requests"] = list()
- for(var/datum/supply_order/SO in SSshuttle.request_list)
+ for(var/datum/supply_order/order in SSshuttle.request_list)
+ var/datum/supply_pack/pack = order.pack
+ amount_by_name[pack.name] += 1
data["requests"] += list(list(
- "object" = SO.pack.name,
- "cost" = SO.pack.get_cost(),
- "orderer" = SO.orderer,
- "reason" = SO.reason,
- "id" = SO.id
+ "object" = pack.name,
+ "cost" = pack.get_cost(),
+ "orderer" = order.orderer,
+ "reason" = order.reason,
+ "id" = order.id
))
+ data["amount_by_name"] = amount_by_name
return data
/obj/machinery/computer/cargo/ui_static_data(mob/user)
var/list/data = list()
+ data["max_order"] = CARGO_MAX_ORDER
data["supplies"] = list()
for(var/pack in SSshuttle.supply_packs)
var/datum/supply_pack/P = SSshuttle.supply_packs[pack]
@@ -187,7 +194,7 @@
var/datum/supply_pack/pack = SSshuttle.supply_packs[id]
if(!istype(pack))
CRASH("Unknown supply pack id given by order console ui. ID: [id]")
- if(amount > 50 || amount < 1) // Holy shit fuck off
+ if(amount > CARGO_MAX_ORDER || amount < 1) // Holy shit fuck off
CRASH("Invalid amount passed into add_item")
if((pack.hidden && !(obj_flags & EMAGGED)) || (pack.contraband && !contraband) || pack.drop_pod_only || (pack.special && !pack.special_enabled))
return
@@ -222,8 +229,11 @@
say("[id_card] lacks the requisite access for this purchase.")
return
+ // The list we are operating on right now
+ var/list/working_list = SSshuttle.shopping_list
var/reason = ""
if(requestonly && !self_paid)
+ working_list = SSshuttle.request_list
reason = tgui_input_text(user, "Reason", name)
if(isnull(reason))
return
@@ -233,6 +243,13 @@
say("ERROR: Small crates may only be purchased by private accounts.")
return
+ var/similar_count = SSshuttle.supply.get_order_count(pack)
+ if(similar_count == OVER_ORDER_LIMIT)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
+ return
+
+ amount = clamp(amount, 1, CARGO_MAX_ORDER - similar_count)
for(var/count in 1 to amount)
var/obj/item/coupon/applied_coupon
for(var/obj/item/coupon/coupon_check in loaded_coupons)
@@ -242,15 +259,8 @@
applied_coupon = coupon_check
break
- //Skyrat Edit Add
- var/datum/supply_order/SO = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon, charge_on_purchase = TRUE)
- //Skyrat Edit End
- //SKYRAT EDIT - ORIGINAL: var/datum/supply_order/SO = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon)
-
- if(requestonly && !self_paid)
- SSshuttle.request_list += SO
- else
- SSshuttle.shopping_list += SO
+ var/datum/supply_order/order = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon, charge_on_purchase = TRUE) //SKYRAT EDIT CHANGE - ORIGINAL: 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)
say("Order processed. The price will be charged to [account.account_holder]'s bank account on delivery.")
@@ -365,9 +375,7 @@
if("remove")
var/order_name = params["order_name"]
//try removing atleast one item with the specified name. An order may not be removed if it was from the department
- //also we create an copy of the cart list else we would get runtimes when removing & iterating over the same SSshuttle.shopping_list
- var/list/shopping_cart = SSshuttle.shopping_list.Copy()
- for(var/datum/supply_order/order in shopping_cart)
+ for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(order.pack.name != order_name)
continue
if(remove_item(order.id))
@@ -378,8 +386,7 @@
var/order_name = params["order_name"]
//clear out all orders with the above mentioned order_name name to make space for the new amount
- var/list/shopping_cart = SSshuttle.shopping_list.Copy() //we operate on the list copy else we would get runtimes when removing & iterating over the same SSshuttle.shopping_list
- for(var/datum/supply_order/order in shopping_cart) //find corresponding order id for the order name
+ for(var/datum/supply_order/order in SSshuttle.shopping_list) //find corresponding order id for the order name
if(order.pack.name == order_name)
remove_item(order.id)
@@ -387,7 +394,7 @@
var/amount = text2num(params["amount"])
if(amount == 0)
return TRUE
- if(amount > 50)
+ if(amount > CARGO_MAX_ORDER)
return
var/supply_pack_id = name_to_id(order_name) //map order name to supply pack id for adding
if(!supply_pack_id)
diff --git a/code/modules/cargo/packs/_packs.dm b/code/modules/cargo/packs/_packs.dm
index 357f32f3cb4101..aaeb55f253385c 100644
--- a/code/modules/cargo/packs/_packs.dm
+++ b/code/modules/cargo/packs/_packs.dm
@@ -86,7 +86,7 @@
return FALSE
var/list/open_turfs = list()
for(var/turf/open/floor/found_turf in get_area_turfs(pick(areas), subtypes = TRUE))
- open_turfs += found_turf
+ open_turfs += found_turf
if(!length(open_turfs))
return FALSE
@@ -110,3 +110,15 @@
name = "[purchaser]'s Mining Order"
src.cost = cost
src.contains = contains
+
+/datum/supply_pack/custom/minerals
+ name = "materials order"
+ crate_name = "galactic materials market delivery crate"
+ access = list()
+ crate_type = /obj/structure/closet/crate/cardboard
+
+/datum/supply_pack/custom/minerals/New(purchaser, cost, list/contains)
+ . = ..()
+ name = "[purchaser]'s Materials Order"
+ src.cost = cost
+ src.contains = contains
diff --git a/code/modules/cargo/packs/general.dm b/code/modules/cargo/packs/general.dm
index fa7a3bfa374cbe..99f8972ccb8c7f 100644
--- a/code/modules/cargo/packs/general.dm
+++ b/code/modules/cargo/packs/general.dm
@@ -68,13 +68,6 @@
contains = list(/obj/item/storage/fish_case/tiziran = 2)
crate_name = "tiziran fish crate"
-/datum/supply_pack/misc/fish_analyzers
- name = "Fish Analyzers"
- desc = "A pack containing three analyzers to monitor fish's status and traits with."
- cost = CARGO_CRATE_VALUE * 2.5
- contains = list(/obj/item/fish_analyzer = 3)
- crate_name = "fish analyzers crate"
-
/datum/supply_pack/misc/bicycle
name = "Bicycle"
desc = "Nanotrasen reminds all employees to never toy with powers outside their control."
@@ -221,7 +214,7 @@
/obj/item/clothing/under/misc/burial = 2,
)
crate_name = "religious supplies crate"
-
+
/datum/supply_pack/misc/candles_bulk
name = "Candle Box Crate"
desc = "Keep your local chapel lit with three candle boxes!"
@@ -292,13 +285,6 @@
crate_value = value
contents_uplink_type = uplink
-/datum/supply_pack/misc/fishing_portal
- name = "Fishing Portal Generator Crate"
- desc = "Not enough fish near your location? Fishing portal has your back."
- cost = CARGO_CRATE_VALUE * 4
- contains = list(/obj/machinery/fishing_portal_generator)
- crate_name = "fishing portal crate"
-
/datum/supply_pack/misc/papercutter
name = "Paper Cutters Crate"
desc = "Contains 3 office-grade paper cutters, equipped with sharp blades that can cut any paper into two thin slips.\
diff --git a/code/modules/cargo/packs/imports.dm b/code/modules/cargo/packs/imports.dm
index ad444efc7405eb..2c90d8603d0694 100644
--- a/code/modules/cargo/packs/imports.dm
+++ b/code/modules/cargo/packs/imports.dm
@@ -303,3 +303,17 @@
contraband = TRUE
contains = list(/obj/item/weaponcrafting/giant_wrench)
crate_name = "unknown parts crate"
+
+/datum/supply_pack/imports/materials_market
+ name = "Galactic Materials Market Crate"
+ desc = "A circuit board to build your own materials market for use by certified market traders. Warning: Losses are not covered by insurance."
+ cost = CARGO_CRATE_VALUE * 3
+ contains = list(
+ /obj/item/circuitboard/machine/materials_market = 1,
+ /obj/item/stack/sheet/iron = 5,
+ /obj/item/stack/cable_coil/five = 2,
+ /obj/item/stock_parts/scanning_module = 1,
+ /obj/item/stock_parts/card_reader = 1
+ )
+ crate_name = "materials market crate"
+ crate_type = /obj/structure/closet/crate
diff --git a/code/modules/cargo/packs/livestock.dm b/code/modules/cargo/packs/livestock.dm
index d2bdd904e3b73b..69dd7e61807722 100644
--- a/code/modules/cargo/packs/livestock.dm
+++ b/code/modules/cargo/packs/livestock.dm
@@ -140,7 +140,7 @@
name = "Goat Crate"
desc = "The goat goes baa! Contains one goat. Warranty void if used as a replacement for Pete."
cost = CARGO_CRATE_VALUE * 5
- contains = list(/mob/living/simple_animal/hostile/retaliate/goat)
+ contains = list(/mob/living/basic/goat)
crate_name = "goat crate"
/datum/supply_pack/critter/rabbit
diff --git a/code/modules/cargo/packs/materials.dm b/code/modules/cargo/packs/materials.dm
index 68dacd730be079..ba9a162698bb82 100644
--- a/code/modules/cargo/packs/materials.dm
+++ b/code/modules/cargo/packs/materials.dm
@@ -16,34 +16,6 @@
contains = list(/obj/item/stack/license_plates/empty/fifty)
crate_name = "empty license plate crate"
-/datum/supply_pack/materials/glass50
- name = "50 Glass Sheets"
- desc = "Let some nice light in with fifty glass sheets!"
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/stack/sheet/glass/fifty)
- crate_name = "glass sheets crate"
-
-/datum/supply_pack/materials/iron50
- name = "50 Iron Sheets"
- desc = "Any construction project begins with a good stack of fifty iron sheets!"
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/stack/sheet/iron/fifty)
- crate_name = "iron sheets crate"
-
-/datum/supply_pack/materials/plasteel20
- name = "20 Plasteel Sheets"
- desc = "Reinforce the station's integrity with twenty plasteel sheets!"
- cost = CARGO_CRATE_VALUE * 15
- contains = list(/obj/item/stack/sheet/plasteel/twenty)
- crate_name = "plasteel sheets crate"
-
-/datum/supply_pack/materials/plasteel50
- name = "50 Plasteel Sheets"
- desc = "For when you REALLY have to reinforce something."
- cost = CARGO_CRATE_VALUE * 33
- contains = list(/obj/item/stack/sheet/plasteel/fifty)
- crate_name = "plasteel sheets crate"
-
/datum/supply_pack/materials/plastic50
name = "50 Plastic Sheets"
desc = "Build a limitless amount of toys with fifty plastic sheets!"
diff --git a/code/modules/cargo/packs/medical.dm b/code/modules/cargo/packs/medical.dm
index 6794a1960e7fec..3cfb824b4e6986 100644
--- a/code/modules/cargo/packs/medical.dm
+++ b/code/modules/cargo/packs/medical.dm
@@ -89,6 +89,7 @@
/obj/item/reagent_containers/cup/beaker/large,
/obj/item/reagent_containers/pill/insulin,
/obj/item/stack/medical/gauze,
+ /obj/item/storage/box/bandages,
/obj/item/storage/box/beakers,
/obj/item/storage/box/medigels,
/obj/item/storage/box/syringes,
diff --git a/code/modules/cargo/packs/organic.dm b/code/modules/cargo/packs/organic.dm
index 73c98cb4b3ab32..eb26d5ec907f77 100644
--- a/code/modules/cargo/packs/organic.dm
+++ b/code/modules/cargo/packs/organic.dm
@@ -299,10 +299,12 @@
ONLY 5000 BUX GET NOW! Contains a grill and fuel."
cost = CARGO_CRATE_VALUE * 8
crate_type = /obj/structure/closet/crate
- contains = list(/obj/item/stack/sheet/mineral/coal/five,
- /obj/machinery/grill/unwrenched,
- /obj/item/reagent_containers/cup/soda_cans/monkey_energy,
- )
+ contains = list(
+ /obj/item/stack/sheet/mineral/coal/five,
+ /obj/item/kitchen/tongs,
+ /obj/item/reagent_containers/cup/soda_cans/monkey_energy,
+ /obj/machinery/grill/unwrenched,
+ )
crate_name = "grilling starter kit crate"
/datum/supply_pack/organic/grillfuel
diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm
index 7abe5f7601e627..784b870fea0842 100644
--- a/code/modules/cargo/packs/security.dm
+++ b/code/modules/cargo/packs/security.dm
@@ -225,6 +225,14 @@
crate_name = "energy gun crate"
crate_type = /obj/structure/closet/crate/secure/plasma
+/datum/supply_pack/security/armory/laser_carbine
+ name = "Laser Carbine Crate"
+ desc = "Contains three laser carbines, capable of rapidly firing weak lasers."
+ cost = CARGO_CRATE_VALUE * 9
+ contains = list(/obj/item/gun/energy/laser/carbine = 3)
+ crate_name = "laser carbine 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/stock_market_items.dm b/code/modules/cargo/packs/stock_market_items.dm
new file mode 100644
index 00000000000000..9744bdf7400ea3
--- /dev/null
+++ b/code/modules/cargo/packs/stock_market_items.dm
@@ -0,0 +1,36 @@
+/**
+ * todo: make this a supply_pack/custom. Drop pog? ohoho yes. Would be VERY fun.
+ */
+/datum/supply_pack/market_materials
+ name = "A Single Sheet of Bananium"
+ desc = "Going market price for this kind of sheet, by Australicus Industrial Mining."
+ cost = CARGO_CRATE_VALUE * 2
+ // contains = list(/obj/item/stack/sheet/mineral/bananium)
+ crate_name = "mineral stock sheet crate"
+ group = "Canisters & Materials"
+ /// What material we are trying to buy sheets of?
+ var/datum/material/material
+ /// How many sheets of the material we are trying to buy at once?
+ var/amount
+
+/datum/supply_pack/market_materials/get_cost()
+ for(var/datum/material/mat as anything in SSstock_market.materials_prices)
+ if(material == mat)
+ return SSstock_market.materials_prices[mat] * amount
+
+/datum/supply_pack/market_materials/fill(obj/structure/closet/crate/C)
+ . = ..()
+ new material.sheet_type(C, amount)
+
+/datum/supply_pack/market_materials/iron
+ name = "Iron Sheets"
+ crate_name = "iron stock crate"
+ material = /datum/material/iron
+MARKET_QUANTITY_HELPERS(/datum/supply_pack/market_materials/iron)
+
+
+/datum/supply_pack/market_materials/gold
+ name = "Gold Sheets"
+ crate_name = "gold stock crate"
+ material = /datum/material/gold
+MARKET_QUANTITY_HELPERS(/datum/supply_pack/market_materials/gold)
diff --git a/code/modules/cargo/supplypod.dm b/code/modules/cargo/supplypod.dm
index feb6c8f8ae6496..ed75e6349676fa 100644
--- a/code/modules/cargo/supplypod.dm
+++ b/code/modules/cargo/supplypod.dm
@@ -222,6 +222,9 @@
stay_after_drop = FALSE
holder.pixel_z = initial(holder.pixel_z)
holder.alpha = initial(holder.alpha)
+ if (holder != src)
+ contents |= holder.contents
+ qdel(holder)
var/shippingLane = GLOB.areas_by_type[/area/centcom/central_command_areas/supplypod/supplypod_temp_holding]
forceMove(shippingLane) //Move to the centcom-z-level until the pod_landingzone says we can drop back down again
if (!reverse_dropoff_coords) //If we're centcom-launched, the reverse dropoff turf will be a centcom loading bay. If we're an extraction pod, it should be the ninja jail. Thus, this shouldn't ever really happen.
@@ -276,7 +279,7 @@
if (effectGib) //effectGib is on, that means whatever's underneath us better be fucking oof'd on
target_living.adjustBruteLoss(5000) //THATS A LOT OF DAMAGE (called just in case gib() doesnt work on em)
if (!QDELETED(target_living))
- target_living.gib() //After adjusting the fuck outta that brute loss we finish the job with some satisfying gibs
+ target_living.gib(DROP_ALL_REMAINS) //After adjusting the fuck outta that brute loss we finish the job with some satisfying gibs
else
target_living.adjustBruteLoss(damage)
var/explosion_sum = B[1] + B[2] + B[3] + B[4]
@@ -293,6 +296,8 @@
if (style == STYLE_GONDOLA) //Checks if we are supposed to be a gondola pod. If so, create a gondolapod mob, and move this pod to nullspace. I'd like to give a shout out, to my man oranges
var/mob/living/simple_animal/pet/gondola/gondolapod/benis = new(turf_underneath, src)
benis.contents |= contents //Move the contents of this supplypod into the gondolapod mob.
+ for (var/mob/living/mob_in_pod in benis.contents)
+ mob_in_pod.reset_perspective(null)
moveToNullspace()
addtimer(CALLBACK(src, PROC_REF(open_pod), benis), delays[POD_OPENING]) //After the opening delay passes, we use the open proc from this supplyprod while referencing the contents of the "holder", in this case the gondolapod mob
else if (style == STYLE_SEETHROUGH)
@@ -315,7 +320,7 @@
playsound(get_turf(holder), openingSound, soundVolume, FALSE, FALSE) //Special admin sound to play
for (var/turf_type in turfs_in_cargo)
turf_underneath.PlaceOnTop(turf_type)
- for (var/cargo in contents)
+ for (var/cargo in holder.contents)
var/atom/movable/movable_cargo = cargo
movable_cargo.forceMove(turf_underneath)
if (!effectQuiet && !openingSound && style != STYLE_SEETHROUGH && !(pod_flags & FIRST_SOUNDS)) //If we aren't being quiet, play the default pod open sound
@@ -487,7 +492,8 @@
. = ..()
if(same_z_layer)
return
- SET_PLANE_EXPLICIT(glow_effect, ABOVE_GAME_PLANE, src)
+ if(glow_effect)
+ SET_PLANE_EXPLICIT(glow_effect, ABOVE_GAME_PLANE, src)
/obj/structure/closet/supplypod/proc/endGlow()
if(!glow_effect)
@@ -537,7 +543,7 @@
/obj/effect/supplypod_smoke/proc/drawSelf(amount)
alpha = max(0, 255-(amount*20))
-/obj/effect/supplypod_rubble //This is the object that forceMoves the supplypod to it's location
+/obj/effect/supplypod_rubble
name = "debris"
desc = "A small crater of rubble. Closer inspection reveals the debris to be made primarily of space-grade metal fragments. You're pretty sure that this will disperse before too long."
icon = 'icons/obj/supplypods.dmi'
diff --git a/code/modules/client/client_colour.dm b/code/modules/client/client_colour.dm
index 444b4d7ec121a6..707267e7143137 100644
--- a/code/modules/client/client_colour.dm
+++ b/code/modules/client/client_colour.dm
@@ -221,6 +221,10 @@
override = TRUE
colour = list(0.8,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0)
+/datum/client_colour/manual_heart_blood
+ priority = PRIORITY_ABSOLUTE
+ colour = COLOR_RED
+
#undef PRIORITY_ABSOLUTE
#undef PRIORITY_HIGH
#undef PRIORITY_NORMAL
diff --git a/code/modules/client/player_details.dm b/code/modules/client/player_details.dm
index 24c6754b95ce1b..8931dffcdb465d 100644
--- a/code/modules/client/player_details.dm
+++ b/code/modules/client/player_details.dm
@@ -2,14 +2,26 @@
///assoc list of ckey -> /datum/player_details
GLOBAL_LIST_EMPTY(player_details)
+/// Tracks information about a client between log in and log outs
/datum/player_details
- var/list/player_actions = list()
+ /// Action datums assigned to this player
+ var/list/datum/action/player_actions = list()
+ /// Tracks client action logging
var/list/logging = list()
+ /// Callbacks invoked when this client logs in again
var/list/post_login_callbacks = list()
+ /// Callbacks invoked when this client logs out
var/list/post_logout_callbacks = list()
- var/list/played_names = list() //List of names this key played under this round
+ /// List of names this key played under this round
+ var/list/played_names = list()
+ /// Lazylist of preference slots this client has joined the round under
+ /// Numbers are stored as strings
+ var/list/joined_as_slots
+ /// Version of byond this client is using
var/byond_version = "Unknown"
+ /// Tracks achievements they have earned
var/datum/achievement_data/achievements
+ /// World.time this player last died
var/time_of_death = 0
/datum/player_details/New(key)
diff --git a/code/modules/client/preferences/middleware/antags.dm b/code/modules/client/preferences/middleware/antags.dm
index 3e51218edf2896..888da90d5b79c5 100644
--- a/code/modules/client/preferences/middleware/antags.dm
+++ b/code/modules/client/preferences/middleware/antags.dm
@@ -116,6 +116,7 @@
/datum/asset/spritesheet/antagonists/create_spritesheets()
// Antagonists that don't have a dynamic ruleset, but do have a preference
var/static/list/non_ruleset_antagonists = list(
+ ROLE_CYBER_POLICE = /datum/antagonist/cyber_police,
ROLE_FUGITIVE = /datum/antagonist/fugitive,
ROLE_LONE_OPERATIVE = /datum/antagonist/nukeop/lone,
ROLE_DRIFTING_CONTRACTOR = /datum/antagonist/contractor, //SKYRAT EDIT
diff --git a/code/modules/client/preferences/species_features/basic.dm b/code/modules/client/preferences/species_features/basic.dm
index faa86cc07e2f18..6e34a234e7d01b 100644
--- a/code/modules/client/preferences/species_features/basic.dm
+++ b/code/modules/client/preferences/species_features/basic.dm
@@ -4,16 +4,13 @@
head_icon = icon('icons/mob/human/bodyparts_greyscale.dmi', "human_head_m")
head_icon.Blend(skintone2hex("caucasian1"), ICON_MULTIPLY)
- if (isnull(sprite_accessory))
- return head_icon
-
- ASSERT(istype(sprite_accessory))
-
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)
- head_accessory_icon.Blend(COLOR_DARK_BROWN, ICON_MULTIPLY)
- final_icon.Blend(head_accessory_icon, ICON_OVERLAY)
+ var/icon/head_accessory_icon = icon(sprite_accessory.icon, sprite_accessory.icon_state)
+ head_accessory_icon.Blend(COLOR_DARK_BROWN, ICON_MULTIPLY)
+ final_icon.Blend(head_accessory_icon, ICON_OVERLAY)
final_icon.Crop(10, 19, 22, 31)
final_icon.Scale(32, 32)
diff --git a/code/modules/clothing/head/hat.dm b/code/modules/clothing/head/hat.dm
index d99c5b0c429315..83c3890eb57f06 100644
--- a/code/modules/clothing/head/hat.dm
+++ b/code/modules/clothing/head/hat.dm
@@ -135,6 +135,10 @@
worn_icon_state = "cowboy_hat_black"
inhand_icon_state = "cowboy_hat_black"
+/// More likely to intercept bullets, since you're likely to not be wearing your modsuit with this on
+/obj/item/clothing/head/cowboy/black/syndicate
+ deflect_chance = 25
+
/obj/item/clothing/head/cowboy/white
name = "ten-gallon hat"
desc = "There are two kinds of people in the world: those with guns and those that dig. You dig?"
diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm
index 76194e721b6859..3c26de45e642ff 100644
--- a/code/modules/clothing/head/jobs.dm
+++ b/code/modules/clothing/head/jobs.dm
@@ -154,6 +154,13 @@
flags_inv = HIDEHAIR
flags_cover = HEADCOVERSEYES
+/obj/item/clothing/head/chaplain/habit_veil
+ name = "nun veil"
+ desc = "No nunsene clothing."
+ icon_state = "nun_hood_alt"
+ flags_inv = HIDEHAIR | HIDEEARS
+ clothing_flags = SNUG_FIT // can't be knocked off by throwing a paper hat.
+
/obj/item/clothing/head/chaplain/bishopmitre
name = "bishop mitre"
desc = "An opulent hat that functions as a radio to God. Or as a lightning rod, depending on who you ask."
diff --git a/code/modules/clothing/head/mind_monkey_helmet.dm b/code/modules/clothing/head/mind_monkey_helmet.dm
index c0463fed74c0aa..d1c423b5b038c8 100644
--- a/code/modules/clothing/head/mind_monkey_helmet.dm
+++ b/code/modules/clothing/head/mind_monkey_helmet.dm
@@ -89,7 +89,7 @@
if(3) //primal gene (gorilla)
magnification.gorillize()
if(4) //genetic mass susceptibility (gib)
- magnification.gib()
+ magnification.gib(DROP_ALL_REMAINS)
//either used up correctly or taken off before polling finished (punish this by destroying the helmet)
UnregisterSignal(magnification, COMSIG_SPECIES_LOSS)
playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE)
diff --git a/code/modules/clothing/masks/moustache.dm b/code/modules/clothing/masks/moustache.dm
index ecfd5d5e007965..aaf59be51e4fdb 100644
--- a/code/modules/clothing/masks/moustache.dm
+++ b/code/modules/clothing/masks/moustache.dm
@@ -2,6 +2,7 @@
name = "fake moustache"
desc = "Warning: moustache is fake."
icon_state = "fake-moustache"
+ alternate_worn_layer = ABOVE_BODY_FRONT_HEAD_LAYER
w_class = WEIGHT_CLASS_TINY
flags_inv = HIDEFACE
species_exception = list(/datum/species/golem)
diff --git a/code/modules/clothing/outfits/plasmaman.dm b/code/modules/clothing/outfits/plasmaman.dm
index daad81ea475b57..a422d2d736e92b 100644
--- a/code/modules/clothing/outfits/plasmaman.dm
+++ b/code/modules/clothing/outfits/plasmaman.dm
@@ -281,3 +281,10 @@
gloves = /obj/item/clothing/gloves/color/plasmaman/clown
head = /obj/item/clothing/head/helmet/space/plasmaman/clown
mask = /obj/item/clothing/mask/gas/clown_hat/plasmaman
+
+/datum/outfit/plasmaman/bitrunner
+ name = "Bitrunner Plasmaman"
+
+ uniform = /obj/item/clothing/under/plasmaman/bitrunner
+ gloves = /obj/item/clothing/gloves/color/plasmaman/black
+ head = /obj/item/clothing/head/helmet/space/plasmaman/bitrunner
diff --git a/code/modules/clothing/shoes/_shoes.dm b/code/modules/clothing/shoes/_shoes.dm
index aa88961e3c465c..118c28c0e5166e 100644
--- a/code/modules/clothing/shoes/_shoes.dm
+++ b/code/modules/clothing/shoes/_shoes.dm
@@ -208,7 +208,7 @@
to_chat(our_guy, span_userdanger("You stamp on [user]'s hand! What the- [user.p_they()] [user.p_were()] [tied ? "knotting" : "untying"] your shoelaces!"))
user.emote("scream")
if(istype(L))
- var/obj/item/bodypart/ouchie = L.get_bodypart(pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ var/obj/item/bodypart/ouchie = L.get_bodypart(pick(GLOB.arm_zones))
if(ouchie)
ouchie.receive_damage(brute = 10)
L.adjustStaminaLoss(40)
diff --git a/code/modules/clothing/shoes/cowboy.dm b/code/modules/clothing/shoes/cowboy.dm
index 05792a72cbd960..0aa518bc1364d8 100644
--- a/code/modules/clothing/shoes/cowboy.dm
+++ b/code/modules/clothing/shoes/cowboy.dm
@@ -6,6 +6,10 @@
custom_price = PAYCHECK_CREW
var/max_occupants = 4
can_be_tied = FALSE
+ /// Do these boots have spur sounds?
+ var/has_spurs = FALSE
+ /// The jingle jangle jingle of our spurs
+ var/list/spur_sound = list('sound/effects/footstep/spurs1.ogg'=1,'sound/effects/footstep/spurs2.ogg'=1,'sound/effects/footstep/spurs3.ogg'=1)
/datum/armor/shoes_cowboy
bio = 90
@@ -19,6 +23,9 @@
//There's a snake in my boot
new /mob/living/basic/snake(src)
+ if(has_spurs)
+ LoadComponent(/datum/component/squeak, spur_sound, 50, falloff_exponent = 20)
+
/obj/item/clothing/shoes/cowboy/equipped(mob/living/carbon/user, slot)
. = ..()
@@ -97,3 +104,10 @@
name = "\improper Hugs-The-Feet lizard skin boots"
desc = "A pair of masterfully crafted lizard skin boots. Finally a good application for the station's most bothersome inhabitants."
icon_state = "lizardboots_blue"
+
+/// Shoes for the nuke-ops cowboy fit
+/obj/item/clothing/shoes/cowboy/black/syndicate
+ name = "black spurred cowboy boots"
+ desc = "And they sing, oh, ain't you glad you're single? And that song ain't so very far from wrong."
+ armor_type = /datum/armor/shoes_combat
+ has_spurs = TRUE
diff --git a/code/modules/clothing/spacesuits/plasmamen.dm b/code/modules/clothing/spacesuits/plasmamen.dm
index ceb31b23a28488..30e43c793aacff 100644
--- a/code/modules/clothing/spacesuits/plasmamen.dm
+++ b/code/modules/clothing/spacesuits/plasmamen.dm
@@ -443,3 +443,8 @@
or they've murdered one of your fellow badasses and have taken it from them as a trophy. Either way, anyone wearing this deserves at least a cursory nod of respect."
icon_state = "syndie_envirohelm"
inhand_icon_state = null
+
+/obj/item/clothing/head/helmet/space/plasmaman/bitrunner
+ name = "bitrunner's plasma envirosuit helmet"
+ desc = "An envirohelmet with extended blue light filters for bitrunning plasmamen."
+ icon_state = "bitrunner_envirohelm"
diff --git a/code/modules/clothing/suits/_suits.dm b/code/modules/clothing/suits/_suits.dm
index 13c9c358fac930..84cb98049e1b1a 100644
--- a/code/modules/clothing/suits/_suits.dm
+++ b/code/modules/clothing/suits/_suits.dm
@@ -8,6 +8,7 @@
/obj/item/tank/internals/emergency_oxygen,
/obj/item/tank/internals/plasmaman,
/obj/item/tank/jetpack/oxygen/captain,
+ /obj/item/storage/belt/holster,
)
armor_type = /datum/armor/none
drop_sound = 'sound/items/handling/cloth_drop.ogg'
diff --git a/code/modules/clothing/suits/jacket.dm b/code/modules/clothing/suits/jacket.dm
index ffd67ce4a65aa2..6db889032c064d 100644
--- a/code/modules/clothing/suits/jacket.dm
+++ b/code/modules/clothing/suits/jacket.dm
@@ -1,7 +1,16 @@
/obj/item/clothing/suit/jacket
icon = 'icons/obj/clothing/suits/jacket.dmi'
worn_icon = 'icons/mob/clothing/suits/jacket.dmi'
- allowed = list(/obj/item/flashlight, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/plasmaman, /obj/item/toy, /obj/item/storage/fancy/cigarettes, /obj/item/lighter, /obj/item/radio)
+ allowed = list(
+ /obj/item/flashlight,
+ /obj/item/tank/internals/emergency_oxygen,
+ /obj/item/tank/internals/plasmaman,
+ /obj/item/toy,
+ /obj/item/storage/fancy/cigarettes,
+ /obj/item/lighter,
+ /obj/item/radio,
+ /obj/item/storage/belt/holster,
+ )
body_parts_covered = CHEST|GROIN|ARMS
cold_protection = CHEST|GROIN|ARMS
min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT
diff --git a/code/modules/clothing/suits/wintercoats.dm b/code/modules/clothing/suits/wintercoats.dm
index 60cbcae1473351..283878339fe323 100644
--- a/code/modules/clothing/suits/wintercoats.dm
+++ b/code/modules/clothing/suits/wintercoats.dm
@@ -247,8 +247,10 @@
icon_state = "coatjanitor"
inhand_icon_state = null
allowed = list(
+ /obj/item/access_key,
/obj/item/grenade/chem_grenade,
/obj/item/holosign_creator,
+ /obj/item/key/janitor,
/obj/item/reagent_containers/cup/beaker,
/obj/item/reagent_containers/cup/bottle,
/obj/item/reagent_containers/cup/tube,
diff --git a/code/modules/clothing/under/costume.dm b/code/modules/clothing/under/costume.dm
index 6ef6489f3893de..c7be95178e4d14 100644
--- a/code/modules/clothing/under/costume.dm
+++ b/code/modules/clothing/under/costume.dm
@@ -362,6 +362,11 @@
inhand_icon_state = null
can_adjust = FALSE
+// For the nuke-ops cowboy fit. Sadly no Lone Ranger fit & I don't wanna bloat costume files further.
+/obj/item/clothing/under/costume/dutch/syndicate
+ desc = "You can feel a god damn plan coming on, and the armor lining in this suit'll do wonders in makin' it work."
+ armor_type = /datum/armor/clothing_under/syndicate
+
/obj/item/clothing/under/costume/osi
name = "O.S.I. jumpsuit"
icon_state = "osi_jumpsuit"
diff --git a/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm b/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
index 1590fa77138f1f..a8674b03c943a2 100644
--- a/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
+++ b/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
@@ -115,6 +115,12 @@
icon_state = "clown_envirosuit"
inhand_icon_state = null
+/obj/item/clothing/under/plasmaman/bitrunner
+ name = "bitrunner envirosuit"
+ desc = "An envirosuit specially designed for plasmamen with bad posture."
+ icon_state = "bitrunner_envirosuit"
+ inhand_icon_state = null
+
/obj/item/clothing/under/plasmaman/clown/Initialize(mapload)
. = ..()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_CLOWN, CELL_VIRUS_TABLE_GENERIC, rand(2,3), 0)
diff --git a/code/modules/clothing/under/jobs/cargo.dm b/code/modules/clothing/under/jobs/cargo.dm
index 4b2e74bff3820d..e3145fb740d71d 100644
--- a/code/modules/clothing/under/jobs/cargo.dm
+++ b/code/modules/clothing/under/jobs/cargo.dm
@@ -62,3 +62,9 @@
desc = "A grey uniform for operating in hazardous environments."
icon_state = "explorer"
inhand_icon_state = null
+
+/obj/item/clothing/under/rank/cargo/bitrunner
+ name = "bitrunner's jumpsuit"
+ desc = "It's a leathery jumpsuit worn by a bitrunner. Tacky, but comfortable to wear if sitting for prolonged periods of time."
+ icon_state = "bitrunner"
+ inhand_icon_state = "w_suit"
diff --git a/code/modules/economy/account.dm b/code/modules/economy/account.dm
index 3be319fd233381..b4703945092cc0 100644
--- a/code/modules/economy/account.dm
+++ b/code/modules/economy/account.dm
@@ -7,6 +7,8 @@
var/account_balance = 0
///How many mining points (shaft miner credits) is held in the bank account, used for mining vendors.
var/mining_points = 0
+ /// Points for bit runner's vendor. Awarded for completing virtual domains.
+ var/bitrunning_points = 0
///Debt. If higher than 0, A portion of the credits is earned (or the whole debt, whichever is lower) will go toward paying it off.
var/account_debt = 0
///If there are things effecting how much income a player will get, it's reflected here 1 is standard for humans.
diff --git a/code/modules/events/ghost_role/revenant_event.dm b/code/modules/events/ghost_role/revenant_event.dm
index 27f3597a7ad2ad..f739a3e13d46bc 100644
--- a/code/modules/events/ghost_role/revenant_event.dm
+++ b/code/modules/events/ghost_role/revenant_event.dm
@@ -54,11 +54,12 @@
if(!spawn_locs.len) //If we can't find THAT, then just give up and cry
return MAP_ERROR
- var/mob/living/simple_animal/revenant/revvie = new(pick(spawn_locs))
- revvie.key = selected.key
+ var/mob/living/basic/revenant/revvie = new(pick(spawn_locs))
+ selected.mind.transfer_to(revvie)
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
+ qdel(selected)
return SUCCESSFUL_SPAWN
#undef REVENANT_SPAWN_THRESHOLD
diff --git a/code/modules/events/ghost_role/sentience.dm b/code/modules/events/ghost_role/sentience.dm
index da3802786084cb..abc57d33a07583 100644
--- a/code/modules/events/ghost_role/sentience.dm
+++ b/code/modules/events/ghost_role/sentience.dm
@@ -4,20 +4,20 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list(
/mob/living/basic/carp/pet/cayenne,
/mob/living/basic/chicken,
/mob/living/basic/cow,
+ /mob/living/basic/goat,
/mob/living/basic/lizard,
/mob/living/basic/mouse/brown/tom,
/mob/living/basic/pet,
/mob/living/basic/pig,
/mob/living/basic/rabbit,
/mob/living/basic/sheep,
+ /mob/living/basic/sloth,
/mob/living/basic/snake,
/mob/living/basic/spider/giant/sgt_araneus,
/mob/living/simple_animal/bot/secbot/beepsky,
- /mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet,
- /mob/living/simple_animal/sloth,
)))
/datum/round_event_control/sentience
diff --git a/code/modules/events/space_vines/vine_structure.dm b/code/modules/events/space_vines/vine_structure.dm
index 47357d558ace3b..155a4c5c083c1f 100644
--- a/code/modules/events/space_vines/vine_structure.dm
+++ b/code/modules/events/space_vines/vine_structure.dm
@@ -37,6 +37,7 @@
)
AddElement(/datum/element/connect_loc, loc_connections)
AddElement(/datum/element/atmos_sensitive, mapload)
+ AddComponent(/datum/component/storm_hating)
/obj/structure/spacevine/examine(mob/user)
. = ..()
@@ -147,7 +148,7 @@
break //only capture one mob at a time
/obj/structure/spacevine/proc/entangle(mob/living/victim)
- if(!victim || isvineimmune(victim))
+ if(isnull(victim) || isvineimmune(victim))
return
for(var/datum/spacevine_mutation/mutation in mutations)
mutation.on_buckle(src, victim)
@@ -157,7 +158,7 @@
/// Finds a target tile to spread to. If checks pass it will spread to it and also proc on_spread on target.
/obj/structure/spacevine/proc/spread()
- if(!master) //If we've lost our controller, something has gone terribly wrong.
+ if(isnull(master)) //If we've lost our controller, something has gone terribly wrong.
return
var/direction = pick(GLOB.cardinals)
@@ -165,20 +166,29 @@
if(!istype(stepturf))
return
- if(!isspaceturf(stepturf) && stepturf.Enter(src))
- var/obj/structure/spacevine/spot_taken = locate() in stepturf //Locates any vine on target turf. Calls that vine "spot_taken".
- var/datum/spacevine_mutation/vine_eating/eating = locate() in mutations //Locates the vine eating trait in our own seed and calls it E.
- if(!spot_taken || (eating && (spot_taken && !spot_taken.mutations?.Find(eating)))) //Proceed if there isn't a vine on the target turf, OR we have vine eater AND target vine is from our seed and doesn't. Vines from other seeds are eaten regardless.
- for(var/datum/spacevine_mutation/mutation in mutations)
- mutation.on_spread(src, stepturf) //Only do the on_spread proc if it actually spreads.
- stepturf = get_step(src,direction) //in case turf changes, to make sure no runtimes happen
- var/obj/structure/spacevine/spawning_vine = master.spawn_spacevine_piece(stepturf, src) //Let's do a cool little animate
- if(NSCOMPONENT(direction))
- spawning_vine.pixel_y = direction == NORTH ? -32 : 32
- animate(spawning_vine, pixel_y = 0, time = 1 SECONDS)
- else
- spawning_vine.pixel_x = direction == EAST ? -32 : 32
- animate(spawning_vine, pixel_x = 0, time = 1 SECONDS)
+ if(isspaceturf(stepturf) || isopenspaceturf(stepturf) || !stepturf.Enter(src))
+ return
+ if(ischasm(stepturf) && !HAS_TRAIT(stepturf, TRAIT_CHASM_STOPPED))
+ return
+ if(islava(stepturf) && !HAS_TRAIT(stepturf, TRAIT_LAVA_STOPPED))
+ return
+ var/obj/structure/spacevine/spot_taken = locate() in stepturf
+ var/datum/spacevine_mutation/vine_eating/eating = locate() in mutations
+ if(!isnull(spot_taken)) //Proceed if there isn't a vine on the target turf, OR we have vine eater AND target vine is from our seed and doesn't.
+ if (isnull(eating))
+ return
+ if (spot_taken.mutations?.Find(eating))
+ return
+ for(var/datum/spacevine_mutation/mutation in mutations)
+ mutation.on_spread(src, stepturf)
+ stepturf = get_step(src, direction)
+ var/obj/structure/spacevine/spawning_vine = master.spawn_spacevine_piece(stepturf, src)
+ if(NSCOMPONENT(direction))
+ spawning_vine.pixel_y = direction == NORTH ? -32 : 32
+ animate(spawning_vine, pixel_y = 0, time = 1 SECONDS)
+ else
+ spawning_vine.pixel_x = direction == EAST ? -32 : 32
+ animate(spawning_vine, pixel_x = 0, time = 1 SECONDS)
/// Destroying an explosive vine sets off a chain reaction
/obj/structure/spacevine/ex_act(severity, target)
diff --git a/code/modules/events/wizard/blobies.dm b/code/modules/events/wizard/blobies.dm
index 307d01ff7eb429..0a9c96d513545b 100644
--- a/code/modules/events/wizard/blobies.dm
+++ b/code/modules/events/wizard/blobies.dm
@@ -10,4 +10,4 @@
/datum/round_event/wizard/blobies/start()
for(var/mob/living/carbon/human/H in GLOB.dead_mob_list)
- new /mob/living/simple_animal/hostile/blob/blobspore(H.loc)
+ new /mob/living/basic/blob_minion/spore/minion(H.loc) // Creates zombies which ghosts can control
diff --git a/code/modules/events/wizard/petsplosion.dm b/code/modules/events/wizard/petsplosion.dm
index 33f7718f740baf..a9664d8dd47e29 100644
--- a/code/modules/events/wizard/petsplosion.dm
+++ b/code/modules/events/wizard/petsplosion.dm
@@ -5,6 +5,7 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list(
/mob/living/basic/carp/pet/cayenne,
/mob/living/basic/chicken,
/mob/living/basic/cow,
+ /mob/living/basic/goat,
/mob/living/basic/lizard,
/mob/living/basic/mothroach,
/mob/living/basic/mouse/brown/tom,
@@ -12,13 +13,12 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list(
/mob/living/basic/pig,
/mob/living/basic/rabbit,
/mob/living/basic/sheep,
+ /mob/living/basic/sloth,
/mob/living/basic/snake,
/mob/living/basic/spider/giant/sgt_araneus,
- /mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet,
- /mob/living/simple_animal/sloth,
)))
/datum/round_event_control/wizard/petsplosion //the horror
diff --git a/code/modules/experisci/destructive_scanner.dm b/code/modules/experisci/destructive_scanner.dm
index 3591a31cd7b085..de0b03b0f09202 100644
--- a/code/modules/experisci/destructive_scanner.dm
+++ b/code/modules/experisci/destructive_scanner.dm
@@ -19,10 +19,17 @@
// Late load to ensure the component initialization occurs after the machines are initialized
/obj/machinery/destructive_scanner/LateInitialize()
. = ..()
+
+ var/static/list/destructive_signals = list(
+ COMSIG_MACHINERY_DESTRUCTIVE_SCAN = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_destructive_experiment),
+ )
+
AddComponent(/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/scanning),\
config_mode = EXPERIMENT_CONFIG_CLICK, \
- start_experiment_callback = CALLBACK(src, PROC_REF(activate)))
+ start_experiment_callback = CALLBACK(src, PROC_REF(activate)), \
+ experiment_signals = destructive_signals, \
+ )
///Activates the machine; checks if it can actually scan, then starts.
/obj/machinery/destructive_scanner/proc/activate()
@@ -82,7 +89,7 @@
if(isliving(movable_atom))
var/mob/living/fucked_up_thing = movable_atom
fucked_up_thing.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- fucked_up_thing.gib()
+ fucked_up_thing.gib(DROP_ALL_REMAINS)
SEND_SIGNAL(src, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, scanned_atoms)
diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm
index 1959a849598a58..1259f56597d6da 100644
--- a/code/modules/experisci/experiment/experiments.dm
+++ b/code/modules/experisci/experiment/experiments.dm
@@ -291,7 +291,6 @@
/obj/machinery/rnd/experimentor = 1,
/obj/machinery/medical_kiosk = 2,
/obj/machinery/piratepad/civilian = 2,
- /obj/machinery/rnd/bepis = 3
)
required_stock_part = /obj/item/stock_parts/scanning_module/adv
@@ -332,7 +331,7 @@
///Damage percent that each mech needs to be at for a scan to work.
var/damage_percent
-/datum/experiment/scanning/random/mecha_damage_scan/New()
+/datum/experiment/scanning/random/mecha_damage_scan/New(datum/techweb/techweb)
. = ..()
damage_percent = rand(15, 95)
//updating the description with the damage_percent var set
diff --git a/code/modules/experisci/experiment/handlers/experiment_handler.dm b/code/modules/experisci/experiment/handlers/experiment_handler.dm
index abc5d4ad1dd640..29e7da95391554 100644
--- a/code/modules/experisci/experiment/handlers/experiment_handler.dm
+++ b/code/modules/experisci/experiment/handlers/experiment_handler.dm
@@ -38,6 +38,7 @@
disallowed_traits = null,
config_flags = null,
datum/callback/start_experiment_callback = null,
+ list/experiment_signals
)
. = ..()
if(!ismovable(parent))
@@ -49,13 +50,8 @@
src.config_flags = config_flags
src.start_experiment_callback = start_experiment_callback
- if(isitem(parent))
- RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(try_run_handheld_experiment))
- RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, PROC_REF(ignored_handheld_experiment_attempt))
- if(istype(parent, /obj/machinery/destructive_scanner))
- RegisterSignal(parent, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, PROC_REF(try_run_destructive_experiment))
- if(istype(parent, /obj/machinery/computer/operating))
- RegisterSignal(parent, COMSIG_OPERATING_COMPUTER_AUTOPSY_COMPLETE, PROC_REF(try_run_autopsy_experiment))
+ for(var/signal in experiment_signals)
+ RegisterSignal(parent, signal, experiment_signals[signal])
// Determine UI display mode
switch(config_mode)
@@ -85,9 +81,9 @@
*/
/datum/component/experiment_handler/proc/try_run_handheld_experiment(datum/source, atom/target, mob/user, params)
SIGNAL_HANDLER
- if (!should_run_handheld_experiment(source, target, user, params))
+ if (!should_run_handheld_experiment(source, target, user))
return
- INVOKE_ASYNC(src, PROC_REF(try_run_handheld_experiment_async), source, target, user, params)
+ INVOKE_ASYNC(src, PROC_REF(try_run_handheld_experiment_async), source, target, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/**
@@ -98,7 +94,7 @@
if (!proximity_flag)
return
. |= COMPONENT_AFTERATTACK_PROCESSED_ITEM
- if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
+ if ((selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE)) || config_flags & EXPERIMENT_CONFIG_SILENT_FAIL)
return .
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, span_notice("[target] is not related to your currently selected experiment."))
@@ -107,7 +103,7 @@
/**
* Checks that an experiment can be run using the provided target, used for preventing the cancellation of the attack chain inappropriately
*/
-/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user, params)
+/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user)
// Check that there is actually an experiment selected
if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
return
@@ -127,16 +123,17 @@
/**
* This proc exists because Jared Fogle really likes async
*/
-/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user, params)
+/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user)
if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
- to_chat(user, span_notice("You do not have an experiment selected!"))
+ if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
+ to_chat(user, span_notice("You do not have an experiment selected!"))
return
- if(!do_after(user, 1 SECONDS, target = target))
+ if(!(config_flags & EXPERIMENT_CONFIG_IMMEDIATE_ACTION) && !do_after(user, 1 SECONDS, target = target))
return
if(action_experiment(source, target))
playsound(user, 'sound/machines/ping.ogg', 25)
to_chat(user, span_notice("You scan [target]."))
- else
+ else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, span_notice("[target] is not related to your currently selected experiment."))
@@ -148,8 +145,9 @@
SIGNAL_HANDLER
var/atom/movable/our_scanner = parent
if (selected_experiment == null)
- playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25)
- to_chat(our_scanner, span_notice("No experiment selected!"))
+ if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
+ playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25)
+ to_chat(our_scanner, span_notice("No experiment selected!"))
return
var/successful_scan
for(var/scan_target in scanned_atoms)
@@ -159,7 +157,7 @@
if(successful_scan)
playsound(our_scanner, 'sound/machines/ping.ogg', 25)
to_chat(our_scanner, span_notice("The scan succeeds."))
- else
+ else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
playsound(src, 'sound/machines/buzz-sigh.ogg', 25)
our_scanner.say("The scan did not result in anything.")
@@ -261,6 +259,7 @@
/datum/component/experiment_handler/proc/link_techweb(datum/techweb/new_web)
if (new_web == linked_web)
return
+ selected_experiment?.on_unselected(src)
selected_experiment = null
linked_web = new_web
@@ -268,6 +267,7 @@
* Unlinks this handler from the selected techweb
*/
/datum/component/experiment_handler/proc/unlink_techweb()
+ selected_experiment?.on_unselected(src)
selected_experiment = null
linked_web = null
@@ -278,13 +278,15 @@
* * experiment - The experiment to attempt to link to
*/
/datum/component/experiment_handler/proc/link_experiment(datum/experiment/experiment)
- if (experiment && can_select_experiment(experiment))
+ if (can_select_experiment(experiment))
selected_experiment = experiment
+ selected_experiment.on_selected(src)
/**
* Unlinks this handler from the selected experiment
*/
/datum/component/experiment_handler/proc/unlink_experiment()
+ selected_experiment?.on_unselected(src)
selected_experiment = null
/**
@@ -299,31 +301,19 @@
return FALSE
// Check against the list of allowed experimentors
- if (experiment.allowed_experimentors && experiment.allowed_experimentors.len)
- var/matched = FALSE
- for (var/experimentor in experiment.allowed_experimentors)
- if (istype(parent, experimentor))
- matched = TRUE
- break
- if (!matched)
- return FALSE
+ if (length(experiment.allowed_experimentors) && !is_type_in_list(parent, experiment.allowed_experimentors))
+ return FALSE
// Check that this experiment is visible currently
- if (!linked_web || !(experiment in linked_web.available_experiments))
+ if (!(experiment in linked_web?.available_experiments))
return FALSE
// Check that this experiment type isn't blacklisted
- for (var/badsci in blacklisted_experiments)
- if (istype(experiment, badsci))
- return FALSE
-
- // Check against the allowed experiment types
- for (var/goodsci in allowed_experiments)
- if (istype(experiment, goodsci))
- return TRUE
+ if(is_type_in_list(experiment, blacklisted_experiments))
+ return FALSE
- // If we haven't returned yet then this shouldn't be allowed
- return FALSE
+ // Finally, check against the allowed experiment types
+ return is_type_in_list(experiment, allowed_experiments)
/datum/component/experiment_handler/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -355,12 +345,13 @@
.["techwebs"] += list(data)
.["experiments"] = list()
if (linked_web)
- for (var/datum/experiment/experiment in linked_web.available_experiments)
+ for (var/datum/experiment/experiment as anything in linked_web.available_experiments)
+ if(!can_select_experiment(experiment))
+ continue
var/list/data = list(
name = experiment.name,
description = experiment.description,
tag = experiment.exp_tag,
- selectable = can_select_experiment(experiment),
selected = selected_experiment == experiment,
progress = experiment.check_progress(),
performance_hint = experiment.performance_hint,
diff --git a/code/modules/experisci/experiment/types/experiment.dm b/code/modules/experisci/experiment/types/experiment.dm
index f760723f8db474..add015622f6219 100644
--- a/code/modules/experisci/experiment/types/experiment.dm
+++ b/code/modules/experisci/experiment/types/experiment.dm
@@ -22,11 +22,16 @@
/// A textual hint shown on the UI in a tooltip to help a user determine how to perform
/// the experiment
var/performance_hint
+ /**
+ * If set, these techweb points will be rewarded for completing the experiment.
+ * Useful for those loose ends not tied to any specific node discount or requirement.
+ */
+ var/list/points_reward
/**
* Performs any necessary initialization of tags and other variables
*/
-/datum/experiment/New()
+/datum/experiment/New(datum/techweb/techweb)
if (traits & EXPERIMENT_TRAIT_DESTRUCTIVE)
exp_tag = "Destructive [exp_tag]"
@@ -60,6 +65,14 @@
/datum/experiment/proc/actionable(...)
return !is_complete()
+///Called when the experiment is selected by an experiment handler, for specific signals and the such.
+/datum/experiment/proc/on_selected(datum/component/experiment_handler/experiment_handler)
+ return
+
+///Called when the opposite happens.
+/datum/experiment/proc/on_unselected(datum/component/experiment_handler/experiment_handler)
+ return
+
/**
* Proc that tries to perform the experiment, and then checks if its completed.
*/
diff --git a/code/modules/experisci/experiment/types/exploration.dm b/code/modules/experisci/experiment/types/exploration.dm
index a6a5d2cd4cf4d8..821e69a103ac59 100644
--- a/code/modules/experisci/experiment/types/exploration.dm
+++ b/code/modules/experisci/experiment/types/exploration.dm
@@ -52,7 +52,7 @@
/// If not null the required_condition will be picked from this list
var/list/possible_random_site_types
-/datum/experiment/exploration_scan/random/New()
+/datum/experiment/exploration_scan/random/New(datum/techweb/techweb)
. = ..()
if(length(possible_random_site_types))
required_site_type = pick(possible_random_site_types)
diff --git a/code/modules/experisci/experiment/types/random_scanning.dm b/code/modules/experisci/experiment/types/random_scanning.dm
index e80a80b5a0efab..c9d39bd47b4463 100644
--- a/code/modules/experisci/experiment/types/random_scanning.dm
+++ b/code/modules/experisci/experiment/types/random_scanning.dm
@@ -8,7 +8,7 @@
/// Max amount of a requirement per type
var/max_requirement_per_type = 100
-/datum/experiment/scanning/random/New()
+/datum/experiment/scanning/random/New(datum/techweb/techweb)
// Generate random contents
if (possible_types.len)
var/picked = 0
diff --git a/code/modules/experisci/experiment/types/scanning.dm b/code/modules/experisci/experiment/types/scanning.dm
index d9bbed88c8f375..54bd2ad637e660 100644
--- a/code/modules/experisci/experiment/types/scanning.dm
+++ b/code/modules/experisci/experiment/types/scanning.dm
@@ -17,16 +17,18 @@
var/list/required_atoms = list()
/// The list of atoms with sub-lists of atom references for scanned atoms contributing to the experiment (Or a count of atoms destoryed for destructive expiriments)
var/list/scanned = list()
+ /// If set, it'll be used in place of the generic "Scan samples of \a [initial(target.name)]" in serialize_progress_stage()
+ var/scan_message
/**
* Initializes the scanned atoms lists
*
* Initializes the internal scanned atoms list to keep track of which atoms have already been scanned
*/
-/datum/experiment/scanning/New()
+/datum/experiment/scanning/New(datum/techweb/techweb)
. = ..()
for (var/req_atom in required_atoms)
- scanned[req_atom] = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 0 : list()
+ scanned[req_atom] = (traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !(traits & EXPERIMENT_TRAIT_TYPECACHE)) ? 0 : list()
/**
* Checks if the scanning experiment is complete
@@ -37,8 +39,12 @@
/datum/experiment/scanning/is_complete()
. = TRUE
var/destructive = traits & EXPERIMENT_TRAIT_DESTRUCTIVE
+ var/typecache = traits & EXPERIMENT_TRAIT_TYPECACHE
for (var/req_atom in required_atoms)
var/list/seen = scanned[req_atom]
+ ///typecache experiments work all the same whether it's destructive or not
+ if(typecache && length(seen) == required_atoms[req_atom])
+ continue
if (destructive && (!(req_atom in scanned) || scanned[req_atom] != required_atoms[req_atom]))
return FALSE
if (!destructive && (!seen || seen.len != required_atoms[req_atom]))
@@ -65,8 +71,9 @@
* * seen_instances - The number of instances seen of this atom
*/
/datum/experiment/scanning/proc/serialize_progress_stage(atom/target, list/seen_instances)
- var/scanned_total = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len
- return EXPERIMENT_PROG_INT("Scan samples of \a [initial(target.name)]", scanned_total, required_atoms[target])
+ var/scanned_total = (traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !(traits & EXPERIMENT_TRAIT_TYPECACHE)) ? scanned[target] : seen_instances.len
+ var/message = scan_message || "Scan samples of \a [initial(target.name)]"
+ return EXPERIMENT_PROG_INT(message, scanned_total, required_atoms[target])
/**
* Attempts to scan an atom towards the experiment's goal
@@ -79,7 +86,10 @@
/datum/experiment/scanning/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, atom/target)
var/contributing_index_value = get_contributing_index(target)
if (contributing_index_value)
- scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : WEAKREF(target)
+ if(traits & EXPERIMENT_TRAIT_TYPECACHE)
+ scanned[contributing_index_value][target.type] = TRUE
+ else
+ scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : WEAKREF(target)
if(traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !isliving(target))//only qdel things when destructive scanning and they're not living (living things get gibbed)
qdel(target)
do_after_experiment(target, contributing_index_value)
diff --git a/code/modules/experisci/experiment/types/scanning_fish.dm b/code/modules/experisci/experiment/types/scanning_fish.dm
new file mode 100644
index 00000000000000..52e58c9104ccb9
--- /dev/null
+++ b/code/modules/experisci/experiment/types/scanning_fish.dm
@@ -0,0 +1,116 @@
+///a superlist containing typecaches shared between the several fish scanning experiments for each techweb.
+GLOBAL_LIST_EMPTY(scanned_fish_by_techweb)
+
+/**
+ * A special scanning experiment that unlocks further settings for the fishing portal generator.
+ * Mainly as an inventive solution to many a fish source being limited to maps that have it,
+ * and to make the fishing portal generator a bit than just gubby and goldfish.
+ */
+/datum/experiment/scanning/fish
+ 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)
+ traits = EXPERIMENT_TRAIT_TYPECACHE
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 750)
+ required_atoms = list(/obj/item/fish = 4)
+ scan_message = "Scan different species of fish"
+ ///Further experiments added to the techweb when this one is completed.
+ var/list/next_experiments = list(/datum/experiment/scanning/fish/second)
+ ///Completing a experiment may also enable a fish source to be used for use for the portal generator.
+ var/fish_source_reward = /datum/fish_source/portal/beach
+
+/**
+ * We make sure the scanned list is shared between all fish scanning experiments for this techweb,
+ * since this is about scanning each species, and having to redo it for each species is a hassle.
+ */
+/datum/experiment/scanning/fish/New(datum/techweb/techweb)
+ . = ..()
+ if(isnull(techweb))
+ return
+ var/techweb_ref = REF(techweb)
+ var/list/scanned_fish = GLOB.scanned_fish_by_techweb[techweb_ref]
+ if(isnull(scanned_fish))
+ scanned_fish = list()
+ GLOB.scanned_fish_by_techweb[techweb_ref] = scanned_fish
+ for(var/atom_type in required_atoms)
+ LAZYINITLIST(scanned_fish[atom_type])
+ scanned = scanned_fish
+
+/**
+ * Registers a couple signals to review the fish scanned so far.
+ * It'd be an hassle not having any way (beside memory) to know which fish species have been scanned already otherwise.
+ */
+/datum/experiment/scanning/fish/on_selected(datum/component/experiment_handler/experiment_handler)
+ RegisterSignal(experiment_handler.parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_handler_examine))
+ RegisterSignal(experiment_handler.parent, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_handler_examine_more))
+
+/datum/experiment/scanning/fish/on_unselected(datum/component/experiment_handler/experiment_handler)
+ UnregisterSignal(experiment_handler.parent, list(COMSIG_ATOM_EXAMINE, COMSIG_ATOM_EXAMINE_MORE))
+
+/datum/experiment/scanning/fish/proc/on_handler_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ examine_list += span_notice("Examine again to review all the species of fish scanned so far.")
+
+/datum/experiment/scanning/fish/proc/on_handler_examine_more(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ var/message = span_notice("Fish species scanned hitherto, if any:")
+ message += ""
+ for(var/atom_type in required_atoms)
+ for(var/obj/item/fish/fish_path as anything in scanned[atom_type])
+ message += "\n[initial(fish_path.name)]"
+ message += ""
+ examine_list += message
+
+///Only scannable fish will contribute towards the experiment.
+/datum/experiment/scanning/fish/final_contributing_index_checks(obj/item/fish/target, typepath)
+ return target.experisci_scannable
+
+/**
+ * After a fish scanning experiment is done, more may be unlocked. If so, add them to the techweb
+ * and automatically link the handler to the next experiment in the list as a bit of qol.
+ */
+/datum/experiment/scanning/fish/finish_experiment(datum/component/experiment_handler/experiment_handler, ...)
+ . = ..()
+ if(next_experiments)
+ experiment_handler.linked_web.add_experiments(next_experiments)
+ var/datum/experiment/next_in_line = locate(next_experiments[1]) in experiment_handler.linked_web.available_experiments
+ experiment_handler.link_experiment(next_in_line)
+
+/datum/experiment/scanning/fish/second
+ name = "Fish Scanning Experiment 2"
+ description = "An experiment requiring more fish species to be scanned to unlock the 'Chasm' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 1500)
+ required_atoms = list(/obj/item/fish = 8)
+ next_experiments = list(/datum/experiment/scanning/fish/third)
+ fish_source_reward = /datum/fish_source/portal/chasm
+
+/datum/experiment/scanning/fish/third
+ name = "Fish Scanning Experiment 3"
+ description = "An experiment requiring even more fish species to be scanned to unlock the 'Ocean' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ required_atoms = list(/obj/item/fish = 14)
+ next_experiments = list(/datum/experiment/scanning/fish/fourth, /datum/experiment/scanning/fish/holographic)
+ fish_source_reward = /datum/fish_source/portal/ocean
+
+/datum/experiment/scanning/fish/holographic
+ name = "Holographic Fish Scanning Experiment"
+ description = "This one actually requires holographic fish to unlock the 'Randomizer' setting for the fishing portal."
+ performance_hint = "Load in the 'Beach' template at the Holodeck to fish some holo-fish."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 500)
+ required_atoms = list(/obj/item/fish/holo = 4)
+ scan_message = "Scan different species of holographic fish"
+ next_experiments = null
+ fish_source_reward = /datum/fish_source/portal/random
+
+///holo fishes are normally unscannable, but this is an experiment for them, so we don't care for the experisci_scannable variable.
+/datum/experiment/scanning/fish/holographic/final_contributing_index_checks(obj/item/fish/target, typepath)
+ return TRUE
+
+/datum/experiment/scanning/fish/fourth
+ name = "Fish Scanning Experiment 4"
+ description = "An experiment requiring lotsa fish species to unlock the 'Hyperspace' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 3250)
+ required_atoms = list(/obj/item/fish = 21)
+ next_experiments = null
+ fish_source_reward = /datum/fish_source/portal/hyperspace
diff --git a/code/modules/experisci/experiment/types/scanning_material.dm b/code/modules/experisci/experiment/types/scanning_material.dm
index 714205289deac8..fb8a7ff354b3b6 100644
--- a/code/modules/experisci/experiment/types/scanning_material.dm
+++ b/code/modules/experisci/experiment/types/scanning_material.dm
@@ -9,7 +9,7 @@
///List of materials actually required, indexed by the atom that is required.
var/required_materials = list()
-/datum/experiment/scanning/random/material/New()
+/datum/experiment/scanning/random/material/New(datum/techweb/techweb)
. = ..()
for(var/req_atom in required_atoms)
var/chosen_material = pick(possible_material_types)
diff --git a/code/modules/experisci/experiment/types/scanning_plants.dm b/code/modules/experisci/experiment/types/scanning_plants.dm
index c34822d6e7e74c..b92a4cc20b4667 100644
--- a/code/modules/experisci/experiment/types/scanning_plants.dm
+++ b/code/modules/experisci/experiment/types/scanning_plants.dm
@@ -10,7 +10,7 @@
///List of plant genes actually required, indexed by the atom that is required.
var/list/required_genes = list()
-/datum/experiment/scanning/random/plants/New()
+/datum/experiment/scanning/random/plants/New(datum/techweb/techweb)
. = ..()
if(possible_plant_genes.len)
for(var/req_atom in required_atoms)
diff --git a/code/modules/experisci/handheld_scanner.dm b/code/modules/experisci/handheld_scanner.dm
index e0fd4d480d5a37..92031107ba51d1 100644
--- a/code/modules/experisci/handheld_scanner.dm
+++ b/code/modules/experisci/handheld_scanner.dm
@@ -19,9 +19,15 @@
// Late initialize to allow for the rnd servers to initialize first
/obj/item/experi_scanner/LateInitialize()
. = ..()
+ var/static/list/handheld_signals = list(
+ COMSIG_ITEM_PRE_ATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_AFTERATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, ignored_handheld_experiment_attempt),
+ )
AddComponent(/datum/component/experiment_handler, \
- allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical),\
- disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE)
+ allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical), \
+ disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE, \
+ experiment_signals = handheld_signals, \
+ )
/obj/item/experi_scanner/suicide_act(mob/living/carbon/user)
user.visible_message(span_suicide("[user] is giving in to the Great Toilet Beyond! It looks like [user.p_theyre()] trying to commit suicide!"))
@@ -53,6 +59,6 @@
icon_state = "experiscanner"
remove_atom_colour(ADMIN_COLOUR_PRIORITY, "#FF0000")
- user.gib(FALSE, TRUE, TRUE) //we delete everything but the brain, as it's going to be moved to the cistern
+ user.gib(DROP_BRAIN) //we delete everything but the brain, as it's going to be moved to the cistern
toilet_brain.forceMove(result_toilet)
result_toilet.w_items += toilet_brain.w_class
diff --git a/code/modules/explorer_drone/adventure.dm b/code/modules/explorer_drone/adventure.dm
index 9718d6d5a49de4..56d83000f905c3 100644
--- a/code/modules/explorer_drone/adventure.dm
+++ b/code/modules/explorer_drone/adventure.dm
@@ -43,6 +43,7 @@
#define REQ_OPERATOR_FIELD "operator"
#define CURRENT_ADVENTURE_VERSION 1
+#define ADVENTURE_LOOK_PATH "strings/exoadventures/"
/// All possible adventures in raw form
GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
@@ -50,34 +51,22 @@ GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
/// Loads all adventures from DB
/proc/load_adventures()
. = list()
- if(!SSdbcore.Connect())
- GLOB.explorer_drone_adventure_db_entries = .
- return
- var/datum/db_query/Query = SSdbcore.NewQuery("SELECT id,adventure_data,uploader,timestamp,approved FROM [format_table_name("text_adventures")]")
- if(!Query.Execute())
- qdel(Query)
- return
- while(Query.NextRow())
+ for(var/filename in flist(ADVENTURE_LOOK_PATH))
+ var/raw_json = file2text(ADVENTURE_LOOK_PATH + filename)
+ var/list/json_decoded = json_decode(raw_json)
var/datum/adventure_db_entry/entry = new()
- entry.id = Query.item[1]
- entry.raw_json = Query.item[2]
- entry.uploader = Query.item[3]
- entry.timestamp = Query.item[4]
- entry.approved = Query.item[5]
+ entry.filename = filename
+ entry.raw_json = raw_json
+ entry.uploader = json_decoded["author"]
entry.extract_metadata()
. += entry
- qdel(Query)
GLOB.explorer_drone_adventure_db_entries = .
/datum/adventure_db_entry
- /// db id or null for freshly created adventures
- var/id
+ /// filename of the adventure
+ var/filename
/// actual adventure json string
var/raw_json
- /// ckey of last change user.
- var/uploader
- /// Time of last change.
- var/timestamp
/// Unapproved adventures won't be used for exploration sites.
var/approved = FALSE
/// Was the adventure used for exploration site this round.
@@ -85,6 +74,8 @@ GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
//Variables below are extracted from the JSON
+ /// whoever made the json
+ var/uploader
/// json version
var/version
/// adventure name
@@ -100,61 +91,13 @@ GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
return FALSE
return TRUE
-/// Updates this entry from db, if possible.
-/datum/adventure_db_entry/proc/refresh()
- if(id)
- //Check if our timestamp is fresh, if not update local and stop
- var/datum/db_query/SelectQuery = SSdbcore.NewQuery("SELECT adventure_data,uploader,timestamp,approved FROM [format_table_name("text_adventures")] WHERE id = :id",list("id" = id))
- if(!SelectQuery.warn_execute() || !SelectQuery.NextRow())
- qdel(SelectQuery)
- return
- raw_json = SelectQuery.item[1]
- uploader = SelectQuery.item[2]
- timestamp = SelectQuery.item[3]
- approved = SelectQuery.item[4]
- extract_metadata()
- qdel(SelectQuery)
- return
- // No ID, nothing to be done.
-
-/// Pushes this entry changes to DB
-/datum/adventure_db_entry/proc/save()
- if(id)
- //We're up to date, update db instead
- var/datum/db_query/UpdateQuery = SSdbcore.NewQuery("UPDATE [format_table_name("text_adventures")] SET adventure_data = :adventure_data,uploader = :uploader,approved = :approved WHERE id = :id AND timestamp < NOW()",
- list("id" = id, "adventure_data" = raw_json, "uploader" = usr.ckey, "approved" = approved))
- UpdateQuery.warn_execute()
- qdel(UpdateQuery)
- else
- // Create new entry
- var/datum/db_query/InsertQuery = SSdbcore.NewQuery("INSERT INTO [format_table_name("text_adventures")] (adventure_data, uploader) VALUES (:raw_json, :uploader)", list("raw_json" = raw_json, "uploader" = usr.ckey))
- if(!InsertQuery.warn_execute())
- qdel(InsertQuery)
- return FALSE
- id = InsertQuery.last_insert_id
- qdel(InsertQuery)
- refresh()
-
-/// Deletes the local AND db entry.
-/datum/adventure_db_entry/proc/remove()
- if(id)
- var/datum/db_query/DelQuery = SSdbcore.NewQuery("DELETE FROM [format_table_name("text_adventures")] WHERE id = :id", list("id" = id))
- if(!DelQuery.warn_execute())
- qdel(DelQuery)
- return FALSE
- log_admin("[key_name(usr)] deleted text adventure with id : [id], name : [name]")
- qdel(DelQuery)
- GLOB.explorer_drone_adventure_db_entries -= src
- qdel(src)
- return TRUE
-
/// Extracts fields that are used by adventure browser / generation before instantiating
/datum/adventure_db_entry/proc/extract_metadata()
if(!raw_json)
CRASH("Trying to extract metadata from empty adventure")
var/list/json_data = json_decode(raw_json)
if(!islist(json_data))
- CRASH("Invalid JSON for adventure with db id:[id]")
+ CRASH("Invalid JSON for adventure with at path:[filename]")
version = json_data[ADVENTURE_VERSION_FIELD] || 0
name = json_data[ADVENTURE_NAME_FIELD]
required_site_traits = json_data[ADVENTURE_REQUIRED_SITE_TRAITS_FIELD]
@@ -169,13 +112,13 @@ GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
/datum/adventure_db_entry/proc/try_loading_adventure()
var/list/json_data = json_decode(raw_json)
if(!islist(json_data))
- CRASH("Invalid JSON in adventure with id:[id], name:[name]")
+ CRASH("Invalid JSON in adventure with path:[filename], name:[name]")
//Basic validation of required fields, don't even bother loading if they are missing.
var/static/list/required_fields = list(ADVENTURE_NAME_FIELD,ADVENTURE_STARTING_NODE_FIELD,ADVENTURE_NODES_FIELD)
for(var/field in required_fields)
if(!json_data[field])
- CRASH("Adventure id:[id], name:[name] missing [field] value")
+ CRASH("Adventure path:[filename], name:[name] missing [field] value")
var/datum/adventure/loaded_adventure = new
//load properties
@@ -191,16 +134,16 @@ GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
var/datum/adventure_node/node = try_loading_node(node_data)
if(node)
if(loaded_adventure.nodes[node.id])
- CRASH("Duplicate [node.id] node in id:[id], name:[name] adventure")
+ CRASH("Duplicate [node.id] node in path:[filename], name:[name] adventure")
loaded_adventure.nodes[node.id] = node
loaded_adventure.triggers = json_data[ADVENTURE_TRIGGERS_FIELD]
if(!loaded_adventure.validate())
- CRASH("Validation failed for id:[id], name:[name] adventure")
+ CRASH("Validation failed for path:[filename], name:[name] adventure")
return loaded_adventure
/datum/adventure_db_entry/proc/try_loading_node(node_data)
if(!islist(node_data))
- CRASH("Invalid adventure node data in id:[id], name:[name] adventure.")
+ CRASH("Invalid adventure node data in path:[filename], name:[name] adventure.")
var/datum/adventure_node/fresh_node = new
fresh_node.id = node_data[NODE_NAME_FIELD]
fresh_node.description = node_data[NODE_DESCRIPTION_FIELD]
@@ -493,6 +436,7 @@ GLOBAL_LIST_EMPTY(explorer_drone_adventure_db_entries)
if("exists")
return qkey in qualities
+#undef ADVENTURE_LOOK_PATH
#undef ADVENTURE_VERSION_FIELD
#undef CURRENT_ADVENTURE_VERSION
diff --git a/code/modules/explorer_drone/example_adventures/a_model_earth.json b/code/modules/explorer_drone/example_adventures/a_model_earth.json
deleted file mode 100644
index 908080b58343ce..00000000000000
--- a/code/modules/explorer_drone/example_adventures/a_model_earth.json
+++ /dev/null
@@ -1,570 +0,0 @@
-{
- "adventure_name": "A Model Earth",
- "version": 1,
- "author": "Armhulen",
- "starting_node": "Planet Start",
- "starting_qualities": {
- "Long Range Scan Report": 0,
- "UFOs Shot Down": 0
- },
- "required_site_traits": [
- "in space"
- ],
- "loot_categories": [
- "research"
- ],
- "scan_band_mods": {},
- "deep_scan_description": "",
- "triggers": [],
- "nodes": [
- {
- "name": "Planet Start",
- "description": "You come across a grey planet. It looks familiar, though you swore you've never come across this sector of space before.",
- "choices": [
- {
- "key": "choice 0",
- "name": "Ignore the planet.",
- "exit_node": "FAIL",
- "delay": 0,
- "delay_message": "Whatever, there's a lot of planets in space. Must be a hunch!"
- },
- {
- "key": "choice 1",
- "name": "Begin Orbital Scan",
- "exit_node": "Scanning from Orbit",
- "requirements": [
- {
- "quality": "Long Range Scan Report",
- "operator": "==",
- "value": 0
- }
- ],
- "delay": 30,
- "delay_message": "Scanning planet..."
- },
- {
- "key": "choice 8",
- "name": "Descend Into Orbit",
- "exit_node": "Orbital Descent",
- "delay": 30,
- "delay_message": "Descending into Orbit..."
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Scanning from Orbit",
- "description": "You initiate a long range scan from orbit:\nThis planet has a smoggy cover that blocks any good look at the surface, yet it has a mostly survivable atmosphere.\nThis planet has zero life signs.\n",
- "choices": [
- {
- "key": "choice 9",
- "name": "Stash that Report...",
- "exit_node": "Planet Start",
- "delay": 0
- }
- ],
- "image": null,
- "on_enter_effects": [
- {
- "effect_type": "Add",
- "quality": "Long Range Scan Report",
- "value": 1
- }
- ],
- "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkBAMAAAAxqGI4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAASUExURQAAAP///1lWUqwyMmlqakZHRwPX/kkAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAL5SURBVGje7ZldbqMwFIXtKnkvO4huyAYmG0CWea8i2P9Wxv/Y2MTYvjBVO4c8FET8+dxzDW5Ldqr73Hcf7fbemfwu+p1tkM+jILTbfeu2stNbDDSkklXnzNRbyUqUqbM/oVr5AFhOus5iUK3A/fmEW0BRHjCtwFPqj6NQS6GIVu5PTXEXnJUOjfLxXEOITYWitXECQk6BUOx6pSDkKAj4F6mrFxLEdNctAdl8gJVmBRBXy6tXcrScwwvXsufikXIP1qId5t2iz0D4osFClAga5MJDDbJaEUDJjF6eyZohxATjlhym9kkcMzhkIBQJ0g+pe2klJFWtG4DrgRSkeEUkjDDWA09TbPJdM6RXnySlDnLpYyO9/GhtQChthYhSMXeyup0aSFkoCYirVqJkdcnHEMaXakVeHIQUQYBPwSFdMB86IEBYolrAtyi1kF4asOIT40Ekq4JVQvS0R3UYI5G5GFLYwuKRO3l69dMEkxnc5MR8K6RCF9VL2sioe2tdLc6GRggROUDve5lcA9usXpw0So4Go5OwE0YySqutED33eZKH12TSyJJTK0RNFx6LFb4yMiI4uajJQmxlWT3tTsybEfxUnA0t9miGmNnCY54DMxNiJu7V6HLx89BO2iHLhIUXPxUleWVjf1SiZbvCHkEuLqX0brIM4hUfwkxERkKvdiPhxksGw3UuNpERmpuLeKGoBRM8k5WP/gsBsnrkAphUVGOJHeuIwPBDMcks6vmAUazkbtjXgAIhqVW+QHAYZHxnBIkRheKbGbAgJH6NIAcitWFlwmQQck28EzEDUdqwQnClrIxH+pA63od1EpgpHiP7i3ccSjEj/yd258SYqendLCR0UfcCyUK0B9KkbCYNBnZLRzIcC9HNdSzDODkYoraOB0eim+vgSIjcKM4HM06J5Hqak1Mi+d9ce3VGc5Ef01zXM5pLQN46oR0O5G0kKP+9nM+CfP1rSDqTwqTqmqvQX91SLINcvzGkLJPrCev9HMj8oyDk10MI+Qt3vM7Ve2h01AAAAABJRU5ErkJggg=="
- },
- {
- "name": "Orbital Descent",
- "description": "As you descend into orbit, you see a flying object headed straight for you!\nA garbled voice begins to call out to your drone, but there's no time to decipher it!",
- "choices": [
- {
- "key": "choice 2",
- "name": "Blast the damn UFO!",
- "exit_node": "Tractor Beam",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Tractor Beam Turns",
- "value": 2
- },
- {
- "effect_type": "Add",
- "quality": "UFOs Shot Down",
- "value": 1
- }
- ],
- "delay": 30,
- "delay_message": "Blasting UFO!"
- },
- {
- "key": "choice 3",
- "name": "Attempt Evasive Maneuvers!",
- "exit_node": "UFO Evasion!",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "UFOs Violently Crashed Into",
- "value": {
- "value_type": "random",
- "low": 0,
- "high": 1
- }
- }
- ],
- "delay": 30,
- "delay_message": "You attempt to dodge the UFO..."
- },
- {
- "key": "choice 7",
- "name": "Do NOTHING. Jesus take the wheel!",
- "exit_node": "FAIL_DEATH",
- "delay": 30,
- "delay_message": "What? Why?!"
- }
- ],
- "image": null,
- "on_enter_effects": [
- {
- "effect_type": "Add",
- "quality": "Garbled Transmissions",
- "value": 1
- }
- ],
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "UFO Evasion!",
- "description": "Were you good enough at flying to avoid the UFO?",
- "choices": [
- {
- "key": "choice 4",
- "name": "No, Back to flight school with you!",
- "exit_node": "FAIL_DEATH",
- "requirements": [
- {
- "quality": "UFOs Violently Crashed Into",
- "operator": "==",
- "value": 1
- }
- ],
- "delay": 0
- },
- {
- "key": "choice 5",
- "name": "You barely avoided them! Nice!",
- "exit_node": "Tractor Beam",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Tractor Beam Turns",
- "value": 2
- }
- ],
- "requirements": [
- {
- "quality": "UFOs Violently Crashed Into",
- "operator": "==",
- "value": 0
- }
- ],
- "delay": 0
- }
- ],
- "image": "default"
- },
- {
- "name": "Tractor Beam",
- "description": "Before you have time to think, your drone is captured by a Tractor beam! Now you've gone and done it.\n\nYou should have some time before you are pulled in.",
- "choices": [
- {
- "key": "choice 6",
- "name": "Decipher Garbled Transmission",
- "exit_node": "Deciphering Transmission",
- "requirements": [
- {
- "quality": "Garbled Transmissions",
- "operator": "==",
- "value": 1
- },
- {
- "quality": "Tractor Beam Turns",
- "operator": ">",
- "value": 1
- }
- ],
- "delay": 30,
- "delay_message": "Decipering..."
- },
- {
- "key": "choice 10",
- "name": "Look at the surface of the planet",
- "exit_node": "Looking at the Surface",
- "requirements": [
- {
- "quality": "Tractor Beam Turns",
- "operator": ">",
- "value": 1
- }
- ],
- "delay": 10,
- "delay_message": "Looking out window..."
- },
- {
- "key": "choice 13",
- "name": "Let the beam pull you in and dock you",
- "exit_node": "Landed, Kinda",
- "on_selection_effects": [
- {
- "effect_type": "Set",
- "quality": "Tractor Beam Turns",
- "value": 0
- }
- ],
- "delay": 50,
- "delay_message": "Getting captured..."
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Deciphering Transmission",
- "description": "You review the incoming transmissions your drone has gotten. These aren't in need of deciphering, just un-garbling it from the bad connection...\n\n\"You are in Space British Air Space! Identify yourself, Tally ho!\"\nWhat the fuck?",
- "choices": [
- {
- "key": "choice 11",
- "name": "Whoops!",
- "exit_node": "Tractor Beam",
- "delay": 0,
- "requirements": [
- {
- "quality": "UFOs Shot Down",
- "operator": "==",
- "value": 1
- }
- ]
- },
- {
- "key": "choice 12",
- "name": "Good thing I didn't blast them, then.",
- "exit_node": "Tractor Beam",
- "delay": 0,
- "requirements": [
- {
- "quality": "UFOs Shot Down",
- "operator": "==",
- "value": 0
- }
- ]
- }
- ],
- "image": null,
- "on_enter_effects": [
- {
- "effect_type": "Add",
- "quality": "Commercial Airliner Transmissions",
- "value": 1
- }
- ],
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Landed, Kinda",
- "description": "You have been locked to the surface the beam. Robots are all around you, pointing at your drone with all sorts of old age weapons.\n\nOne of them angrily shouts at you, \"By God, Queen and Country, we've got you now!\"",
- "choices": [
- {
- "key": "choice 14",
- "name": "Go out with a bang. Initiate self destruct!",
- "exit_node": "FAIL",
- "delay": 0
- },
- {
- "key": "choice 15",
- "name": "Surrender to the funny looking robot brits...",
- "exit_node": "British Courtroom Start",
- "delay": 50,
- "delay_message": "You are being transported somewhere..."
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "British Courtroom Start",
- "description": "Wow, that took forever. A one eye judge robot smacks their gavel, and points their hammer at you.\n\"I will now read out the crimes you are accused of!\"\nIs that how the judicial process works? You dunno.\n\"ONE ACCOUNT OF PLANETARY TRESSPASS!\"\nThis blows.",
- "choices": [
- {
- "key": "choice 16",
- "name": "That's not that bad.",
- "exit_node": "British Courtroom, Continued...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Crimes Committed",
- "value": 1
- }
- ],
- "delay": 0
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "British Courtroom, Continued...",
- "description": "This big idiot they call a judge just keeps piling on crimes. With each one, some robot you'd like to drone-punch gasps in the back.",
- "choices": [
- {
- "key": "choice 17",
- "name": "\"INTRUSIVE SCANNING ON OUR CITIZENS\"",
- "exit_node": "British Courtroom, Continued...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Long Range Scan Report",
- "value": -1
- },
- {
- "effect_type": "Add",
- "quality": "Crimes Committed",
- "value": 1
- }
- ],
- "requirements": [
- {
- "quality": "Long Range Scan Report",
- "operator": ">",
- "value": 0
- }
- ],
- "delay": 0
- },
- {
- "key": "choice 18",
- "name": "\"SHOOTING DOWN A COMMERCIAL AIRLINER\"",
- "exit_node": "British Courtroom, Continued...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "UFOs Shot Down",
- "value": -1
- },
- {
- "effect_type": "Add",
- "quality": "Crimes Committed",
- "value": 1
- }
- ],
- "requirements": [
- {
- "quality": "UFOs Shot Down",
- "operator": "==",
- "value": 1
- }
- ],
- "delay": 0
- },
- {
- "key": "choice 19",
- "name": "\"AND WE HAVE PROOF YOU KNEW IT WAS JUST AN AIRLINER!\"",
- "exit_node": "British Courtroom, Continued...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Commercial Airliner Transmissions",
- "value": -1
- },
- {
- "effect_type": "Add",
- "quality": "Crimes Committed",
- "value": 1
- }
- ],
- "requirements": [
- {
- "quality": "Commercial Airliner Transmissions",
- "operator": "==",
- "value": 1
- }
- ],
- "delay": 0
- },
- {
- "key": "choice 20",
- "name": "\"THE TRANSMISSIONS WERE NOT LEGIBLE AND UNTRANSLATED. WE CANNOT PROVE MALICE!\"",
- "exit_node": "British Courtroom, Continued...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Garbled Transmissions",
- "value": -1
- }
- ],
- "requirements": [
- {
- "quality": "Garbled Transmissions",
- "operator": "==",
- "value": 1
- }
- ],
- "delay": 0
- },
- {
- "key": "choice 24",
- "name": "I think it's done listing crimes, thank god. Time to argue my case.",
- "exit_node": "Verdict",
- "requirements": [
- {
- "quality": "Long Range Scan Report",
- "operator": "==",
- "value": 0
- },
- {
- "group_type": "AND",
- "requirements": [
- {
- "quality": "Garbled Transmissions",
- "operator": "==",
- "value": 0
- },
- {
- "quality": "Commercial Airliner Transmissions",
- "operator": "!=",
- "value": 1
- }
- ]
- },
- {
- "quality": "UFOs Shot Down",
- "operator": "!=",
- "value": 1
- }
- ],
- "delay": 0
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Looking at the Surface",
- "description": "The surface of this world looks exactly like a grey version of Earth! It seems to be an exact replica of some kind.\n\nYou see creatures moving around on the streets",
- "choices": [
- {
- "key": "choice 21",
- "name": "Cool!",
- "exit_node": "Tractor Beam",
- "delay": 0
- },
- {
- "key": "choice 22",
- "name": "Scan the creatures.",
- "exit_node": "Robo Brits!",
- "delay": 30,
- "delay_message": "Scanning..."
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Robo Brits!",
- "description": "Wow, they're robotic humanoids that look and act exactly like the British! You see some robots laughing and chatting at a pub, and a Bobby walking down the street looking for crooks. Who created this earth replica? You only know Earth from the books, but you should be where London is!",
- "choices": [
- {
- "key": "choice 23",
- "name": "Nice! Well, back to getting pulled in by a Tractor beam...",
- "exit_node": "Tractor Beam",
- "delay": 0
- }
- ],
- "image": null,
- "on_enter_effects": [
- {
- "effect_type": "Add",
- "quality": "Long Range Scan Report",
- "value": 1
- }
- ],
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Verdict",
- "description": "Before even getting to defend your case, the judge says \"I've heard ENOUGH! It's time for a verdict!\"\nDang! You're definitely in kangaroo court!\n\"GUILTY! AND YOUR SENTENCE IS:\"",
- "choices": [
- {
- "key": "choice 25",
- "name": "\"DEATH!\"",
- "exit_node": "FAIL_DEATH",
- "requirements": [
- {
- "quality": "Crimes Committed",
- "operator": ">=",
- "value": 4
- }
- ],
- "delay": 0
- },
- {
- "key": "choice 26",
- "name": "\"SPACE JAIL!\"",
- "exit_node": "Not Actually Space Jail, Just Normal Jail",
- "requirements": [
- {
- "quality": "Crimes Committed",
- "operator": ">",
- "value": 1
- }
- ],
- "delay": 20,
- "delay_message": "You are being jailed..."
- },
- {
- "key": "choice 27",
- "name": "\"FORGIVENESS! Just trespass? That's not that bad!\"",
- "exit_node": "Sweet Sweet Freedom!",
- "requirements": [
- {
- "quality": "Crimes Committed",
- "operator": "==",
- "value": 1
- }
- ],
- "delay": 10,
- "delay_message": "WOOP WOOP"
- }
- ],
- "image": "default"
- },
- {
- "name": "Not Actually Space Jail, Just Normal Jail",
- "description": "You'll have to wait out your crimes against the robo brits.",
- "choices": [
- {
- "key": "choice 28",
- "name": "Sit out your sentence",
- "exit_node": "Sweet Sweet Freedom!",
- "delay": 1200,
- "delay_message": "Sitting out your sentence..."
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- },
- {
- "name": "Sweet Sweet Freedom!",
- "description": "You're free! You spend a good while travelling around England (or at least the replica) and enjoying the cuisine, people and culture! Good society research is gained from this or something, but really you're just enjoying the sights and sounds.",
- "choices": [
- {
- "key": "choice 29",
- "name": "Nice!",
- "exit_node": "WIN",
- "delay": 0
- }
- ],
- "image": null,
- "raw_image": "data:image/png;base64,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"
- }
- ]
- }
diff --git a/code/modules/explorer_drone/manager.dm b/code/modules/explorer_drone/manager.dm
index 74a972216a41cb..9ca85ca4a20a59 100644
--- a/code/modules/explorer_drone/manager.dm
+++ b/code/modules/explorer_drone/manager.dm
@@ -24,27 +24,7 @@
if(.)
return
feedback_message = ""
- var/mob/user = usr
switch(action)
- if("create")
- var/datum/adventure_db_entry/new_entry = new
- new_entry.name = "New Adventure"
- new_entry.uploader = user.ckey
- GLOB.explorer_drone_adventure_db_entries += new_entry
- return TRUE
- if("delete")
- var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
- if(!target)
- return
- if(!target.remove())
- feedback_message = "Failed to remove adventure"
- return TRUE
- if("approve")
- var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
- if(!target)
- return
- target.approved = !target.approved
- return TRUE
if("play")
var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
if(!target)
@@ -67,43 +47,6 @@
QDEL_NULL(temp_adventure)
feedback_message = "Adventure stopped"
return TRUE
- if("refresh")
- var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
- if(!target)
- return
- target.refresh()
- return TRUE
- if("save")
- var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
- if(!target)
- return
- target.save()
- return TRUE
- if("download")
- var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
- if(!target)
- return
- var/temp_file = file("data/AdventureDownloadTempFile")
- fdel(temp_file)
- WRITE_FILE(temp_file, target.raw_json)
- user << ftp(temp_file,"[target.name].json")
- return TRUE
- if("upload")
- var/datum/adventure_db_entry/target = locate(params["ref"]) in GLOB.explorer_drone_adventure_db_entries
- if(!target)
- return
- var/source_json = input(user,"Select adventure JSON file.","Adventure Upload") as file|null
- if(!source_json)
- return
- var/raw_json = file2text(source_json)
- var/json = json_decode(raw_json)
- if(!json)
- feedback_message = "Decoding JSON failed."
- return
- //do basic validation here
- target.raw_json = raw_json
- target.extract_metadata()
- return TRUE
/datum/adventure_browser/ui_data(mob/user)
. = ..()
@@ -111,11 +54,10 @@
for(var/datum/adventure_db_entry/db_entry in GLOB.explorer_drone_adventure_db_entries)
adventure_data += list(list(
"ref" = ref(db_entry),
- "id" = db_entry.id,
+ "filename" = db_entry.filename,
"name" = db_entry.name,
"version" = db_entry.version,
"uploader" = db_entry.uploader,
- "timestamp" = db_entry.timestamp,
"approved" = db_entry.approved,
"json_status" = db_entry.raw_json ? "Valid JSON" : "Empty"
))
diff --git a/code/modules/fishing/aquarium/fish_analyzer.dm b/code/modules/fishing/aquarium/fish_analyzer.dm
index c7feab860ca0b4..a83aa4296b6c91 100644
--- a/code/modules/fishing/aquarium/fish_analyzer.dm
+++ b/code/modules/fishing/aquarium/fish_analyzer.dm
@@ -33,6 +33,17 @@
case_color = rgb(rand(16, 255), rand(16, 255), rand(16, 255))
set_greyscale(colors = list(case_color))
. = ..()
+
+ var/static/list/fishe_signals = list(
+ COMSIG_FISH_ANALYZER_ANALYZE_STATUS = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ )
+ AddComponent(/datum/component/experiment_handler, \
+ config_mode = EXPERIMENT_CONFIG_ALTCLICK, \
+ allowed_experiments = list(/datum/experiment/scanning/fish), \
+ config_flags = EXPERIMENT_CONFIG_SILENT_FAIL|EXPERIMENT_CONFIG_IMMEDIATE_ACTION, \
+ experiment_signals = fishe_signals, \
+ )
+
register_item_context()
update_appearance()
@@ -42,6 +53,10 @@
radial_choices = null
return ..()
+/obj/item/fish_analyzer/examine(mob/user)
+ . = ..()
+ . += span_notice("Alt-Click to access the Experiment Configuration UI")
+
/obj/item/fish_analyzer/update_icon_state()
. = ..()
icon_state = base_icon_state
@@ -190,21 +205,24 @@
if(fish.status != FISH_DEAD)
render_list += "\n"
- var/hunger = PERCENT(min((world.time - fish.last_feeding) / fish.feeding_frequency, 1))
- var/hunger_string = "[hunger]%"
- switch(hunger)
- if(0 to 60)
- hunger_string = span_info(hunger_string)
- if(60 to 90)
- hunger_string = span_warning(hunger_string)
- if(90 to 100)
- hunger_string = span_alert(hunger_string)
- render_list += "Hunger: [hunger_string]\n"
+ if(!HAS_TRAIT(fish, TRAIT_FISH_NO_HUNGER))
+ var/hunger = PERCENT(min((world.time - fish.last_feeding) / fish.feeding_frequency, 1))
+ var/hunger_string = "[hunger]%"
+ switch(hunger)
+ if(0 to 60)
+ hunger_string = span_info(hunger_string)
+ if(60 to 90)
+ hunger_string = span_warning(hunger_string)
+ if(90 to 100)
+ hunger_string = span_alert(hunger_string)
+ render_list += "Hunger: [hunger_string]\n"
var/time_left = round(max(fish.breeding_wait - world.time, 0)/10)
render_list += "Time until it can breed: [time_left] seconds"
to_chat(user, examine_block(jointext(render_list, "")), type = MESSAGE_TYPE_INFO)
+ SEND_SIGNAL(src, COMSIG_FISH_ANALYZER_ANALYZE_STATUS, fish, user)
+
/**
* Called when a fish or a menu choice is left-clicked.
* This returns the fish's progenitors, traits and their inheritability.
diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm
index 21585a075466f2..1275886141b52f 100644
--- a/code/modules/fishing/fish/_fish.dm
+++ b/code/modules/fishing/fish/_fish.dm
@@ -136,6 +136,9 @@
var/min_pressure = WARNING_LOW_PRESSURE
var/max_pressure = HAZARD_HIGH_PRESSURE
+ /// If this fish type counts towards the Fish Species Scanning experiments
+ var/experisci_scannable = TRUE
+
/obj/item/fish/Initialize(mapload, apply_qualities = TRUE)
. = ..()
AddComponent(/datum/component/aquarium_content, PROC_REF(get_aquarium_animation), list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED))
@@ -175,7 +178,7 @@
balloon_alert(user, "[src] is dead!")
return TRUE
feed(item.reagents)
- balloon_alert(user, "you feed [src]")
+ balloon_alert(user, "fed [src]")
return TRUE
/obj/item/fish/examine(mob/user)
@@ -185,14 +188,14 @@
. += span_notice("It weighs [weight] g.")
///Randomizes weight and size.
-/obj/item/fish/proc/randomize_size_and_weight(avg_size = average_size, avg_weight = average_weight, deviation = 0.2, first_run = FALSE)
- var/size_deviation = 0.2 * avg_size
- var/new_size = round(max(1,gaussian(avg_size, size_deviation)), 1)
+/obj/item/fish/proc/randomize_size_and_weight(base_size = average_size, base_weight = average_weight, deviation = 0.2)
+ var/size_deviation = 0.2 * base_size
+ var/new_size = round(clamp(gaussian(base_size, size_deviation), average_size * 1/MAX_FISH_DEVIATION_COEFF, average_size * MAX_FISH_DEVIATION_COEFF))
- var/weight_deviation = 0.2 * avg_weight
- var/new_weight = round(max(1,gaussian(avg_weight, weight_deviation)), 1)
+ var/weight_deviation = 0.2 * base_weight
+ var/new_weight = round(clamp(gaussian(base_weight, weight_deviation), average_weight * 1/MAX_FISH_DEVIATION_COEFF, average_weight * MAX_FISH_DEVIATION_COEFF))
- update_size_and_weight(new_size, new_weight, first_run)
+ update_size_and_weight(new_size, new_weight)
///Updates weight and size, along with weight class, number of fillets you can get and grind results.
/obj/item/fish/proc/update_size_and_weight(new_size = average_size, new_weight = average_weight)
@@ -414,11 +417,14 @@
return FALSE
return TRUE
+/obj/item/fish/proc/is_hungry()
+ return !HAS_TRAIT(src, TRAIT_FISH_NO_HUNGER) && world.time - last_feeding >= feeding_frequency
+
/obj/item/fish/proc/process_health(seconds_per_tick)
var/health_change_per_second = 0
if(!proper_environment())
health_change_per_second -= 3 //Dying here
- if(world.time - last_feeding >= feeding_frequency)
+ if(is_hungry())
health_change_per_second -= 0.5 //Starving
else
health_change_per_second += 0.5 //Slowly healing
@@ -541,10 +547,11 @@
#define FLOP_SINGLE_MOVE_TIME 1.5
#define JUMP_X_DISTANCE 5
#define JUMP_Y_DISTANCE 6
-/// This animation should be applied to actual parent atom instead of vc_object.
-/proc/flop_animation(atom/movable/animation_target)
+
+/// This flopping animation played while the fish is alive.
+/obj/item/fish/proc/flop_animation()
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
- animate(animation_target, time = pause_between, loop = -1)
+ animate(src, time = pause_between, loop = -1)
//move nose down and up
for(var/_ in 1 to FLOP_COUNT)
var/matrix/up_matrix = matrix()
@@ -565,6 +572,7 @@
animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN)
animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
+
#undef PAUSE_BETWEEN_PHASES
#undef PAUSE_BETWEEN_FLOPS
#undef FLOP_COUNT
@@ -578,7 +586,7 @@
if(flopping) //Requires update_transform/animate_wrappers to be less restrictive.
return
flopping = TRUE
- flop_animation(src)
+ flop_animation()
/// Stops flopping animation
/obj/item/fish/proc/stop_flopping()
@@ -593,7 +601,7 @@
/obj/item/fish/proc/refresh_flopping()
if(flopping)
- flop_animation(src)
+ flop_animation()
/// Returns random fish, using random_case_rarity probabilities.
/proc/random_fish_type(required_fluid)
diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm
index bec868ad24e77f..63de4cfdf1dced 100644
--- a/code/modules/fishing/fish/fish_traits.dm
+++ b/code/modules/fishing/fish/fish_traits.dm
@@ -12,6 +12,8 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
var/diff_traits_inheritability = 50
/// fishes of types within this list are granted to have this trait, no matter the probability
var/list/guaranteed_inheritance_types
+ /// Depending on the value, fish with trait will be reported as more or less difficult in the catalog.
+ var/added_difficulty = 0
/// Difficulty modifier from this mod, needs to return a list with two values
/datum/fish_trait/proc/difficulty_mod(obj/item/fishing_rod/rod, mob/fisherman)
@@ -161,7 +163,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/necrophage
name = "Necrophage"
- catalog_description = "This fish will eat the carcasses of dead fishes when hungry."
+ catalog_description = "This fish will eat carcasses of dead fish when hungry."
incompatible_traits = list(/datum/fish_trait/vegan)
/datum/fish_trait/necrophage/apply_to_fish(obj/item/fish/fish)
@@ -169,7 +171,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/necrophage/proc/eat_dead_fishes(obj/item/fish/source, seconds_per_tick)
SIGNAL_HANDLER
- if(world.time - source.last_feeding < source.feeding_frequency || !isaquarium(source.loc))
+ if(!source.is_hungry() || !isaquarium(source.loc))
return
for(var/obj/item/fish/victim in source.loc)
if(victim.status != FISH_DEAD || victim == source || HAS_TRAIT(victim, TRAIT_YUCKY_FISH))
@@ -233,7 +235,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/predator/proc/eat_fishes(obj/item/fish/source, seconds_per_tick)
SIGNAL_HANDLER
- if(world.time - source.last_feeding < source.feeding_frequency || !isaquarium(source.loc))
+ if(!source.is_hungry() || !isaquarium(source.loc))
return
var/obj/structure/aquarium/aquarium = source.loc
for(var/obj/item/fish/victim in aquarium.get_fishes(TRUE, source))
@@ -334,6 +336,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
diff_traits_inheritability = 45
guaranteed_inheritance_types = list(/obj/item/fish/clownfish/lube)
catalog_description = "This fish exudes a viscous, slippery lubrificant. It's reccomended not to step on it."
+ added_difficulty = 5
/datum/fish_trait/lubed/apply_to_fish(obj/item/fish/fish)
fish.AddComponent(/datum/component/slippery, 8 SECONDS, SLIDE|GALOSHES_DONT_HELP)
@@ -352,3 +355,26 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
ADD_TRAIT(fish, TRAIT_FISH_AMPHIBIOUS, FISH_TRAIT_DATUM)
if(fish.required_fluid_type == AQUARIUM_FLUID_AIR)
fish.required_fluid_type = AQUARIUM_FLUID_FRESHWATER
+
+/datum/fish_trait/mixotroph
+ name = "Mixotroph"
+ inheritability = 75
+ diff_traits_inheritability = 25
+ catalog_description = "This fish is capable of substaining itself by producing its own sources of energy (food)."
+ incompatible_traits = list(/datum/fish_trait/predator, /datum/fish_trait/necrophage)
+
+/datum/fish_trait/antigrav/apply_to_fish(obj/item/fish/fish)
+ ADD_TRAIT(fish, TRAIT_FISH_NO_HUNGER, FISH_TRAIT_DATUM)
+
+/datum/fish_trait/antigrav
+ name = "Anti-Gravity"
+ inheritability = 75
+ diff_traits_inheritability = 25
+ catalog_description = "This fish will invert the gravity of the bait at random. May fall upward outside after being caught."
+ added_difficulty = 15
+
+/datum/fish_trait/antigrav/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/minigame)
+ minigame.special_effects |= FISHING_MINIGAME_RULE_ANTIGRAV
+
+/datum/fish_trait/antigrav/apply_to_fish(obj/item/fish/fish)
+ fish.AddElement(/datum/element/forced_gravity, NEGATIVE_GRAVITY)
diff --git a/code/modules/fishing/fish/fish_types.dm b/code/modules/fishing/fish/fish_types.dm
index 652b0aba8aa495..5ff62266ad9e66 100644
--- a/code/modules/fishing/fish/fish_types.dm
+++ b/code/modules/fishing/fish/fish_types.dm
@@ -341,7 +341,6 @@
icon_state = "sludgefish_purple"
dedicated_in_aquarium_icon_state = "sludgefish_purple_small"
random_case_rarity = FISH_RARITY_NOPE
- random_case_rarity = FISH_RARITY_VERY_RARE
fish_traits = list(/datum/fish_trait/parthenogenesis)
/obj/item/fish/slimefish
@@ -439,11 +438,13 @@
fillet_type = null
death_text = "%SRC gently disappears."
fish_traits = list(/datum/fish_trait/no_mating) //just to be sure, these shouldn't reproduce
+ experisci_scannable = FALSE
/obj/item/fish/holo/Initialize(mapload)
. = ..()
var/area/station/holodeck/holo_area = get_area(src)
if(!istype(holo_area))
+ addtimer(CALLBACK(src, PROC_REF(set_status), FISH_DEAD), 1 MINUTES)
return
holo_area.linked.add_to_spawned(src)
@@ -488,9 +489,9 @@
sprite_height = 5
/obj/item/fish/holo/checkered
- name = "unrendered holographic fish" //it's a meta joke, buddy.
+ name = "unrendered holographic fish"
desc = "A checkered silhoutte of searing purple and pitch black presents itself before your eyes, like a tear in fabric of reality. It hurts to watch."
- icon_state = "checkered"
+ icon_state = "checkered" //it's a meta joke, buddy.
dedicated_in_aquarium_icon_state = "checkered_small"
sprite_width = 4
@@ -502,3 +503,37 @@
sprite_height = 4
sprite_width = 10
average_size = 50
+
+/obj/item/fish/starfish
+ name = "cosmostarfish"
+ desc = "A peculiar, gravity-defying, echinoderm-looking critter from hyperspace."
+ icon_state = "starfish"
+ dedicated_in_aquarium_icon_state = "starfish_small"
+ icon_state_dead = "starfish_dead"
+ sprite_width = 4
+ average_size = 30
+ average_weight = 300
+ stable_population = 3
+ required_fluid_type = AQUARIUM_FLUID_AIR
+ random_case_rarity = FISH_RARITY_NOPE
+ required_temperature_min = 0
+ required_temperature_max = INFINITY
+ safe_air_limits = null
+ min_pressure = 0
+ max_pressure = INFINITY
+ grind_results = list(/datum/reagent/bluespace = 10, /datum/reagent/consumable/liquidgibs = 5)
+ fillet_type = null
+ fish_traits = list(/datum/fish_trait/antigrav, /datum/fish_trait/mixotroph)
+
+/obj/item/fish/starfish/Initialize(mapload)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/item/fish/starfish/update_overlays()
+ . = ..()
+ if(status == FISH_ALIVE)
+ . += emissive_appearance(icon, "starfish_emissive", src)
+
+///It spins, and dimly glows in the dark.
+/obj/item/fish/starfish/flop_animation()
+ DO_FLOATING_ANIM(src)
diff --git a/code/modules/fishing/fish_catalog.dm b/code/modules/fishing/fish_catalog.dm
index d41b4657e50a0f..a0f66c2227d0d3 100644
--- a/code/modules/fishing/fish_catalog.dm
+++ b/code/modules/fishing/fish_catalog.dm
@@ -68,14 +68,6 @@
if(source.catalog_description && (fish_type in source.fish_table))
spot_descriptions += source.catalog_description
.["spots"] = english_list(spot_descriptions, nothing_text = "Unknown")
- ///Difficulty descriptor
- switch(initial(fishy.fishing_difficulty_modifier))
- if(-INFINITY to 10)
- .["difficulty"] = "Easy"
- if(20 to 30)
- .["difficulty"] = "Medium"
- else
- .["difficulty"] = "Hard"
var/list/fish_list_properties = collect_fish_properties()
var/list/fav_bait = fish_list_properties[fishy][NAMEOF(fishy, favorite_bait)]
var/list/disliked_bait = fish_list_properties[fishy][NAMEOF(fishy, disliked_bait)]
@@ -91,12 +83,22 @@
// Fish traits description
var/list/trait_descriptions = list()
var/list/fish_traits = fish_list_properties[fishy][NAMEOF(fishy, fish_traits)]
+ var/fish_difficulty = initial(fishy.fishing_difficulty_modifier)
for(var/fish_trait in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
trait_descriptions += trait.catalog_description
+ fish_difficulty += trait.added_difficulty
if(!length(trait_descriptions))
trait_descriptions += "This fish exhibits no special behavior."
.["traits"] = trait_descriptions
+ ///Difficulty descriptor
+ switch(fish_difficulty)
+ if(-INFINITY to 9)
+ .["difficulty"] = "Easy"
+ if(10 to 19)
+ .["difficulty"] = "Medium"
+ else
+ .["difficulty"] = "Hard"
return .
/obj/item/book/fish_catalog/ui_assets(mob/user)
diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm
index 6169b41fd88fce..3f34222ad87323 100644
--- a/code/modules/fishing/fishing_equipment.dm
+++ b/code/modules/fishing/fishing_equipment.dm
@@ -11,7 +11,8 @@
desc = "Simple fishing line."
icon = 'icons/obj/fishing.dmi'
icon_state = "reel_blue"
- var/fishing_line_traits = NONE
+ ///A list of traits that this fishing line has, checked by fish traits and the minigame.
+ var/list/fishing_line_traits
/// Color of the fishing line
var/line_color = "#808080"
@@ -59,7 +60,8 @@
icon_state = "hook"
w_class = WEIGHT_CLASS_TINY
- var/fishing_hook_traits = NONE
+ /// A list of traits that this fishing hook has, checked by fish traits and the minigame
+ var/list/fishing_hook_traits
/// icon state added to main rod icon when this hook is equipped
var/rod_overlay_icon_state = "hook_overlay"
/// What subtype of `/obj/item/chasm_detritus` do we fish out of chasms? Defaults to `/obj/item/chasm_detritus`.
@@ -175,7 +177,7 @@
name = "jawed hook"
desc = "Despite hints of rust, this gritty beartrap-like hook hybrid manages to look even more threating than the real thing. May neptune have mercy of whatever gets caught in its jaws."
icon_state = "jaws"
- fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_ENSNARE|FISHING_HOOK_KILL
+ fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_KILL
rod_overlay_icon_state = "hook_jaws_overlay"
/obj/item/storage/toolbox/fishing
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index 478279749e406e..71617b4de07b34 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -43,8 +43,12 @@
var/fish_ai = FISH_AI_DUMB
/// Rule modifiers (eg weighted bait)
var/special_effects = NONE
- /// Did the game get past the baiting phase, used to track if bait should be consumed afterwards
- var/bait_taken = FALSE
+ /// A list of possible active minigame effects. If not empty, one will be picked from time to time.
+ var/list/active_effects
+ /// The cooldown between switching active effects
+ COOLDOWN_DECLARE(active_effect_cd)
+ /// The current active effect
+ var/current_active_effect
/// Result path
var/reward_path = FISHING_DUD
/// Minigame difficulty
@@ -61,6 +65,8 @@
var/obj/effect/fishing_lure/lure
/// Background icon state from fishing_hud.dmi
var/background = "background_default"
+ /// Fish icon state from fishing_hud.dmi
+ var/fish_icon = "fish"
/// Fishing line visual
var/datum/beam/fishing_line
@@ -168,7 +174,10 @@
RegisterSignal(user, COMSIG_MOB_FISHING_REWARD_DISPENSED, PROC_REF(hurt_fish))
difficulty += comp.fish_source.calculate_difficulty(reward_path, rod, user, src)
- difficulty = round(difficulty)
+ difficulty = clamp(round(difficulty), 1, 100)
+
+ if(HAS_TRAIT(user, TRAIT_REVEAL_FISH) || (user.mind && HAS_TRAIT(user.mind, TRAIT_REVEAL_FISH)))
+ fish_icon = GLOB.specific_fish_icons[reward_path] || "fish"
/**
* If the chances are higher than 1% (100% at maximum difficulty), they'll scale
@@ -194,6 +203,8 @@
if(!completed)
complete(win = FALSE)
if(fishing_line)
+ //Stops the line snapped message from appearing everytime the minigame is over.
+ UnregisterSignal(fishing_line, COMSIG_QDELETING)
QDEL_NULL(fishing_line)
if(lure)
QDEL_NULL(lure)
@@ -207,19 +218,21 @@
lure_turf?.balloon_alert(user, message)
/datum/fishing_challenge/proc/on_spot_gone(datum/source)
+ SIGNAL_HANDLER
send_alert("fishing spot gone!")
- interrupt(balloon_alert = FALSE)
+ interrupt()
/datum/fishing_challenge/proc/interrupt_challenge(datum/source, reason)
if(reason)
send_alert(reason)
- interrupt(balloon_alert = FALSE)
+ interrupt()
/datum/fishing_challenge/proc/start(mob/living/user)
/// Create fishing line visuals
fishing_line = used_rod.create_fishing_line(lure, target_py = 5)
+ active_effects = bitfield_to_list(special_effects & FISHING_MINIGAME_ACTIVE_EFFECTS)
// If fishing line breaks los / rod gets dropped / deleted
- RegisterSignal(fishing_line, COMSIG_FISHING_LINE_SNAPPED, PROC_REF(interrupt))
+ RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(on_line_deleted))
RegisterSignal(used_rod, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self))
ADD_TRAIT(user, TRAIT_GONE_FISHING, REF(src))
user.add_mood_event("fishing", /datum/mood_event/fishing)
@@ -228,6 +241,13 @@
to_chat(user, span_notice("You start fishing..."))
playsound(lure, 'sound/effects/splash.ogg', 100)
+/datum/fishing_challenge/proc/on_line_deleted(datum/source)
+ 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")
+ interrupt()
+
/datum/fishing_challenge/proc/handle_click(mob/source, atom/target, modifiers)
SIGNAL_HANDLER
//You need to be holding the rod to use it.
@@ -241,12 +261,9 @@
return COMSIG_MOB_CANCEL_CLICKON
/// Challenge interrupted by something external
-/datum/fishing_challenge/proc/interrupt(datum/source, balloon_alert = TRUE)
- SIGNAL_HANDLER
+/datum/fishing_challenge/proc/interrupt()
if(!completed)
experience_multiplier *= 0.5
- if(balloon_alert)
- send_alert(user.is_holding(used_rod) ? "line snapped" : "tool dropped")
complete(FALSE)
/datum/fishing_challenge/proc/on_attack_self(obj/item/source, mob/user)
@@ -278,7 +295,8 @@
if(reward_path != FISHING_DUD)
playsound(lure, 'sound/effects/bigsplash.ogg', 100)
SEND_SIGNAL(src, COMSIG_FISHING_CHALLENGE_COMPLETED, user, win)
- qdel(src)
+ if(!QDELETED(src))
+ qdel(src)
/datum/fishing_challenge/proc/start_baiting_phase()
deltimer(next_phase_timer)
@@ -294,7 +312,30 @@
phase = BITING_PHASE
// Trashing animation
playsound(lure, 'sound/effects/fish_splash.ogg', 100)
- send_alert("!!!")
+ if(HAS_TRAIT(user, TRAIT_REVEAL_FISH) || (user.mind && HAS_TRAIT(user.mind, TRAIT_REVEAL_FISH)))
+ switch(fish_icon)
+ if(FISH_ICON_DEF)
+ send_alert("fish!!!")
+ if(FISH_ICON_HOSTILE)
+ send_alert("hostile!!!")
+ if(FISH_ICON_STAR)
+ send_alert("starfish!!!")
+ if(FISH_ICON_CHUNKY)
+ send_alert("round fish!!!")
+ if(FISH_ICON_JELLYFISH)
+ send_alert("jellyfish!!!")
+ if(FISH_ICON_SLIME)
+ send_alert("slime!!!")
+ if(FISH_ICON_COIN)
+ send_alert("valuable!!!")
+ if(FISH_ICON_GEM)
+ send_alert("ore!!!")
+ if(FISH_ICON_CRAB)
+ send_alert("crustacean!!!")
+ if(FISH_ICON_BONE)
+ send_alert("bones!!!")
+ else
+ send_alert("!!!")
animate(lure, pixel_y = 3, time = 5, loop = -1, flags = ANIMATION_RELATIVE)
animate(pixel_y = -3, time = 5, flags = ANIMATION_RELATIVE)
// Setup next phase
@@ -307,7 +348,7 @@
///The player is no longer around to play the minigame, so we interrupt it.
/datum/fishing_challenge/proc/on_user_logout(datum/source)
SIGNAL_HANDLER
- interrupt(balloon_alert = FALSE)
+ interrupt()
/datum/fishing_challenge/proc/win_anyway()
if(!completed)
@@ -345,6 +386,9 @@
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(start_reeling))
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEUP, PROC_REF(stop_reeling))
RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(on_user_logout))
+ if(length(active_effects))
+ // Give the player a moment to prepare for active minigame effects
+ COOLDOWN_START(src, active_effect_cd, rand(5, 9) SECONDS)
START_PROCESSING(SSfishing, src)
///Stop processing and remove references to the minigame hud
@@ -369,11 +413,44 @@
///Update the state of the fish, the bait and the hud
/datum/fishing_challenge/process(seconds_per_tick)
+ if(length(active_effects) && COOLDOWN_FINISHED(src, active_effect_cd))
+ select_active_effect()
move_fish(seconds_per_tick)
move_bait(seconds_per_tick)
if(!QDELETED(fishing_hud))
update_visuals()
+///The proc that handles fancy effects like flipping the hud or skewing movement
+/datum/fishing_challenge/proc/select_active_effect()
+ ///bring forth an active effect
+ if(isnull(current_active_effect))
+ current_active_effect = pick(active_effects)
+ switch(current_active_effect)
+ if(FISHING_MINIGAME_RULE_ANTIGRAV)
+ fishing_hud.icon_state = "background_antigrav"
+ SEND_SOUND(user, sound('sound/effects/arcade_jump.ogg', volume = 50))
+ COOLDOWN_START(src, active_effect_cd, rand(6, 9) SECONDS)
+ if(FISHING_MINIGAME_RULE_FLIP)
+ fishing_hud.icon_state = "background_flip"
+ fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
+ SEND_SOUND(user, sound('sound/effects/boing.ogg'))
+ COOLDOWN_START(src, active_effect_cd, rand(5, 6) SECONDS)
+ return
+
+ ///go back to normal
+ switch(current_active_effect)
+ if(FISHING_MINIGAME_RULE_ANTIGRAV)
+ var/sound/inverted_sound = sound('sound/effects/arcade_jump.ogg', volume = 50)
+ inverted_sound.frequency = -1
+ SEND_SOUND(user, inverted_sound)
+ COOLDOWN_START(src, active_effect_cd, rand(10, 13) SECONDS)
+ if(FISHING_MINIGAME_RULE_FLIP)
+ fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
+ COOLDOWN_START(src, active_effect_cd, rand(8, 12) SECONDS)
+
+ fishing_hud.icon_state = background
+ current_active_effect = null
+
///The proc that moves the fish around, just like in the old TGUI, mostly.
/datum/fishing_challenge/proc/move_fish(seconds_per_tick)
var/long_chance = long_jump_chance * seconds_per_tick * 10
@@ -464,6 +541,9 @@
velocity_change = round(velocity_change)
+ if(current_active_effect == FISHING_MINIGAME_RULE_ANTIGRAV)
+ velocity_change = -velocity_change
+
/**
* Pull the brake on the velocity if the current velocity and the acceleration
* have different directions, making the bait less slippery, thus easier to control
@@ -524,7 +604,7 @@
icon_state = challenge.background
add_overlay("frame")
hud_bait = new(null, null, challenge)
- hud_fish = new
+ hud_fish = new(null, null, challenge)
hud_completion = new(null, null, challenge)
vis_contents += list(hud_bait, hud_fish, hud_completion)
challenge.user.client.screen += src
@@ -557,6 +637,11 @@
icon_state = "fish"
vis_flags = VIS_INHERIT_ID
+/atom/movable/screen/hud_fish/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
+ . = ..()
+ if(challenge)
+ icon_state = challenge.fish_icon
+
/atom/movable/screen/hud_completion
icon = 'icons/hud/fishing_hud.dmi'
icon_state = "completion_0"
diff --git a/code/modules/fishing/fishing_portal_machine.dm b/code/modules/fishing/fishing_portal_machine.dm
index 3fc6b0eb938a6d..b156a37ba05503 100644
--- a/code/modules/fishing/fishing_portal_machine.dm
+++ b/code/modules/fishing/fishing_portal_machine.dm
@@ -1,40 +1,58 @@
/obj/machinery/fishing_portal_generator
name = "fish-porter 3000"
- desc = "fishing anywhere, anytime, anyway what was i talking about"
-
+ desc = "Fishing anywhere, anytime... anyway what was I talking about?"
icon = 'icons/obj/fishing.dmi'
- icon_state = "portal_off"
-
+ icon_state = "portal"
idle_power_usage = 0
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
-
anchored = FALSE
density = TRUE
+ circuit = /obj/item/circuitboard/machine/fishing_portal_generator
- var/fishing_source = /datum/fish_source/portal
+ ///The current fishing spot loaded in
var/datum/component/fishing_spot/active
+/obj/machinery/fishing_portal_generator/on_set_panel_open()
+ update_appearance()
+ return ..()
+
/obj/machinery/fishing_portal_generator/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
return TOOL_ACT_TOOLTYPE_SUCCESS
+/obj/machinery/fishing_portal_generator/examine(mob/user)
+ . = ..()
+ . += span_notice("You can unlock further portal settings by completing fish scanning experiments.")
+
+/obj/machinery/fishing_portal_generator/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ obj_flags |= EMAGGED
+ balloon_alert(user, "syndicate setting loaded")
+ playsound(src, SFX_SPARKS, 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
/obj/machinery/fishing_portal_generator/interact(mob/user, special_state)
. = ..()
if(active)
deactivate()
else
- activate()
+ select_fish_source(user)
-/obj/machinery/fishing_portal_generator/update_icon(updates)
+/obj/machinery/fishing_portal_generator/update_overlays()
. = ..()
- if(active)
- icon_state = "portal_on"
- else
- icon_state = "portal_off"
+ if(panel_open)
+ . += "portal_open"
+ if(!active)
+ return
+ . += "portal_on"
+ var/datum/fish_source/portal/portal = active.fish_source
+ . += portal.overlay_state
+ . += emissive_appearance(icon, "portal_emissive", src)
-/obj/machinery/fishing_portal_generator/proc/activate()
- active = AddComponent(/datum/component/fishing_spot, fishing_source)
+/obj/machinery/fishing_portal_generator/proc/activate(datum/fish_source/selected_source)
+ active = AddComponent(/datum/component/fishing_spot, selected_source)
use_power = ACTIVE_POWER_USE
update_icon()
@@ -46,3 +64,39 @@
/obj/machinery/fishing_portal_generator/on_set_is_operational(old_value)
if(old_value)
deactivate()
+
+///Create a radial menu from a list of available fish sources. If only the default is available, activate it right away.
+/obj/machinery/fishing_portal_generator/proc/select_fish_source(mob/user)
+ var/datum/fish_source/portal/default = GLOB.preset_fish_sources[/datum/fish_source/portal]
+ var/list/available_fish_sources = list(default.radial_name = default)
+ if(obj_flags & EMAGGED)
+ var/datum/fish_source/portal/syndicate = GLOB.preset_fish_sources[/datum/fish_source/portal/syndicate]
+ available_fish_sources[syndicate.radial_name] = syndicate
+ for (var/datum/techweb/techweb as anything in SSresearch.techwebs)
+ var/get_fish_sources = FALSE
+ for(var/obj/machinery/rnd/server/server as anything in techweb.techweb_servers)
+ if(!is_valid_z_level(get_turf(server), get_turf(src)))
+ continue
+ get_fish_sources = TRUE
+ break
+ if(!get_fish_sources)
+ continue
+ for(var/experiment_type in typesof(/datum/experiment/scanning/fish))
+ var/datum/experiment/scanning/fish/experiment = techweb.completed_experiments[experiment_type]
+ if(!experiment)
+ continue
+ var/datum/fish_source/portal/reward = GLOB.preset_fish_sources[experiment.fish_source_reward]
+ available_fish_sources[reward.radial_name] = reward
+
+ if(length(available_fish_sources) == 1)
+ activate(default)
+ return
+ var/list/choices = list()
+ for(var/radial_name in available_fish_sources)
+ var/datum/fish_source/portal/source = available_fish_sources[radial_name]
+ choices[radial_name] = image(icon = 'icons/hud/radial_fishing.dmi', icon_state = source.radial_state)
+
+ var/choice = show_radial_menu(user, src, choices, radius = 38, custom_check = CALLBACK(src, TYPE_PROC_REF(/atom, can_interact), user), tooltips = TRUE)
+ if(!choice || !can_interact(user))
+ return
+ activate(available_fish_sources[choice])
diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm
index 7e01f693dd5434..1abba8e414cb06 100644
--- a/code/modules/fishing/fishing_rod.dm
+++ b/code/modules/fishing/fishing_rod.dm
@@ -30,14 +30,11 @@
var/obj/item/currently_hooked_item
/// Fishing line visual for the hooked item
- var/datum/beam/hooked_item_fishing_line
+ var/datum/beam/fishing_line/fishing_line
/// Are we currently casting
var/casting = FALSE
- /// List of fishing line beams
- var/list/fishing_lines = list()
-
/// The default color for the reel overlay if no line is equipped.
var/default_line_color = "gray"
@@ -66,13 +63,10 @@
return NONE
/obj/item/fishing_rod/Destroy(force)
- . = ..()
- //Remove any leftover fishing lines
- QDEL_LIST(fishing_lines)
+ return ..()
/obj/item/fishing_rod/examine(mob/user)
. = ..()
- . += "Right-Click in your active hand to access its slots UI"
var/list/equipped_stuff = list()
if(line)
equipped_stuff += "[icon2html(line, user)] [line.name]"
@@ -84,6 +78,7 @@
. += span_notice("\a [icon2html(bait, user)] [bait] is being used as bait.")
else
. += span_warning("It doesn't have any bait attached. Fishing will be more tedious!")
+ . += span_notice("Right-Click in your active hand to access its slots UI")
/**
* Catch weight modifier for the given fish_type (or FISHING_DUD)
@@ -140,7 +135,7 @@
// Should probably respect and used force move later
step_towards(currently_hooked_item, get_turf(src))
if(get_dist(currently_hooked_item,get_turf(src)) < 1)
- clear_hooked_item()
+ QDEL_NULL(fishing_line)
/obj/item/fishing_rod/attack_self_secondary(mob/user, modifiers)
. = ..()
@@ -159,30 +154,28 @@
var/mob/user = loc
if(!istype(user))
return
+ if(fishing_line)
+ QDEL_NULL(fishing_line)
var/beam_color = line?.line_color || default_line_color
- var/datum/beam/fishing_line/fishing_line_beam = new(user, target, icon_state = "fishing_line", beam_color = beam_color, emissive = FALSE, override_target_pixel_y = target_py)
- fishing_line_beam.lefthand = user.get_held_index_of_item(src) % 2 == 1
- RegisterSignal(fishing_line_beam, COMSIG_BEAM_BEFORE_DRAW, PROC_REF(check_los))
- RegisterSignal(fishing_line_beam, COMSIG_QDELETING, PROC_REF(clear_line))
- fishing_lines += fishing_line_beam
- INVOKE_ASYNC(fishing_line_beam, TYPE_PROC_REF(/datum/beam/, Start))
+ fishing_line = new(user, target, icon_state = "fishing_line", beam_color = beam_color, emissive = FALSE, override_target_pixel_y = target_py)
+ fishing_line.lefthand = user.get_held_index_of_item(src) % 2 == 1
+ RegisterSignal(fishing_line, COMSIG_BEAM_BEFORE_DRAW, PROC_REF(check_los))
+ RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(clear_line))
+ INVOKE_ASYNC(fishing_line, TYPE_PROC_REF(/datum/beam/, Start))
user.update_held_items()
- return fishing_line_beam
+ return fishing_line
/obj/item/fishing_rod/proc/clear_line(datum/source)
SIGNAL_HANDLER
- fishing_lines -= source
if(ismob(loc))
var/mob/user = loc
user.update_held_items()
+ fishing_line = null
+ currently_hooked_item = null
/obj/item/fishing_rod/dropped(mob/user, silent)
. = ..()
- if(currently_hooked_item)
- clear_hooked_item()
- for(var/datum/beam/fishing_line in fishing_lines)
- SEND_SIGNAL(fishing_line, COMSIG_FISHING_LINE_SNAPPED)
- QDEL_LIST(fishing_lines)
+ QDEL_NULL(fishing_line)
/// Hooks the item
/obj/item/fishing_rod/proc/hook_item(mob/user, atom/target_atom)
@@ -191,28 +184,21 @@
if(!can_be_hooked(target_atom))
return
currently_hooked_item = target_atom
- hooked_item_fishing_line = create_fishing_line(target_atom)
- RegisterSignal(hooked_item_fishing_line, COMSIG_FISHING_LINE_SNAPPED, PROC_REF(clear_hooked_item))
+ create_fishing_line(target_atom)
+ SEND_SIGNAL(src, COMSIG_FISHING_ROD_HOOKED_ITEM, target_atom, user)
/// Checks what can be hooked
/obj/item/fishing_rod/proc/can_be_hooked(atom/movable/target)
// Could be made dependent on actual hook, ie magnet to hook metallic items
return isitem(target)
-/obj/item/fishing_rod/proc/clear_hooked_item()
- SIGNAL_HANDLER
-
- if(!QDELETED(hooked_item_fishing_line))
- QDEL_NULL(hooked_item_fishing_line)
- currently_hooked_item = null
-
// Checks fishing line for interruptions and range
/obj/item/fishing_rod/proc/check_los(datum/beam/source)
SIGNAL_HANDLER
. = NONE
- if(!isturf(source.origin.loc) || !isturf(source.target.loc) || !CheckToolReach(src, source.target, cast_range))
- SEND_SIGNAL(source, COMSIG_FISHING_LINE_SNAPPED) //Stepped out of range or los interrupted
+ if(!CheckToolReach(src, source.target, cast_range))
+ qdel(source)
return BEAM_CANCEL_DRAW
/obj/item/fishing_rod/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
@@ -300,7 +286,7 @@
reel_overlay.color = line_color
. += reel_overlay
/// if we don't have anything hooked show the dangling hook & line
- if(isinhands && length(fishing_lines) == 0)
+ if(isinhands && !fishing_line)
var/mutable_appearance/line_overlay = mutable_appearance(icon_file, "line_overlay")
line_overlay.appearance_flags |= RESET_COLOR
line_overlay.color = line_color
@@ -507,8 +493,8 @@
balloon_alert(user, active ? "extended" : "collapsed")
playsound(src, 'sound/weapons/batonextend.ogg', 50, TRUE)
update_appearance(UPDATE_OVERLAYS)
- if(currently_hooked_item)
- clear_hooked_item()
+ if(fishing_line)
+ QDEL_NULL(fishing_line)
return COMPONENT_NO_DEFAULT_MESSAGE
/obj/item/fishing_rod/telescopic/master
@@ -523,16 +509,34 @@
/obj/item/fishing_rod/tech
name = "advanced fishing rod"
desc = "An embedded universal constructor along with micro-fusion generator makes this marvel of technology never run out of bait. Interstellar treaties prevent using it outside of recreational fishing. And you can fish with this. "
- ui_description = "This rod has an infinite supply of synthetic bait."
+ ui_description = "This rod has an infinite supply of synth-bait. Also doubles as an Experi-Scanner for fish."
icon_state = "fishing_rod_science"
reel_overlay = "reel_science"
/obj/item/fishing_rod/tech/Initialize(mapload)
. = ..()
+
+ var/static/list/fishing_signals = list(
+ COMSIG_FISHING_ROD_HOOKED_ITEM = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_FISHING_ROD_CAUGHT_FISH = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_PRE_ATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_AFTERATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, ignored_handheld_experiment_attempt),
+ )
+ AddComponent(/datum/component/experiment_handler, \
+ config_mode = EXPERIMENT_CONFIG_ALTCLICK, \
+ allowed_experiments = list(/datum/experiment/scanning/fish), \
+ config_flags = EXPERIMENT_CONFIG_SILENT_FAIL|EXPERIMENT_CONFIG_IMMEDIATE_ACTION, \
+ experiment_signals = fishing_signals, \
+ )
+
var/obj/item/food/bait/doughball/synthetic/infinite_supply_of_bait = new(src)
bait = infinite_supply_of_bait
update_icon()
+/obj/item/fishing_rod/tech/examine(mob/user)
+ . = ..()
+ . += span_notice("Alt-Click to access the Experiment Configuration UI")
+
/obj/item/fishing_rod/tech/consume_bait(atom/movable/reward)
return
diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm
index 5a34db9ce5fbeb..657b2f30968b74 100644
--- a/code/modules/fishing/sources/_fish_source.dm
+++ b/code/modules/fishing/sources/_fish_source.dm
@@ -1,11 +1,47 @@
GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_source, list()))
+/**
+ * When adding new fishable rewards to a table/counts, you can specify an icon to show in place of the
+ * generic fish icon in the minigame UI should the user have the TRAIT_REVEAL_FISH trait, by adding it to
+ * this list.
+ *
+ * A lot of the icons here may be a tad inaccurate, but since we're limited to the free font awesome icons we
+ * have access to, we got to make do.
+ */
+GLOBAL_LIST_INIT(specific_fish_icons, zebra_typecacheof(list(
+ /mob/living/basic/carp = FISH_ICON_DEF,
+ /mob/living/basic/mining = FISH_ICON_HOSTILE,
+ /obj/effect/decal/remains = FISH_ICON_BONE,
+ /obj/effect/mob_spawn/corpse = FISH_ICON_BONE,
+ /obj/item/coin = FISH_ICON_COIN,
+ /obj/item/fish = FISH_ICON_DEF,
+ /obj/item/fish/armorfish = FISH_ICON_CRAB,
+ /obj/item/fish/boned = FISH_ICON_BONE,
+ /obj/item/fish/chasm_crab = FISH_ICON_CRAB,
+ /obj/item/fish/gunner_jellyfish = FISH_ICON_JELLYFISH,
+ /obj/item/fish/holo/crab = FISH_ICON_CRAB,
+ /obj/item/fish/holo/puffer = FISH_ICON_CHUNKY,
+ /obj/item/fish/mastodon = FISH_ICON_BONE,
+ /obj/item/fish/pufferfish = FISH_ICON_CHUNKY,
+ /obj/item/fish/slimefish = FISH_ICON_SLIME,
+ /obj/item/fish/sludgefish = FISH_ICON_SLIME,
+ /obj/item/fish/starfish = FISH_ICON_STAR,
+ /obj/item/storage/wallet = FISH_ICON_COIN,
+ /obj/item/stack/sheet/bone = FISH_ICON_BONE,
+ /obj/item/stack/sheet/mineral = FISH_ICON_GEM,
+ /obj/item/stack/ore = FISH_ICON_GEM,
+ /obj/structure/closet/crate = FISH_ICON_COIN,
+)))
+
/**
* Where the fish actually come from - every fishing spot has one assigned but multiple fishing holes
* can share single source, ie single shared one for ocean/lavaland river
*/
/datum/fish_source
- /// Fish catch weight table - these are relative weights
+ /**
+ * Fish catch weight table - these are relative weights
+ *
+ */
var/list/fish_table = list()
/// If a key from fish_table is present here, that fish is availible in limited quantity and is reduced by one on successful fishing
var/list/fish_counts = list()
@@ -25,6 +61,10 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
if(!(path in fish_table))
stack_trace("path [path] found in the 'fish_counts' list but not in the fish_table one of [type]")
+///Called when src is set as the fish source of a fishing spot component
+/datum/fish_source/proc/on_fishing_spot_init(/datum/component/fishing_spot/spot)
+ 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)
return rod.reason_we_cant_fish(src)
@@ -35,7 +75,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
* This includes the source's fishing difficulty, that of the fish, the rod,
* favorite and disliked baits, fish traits and the fisherman skill.
*
- * For non-fish, it's just the source's fishing difficulty minus the fisherman skill, rod and settler modifiers.
+ * For non-fish, it's just the source's fishing difficulty minus the fisherman skill.
*/
/datum/fish_source/proc/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge)
. = fishing_difficulty
@@ -96,7 +136,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
* Used to register signals or add traits and the such right after conditions have been cleared
* and before the minigame starts.
*/
-/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user)
+/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
return
///Proc called when the challenge is interrupted within the fish source code.
@@ -116,7 +156,9 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = initial(caught.name))
var/turf/fishing_spot = get_turf(source.lure)
var/atom/movable/reward = dispense_reward(source.reward_path, user, fishing_spot)
- source.used_rod?.consume_bait(reward)
+ if(source.used_rod)
+ SEND_SIGNAL(source.used_rod, COMSIG_FISHING_ROD_CAUGHT_FISH, reward, user)
+ source.used_rod.consume_bait(reward)
/// Gives out the reward if possible
/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman, turf/fishing_spot)
diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm
index ffb37753881d56..e2e5491dd1d3db 100644
--- a/code/modules/fishing/sources/source_types.dm
+++ b/code/modules/fishing/sources/source_types.dm
@@ -19,11 +19,168 @@
/datum/fish_source/portal
fish_table = list(
- FISHING_DUD = 5,
+ FISHING_DUD = 7,
/obj/item/fish/goldfish = 10,
/obj/item/fish/guppy = 10,
+ /obj/item/fish/angelfish = 10,
+ )
+ catalog_description = "Aquarium dimension (Fishing portal generator)"
+ ///The name of this option shown in the radial menu on the fishing portal generator
+ var/radial_name = "Aquarium"
+ ///The icon state shown for this option in the radial menu
+ var/radial_state = "fish_tank"
+ ///The icon state of the overlay shown on the machine when active.
+ var/overlay_state = "portal_aquarium"
+
+/datum/fish_source/portal/beach
+ fish_table = list(
+ FISHING_DUD = 10,
+ /obj/item/fish/clownfish = 10,
+ /obj/item/fish/pufferfish = 10,
+ /obj/item/fish/cardinal = 10,
+ /obj/item/fish/greenchromis = 10,
+ )
+ catalog_description = "Beach dimension (Fishing portal generator)"
+ radial_name = "Beach"
+ radial_state = "palm_beach"
+
+/datum/fish_source/portal/chasm
+ background = "background_lavaland"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/chasm_crab = 10,
+ /obj/item/fish/boned = 5,
+ /obj/item/stack/sheet/bone = 5,
+ )
+ catalog_description = "Chasm dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Chasm"
+ overlay_state = "portal_chasm"
+ radial_state = "ground_hole"
+
+/datum/fish_source/portal/ocean
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/lanternfish = 5,
+ /obj/item/fish/firefish = 5,
+ /obj/item/fish/dwarf_moonfish = 5,
+ /obj/item/fish/gunner_jellyfish = 5,
+ /obj/item/fish/needlefish = 5,
+ /obj/item/fish/armorfish = 5,
+ )
+ catalog_description = "Ocean dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Ocean"
+ overlay_state = "portal_ocean"
+ radial_state = "seaboat"
+
+/datum/fish_source/portal/hyperspace
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/starfish = 6,
+ /obj/item/stack/ore/bluespace_crystal = 2,
+ /mob/living/basic/carp = 2,
+ )
+ fish_counts = list(
+ /obj/item/stack/ore/bluespace_crystal = 10,
)
- catalog_description = "Fish dimension (Fishing portal generator)"
+ catalog_description = "Hyperspace dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Hyperspace"
+ overlay_state = "portal_hyperspace"
+ radial_state = "space_rocket"
+
+///Unlocked by emagging the fishing portal generator with an emag.
+/datum/fish_source/portal/syndicate
+ background = "background_lavaland"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/donkfish = 5,
+ /obj/item/fish/emulsijack = 5,
+ )
+ catalog_description = "Syndicate dimension (Fishing portal generator)"
+ radial_name = "Syndicate"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15
+ overlay_state = "portal_syndicate"
+ radial_state = "syndi_snake"
+
+/**
+ * A special portal fish source which fish table is populated on init with the contents of all
+ * portal fish sources, except for FISHING_DUD, and a couple more caveats.
+ */
+/datum/fish_source/portal/random
+ fish_table = null //It's populated the first time the source is loaded on a fishing portal generator.
+ catalog_description = null // it'd make a bad entry in the catalog.
+ radial_name = "Randomizer"
+ overlay_state = "portal_randomizer"
+ var/static/list/all_portal_fish_sources_at_once
+ radial_state = "misaligned_question_mark"
+
+///Generate the fish table if we don't have one already.
+/datum/fish_source/portal/random/on_fishing_spot_init(datum/component/fishing_spot/spot)
+ if(fish_table)
+ return
+
+ ///rewards not found in other fishing portals
+ fish_table = list(
+ /obj/item/fish/holo/checkered = 1,
+ )
+
+ for(var/portal_type in GLOB.preset_fish_sources)
+ if(portal_type == type || !ispath(portal_type, /datum/fish_source/portal))
+ continue
+ var/datum/fish_source/portal/preset_portal = GLOB.preset_fish_sources[portal_type]
+ fish_table |= preset_portal.fish_table
+
+ ///We don't serve duds.
+ fish_table -= FISHING_DUD
+
+ for(var/reward_path in fish_table)
+ fish_table[reward_path] = rand(1, 4)
+
+///Difficulty has to be calculated before the rest, because of how it influences jump chances
+/datum/fish_source/portal/random/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge)
+ . = ..()
+ . += rand(-10, 15)
+
+///In the spirit of randomness, we skew a few values here and there
+/datum/fish_source/portal/random/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
+ challenge.bait_bounce_mult = clamp(challenge.bait_bounce_mult + (rand(-3, 3) * 0.1), 0.1, 1)
+ challenge.completion_loss = max(challenge.completion_loss + rand(-2, 2), 0)
+ challenge.completion_gain = max(challenge.completion_gain + rand(-1, 1), 2)
+ challenge.short_jump_velocity_limit += rand(-100, 100)
+ challenge.long_jump_velocity_limit += rand(-100, 100)
+ var/static/list/active_effects = bitfield_to_list(FISHING_MINIGAME_ACTIVE_EFFECTS)
+ for(var/effect in active_effects)
+ if(prob(30))
+ challenge.special_effects |= effect
+
+///Cherry on top, fish caught from the randomizer portal also have (almost completely) random traits
+/datum/fish_source/portal/random/spawn_reward(reward_path, mob/fisherman, turf/fishing_spot)
+ if(!ispath(reward_path, /obj/item/fish))
+ return ..()
+
+ var/static/list/weighted_traits
+ if(!weighted_traits)
+ weighted_traits = list()
+ for(var/trait_type as anything in GLOB.fish_traits)
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ weighted_traits[trait.type] = round(trait.inheritability**2/100)
+
+ var/obj/item/fish/caught_fish = new reward_path(get_turf(fisherman), FALSE)
+ var/list/fixed_traits = list()
+ for(var/trait_type in caught_fish.fish_traits)
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ if(caught_fish.type in trait.guaranteed_inheritance_types)
+ fixed_traits += trait_type
+ var/list/new_traits = list()
+ for(var/iteration in rand(1, 4))
+ new_traits |= pick_weight(weighted_traits)
+ caught_fish.inherit_traits(new_traits, fixed_traits = fixed_traits)
+ caught_fish.randomize_size_and_weight(deviation = 0.3)
+ caught_fish.progenitors = full_capitalize(caught_fish.name)
+ return caught_fish
+
/datum/fish_source/chasm
catalog_description = "Chasm depths"
@@ -99,7 +256,7 @@
fish_table = list(
FISHING_DUD = 18,
/obj/item/fish/sludgefish = 18,
- /obj/item/fish/slimefish = 2,
+ /obj/item/fish/slimefish = 4,
/obj/item/storage/wallet/money = 2,
)
fish_counts = list(
@@ -125,7 +282,7 @@
if(!istype(get_area(fisherman), /area/station/holodeck))
return "You need to be inside the Holodeck to catch holographic fish."
-/datum/fish_source/holographic/pre_challenge_started(obj/item/fishing_rod/rod, mob/user)
+/datum/fish_source/holographic/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(check_area))
/datum/fish_source/holographic/proc/check_area(mob/user)
diff --git a/code/modules/food_and_drinks/machinery/deep_fryer.dm b/code/modules/food_and_drinks/machinery/deep_fryer.dm
index fc20112f08a89e..071cc18febc433 100644
--- a/code/modules/food_and_drinks/machinery/deep_fryer.dm
+++ b/code/modules/food_and_drinks/machinery/deep_fryer.dm
@@ -216,7 +216,7 @@ GLOBAL_LIST_INIT(oilfry_blacklisted_items, typecacheof(list(
if(target_temp < TCMB + 10) // a tiny bit of leeway
dunking_target.visible_message(span_userdanger("[dunking_target] explodes from the entropic difference! Holy fuck!"))
dunking_target.investigate_log("has been gibbed by entropic difference (being dunked into [src]).", INVESTIGATE_DEATHS)
- dunking_target.gib()
+ dunking_target.gib(DROP_ALL_REMAINS)
log_combat(user, dunking_target, "blew up", null, "by dunking them into [src]")
return
diff --git a/code/modules/food_and_drinks/machinery/gibber.dm b/code/modules/food_and_drinks/machinery/gibber.dm
index ebb2b85e2b931c..121acceb4c4976 100644
--- a/code/modules/food_and_drinks/machinery/gibber.dm
+++ b/code/modules/food_and_drinks/machinery/gibber.dm
@@ -198,6 +198,7 @@
occupant.reagents.trans_to(newmeat, occupant_volume / meat_produced, remove_blacklisted = TRUE)
if(sourcejob)
newmeat.subjectjob = sourcejob
+
allmeat[i] = newmeat
if(typeofskin)
@@ -221,12 +222,24 @@
skin.throw_at(pick(nearby_turfs),meat_produced,3)
for (var/i=1 to meat_produced)
var/obj/item/meatslab = allmeat[i]
+
+ if(LAZYLEN(diseases))
+ var/list/datum/disease/diseases_to_add = list()
+ for(var/datum/disease/disease as anything in diseases)
+ // admin or special viruses that should not be reproduced
+ if(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
+ continue
+
+ diseases_to_add += disease
+ if(LAZYLEN(diseases_to_add))
+ meatslab.AddComponent(/datum/component/infective, diseases_to_add)
+
meatslab.forceMove(loc)
meatslab.throw_at(pick(nearby_turfs),i,3)
for (var/turfs=1 to meat_produced)
var/turf/gibturf = pick(nearby_turfs)
if (!gibturf.density && (src in view(gibturf)))
- new gibtype(gibturf,i,diseases)
+ new gibtype(gibturf, i, diseases)
pixel_x = base_pixel_x //return to its spot after shaking
operating = FALSE
@@ -243,4 +256,4 @@
if(victim.loc == input)
victim.forceMove(src)
- victim.gib()
+ victim.gib(DROP_ALL_REMAINS)
diff --git a/code/modules/food_and_drinks/machinery/microwave.dm b/code/modules/food_and_drinks/machinery/microwave.dm
index 36d98c907a5a6d..d52e2213a5b281 100644
--- a/code/modules/food_and_drinks/machinery/microwave.dm
+++ b/code/modules/food_and_drinks/machinery/microwave.dm
@@ -14,11 +14,15 @@
/// The max amount of dirtiness a microwave can be
#define MAX_MICROWAVE_DIRTINESS 100
-/obj/machinery/microwave//SKYRAT EDIT - ICON OVERRIDEN BY AESTHETICS - SEE MODULE
+/// For the wireless version, and display fluff
+#define TIER_1_CELL_CHARGE_RATE 250
+
+/obj/machinery/microwave
name = "microwave oven"
desc = "Cooks and boils stuff."
icon = 'icons/obj/machines/microwave.dmi'
- icon_state = "map_icon"
+ base_icon_state = ""
+ icon_state = "mw_complete"
appearance_flags = KEEP_TOGETHER | LONG_GLIDE | PIXEL_SCALE
layer = BELOW_OBJ_LAYER
density = TRUE
@@ -27,30 +31,63 @@
light_color = LIGHT_COLOR_DIM_YELLOW
light_power = 3
anchored_tabletop_offset = 6
- var/wire_disabled = FALSE // is its internal wire cut?
+ /// Is its function wire cut?
+ var/wire_disabled = FALSE
+ /// Wire cut to run mode backwards
+ var/wire_mode_swap = FALSE
var/operating = FALSE
/// How dirty is it?
var/dirty = 0
var/dirty_anim_playing = FALSE
/// How broken is it? NOT_BROKEN, KINDA_BROKEN, REALLY_BROKEN
var/broken = NOT_BROKEN
+ /// Microwave door position
var/open = FALSE
+ /// Microwave max capacity
var/max_n_of_items = 10
+ /// Microwave efficiency (power) based on the stock components
var/efficiency = 0
+ /// If we use a cell instead of powernet
+ var/cell_powered = FALSE
+ /// The cell we charge with
+ var/obj/item/stock_parts/cell/cell
+ /// The cell we're charging
+ var/obj/item/stock_parts/cell/vampire_cell
+ /// Capable of vampire charging PDAs
+ var/vampire_charging_capable = FALSE
+ /// Charge contents of microwave instead of cook
+ var/vampire_charging_enabled = FALSE
var/datum/looping_sound/microwave/soundloop
- var/list/ingredients = list() // may only contain /atom/movables
-
+ /// May only contain /atom/movables
+ var/list/ingredients = list()
+ /// When this is the nth ingredient, whats its pixel_x?
+ var/list/ingredient_shifts_x = list(
+ -2,
+ 1,
+ -5,
+ 2,
+ -6,
+ 0,
+ -4,
+ )
+ /// When this is the nth ingredient, whats its pixel_y?
+ var/list/ingredient_shifts_y = list(
+ -4,
+ -2,
+ -3,
+ )
var/static/radial_examine = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_examine")
var/static/radial_eject = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_eject")
- var/static/radial_use = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_use")
+ var/static/radial_cook = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_cook")
+ var/static/radial_charge = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_charge")
// we show the button even if the proc will not work
- var/static/list/radial_options = list("eject" = radial_eject, "use" = radial_use)
- var/static/list/ai_radial_options = list("eject" = radial_eject, "use" = radial_use, "examine" = radial_examine)
+ var/static/list/radial_options = list("eject" = radial_eject, "cook" = radial_cook, "charge" = radial_charge)
+ var/static/list/ai_radial_options = list("eject" = radial_eject, "cook" = radial_cook, "charge" = radial_charge, "examine" = radial_examine)
/obj/machinery/microwave/Initialize(mapload)
. = ..()
-
+ register_context()
set_wires(new /datum/wires/microwave(src))
create_reagents(100)
soundloop = new(src, FALSE)
@@ -66,7 +103,6 @@
itemized_ingredient.pixel_y = itemized_ingredient.base_pixel_y + rand(-5, 6)
return ..()
-
/obj/machinery/microwave/on_deconstruction()
eject()
return ..()
@@ -75,21 +111,70 @@
QDEL_LIST(ingredients)
QDEL_NULL(wires)
QDEL_NULL(soundloop)
+ if(!isnull(cell))
+ QDEL_NULL(cell)
return ..()
+/obj/machinery/microwave/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(cell_powered)
+ if(!isnull(cell))
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = "Remove cell"
+ 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"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ if(broken > NOT_BROKEN)
+ if(broken == REALLY_BROKEN && held_item?.tool_behaviour == TOOL_WIRECUTTER)
+ context[SCREENTIP_CONTEXT_LMB] = "Repair"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ else if(broken == KINDA_BROKEN && held_item?.tool_behaviour == TOOL_WELDER)
+ context[SCREENTIP_CONTEXT_LMB] = "Repair"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ context[SCREENTIP_CONTEXT_LMB] = "Show menu"
+
+ if(vampire_charging_capable)
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Change to [vampire_charging_enabled ? "cook" : "charge"]"
+
+ if(length(ingredients) != 0)
+ context[SCREENTIP_CONTEXT_RMB] = "Start [vampire_charging_enabled ? "charging" : "cooking"]"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
/obj/machinery/microwave/RefreshParts()
. = ..()
efficiency = 0
+ vampire_charging_capable = FALSE
for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
efficiency += micro_laser.tier
for(var/datum/stock_part/matter_bin/matter_bin in component_parts)
max_n_of_items = 10 * matter_bin.tier
break
+ for(var/datum/stock_part/capacitor/capacitor in component_parts)
+ if(capacitor.tier >= 2)
+ vampire_charging_capable = TRUE
+ visible_message(span_notice("The [EXAMINE_HINT("Charge Ready")] light on \the [src] flickers to life."))
+ break
/obj/machinery/microwave/examine(mob/user)
. = ..()
+ if(vampire_charging_capable)
+ . += span_info("This model features Waveâ„¢: a Nanotrasen exclusive. Our latest and greatest, Waveâ„¢ allows your PDA to be charged wirelessly through microwave frequencies! You can Wave-charge your device by placing it inside and selecting the charge mode.")
+ . += span_info("Because nothing says 'future' like charging your PDA while overcooking your leftovers. Nanotrasen Waveâ„¢ - Multitasking, redefined.")
+
+ if(cell_powered)
+ . += span_notice("This model is wireless, powered by portable cells. [isnull(cell) ? "The cell slot is empty." : "[EXAMINE_HINT("Ctrl-click")] to remove the power cell."]")
+
if(!operating)
- . += span_notice("Right-click [src] to turn it on.")
+ if(!operating && vampire_charging_capable)
+ . += span_notice("[EXAMINE_HINT("Alt-click")] to change default mode.")
+
+ . += span_notice("[EXAMINE_HINT("Right-click")] to start [vampire_charging_enabled ? "charging" : "cooking"] cycle.")
if(!in_range(user, src) && !issilicon(user) && !isobserver(user))
. += span_warning("You're too far away to examine [src]'s contents and display!")
@@ -106,46 +191,37 @@
var/list/items_counts = new
for(var/i in ingredients)
if(isstack(i))
- var/obj/item/stack/S = i
- items_counts[S.name] += S.amount
+ var/obj/item/stack/item_stack = i
+ items_counts[item_stack.name] += item_stack.amount
else
- var/atom/movable/AM = i
- items_counts[AM.name]++
- for(var/O in items_counts)
- . += span_notice("- [items_counts[O]]x [O].")
+ var/atom/movable/single_item = i
+ items_counts[single_item.name]++
+ for(var/item in items_counts)
+ . += span_notice("- [items_counts[item]]x [item].")
else
. += span_notice("\The [src] is empty.")
if(!(machine_stat & (NOPOWER|BROKEN)))
. += "[span_notice("The status display reads:")]\n"+\
+ "[span_notice("- Mode: [vampire_charging_enabled ? "Charge" : "Cook"].")]\n"+\
"[span_notice("- Capacity: [max_n_of_items] items.")]\n"+\
- span_notice("- Cook time reduced by [(efficiency - 1) * 25]%.")
+ span_notice("- Power: [efficiency * TIER_1_CELL_CHARGE_RATE]W.")
+
+ if(cell_powered)
+ . += span_notice("- Charge: [isnull(cell) ? "INSERT CELL" : "[round(cell.percent())]%"].")
#define MICROWAVE_INGREDIENT_OVERLAY_SIZE 24
/obj/machinery/microwave/update_overlays()
- // When this is the nth ingredient, whats its pixel_x?
- var/static/list/ingredient_shifts = list(
- // SKYRAT EDIT CHANGE START - All values offset by -3 from original
- -3,
- 0,
- -6,
- 1,
- -7,
- -1,
- -5,
- // SKYRAT EDIT CHANGE END
- )
-
. = ..()
// 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
- for (var/atom/movable/ingredient as anything in ingredients)
+ for(var/atom/movable/ingredient as anything in ingredients)
var/image/ingredient_overlay = image(ingredient, src)
var/list/icon_dimensions = get_icon_dimensions(ingredient.icon)
@@ -154,11 +230,11 @@
MICROWAVE_INGREDIENT_OVERLAY_SIZE / icon_dimensions["height"],
)
- ingredient_overlay.pixel_y = -4
+ ingredient_overlay.pixel_x = ingredient_shifts_x[(ingredient_count % ingredient_shifts_x.len) + 1]
+ ingredient_overlay.pixel_y = ingredient_shifts_y[(ingredient_count % ingredient_shifts_y.len) + 1]
ingredient_overlay.layer = FLOAT_LAYER
ingredient_overlay.plane = FLOAT_PLANE
ingredient_overlay.blend_mode = BLEND_INSET_OVERLAY
- ingredient_overlay.pixel_x = ingredient_shifts[(ingredient_count % ingredient_shifts.len) + 1]
ingredient_count += 1
@@ -167,46 +243,67 @@
var/border_icon_state
var/door_icon_state
- if (open)
- door_icon_state = "door_open"
- border_icon_state = "mwo"
- else if (operating)
- door_icon_state = "door_on"
- border_icon_state = "mw1"
+ if(open)
+ door_icon_state = "[base_icon_state]door_open"
+ border_icon_state = "[base_icon_state]mwo"
+ else if(operating)
+ if(vampire_charging_enabled)
+ door_icon_state = "[base_icon_state]door_charge"
+ else
+ door_icon_state = "[base_icon_state]door_on"
+ border_icon_state = "[base_icon_state]mw1"
else
- door_icon_state = "door_off"
- border_icon_state = "mw"
+ door_icon_state = "[base_icon_state]door_off"
+ border_icon_state = "[base_icon_state]mw"
+
. += mutable_appearance(
icon,
door_icon_state,
- alpha = ingredients.len > 0 ? 128 : 255,
)
. += border_icon_state
- if (!open)
- . += "door_handle"
+ if(!open)
+ . += "[base_icon_state]door_handle"
+
+ if(!(machine_stat & NOPOWER) || cell_powered)
+ . += emissive_appearance(icon, "emissive_[border_icon_state]", src, alpha = src.alpha)
+
+ if(cell_powered && !isnull(cell))
+ switch(cell.percent())
+ if(75 to 100)
+ . += mutable_appearance(icon, "[base_icon_state]cell_100")
+ . += emissive_appearance(icon, "[base_icon_state]cell_100", src, alpha = src.alpha)
+ if(50 to 75)
+ . += mutable_appearance(icon, "[base_icon_state]cell_75")
+ . += emissive_appearance(icon, "[base_icon_state]cell_75", src, alpha = src.alpha)
+ if(25 to 50)
+ . += mutable_appearance(icon, "[base_icon_state]cell_25")
+ . += emissive_appearance(icon, "[base_icon_state]cell_25", src, alpha = src.alpha)
+ else
+ . += mutable_appearance(icon, "[base_icon_state]cell_0")
+ . += emissive_appearance(icon, "[base_icon_state]cell_0", src, alpha = src.alpha)
return .
#undef MICROWAVE_INGREDIENT_OVERLAY_SIZE
/obj/machinery/microwave/update_icon_state()
- if (broken)
- icon_state = "mwb"
- else if (dirty_anim_playing)
- icon_state = "mwbloody1"
- else if (dirty == MAX_MICROWAVE_DIRTINESS)
- icon_state = open ? "mwbloodyo" : "mwbloody"
+ if(broken)
+ icon_state = "[base_icon_state]mwb"
+ else if(dirty_anim_playing)
+ icon_state = "[base_icon_state]mwbloody1"
+ else if(dirty == MAX_MICROWAVE_DIRTINESS)
+ icon_state = open ? "[base_icon_state]mwbloodyo" : "[base_icon_state]mwbloody"
else if(operating)
- icon_state = "back_on"
+ icon_state = "[base_icon_state]back_on"
else if(open)
- icon_state = "back_open"
+ icon_state = "[base_icon_state]back_open"
else if(panel_open)
- icon_state = "mw-o"
+ icon_state = "[base_icon_state]mw-o"
else
- icon_state = "back_off"
+ icon_state = "[base_icon_state]back_off"
return ..()
@@ -234,23 +331,23 @@
update_appearance()
return TOOL_ACT_TOOLTYPE_SUCCESS
-/obj/machinery/microwave/attackby(obj/item/O, mob/living/user, params)
+/obj/machinery/microwave/attackby(obj/item/item, mob/living/user, params)
if(operating)
return
- if(panel_open && is_wire_tool(O))
+ if(panel_open && is_wire_tool(item))
wires.interact(user)
return TRUE
if(broken > NOT_BROKEN)
- if(broken == REALLY_BROKEN && O.tool_behaviour == TOOL_WIRECUTTER) // If it's broken and they're using a TOOL_WIRECUTTER
+ 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(O.use_tool(src, user, 20))
+ 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 && O.tool_behaviour == TOOL_WELDER) // If it's broken and they're doing the wrench
+ 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(O.use_tool(src, user, 20))
+ 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()
@@ -260,8 +357,9 @@
return TRUE
return
- if(istype(O, /obj/item/reagent_containers/spray))
- var/obj/item/reagent_containers/spray/clean_spray = O
+ 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)
@@ -272,56 +370,83 @@
to_chat(user, span_warning("You need more space cleaner!"))
return TRUE
- if(istype(O, /obj/item/soap) || istype(O, /obj/item/reagent_containers/cup/rag))
+ if(istype(item, /obj/item/soap) || istype(item, /obj/item/reagent_containers/cup/rag))
var/cleanspeed = 50
- if(istype(O, /obj/item/soap))
- var/obj/item/soap/used_soap = O
+ 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
+ if(istype(item, /obj/item/stock_parts/cell) && cell_powered)
+ var/swapped = FALSE
+ if(!isnull(cell))
+ cell.forceMove(drop_location())
+ if(!issilicon(user) && Adjacent(user))
+ user.put_in_hands(cell)
+ cell = null
+ swapped = TRUE
+ if(!user.transferItemToLoc(item, src))
+ update_appearance()
+ return TRUE
+ cell = item
+ balloon_alert(user, "[swapped ? "swapped" : "inserted"] cell")
+ update_appearance()
+ return TRUE
+
+ if(!anchored)
+ balloon_alert(user, "not secured!")
+ 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(istype(O, /obj/item/storage))
- var/obj/item/storage/T = O
+ if(vampire_charging_capable && istype(item, /obj/item/modular_computer/pda) && ingredients.len > 0)
+ balloon_alert(user, "max 1 pda!")
+ return FALSE
+
+ if(istype(item, /obj/item/storage))
+ var/obj/item/storage/tray = item
var/loaded = 0
- if(!istype(O, /obj/item/storage/bag/tray))
+ if(!istype(item, /obj/item/storage/bag/tray))
// Non-tray dumping requires a do_after
- to_chat(user, span_notice("You start dumping out the contents of [O] into [src]..."))
- if(!do_after(user, 2 SECONDS, target = T))
+ to_chat(user, span_notice("You start dumping out the contents of [item] into [src]..."))
+ if(!do_after(user, 2 SECONDS, target = tray))
return
- for(var/obj/S in T.contents)
- if(!IS_EDIBLE(S))
+ for(var/obj/tray_item in tray.contents)
+ if(!IS_EDIBLE(tray_item))
continue
if(ingredients.len >= max_n_of_items)
balloon_alert(user, "it's full!")
return TRUE
- if(T.atom_storage.attempt_remove(S, src))
+ if(tray.atom_storage.attempt_remove(tray_item, src))
loaded++
- ingredients += S
+ ingredients += tray_item
if(loaded)
+ open(autoclose = 0.6 SECONDS)
to_chat(user, span_notice("You insert [loaded] items into \the [src]."))
update_appearance()
return
- if(O.w_class <= WEIGHT_CLASS_NORMAL && !istype(O, /obj/item/storage) && !user.combat_mode)
+ if(item.w_class <= WEIGHT_CLASS_NORMAL && !istype(item, /obj/item/storage) && !user.combat_mode)
if(ingredients.len >= max_n_of_items)
balloon_alert(user, "it's full!")
return TRUE
- if(!user.transferItemToLoc(O, src))
+ if(!user.transferItemToLoc(item, src))
balloon_alert(user, "it's stuck to your hand!")
return FALSE
- ingredients += O
- user.visible_message(span_notice("[user] adds \a [O] to \the [src]."), span_notice("You add [O] to \the [src]."))
+ ingredients += item
+ open(autoclose = 0.6 SECONDS)
+ user.visible_message(span_notice("[user] adds \a [item] to \the [src]."), span_notice("You add [item] to \the [src]."))
update_appearance()
return
@@ -332,13 +457,34 @@
if(!length(ingredients))
balloon_alert(user, "it's empty!")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
- cook(user)
+
+ start_cycle(user)
+
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+/obj/machinery/microwave/AltClick(mob/user, list/modifiers)
+ if(user.can_perform_action(src, ALLOW_SILICON_REACH))
+ if(!vampire_charging_capable)
+ return
+
+ vampire_charging_enabled = !vampire_charging_enabled
+ balloon_alert(user, "set to [vampire_charging_enabled ? "charge" : "cook"]")
+
+/obj/machinery/microwave/CtrlClick(mob/user)
+ . = ..()
+ if(cell_powered && !isnull(cell) && anchored)
+ user.put_in_hands(cell)
+ balloon_alert(user, "removed cell")
+ cell = null
+ update_appearance()
+
/obj/machinery/microwave/ui_interact(mob/user)
. = ..()
- if(operating || panel_open || !anchored || !user.can_perform_action(src, ALLOW_SILICON_REACH))
+ if(!anchored)
+ balloon_alert(user, "not secured!")
+ return
+ if(operating || panel_open || !user.can_perform_action(src, ALLOW_SILICON_REACH))
return
if(isAI(user) && (machine_stat & NOPOWER))
return
@@ -353,17 +499,21 @@
var/choice = show_radial_menu(user, src, isAI(user) ? ai_radial_options : radial_options, require_near = !issilicon(user))
// post choice verification
- if(operating || panel_open || !anchored || !user.can_perform_action(src, ALLOW_SILICON_REACH))
+ if(operating || panel_open || (!vampire_charging_capable && !anchored) || !user.can_perform_action(src, ALLOW_SILICON_REACH))
return
if(isAI(user) && (machine_stat & NOPOWER))
return
- usr.set_machine(src)
+ user.set_machine(src)
switch(choice)
if("eject")
eject()
- if("use")
- cook(user)
+ if("cook")
+ vampire_charging_enabled = FALSE
+ start_cycle(user)
+ if("charge")
+ vampire_charging_enabled = TRUE
+ start_cycle(user)
if("examine")
examine(user)
@@ -371,8 +521,20 @@
var/atom/drop_loc = drop_location()
for(var/atom/movable/movable_ingredient as anything in ingredients)
movable_ingredient.forceMove(drop_loc)
- open()
- playsound(loc, 'sound/machines/click.ogg', 15, TRUE, -3)
+ open(autoclose = 1.4 SECONDS)
+
+/obj/machinery/microwave/proc/start_cycle(mob/user)
+ if(wire_mode_swap)
+ spark()
+ if(vampire_charging_enabled)
+ cook(user)
+ else
+ charge(user)
+
+ else if(vampire_charging_enabled)
+ charge(user)
+ else
+ cook(user)
/**
* Begins the process of cooking the included ingredients.
@@ -382,6 +544,7 @@
/obj/machinery/microwave/proc/cook(mob/cooker)
if(machine_stat & (NOPOWER|BROKEN))
return
+
if(operating || broken > 0 || panel_open || !anchored || dirty >= MAX_MICROWAVE_DIRTINESS)
return
@@ -390,9 +553,15 @@
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
return
+ if(cell_powered && cell?.charge < TIER_1_CELL_CHARGE_RATE * efficiency)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ balloon_alert(cooker, "replace cell!")
+ return
+
if(cooker && HAS_TRAIT(cooker, TRAIT_CURSED) && prob(7))
muck()
return
+
if(prob(max((5 / efficiency) - 5, dirty * 5))) //a clean unupgraded microwave has no risk of failure
muck()
return
@@ -413,16 +582,18 @@
/obj/machinery/microwave/proc/wzhzhzh()
visible_message(span_notice("\The [src] turns on."), null, span_hear("You hear a microwave humming."))
operating = TRUE
+ if(cell_powered && !isnull(cell))
+ cell.use(TIER_1_CELL_CHARGE_RATE * efficiency)
- set_light(1.5)
+ set_light(l_range = 1.5, l_power = 1.2, l_on = TRUE)
soundloop.start()
update_appearance()
/obj/machinery/microwave/proc/spark()
visible_message(span_warning("Sparks fly around [src]!"))
- var/datum/effect_system/spark_spread/s = new
- s.set_up(2, 1, src)
- s.start()
+ var/datum/effect_system/spark_spread/sparks = new
+ sparks.set_up(2, 1, src)
+ sparks.start()
/**
* The start of the cook loop
@@ -431,7 +602,7 @@
*/
/obj/machinery/microwave/proc/start(mob/cooker)
wzhzhzh()
- loop(MICROWAVE_NORMAL, 10, cooker = cooker)
+ cook_loop(type = MICROWAVE_NORMAL, cycles = 10, cooker = cooker)
/**
* The start of the cook loop, but can fail (result in a splat / dirty microwave)
@@ -440,29 +611,29 @@
*/
/obj/machinery/microwave/proc/start_can_fail(mob/cooker)
wzhzhzh()
- loop(MICROWAVE_PRE, 4, cooker = cooker)
+ cook_loop(type = MICROWAVE_PRE, cycles = 4, cooker = cooker)
/obj/machinery/microwave/proc/muck()
wzhzhzh()
playsound(loc, 'sound/effects/splat.ogg', 50, TRUE)
dirty_anim_playing = TRUE
update_appearance()
- loop(MICROWAVE_MUCK, 4)
+ cook_loop(type = MICROWAVE_MUCK, cycles = 4)
/**
* The actual cook loop started via [proc/start] or [proc/start_can_fail]
*
- * * type - the type of cooking, determined via how this iteration of loop is called, and determines the result
+ * * type - the type of cooking, determined via how this iteration of cook_loop is called, and determines the result
* * time - how many loops are left, base case for recursion
* * wait - deciseconds between loops
* * 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/loop(type, time, wait = max(12 - 2 * efficiency, 2), mob/cooker) // standard wait is 10
+/obj/machinery/microwave/proc/cook_loop(type, cycles, wait = max(12 - 2 * efficiency, 2), mob/cooker) // standard wait is 10
if((machine_stat & BROKEN) && type == MICROWAVE_PRE)
pre_fail()
return
- if(time <= 0 || !length(ingredients))
+ if(cycles <= 0 || !length(ingredients))
switch(type)
if(MICROWAVE_NORMAL)
loop_finish(cooker)
@@ -471,18 +642,21 @@
if(MICROWAVE_PRE)
pre_success(cooker)
return
- time--
+ cycles--
use_power(active_power_usage)
- addtimer(CALLBACK(src, PROC_REF(loop), type, time, wait, cooker), wait)
+ addtimer(CALLBACK(src, PROC_REF(cook_loop), type, cycles, wait, cooker), wait)
/obj/machinery/microwave/power_change()
. = ..()
+ if(cell_powered)
+ return
+
if((machine_stat & NOPOWER) && operating)
pre_fail()
eject()
/**
- * Called when the loop is done successfully, no dirty mess or whatever
+ * Called when the cook_loop is done successfully, no dirty mess or whatever
*
* * cooker - The mob that initiated the cook cycle, can be null if no apparent mob triggered it (such as via emp)
*/
@@ -492,6 +666,11 @@
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))
@@ -502,7 +681,7 @@
metal_amount += (cooked_item.custom_materials?[GET_MATERIAL_REF(/datum/material/iron)] || 0)
- if(cursed_chef && prob(5))
+ if(cursed_chef && (metal_amount || prob(5))) // If we're unlucky and have metal, we're guaranteed to explode
spark()
broken = REALLY_BROKEN
explosion(src, light_impact_range = 2, flame_range = 1)
@@ -510,10 +689,8 @@
if(metal_amount)
spark()
broken = REALLY_BROKEN
- if(cursed_chef || prob(max(metal_amount / 2, 33))) // If we're unlucky and have metal, we're guaranteed to explode
+ if(prob(max(metal_amount / 2, 33)))
explosion(src, heavy_impact_range = 1, light_impact_range = 2)
- else
- dump_inventory_contents()
after_finish_loop()
@@ -524,7 +701,7 @@
after_finish_loop()
/obj/machinery/microwave/proc/pre_success(mob/cooker)
- loop(MICROWAVE_NORMAL, 10, cooker = cooker)
+ cook_loop(type = MICROWAVE_NORMAL, cycles = 10, cooker = cooker)
/obj/machinery/microwave/proc/muck_finish()
visible_message(span_warning("\The [src] gets covered in muck!"))
@@ -536,19 +713,121 @@
after_finish_loop()
/obj/machinery/microwave/proc/after_finish_loop()
- set_light(0)
+ set_light(l_on = FALSE)
soundloop.stop()
- open()
+ eject()
+ open(autoclose = 2 SECONDS)
-/obj/machinery/microwave/proc/open()
+/obj/machinery/microwave/proc/open(autoclose = 2 SECONDS)
open = TRUE
+ playsound(loc, 'sound/machines/click.ogg', 15, TRUE, -3)
update_appearance()
- addtimer(CALLBACK(src, PROC_REF(close)), 0.8 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(close)), autoclose)
/obj/machinery/microwave/proc/close()
open = FALSE
update_appearance()
+/**
+ * The start of the charge loop
+ *
+ * * 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)
+ after_finish_loop()
+ return
+
+ vampire_cell = vampire_pda.internal_cell
+ if(isnull(vampire_pda))
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ after_finish_loop()
+ return
+
+ var/vampire_charge_amount = vampire_cell.maxcharge - vampire_cell.charge
+ charge_loop(vampire_charge_amount, cooker = cooker)
+
+/obj/machinery/microwave/proc/charge(mob/cooker)
+ if(!vampire_charging_capable)
+ balloon_alert(cooker, "needs upgrade!")
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ return
+
+ if(operating || broken > 0 || panel_open || dirty >= MAX_MICROWAVE_DIRTINESS)
+ return
+
+ if(wire_disabled)
+ audible_message("[src] buzzes.")
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ return
+
+ // We should only be charging PDAs
+ for(var/atom/movable/potential_item as anything in ingredients)
+ if(!istype(potential_item, /obj/item/modular_computer/pda))
+ balloon_alert(cooker, "pda only!")
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ eject()
+ return
+
+ vampire(cooker)
+
+/**
+ * The actual cook loop started via [proc/start] or [proc/start_can_fail]
+ *
+ * * type - the type of charging, determined via how this iteration of cook_loop is called, and determines the result
+ * * time - how many loops are left, base case for recursion
+ * * wait - deciseconds between loops
+ * * 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/charge_loop(vampire_charge_amount, wait = max(12 - 2 * efficiency, 2), mob/cooker) // standard wait is 10
+ if(machine_stat & BROKEN)
+ pre_fail()
+ return
+
+ if(!vampire_charge_amount || !length(ingredients) || (!isnull(cell) && !cell.charge) || vampire_charge_amount < 25)
+ vampire_cell = null
+ charge_loop_finish(cooker)
+ return
+
+ var/charge_rate = vampire_cell.chargerate * (1 + ((efficiency - 1) * 0.25))
+ if(charge_rate > vampire_charge_amount)
+ charge_rate = vampire_charge_amount
+
+ if(cell_powered && !cell.use(charge_rate))
+ charge_loop_finish(cooker)
+
+ vampire_cell.give(charge_rate * (0.85 + (efficiency * 0.5))) // we lose a tiny bit of power in the transfer as heat
+ use_power(charge_rate)
+
+ vampire_charge_amount = vampire_cell.maxcharge - vampire_cell.charge
+
+ addtimer(CALLBACK(src, PROC_REF(charge_loop), vampire_charge_amount, wait, cooker), wait)
+
+/obj/machinery/microwave/power_change()
+ . = ..()
+ if((machine_stat & NOPOWER) && operating)
+ pre_fail()
+ eject()
+
+/**
+ * Called when the charge_loop is done successfully, no dirty mess or whatever
+ *
+ * * 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/charge_loop_finish(mob/cooker)
+ operating = FALSE
+ var/cursed_chef = cooker && HAS_TRAIT(cooker, TRAIT_CURSED)
+ if(cursed_chef && prob(5))
+ spark()
+ broken = REALLY_BROKEN
+ explosion(src, light_impact_range = 2, flame_range = 1)
+
+ // playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
+ after_finish_loop()
+
/// Type of microwave that automatically turns it self on erratically. Probably don't use this outside of the holodeck program "Microwave Paradise".
/// You could also live your life with a microwave that will continously run in the background of everything while also not having any power draw. I think the former makes more sense.
/obj/machinery/microwave/hell
@@ -564,13 +843,48 @@
//The microwave should turn off asynchronously from any other microwaves that initialize at the same time. Keep in mind this will not turn off, since there is nothing to call the proc that ends this microwave's looping
addtimer(CALLBACK(src, PROC_REF(wzhzhzh)), rand(0.5 SECONDS, 3 SECONDS))
+/obj/machinery/microwave/engineering
+ name = "wireless microwave oven"
+ desc = "For the hard-working tradesperson who's in the middle of nowhere and just wants to warm up their pastry-based savoury item from an overpriced vending machine."
+ base_icon_state = "engi_"
+ icon_state = "engi_mw_complete"
+ circuit = /obj/item/circuitboard/machine/microwave/engineering
+ light_color = LIGHT_COLOR_BABY_BLUE
+ // We don't use area power, we always use the cell
+ use_power = NO_POWER_USE
+ cell_powered = TRUE
+ vampire_charging_capable = TRUE
+ ingredient_shifts_x = list(
+ 0,
+ 5,
+ -5,
+ 3,
+ -3,
+ )
+ ingredient_shifts_y = list(
+ 0,
+ 2,
+ -2,
+ )
+
+/obj/machinery/microwave/engineering/Initialize(mapload)
+ . = ..()
+ if(mapload)
+ cell = new /obj/item/stock_parts/cell/upgraded/plus
+ update_appearance()
+
+/obj/machinery/microwave/engineering/cell_included/Initialize(mapload)
+ . = ..()
+ cell = new /obj/item/stock_parts/cell/upgraded/plus
+ update_appearance()
+
#undef MICROWAVE_NORMAL
#undef MICROWAVE_MUCK
#undef MICROWAVE_PRE
-
#undef NOT_BROKEN
#undef KINDA_BROKEN
#undef REALLY_BROKEN
#undef MAX_MICROWAVE_DIRTINESS
+#undef TIER_1_CELL_CHARGE_RATE
diff --git a/code/modules/food_and_drinks/machinery/processor.dm b/code/modules/food_and_drinks/machinery/processor.dm
index 2c1b0b9d3b3475..60679f8934567d 100644
--- a/code/modules/food_and_drinks/machinery/processor.dm
+++ b/code/modules/food_and_drinks/machinery/processor.dm
@@ -76,7 +76,7 @@
if(isliving(what))
var/mob/living/themob = what
- themob.gib(TRUE,TRUE,TRUE)
+ themob.gib()
else
qdel(what)
LAZYREMOVE(processor_contents, what)
diff --git a/code/modules/food_and_drinks/machinery/smartfridge.dm b/code/modules/food_and_drinks/machinery/smartfridge.dm
index 6d6645e3368dc0..dfe3987dfe2a14 100644
--- a/code/modules/food_and_drinks/machinery/smartfridge.dm
+++ b/code/modules/food_and_drinks/machinery/smartfridge.dm
@@ -630,7 +630,7 @@
/obj/item/reagent_containers/cup/beaker,
/obj/item/reagent_containers/spray,
/obj/item/reagent_containers/medigel,
- /obj/item/reagent_containers/cup/vial, //SKYRAT EDIT HYPOSPRAYS
+ /obj/item/reagent_containers/cup/vial, //SKYRAT EDIT ADDITION - HYPOSPRAYS
/obj/item/reagent_containers/chem_pack
))
diff --git a/code/modules/forensics/_forensics.dm b/code/modules/forensics/_forensics.dm
index 76a051f1d727a6..5936ce4b5c4dee 100644
--- a/code/modules/forensics/_forensics.dm
+++ b/code/modules/forensics/_forensics.dm
@@ -232,6 +232,8 @@
/datum/forensics/proc/check_blood()
if(!parent || !isitem(parent.resolve()))
return
+ if(isorgan(parent.resolve())) // organs don't spawn with blood decals by default
+ return
if(!length(blood_DNA))
return
var/atom/parent_atom = parent.resolve()
diff --git a/code/modules/hallucination/fake_sound.dm b/code/modules/hallucination/fake_sound.dm
index 0a783df3adadf2..ec578f101d376f 100644
--- a/code/modules/hallucination/fake_sound.dm
+++ b/code/modules/hallucination/fake_sound.dm
@@ -167,7 +167,7 @@
sound_vary = FALSE
no_source = TRUE
sound_type = list(
- 'sound/ambience/antag/bloodcult.ogg',
+ 'sound/ambience/antag/bloodcult/bloodcult_gain.ogg',
'sound/ambience/antag/clockcultalr.ogg',
'sound/ambience/antag/ecult_op.ogg',
'sound/ambience/antag/ling_alert.ogg',
diff --git a/code/modules/hallucination/station_message.dm b/code/modules/hallucination/station_message.dm
index 30b537afbc7e8d..fa51e103cca33b 100644
--- a/code/modules/hallucination/station_message.dm
+++ b/code/modules/hallucination/station_message.dm
@@ -78,7 +78,7 @@
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(SSstation.announcer.event_sounds[ANNOUNCER_SPANOMALIES]))
+ SEND_SOUND(hallucinator, 'sound/ambience/antag/bloodcult/bloodcult_scribe.ogg')
return ..()
/datum/hallucination/station_message/meteors
diff --git a/code/modules/holodeck/holo_effect.dm b/code/modules/holodeck/holo_effect.dm
index 02502c1e6fb5bc..a8d51c8772829f 100644
--- a/code/modules/holodeck/holo_effect.dm
+++ b/code/modules/holodeck/holo_effect.dm
@@ -61,6 +61,7 @@
mobtype = pick(mobtype)
our_mob = new mobtype(loc)
our_mob.flags_1 |= HOLOGRAM_1
+ ADD_TRAIT(our_mob, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
// these vars are not really standardized but all would theoretically create stuff on death
for(var/v in list("butcher_results","corpse","weapon1","weapon2","blood_volume") & our_mob.vars)
diff --git a/code/modules/hydroponics/grown/melon.dm b/code/modules/hydroponics/grown/melon.dm
index 80962a2d182992..f69f8a68317cb0 100644
--- a/code/modules/hydroponics/grown/melon.dm
+++ b/code/modules/hydroponics/grown/melon.dm
@@ -17,7 +17,7 @@
/obj/item/seeds/watermelon/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is swallowing [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
new product(drop_location())
qdel(src)
return MANUAL_SUICIDE
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 8c42dce397fb36..de8458cb3a94ae 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -247,11 +247,7 @@
// So we'll let it leak in, and move the water over.
set_recipient_reagents_holder(nutri_reagents)
reagents = nutri_reagents
- process_request(
- amount = MACHINE_REAGENT_TRANSFER,
- reagent = null,
- dir = dir
- )
+ process_request(dir = dir)
// Move the leaked water from nutrients to... water
var/leaking_water_amount = nutri_reagents.get_reagent_amount(/datum/reagent/water)
diff --git a/code/modules/industrial_lift/industrial_lift.dm b/code/modules/industrial_lift/industrial_lift.dm
index dd1b3f9604a5d1..0bcc2a49bc84a7 100644
--- a/code/modules/industrial_lift/industrial_lift.dm
+++ b/code/modules/industrial_lift/industrial_lift.dm
@@ -326,7 +326,7 @@ GLOBAL_LIST_INIT(all_radial_directions, list(
if(violent_landing)
// Violent landing = gibbed. But the nicest kind of gibbing, keeping everything intact.
crushed.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- crushed.gib(FALSE, FALSE, FALSE)
+ crushed.gib(DROP_ALL_REMAINS)
else
// Less violent landing simply crushes every bone in your body.
crushed.Paralyze(30 SECONDS, ignore_canstun = TRUE)
diff --git a/code/modules/industrial_lift/tram/tram_windows.dm b/code/modules/industrial_lift/tram/tram_windows.dm
index 1a98a56a0ba673..55ec5aa283fcae 100644
--- a/code/modules/industrial_lift/tram/tram_windows.dm
+++ b/code/modules/industrial_lift/tram/tram_windows.dm
@@ -36,8 +36,9 @@
if(fulltile)
return ..()
smoothing_junction = new_junction
- var/smooth_left = (smoothing_junction & turn(dir, 90))
- var/smooth_right = (smoothing_junction & turn(dir, -90))
+ 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)
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index ce39b5a740563b..0d8e57f13eb030 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -178,6 +178,8 @@
for(var/i in roundstart_experience)
spawned_human.mind.adjust_experience(i, roundstart_experience[i], TRUE)
+/// Announce that this job as joined the round to all crew members.
+/// Note the joining mob has no client at this point.
/datum/job/proc/announce_job(mob/living/joining_mob, job_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: /datum/job/proc/announce_job(mob/living/joining_mob)
if(head_announce)
announce_head(joining_mob, head_announce, job_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: announce_head(joining_mob, head_announce)
@@ -191,13 +193,27 @@
/mob/living/proc/on_job_equipping(datum/job/equipping)
return
-/mob/living/carbon/human/on_job_equipping(datum/job/equipping, datum/preferences/used_pref) //SKYRAT EDIT CHANGE
+#define VERY_LATE_ARRIVAL_TOAST_PROB 20
+
+/mob/living/carbon/human/on_job_equipping(datum/job/equipping, datum/preferences/used_pref, client/player_client) //SKYRAT EDIT CHANGE - ORIGINAL: /mob/living/carbon/human/on_job_equipping(datum/job/equipping)
var/datum/bank_account/bank_account = new(real_name, equipping, dna.species.payday_modifier)
bank_account.payday(STARTING_PAYCHECKS, TRUE)
account_id = bank_account.account_id
bank_account.replaceable = FALSE
- dress_up_as_job(equipping, FALSE, used_pref) //SKYRAT EDIT CHANGE
+ add_mob_memory(/datum/memory/key/account, remembered_id = account_id)
+
+ dress_up_as_job(equipping, FALSE, used_pref) //SKYRAT EDIT CHANGE - ORIGINAL: dress_up_as_job(equipping)
+
+ if(EMERGENCY_PAST_POINT_OF_NO_RETURN && prob(VERY_LATE_ARRIVAL_TOAST_PROB))
+ //equipping.equip_to_slot_or_del(new /obj/item/food/griddle_toast(equipping), ITEM_SLOT_MASK) // SKYRAT EDIT REMOVAL - See below
+ // SKYRAT EDIT ADDITION - Lizards
+ if(islizard(equipping))
+ equip_to_slot_or_del(new /obj/item/food/breadslice/root(equipping), ITEM_SLOT_MASK)
+ else
+ equip_to_slot_or_del(new /obj/item/food/griddle_toast(equipping), ITEM_SLOT_MASK)
+ // SKYRAT EDIT ADDITION END - Lizards
+#undef VERY_LATE_ARRIVAL_TOAST_PROB
/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE)
return
@@ -267,8 +283,43 @@
return TRUE
-/datum/job/proc/radio_help_message(mob/M)
- to_chat(M, "Prefix your message with :h to speak on your department's radio. To see other prefixes, look closely at your headset.")
+/// Gets the message that shows up when spawning as this job
+/datum/job/proc/get_spawn_message(alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: /datum/job/proc/get_spawn_message()
+ SHOULD_NOT_OVERRIDE(TRUE)
+ return examine_block(span_infoplain(jointext(get_spawn_message_information(alt_title), "\n• "))) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLED - ORIGINAL: return examine_block(span_infoplain(jointext(get_spawn_message_information(), "\n• ")))
+
+/// Returns a list of strings that correspond to chat messages sent to this mob when they join the round.
+/datum/job/proc/get_spawn_message_information(alt_title = title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: /datum/job/proc/get_spawn_message_information()
+ SHOULD_CALL_PARENT(TRUE)
+ var/list/info = list()
+ info += "You are the [alt_title].\n" // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: info += "You are the [title].\n"
+ var/related_policy = get_policy(title)
+ var/radio_info = get_radio_information()
+ if(related_policy)
+ info += related_policy
+ if(supervisors)
+ info += "As the [alt_title == title ? alt_title : "[alt_title] ([title])"] you answer directly to [supervisors]. Special circumstances may change this." // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: info += "As the [title] you answer directly to [supervisors]. Special circumstances may change this."
+ if(radio_info)
+ info += radio_info
+ if(req_admin_notify)
+ info += "You are playing a job that is important for Game Progression. \
+ If you have to disconnect, please notify the admins via adminhelp."
+ if(CONFIG_GET(number/minimal_access_threshold))
+ info += span_boldnotice("As this station was initially staffed with a \
+ [CONFIG_GET(flag/jobs_have_minimal_access) ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] \
+ have been added to your ID card.")
+ //SKYRAT EDIT ADDITION START - ALTERNATIVE_JOB_TITLES
+ if(alt_title != title)
+ info += span_warning("Remember that alternate titles are purely for flavor and roleplay.")
+ info += span_doyourjobidiot("Do not use your \"[alt_title]\" alt title as an excuse to forego your duties as a [title].")
+ //SKYRAT EDIT END
+
+ return info
+
+/// Returns information pertaining to this job's radio.
+/datum/job/proc/get_radio_information()
+ if(job_flags & JOB_CREW_MEMBER)
+ return "Prefix your message with :h to speak on your department's radio. To see other prefixes, look closely at your headset."
/datum/outfit/job
name = "Standard Gear"
diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm
index f3f8b23837b276..0b38d6e081d39a 100644
--- a/code/modules/jobs/job_types/ai.dm
+++ b/code/modules/jobs/job_types/ai.dm
@@ -117,5 +117,5 @@
/datum/job/ai/config_check()
return CONFIG_GET(flag/allow_ai)
-/datum/job/ai/radio_help_message(mob/M)
- to_chat(M, "Prefix your message with :b to speak with cyborgs and other AIs.")
+/datum/job/ai/get_radio_information()
+ return "Prefix your message with :b to speak with cyborgs and other AIs."
diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm
index e788202b445190..d8ae6d335b3202 100755
--- a/code/modules/jobs/job_types/captain.dm
+++ b/code/modules/jobs/job_types/captain.dm
@@ -51,6 +51,9 @@
/datum/job/captain/get_captaincy_announcement(mob/living/captain)
return "Captain [captain.real_name] on deck!"
+/datum/job/captain/get_radio_information()
+ . = ..()
+ . += "\nYou have access to all radio channels, but they are not automatically tuned. Check your radio for more information."
/datum/outfit/job/captain
name = "Captain"
diff --git a/code/modules/jobs/job_types/chaplain/chaplain.dm b/code/modules/jobs/job_types/chaplain/chaplain.dm
index be4516a0db764c..58821ec5358760 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain.dm
@@ -25,7 +25,6 @@
mail_goodies = list(
/obj/item/reagent_containers/cup/glass/bottle/holywater = 30,
- /obj/item/toy/plush/awakenedplushie = 10,
/obj/item/grenade/chem_grenade/holy = 5,
/obj/item/toy/plush/narplush = 2,
/obj/item/toy/plush/ratplush = 1
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm b/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm
index f2ab21c9c35261..637177adffbcd3 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm
@@ -50,6 +50,14 @@
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
flags_inv = HIDEJUMPSUIT
+/obj/item/clothing/suit/chaplainsuit/habit
+ name = "religious tunic"
+ desc = "No nunsene clothing."
+ icon_state = "habit"
+ alternate_worn_layer = GLOVES_LAYER // since the sleeves cover a part of the hands, this way it looks better while retaining glove overlay correctly.
+ body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
+ flags_inv = HIDEJUMPSUIT
+
/obj/item/clothing/suit/chaplainsuit/bishoprobe
name = "bishop's robes"
desc = "Glad to see the tithes you collected were well spent."
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
index a214eb48e0cc95..805c72a326749b 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
@@ -31,7 +31,7 @@
on_clear_callback = CALLBACK(src, PROC_REF(on_cult_rune_removed)), \
effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune), \
)
- AddElement(/datum/element/bane, target_type = /mob/living/simple_animal/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
+ AddElement(/datum/element/bane, target_type = /mob/living/basic/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
if(!GLOB.holy_weapon_type && type == /obj/item/nullrod)
var/list/rods = list()
diff --git a/code/modules/jobs/job_types/cook.dm b/code/modules/jobs/job_types/cook.dm
index a3f03c2d9b13dd..e96bae827fa9b7 100644
--- a/code/modules/jobs/job_types/cook.dm
+++ b/code/modules/jobs/job_types/cook.dm
@@ -33,13 +33,14 @@
// Adds up to 100, don't mess it up
mail_goodies = list(
/obj/item/storage/box/ingredients/random = 40,
- /obj/item/reagent_containers/cup/bottle/caramel = 8,
- /obj/item/reagent_containers/condiment/flour = 8,
- /obj/item/reagent_containers/condiment/rice = 8,
- /obj/item/reagent_containers/condiment/ketchup = 8,
- /obj/item/reagent_containers/condiment/enzyme = 8,
- /obj/item/reagent_containers/condiment/soymilk = 8,
+ /obj/item/reagent_containers/cup/bottle/caramel = 7,
+ /obj/item/reagent_containers/condiment/flour = 7,
+ /obj/item/reagent_containers/condiment/rice = 7,
+ /obj/item/reagent_containers/condiment/ketchup = 7,
+ /obj/item/reagent_containers/condiment/enzyme = 7,
+ /obj/item/reagent_containers/condiment/soymilk = 7,
/obj/item/kitchen/spoon/soup_ladle = 6,
+ /obj/item/kitchen/tongs = 6,
/obj/item/knife/kitchen = 4,
/obj/item/knife/butcher = 2,
)
diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm
index ae6b7c142cc4ff..9209407303321a 100644
--- a/code/modules/jobs/job_types/cyborg.dm
+++ b/code/modules/jobs/job_types/cyborg.dm
@@ -4,7 +4,7 @@
auto_deadmin_role_flags = DEADMIN_POSITION_SILICON
faction = FACTION_STATION
total_positions = 3 // SKYRAT EDIT: Original value (0)
- spawn_positions = 3 // SKYRAT EDIT: Original value (1)
+ spawn_positions = 3
supervisors = "your laws and the AI" //Nodrak
spawn_type = /mob/living/silicon/robot
minimal_player_age = 21
@@ -58,5 +58,5 @@
if(!robot_spawn.connected_ai) // Only log if there's no Master AI
robot_spawn.log_current_laws()
-/datum/job/cyborg/radio_help_message(mob/M)
- to_chat(M, "Prefix your message with :b to speak with other cyborgs and AI.")
+/datum/job/cyborg/get_radio_information()
+ return "Prefix your message with :b to speak with other cyborgs and AI."
diff --git a/code/modules/jobs/job_types/paramedic.dm b/code/modules/jobs/job_types/paramedic.dm
index 3dac90b4baf668..2fd4f3a93a6da0 100644
--- a/code/modules/jobs/job_types/paramedic.dm
+++ b/code/modules/jobs/job_types/paramedic.dm
@@ -32,7 +32,8 @@
/obj/item/reagent_containers/hypospray/medipen/salacid = 10,
/obj/item/reagent_containers/hypospray/medipen/salbutamol = 10,
/obj/item/reagent_containers/hypospray/medipen/penacid = 10,
- /obj/item/reagent_containers/hypospray/medipen/survival/luxury = 5
+ /obj/item/reagent_containers/hypospray/medipen/survival/luxury = 5,
+ /obj/item/storage/box/bandages = 5,
)
rpg_title = "Corpse Runner"
job_flags = STATION_JOB_FLAGS
@@ -56,6 +57,7 @@
gloves = /obj/item/clothing/gloves/latex/nitrile
shoes = /obj/item/clothing/shoes/sneakers/blue
l_pocket = /obj/item/modular_computer/pda/medical/paramedic
+ r_pocket = /obj/item/storage/box/bandages
backpack = /obj/item/storage/backpack/medic
satchel = /obj/item/storage/backpack/satchel/med
diff --git a/code/modules/library/bibles.dm b/code/modules/library/bibles.dm
index 7a69fa2795764c..8a10a058341fe8 100644
--- a/code/modules/library/bibles.dm
+++ b/code/modules/library/bibles.dm
@@ -132,7 +132,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list(
to_chat(user, span_userdanger("[deity_name] SMITE thee!"))
add_memory_in_range(user, 7, /datum/memory/witnessed_gods_wrath, protagonist = user, deuteragonist = src, antagonist = deity_name)
user.client?.give_award(/datum/award/achievement/misc/gods_wrath, user)
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
else
to_chat(user, span_userdanger("[deity_name] cast a curse upon thee!"))
user.AddComponent(/datum/component/omen/bible)
@@ -223,7 +223,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list(
var/list/hurt_limbs = built_in_his_image.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC)
if(length(hurt_limbs))
for(var/obj/item/bodypart/affecting as anything in hurt_limbs)
- if(affecting.heal_damage(heal_amt, heal_amt, BODYTYPE_ORGANIC))
+ if(affecting.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC))
built_in_his_image.update_damage_overlays()
built_in_his_image.visible_message(span_notice("[user] heals [built_in_his_image] with the power of [deity_name]!"))
to_chat(built_in_his_image, span_boldnotice("May the power of [deity_name] compel you to be healed!"))
@@ -369,7 +369,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list(
tip_text = "Clear rune", \
effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune), \
)
- AddElement(/datum/element/bane, target_type = /mob/living/simple_animal/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
+ AddElement(/datum/element/bane, target_type = /mob/living/basic/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
/obj/item/book/bible/syndicate/attack_self(mob/living/carbon/human/user, modifiers)
if(!uses || !istype(user))
diff --git a/code/modules/library/skill_learning/skillchip.dm b/code/modules/library/skill_learning/skillchip.dm
index f9fd629c1ed4dc..762e8e0162c528 100644
--- a/code/modules/library/skill_learning/skillchip.dm
+++ b/code/modules/library/skill_learning/skillchip.dm
@@ -489,4 +489,13 @@
activate_message = span_notice("You recall learning from your grandmother how they baked their cookies with love.")
deactivate_message = span_notice("You forget all memories imparted upon you by your grandmother. Were they even your real grandma?")
+/obj/item/skillchip/master_angler
+ name = "Mast-Angl-Er skillchip"
+ auto_traits = list(TRAIT_REVEAL_FISH)
+ skill_name = "Fisherman's Discernment"
+ skill_description = "While fishing, it'll make a smidge easier to guess whatever you're trying to catch."
+ skill_icon = "fish"
+ activate_message = span_notice("You feel the knowledge and passion of several sunbaked, seasoned fishermen burn within you.")
+ deactivate_message = span_notice("You no longer feel like casting a fishing rod by the sunny riverside.")
+
#undef SKILLCHIP_CATEGORY_GENERAL
diff --git a/code/modules/mafia/controller.dm b/code/modules/mafia/controller.dm
index 1916a65f7b17a2..f8d9db131064d3 100644
--- a/code/modules/mafia/controller.dm
+++ b/code/modules/mafia/controller.dm
@@ -578,7 +578,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
for(var/datum/mafia_role/role as anything in all_roles)
var/mob/living/carbon/human/H = new(get_turf(role.assigned_landmark))
- H.add_traits(list(TRAIT_NOFIRE, TRAIT_NOBREATH, TRAIT_CANNOT_CRYSTALIZE), MAFIA_TRAIT)
+ H.add_traits(list(TRAIT_NOFIRE, TRAIT_NOBREATH, TRAIT_CANNOT_CRYSTALIZE, TRAIT_PERMANENTLY_MORTAL), MAFIA_TRAIT)
H.equipOutfit(outfit_to_distribute)
H.status_flags |= GODMODE
RegisterSignal(H, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(display_votes))
diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
index 5e459758ee4054..aa201292f72a23 100644
--- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
+++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
@@ -60,6 +60,12 @@
if(prob(floor_variance))
icon_state = "[base_icon_state][rand(0,6)]"
+/turf/open/misc/asteroid/basalt/wasteland/basin
+ icon_state = "wasteland_dug"
+ base_icon_state = "wasteland_dug"
+ floor_variance = 0
+ dug = TRUE
+
/turf/closed/mineral/strong/wasteland
name = "ancient dry rock"
color = "#B5651D"
diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm b/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm
new file mode 100644
index 00000000000000..75ac48c2467c4a
--- /dev/null
+++ b/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm
@@ -0,0 +1,263 @@
+/obj/effect/mob_spawn/corpse/goliath/pierced
+ corpse_description = "Seems to have been pierced through the heart by a Watcher spike."
+ naive_corpse_description = "It's got a pretty big boo-boo, might need one of the large plasters."
+
+/obj/effect/mob_spawn/corpse/watcher/goliath_chewed
+ corpse_description = "Prior to its death, it was badly mangled by the jaws of a Goliath."
+ naive_corpse_description = "It's all tuckered out after playing rough with a Goliath."
+
+/obj/effect/mob_spawn/corpse/watcher/crushed
+ corpse_description = "Crushed by a rockslide, it seemed to have been scraping frantically at the rocks even as it perished."
+ naive_corpse_description = "All of those rocks probably don't make a comfortable blanket."
+
+
+#define WATCHER_EGG_LIVELY_MOD 0.75
+#define WATCHER_EGG_ACTIVE_MOD 0.5
+
+/// Egg which hatches into a helpful pet. Or you can eat it if you want.
+/obj/item/food/egg/watcher
+ name = "watcher egg"
+ desc = "A lonely egg still pulsing with life, somehow untouched by the corruption of the Necropolis."
+ icon_state = "egg_watcher"
+ chick_throw_prob = 100
+ tastes = list("ocular fluid" = 6, "loneliness" = 1)
+ preserved_food = TRUE
+ /// How far have we moved?
+ var/steps_travelled = 0
+ /// How far should we travel to hatch?
+ var/steps_to_hatch = 600
+ /// Datum used to measure our steps
+ var/datum/movement_detector/pedometer
+
+/obj/item/food/egg/watcher/Initialize(mapload)
+ . = ..()
+ pedometer = new(src, CALLBACK(src, PROC_REF(on_stepped)))
+
+/obj/item/food/egg/watcher/Destroy(force)
+ . = ..()
+ QDEL_NULL(pedometer)
+
+/obj/item/food/egg/watcher/spawn_impact_chick(turf/spawn_turf)
+ new /obj/effect/spawner/random/lavaland_mob/watcher(spawn_turf)
+
+/obj/item/food/egg/watcher/examine(mob/user)
+ return ..() + span_notice("Watch it more closely to see how it is doing...")
+
+/obj/item/food/egg/watcher/examine_more(mob/user)
+ . = ..()
+ if (steps_travelled < (steps_to_hatch * WATCHER_EGG_ACTIVE_MOD))
+ return . + span_notice("Something stirs listlessly inside.")
+ if (steps_travelled < steps_to_hatch * WATCHER_EGG_LIVELY_MOD)
+ return . + span_notice("Something is moving actively inside.")
+ return . + span_boldnotice("It's jiggling wildly, it's about to hatch!")
+
+/// Called when we are moved, whether inside an inventory or by ourself somehow
+/obj/item/food/egg/watcher/proc/on_stepped(atom/movable/egg, atom/mover, atom/old_loc, direction)
+ var/new_loc = get_turf(egg)
+ if (isnull(new_loc) || new_loc == get_turf(old_loc))
+ return // Didn't actually go anywhere
+ steps_travelled++
+ if (steps_travelled == steps_to_hatch * WATCHER_EGG_ACTIVE_MOD)
+ jiggle()
+ return
+ if (steps_travelled < steps_to_hatch)
+ return
+ visible_message(span_boldnotice("[src] splits and unfurls into a baby Watcher!"))
+ playsound(new_loc, 'sound/effects/splat.ogg', 50, TRUE)
+ new /obj/effect/decal/cleanable/greenglow(new_loc)
+ new /obj/item/watcher_hatchling(new_loc)
+ qdel(src)
+
+/// Animate the egg
+/obj/item/food/egg/watcher/proc/jiggle()
+ var/animation = isturf(loc) ? rand(1, 3) : 1 // Pixel_x/y animations don't work in an inventory
+ switch(animation)
+ if (1)
+ animate(src, transform = transform.Scale(1.3), time = 1 SECONDS, easing = BOUNCE_EASING)
+ animate(transform = matrix(), time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+ if (2)
+ animate(src, pixel_y = 8, time = 0.5 SECONDS, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+ animate(pixel_y = 4, time = 0.5 SECONDS, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = 0.5 SECONDS, easing = BOUNCE_EASING | EASE_IN)
+ if (3)
+ Shake(pixelshiftx = 2, pixelshifty = 0, shake_interval = 0.3 SECONDS)
+ var/next_jiggle = rand(5 SECONDS, 10 SECONDS) / (steps_travelled >= steps_to_hatch * WATCHER_EGG_LIVELY_MOD ? 2 : 1)
+ addtimer(CALLBACK(src, PROC_REF(jiggle)), next_jiggle, TIMER_DELETE_ME)
+
+#undef WATCHER_EGG_LIVELY_MOD
+#undef WATCHER_EGG_ACTIVE_MOD
+
+
+/// A cute pet who will occasionally attack lavaland mobs for you
+/obj/item/watcher_hatchling
+ name = "watcher hatchling"
+ desc = "A newly born watcher, apparently free of the Necropolis' corruption. Perhaps one of the last."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "watcher_baby"
+ w_class = WEIGHT_CLASS_SMALL
+ /// The effect we create when out and about
+ var/obj/effect/watcher_orbiter/orbiter
+ /// Who are we orbiting?
+ var/mob/living/owner
+
+/obj/item/watcher_hatchling/attack_self(mob/user, modifiers)
+ . = ..()
+ if (!isnull(orbiter))
+ watcher_return()
+ return
+ orbiter = new (get_turf(src))
+ orbiter.follow(user)
+ owner = user
+ RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(remove_owner))
+ RegisterSignal(orbiter, COMSIG_QDELETING, PROC_REF(remove_orbiter))
+
+/obj/item/watcher_hatchling/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if (isnull(orbiter))
+ return
+ var/mob/holder = recursive_loc_check(src, /mob)
+ if (holder != owner)
+ watcher_return()
+
+/// If the guy we are orbiting is deleted but somehow we aren't
+/obj/item/watcher_hatchling/proc/remove_owner()
+ SIGNAL_HANDLER
+ UnregisterSignal(owner, COMSIG_QDELETING)
+ owner = null
+
+/// In the more likely event that our orbiter is deleted, stop holding a reference to it
+/obj/item/watcher_hatchling/proc/remove_orbiter()
+ SIGNAL_HANDLER
+ orbiter = null // No need to unregister signal because we only call this when it deletes
+
+/// Get back in your ball pikachu
+/obj/item/watcher_hatchling/proc/watcher_return()
+ qdel(orbiter)
+ remove_owner()
+
+
+/// Orbiting visual which shoots at mining mobs
+/obj/effect/watcher_orbiter
+ name = "watcher hatchling"
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "watcher_baby"
+ layer = EDGED_TURF_LAYER // Don't render under lightbulbs
+ plane = GAME_PLANE_UPPER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ pixel_y = 22
+ alpha = 0
+ /// Who are we following?
+ var/atom/parent
+ /// Datum which keeps us hanging out with our parent
+ var/datum/movement_detector/tracker
+ /// Type of projectile we fire
+ var/projectile_type = /obj/projectile/baby_watcher_blast
+ /// Sound to make when we shoot
+ var/projectile_sound = 'sound/weapons/pierce.ogg'
+ /// Time between taking potshots at goliaths
+ var/fire_delay = 5 SECONDS
+ /// How much faster do we shoot when avenging our parent?
+ var/on_death_multiplier = 5
+ /// Time taken between shots
+ COOLDOWN_DECLARE(shot_cooldown)
+ /// Types of mobs to attack
+ var/list/target_faction = list(FACTION_MINING)
+
+/obj/effect/watcher_orbiter/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+
+// Shuttle rotation fucks with our position, we just want to stick with our guy
+/obj/effect/watcher_orbiter/shuttleRotate(rotation, params)
+ return
+
+/obj/effect/watcher_orbiter/Destroy(force)
+ STOP_PROCESSING(SSobj, src)
+ QDEL_NULL(tracker)
+ return ..()
+
+/obj/effect/watcher_orbiter/process(seconds_per_tick)
+ if (!COOLDOWN_FINISHED(src, shot_cooldown))
+ return
+ for (var/mob/living/potential_target in oview(5, src))
+ if (!ismining(potential_target) || potential_target.stat == DEAD)
+ continue
+ if (!faction_check(target_faction, potential_target.faction))
+ continue
+ shoot_at(potential_target)
+ return
+
+/// Take a shot
+/obj/effect/watcher_orbiter/proc/shoot_at(atom/target)
+ COOLDOWN_START(src, shot_cooldown, fire_delay)
+ fire_projectile(projectile_type, target, projectile_sound, ignore_targets = list(parent))
+
+/// Set ourselves up to track and orbit around a guy
+/obj/effect/watcher_orbiter/proc/follow(atom/movable/target)
+ parent = target
+ glide_size = target.glide_size
+ animate(src, pixel_y = 26, alpha = 255, time = 0.5 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(orbit_animation)), 0.5 SECONDS, TIMER_DELETE_ME)
+ tracker = new(target, CALLBACK(src, PROC_REF(on_parent_moved)))
+ RegisterSignal(target, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(on_glide_size_changed))
+ RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_parent_deleted))
+ RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_parent_died))
+ RegisterSignal(target, COMSIG_LIVING_REVIVE, PROC_REF(on_parent_revived))
+
+/// Do our orbiting animation
+/obj/effect/watcher_orbiter/proc/orbit_animation()
+ animate(src, pixel_y = 26, time = 1 SECONDS, loop = -1, easing = SINE_EASING, flags = ANIMATION_PARALLEL)
+ animate(pixel_y = 18, time = 1 SECONDS, easing = SINE_EASING)
+ animate(src, pixel_x = 20, time = 0.5 SECONDS, loop = -1, easing = SINE_EASING | EASE_OUT, flags = ANIMATION_PARALLEL)
+ animate(pixel_x = 0, time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+ animate(pixel_x = -20, time = 0.5 SECONDS, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_x = 0, time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+
+/// Follow our parent
+/obj/effect/watcher_orbiter/proc/on_parent_moved(atom/movable/parent, atom/mover, atom/old_loc, direction)
+ if(parent.loc == old_loc)
+ return
+ var/turf/new_turf = get_turf(parent)
+ if(isnull(new_turf))
+ qdel(src)
+ return
+ if (loc != new_turf)
+ abstract_move(new_turf)
+
+/// Make sure we glide at the same speed as our parent
+/obj/effect/watcher_orbiter/proc/on_glide_size_changed(atom/source, new_glide_size)
+ SIGNAL_HANDLER
+ glide_size = new_glide_size
+
+/// Called if the guy we're tracking is deleted somehow
+/obj/effect/watcher_orbiter/proc/on_parent_deleted()
+ SIGNAL_HANDLER
+ parent = null
+ qdel(src)
+
+/// We must guard this corpse
+/obj/effect/watcher_orbiter/proc/on_parent_died(mob/living/parent)
+ SIGNAL_HANDLER
+ visible_message(span_notice("[src] emits a piteous keening in mourning of [parent]!"))
+ fire_delay /= on_death_multiplier
+
+/// Exit hyperactive mode
+/obj/effect/watcher_orbiter/proc/on_parent_revived(mob/living/parent)
+ SIGNAL_HANDLER
+ visible_message(span_notice("[src] chirps happily as [parent] suddenly gasps for breath!"))
+ fire_delay *= on_death_multiplier
+
+
+/// Beam fired by a baby watcher, doesn't actually do less damage than its parent
+/obj/projectile/baby_watcher_blast
+ name = "hatchling beam"
+ icon_state = "ice_2"
+ damage = 10
+ damage_type = BRUTE // Mining mobs don't take a lot of burn damage so we'll pretend
+ speed = 1
+ pixel_speed_multiplier = 0.5
+
+/obj/projectile/baby_watcher_blast/Initialize(mapload)
+ . = ..()
+ transform = transform.Scale(0.5)
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 505b6cd5d796e6..5775f0a20f2320 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
@@ -57,10 +57,10 @@
visible_message(span_notice("Serrated tendrils eagerly pull [H] apart, but find nothing of interest."))
return
- if(H.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (H.key || H.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
+ if(H.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (H.ckey || H.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
visible_message(span_warning("Serrated tendrils carefully pull [H] to [src], absorbing the body and creating it anew."))
var/datum/mind/deadmind
- if(H.key)
+ if(H.ckey)
deadmind = H
else
deadmind = H.get_ghost(FALSE, TRUE)
@@ -77,14 +77,14 @@
meat_counter++
visible_message(span_warning("Serrated tendrils eagerly pull [H] to [src], tearing the body apart as its blood seeps over the eggs."))
playsound(get_turf(src),'sound/magic/demon_consume.ogg', 100, TRUE)
- var/deliverykey = H.fingerprintslast //key of whoever brought the body
- var/mob/living/deliverymob = get_mob_by_key(deliverykey) //mob of said key
+ var/deliverykey = H.fingerprintslast //ckey of whoever brought the body
+ var/mob/living/deliverymob = get_mob_by_key(deliverykey) //mob of said ckey
//there is a 40% chance that the Lava Lizard unlocks their respawn with each sacrifice
if(deliverymob && (deliverymob.mind?.has_antag_datum(/datum/antagonist/ashwalker)) && (deliverykey in ashies.players_spawned) && (prob(40)))
to_chat(deliverymob, span_warning("The Necropolis is pleased with your sacrifice. You feel confident your existence after death is secure."))
ashies.players_spawned -= deliverykey
H.investigate_log("has been gibbed by the necropolis tendril.", INVESTIGATE_DEATHS)
- H.gib()
+ H.gib(DROP_ALL_REMAINS)
atom_integrity = min(atom_integrity + max_integrity*0.05,max_integrity)//restores 5% hp of tendril
for(var/mob/living/L in view(src, 5))
if(L.mind?.has_antag_datum(/datum/antagonist/ashwalker))
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
index 424e1db299ec57..af316034f939bd 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
@@ -428,7 +428,7 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999))
if(unforeseen_consequences)
to_chat(unforeseen_consequences, span_warning("\The [H] starts to resonate. Forcing it to enter itself induces a bluespace paradox, violently tearing your body apart."))
unforeseen_consequences.investigate_log("has been gibbed by using [H] while inside of it.", INVESTIGATE_DEATHS)
- unforeseen_consequences.gib()
+ unforeseen_consequences.gib(DROP_ALL_REMAINS)
var/turf/targetturf = find_safe_turf()
if(!targetturf)
diff --git a/code/modules/mapping/access_helpers.dm b/code/modules/mapping/access_helpers.dm
index 16da29837fbd50..c208e2eea9ea9e 100644
--- a/code/modules/mapping/access_helpers.dm
+++ b/code/modules/mapping/access_helpers.dm
@@ -386,6 +386,11 @@
access_list += list(ACCESS_CARGO, ACCESS_MAINT_TUNNELS)
return access_list
+/obj/effect/mapping_helpers/airlock/access/any/supply/bit_den/get_access()
+ var/list/access_list = ..()
+ access_list += ACCESS_BIT_DEN
+ return access_list
+
// -------------------- Syndicate access helpers
/obj/effect/mapping_helpers/airlock/access/any/syndicate
icon_state = "access_helper_syn"
diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm
index 237ae9f2d52e4e..7917da4d542302 100644
--- a/code/modules/mapping/map_template.dm
+++ b/code/modules/mapping/map_template.dm
@@ -109,7 +109,6 @@
// need these two below?
SSmachines.setup_template_powernets(cables)
SSair.setup_template_machinery(atmos_machines)
- SSshuttle.setup_shuttles(ports)
//calculate all turfs inside the border
var/list/template_and_bordering_turfs = block(
diff --git a/code/modules/meteors/meteor_spawning.dm b/code/modules/meteors/meteor_spawning.dm
index eac365bc2a83e9..97c359d03bfbae 100644
--- a/code/modules/meteors/meteor_spawning.dm
+++ b/code/modules/meteors/meteor_spawning.dm
@@ -109,7 +109,7 @@
new_changeling.log_message("was spawned as a midround space changeling by an event.", LOG_GAME)
var/datum/antagonist/changeling/changeling_datum = locate() in player_mind.antag_datums
- changeling_datum.give_power(/datum/action/changeling/suit/organic_space_suit)
+ changeling_datum.give_power(/datum/action/changeling/void_adaption)
changeling_datum.give_power(/datum/action/changeling/weapon/arm_blade)
new_changeling.equipOutfit(/datum/outfit/changeling_space)
diff --git a/code/modules/mining/abandoned_crates.dm b/code/modules/mining/abandoned_crates.dm
index a656aa467d65d5..c9fdb9747e5bdc 100644
--- a/code/modules/mining/abandoned_crates.dm
+++ b/code/modules/mining/abandoned_crates.dm
@@ -188,7 +188,9 @@
new /obj/item/clothing/suit/costume/wellworn_shirt/graphic/ian(src)
new /obj/item/clothing/suit/hooded/ian_costume(src)
if(67 to 68)
- new /obj/item/toy/plush/awakenedplushie(src)
+ var/obj/item/gibtonite/free_bomb = new /obj/item/gibtonite(src)
+ free_bomb.quality = rand(1, 3)
+ free_bomb.GibtoniteReaction(null, "A secure loot closet has spawned a live")
if(69 to 70)
new /obj/item/stack/ore/bluespace_crystal(src, 5)
if(71 to 72)
diff --git a/code/modules/mining/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm
index caee1bbac8d2bb..4ceddb09854cad 100644
--- a/code/modules/mining/equipment/kinetic_crusher.dm
+++ b/code/modules/mining/equipment/kinetic_crusher.dm
@@ -437,3 +437,36 @@
chaser.monster_damage_boost = FALSE // Weaker cuz no cooldown
chaser.damage = 20
log_combat(user, target, "fired a chaser at", src)
+
+/obj/item/crusher_trophy/ice_demon_cube
+ name = "demonic cube"
+ desc = "A stone cold cube dropped from an ice demon."
+ icon_state = "ice_demon_cube"
+ denied_type = /obj/item/crusher_trophy/ice_demon_cube
+ ///how many will we summon?
+ var/summon_amount = 2
+ ///cooldown to summon demons upon the target
+ COOLDOWN_DECLARE(summon_cooldown)
+
+/obj/item/crusher_trophy/ice_demon_cube/effect_desc()
+ return "mark detonation to unleash demonic ice clones upon the target"
+
+/obj/item/crusher_trophy/ice_demon_cube/on_mark_detonation(mob/living/target, mob/living/user)
+ if(isnull(target) || !COOLDOWN_FINISHED(src, summon_cooldown))
+ return
+ for(var/i in 1 to summon_amount)
+ var/turf/drop_off = find_dropoff_turf(target, user)
+ var/mob/living/basic/mining/demon_afterimage/crusher/friend = new(drop_off)
+ friend.faction = list(FACTION_NEUTRAL)
+ friend.befriend(user)
+ friend.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target)
+ COOLDOWN_START(src, summon_cooldown, 30 SECONDS)
+
+///try to make them spawn all around the target to surround him
+/obj/item/crusher_trophy/ice_demon_cube/proc/find_dropoff_turf(mob/living/target, mob/living/user)
+ var/list/turfs_list = get_adjacent_open_turfs(target)
+ for(var/turf/possible_turf in turfs_list)
+ if(possible_turf.is_blocked_turf())
+ continue
+ return possible_turf
+ return get_turf(user)
diff --git a/code/modules/mining/ores_coins.dm b/code/modules/mining/ores_coins.dm
index e9c652fd7bdf48..b01b537ec80d94 100644
--- a/code/modules/mining/ores_coins.dm
+++ b/code/modules/mining/ores_coins.dm
@@ -267,7 +267,7 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
return
if(I.tool_behaviour == TOOL_MINING || istype(I, /obj/item/resonator) || I.force >= 10)
- GibtoniteReaction(user)
+ GibtoniteReaction(user, "A resonator has primed for detonation a")
return
if(istype(I, /obj/item/mining_scanner) || istype(I, /obj/item/t_scanner/adv_mining_scanner) || I.tool_behaviour == TOOL_MULTITOOL)
@@ -294,14 +294,14 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
return ..()
/obj/item/gibtonite/bullet_act(obj/projectile/P)
- GibtoniteReaction(P.firer)
+ GibtoniteReaction(P.firer, "A projectile has primed for detonation a")
return ..()
/obj/item/gibtonite/ex_act()
- GibtoniteReaction(null, 1)
+ GibtoniteReaction(null, "An explosion has primed for detonation a")
return TRUE
-/obj/item/gibtonite/proc/GibtoniteReaction(mob/user, triggered_by = 0)
+/obj/item/gibtonite/proc/GibtoniteReaction(mob/user, triggered_by)
if(primed)
return
primed = TRUE
@@ -311,18 +311,16 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
if(!is_mining_level(z))//Only annoy the admins ingame if we're triggered off the mining zlevel
notify_admins = TRUE
- if(triggered_by == 1)
- log_bomber(null, "An explosion has primed a", src, "for detonation", notify_admins)
- else if(triggered_by == 2)
- var/turf/bombturf = get_turf(src)
- if(notify_admins)
- message_admins("A signal has triggered a [name] to detonate at [ADMIN_VERBOSEJMP(bombturf)]. Igniter attacher: [ADMIN_LOOKUPFLW(attacher)]")
- var/bomb_message = "A signal has primed a [name] for detonation at [AREACOORD(bombturf)]. Igniter attacher: [key_name(attacher)]."
- log_game(bomb_message)
- GLOB.bombers += bomb_message
- else
+ if(user)
user.visible_message(span_warning("[user] strikes \the [src], causing a chain reaction!"), span_danger("You strike \the [src], causing a chain reaction."))
- log_bomber(user, "has primed a", src, "for detonation", notify_admins)
+
+ var/attacher_text = attacher ? "Igniter attacher: [ADMIN_LOOKUPFLW(attacher)]" : null
+
+ if(triggered_by)
+ log_bomber(user, triggered_by, src, attacher_text, notify_admins)
+ else
+ log_bomber(user, "Something has primed a", src, "for detonation.[attacher_text ? " " : ""][attacher_text]", notify_admins)
+
det_timer = addtimer(CALLBACK(src, PROC_REF(detonate), notify_admins), det_time, TIMER_STOPPABLE)
/obj/item/gibtonite/proc/detonate(notify_admins)
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index a38fe1711742ec..5c07c8c8fd405e 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -176,6 +176,12 @@
/mob/dead/new_player/proc/AttemptLateSpawn(rank)
+ // Check that they're picking someone new for new character respawning
+ if(CONFIG_GET(flag/allow_respawn) == RESPAWN_FLAG_NEW_CHARACTER)
+ if("[client.prefs.default_slot]" in client.player_details.joined_as_slots)
+ tgui_alert(usr, "You already have played this character in this round!")
+ return FALSE
+
var/error = IsJobUnavailable(rank)
if(error != JOB_AVAILABLE)
tgui_alert(usr, get_job_unavailable_error_message(error, rank))
@@ -305,6 +311,8 @@
preserved_mind.original_character_slot_index = client.prefs.default_slot
preserved_mind.transfer_to(spawning_mob) //won't transfer key since the mind is not active
preserved_mind.set_original_character(spawning_mob)
+
+ LAZYADD(client.player_details.joined_as_slots, "[client.prefs.default_slot]")
client.init_verbs()
. = spawning_mob
new_character = .
diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm
index 7e288ebcee560c..4660a1b0546510 100644
--- a/code/modules/mob/inventory.dm
+++ b/code/modules/mob/inventory.dm
@@ -286,6 +286,10 @@
I.dropped(src)
return FALSE
+/// Returns true if a mob is holding something
+/mob/proc/is_holding_items()
+ return !!locate(/obj/item) in held_items
+
/mob/proc/drop_all_held_items()
. = FALSE
for(var/obj/item/I in held_items)
diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm
index aebb54770d7028..4802ef347a02d0 100644
--- a/code/modules/mob/living/basic/basic.dm
+++ b/code/modules/mob/living/basic/basic.dm
@@ -117,13 +117,23 @@
if(speak_emote)
speak_emote = string_list(speak_emote)
- if(unsuitable_atmos_damage != 0)
- //String assoc list returns a cached list, so this is like a static list to pass into the element below.
- habitable_atmos = string_assoc_list(habitable_atmos)
- AddElement(/datum/element/atmos_requirements, habitable_atmos, unsuitable_atmos_damage)
+ apply_atmos_requirements()
+ apply_temperature_requirements()
+
+/// Ensures this mob can take atmospheric damage if it's supposed to
+/mob/living/basic/proc/apply_atmos_requirements()
+ if(unsuitable_atmos_damage == 0)
+ return
+ //String assoc list returns a cached list, so this is like a static list to pass into the element below.
+ habitable_atmos = string_assoc_list(habitable_atmos)
+ AddElement(/datum/element/atmos_requirements, habitable_atmos, unsuitable_atmos_damage)
+
+/// Ensures this mob can take temperature damage if it's supposed to
+/mob/living/basic/proc/apply_temperature_requirements()
+ if(unsuitable_cold_damage == 0 && unsuitable_heat_damage == 0)
+ return
+ AddElement(/datum/element/basic_body_temp_sensitive, minimum_survivable_temperature, maximum_survivable_temperature, unsuitable_cold_damage, unsuitable_heat_damage)
- if(unsuitable_cold_damage != 0 && unsuitable_heat_damage != 0)
- AddElement(/datum/element/basic_body_temp_sensitive, minimum_survivable_temperature, maximum_survivable_temperature, unsuitable_cold_damage, unsuitable_heat_damage)
/mob/living/basic/Life(seconds_per_tick = SSMOBS_DT, times_fired)
. = ..()
@@ -191,7 +201,7 @@
. = ..()
if(stat != DEAD)
return
- . += span_deadsay("Upon closer examination, [p_they()] appear[p_s()] to be [HAS_TRAIT(user.mind, TRAIT_NAIVE) ? "asleep" : "dead"].")
+ . += span_deadsay("Upon closer examination, [p_they()] appear[p_s()] to be [HAS_MIND_TRAIT(user, TRAIT_NAIVE) ? "asleep" : "dead"].")
/mob/living/basic/proc/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
face_atom(target)
@@ -207,10 +217,24 @@
melee_attack(attack_target, modifiers)
/mob/living/basic/vv_edit_var(vname, vval)
+ switch(vname)
+ if(NAMEOF(src, habitable_atmos), NAMEOF(src, unsuitable_atmos_damage))
+ RemoveElement(/datum/element/atmos_requirements, habitable_atmos, unsuitable_atmos_damage)
+ . = TRUE
+ if(NAMEOF(src, minimum_survivable_temperature), NAMEOF(src, maximum_survivable_temperature), NAMEOF(src, unsuitable_cold_damage), NAMEOF(src, unsuitable_heat_damage))
+ RemoveElement(/datum/element/basic_body_temp_sensitive, minimum_survivable_temperature, maximum_survivable_temperature, unsuitable_cold_damage, unsuitable_heat_damage)
+ . = TRUE
+
. = ..()
- if(vname == NAMEOF(src, speed))
- datum_flags |= DF_VAR_EDITED
- set_varspeed(vval)
+
+ switch(vname)
+ if(NAMEOF(src, habitable_atmos), NAMEOF(src, unsuitable_atmos_damage))
+ apply_atmos_requirements()
+ if(NAMEOF(src, minimum_survivable_temperature), NAMEOF(src, maximum_survivable_temperature), NAMEOF(src, unsuitable_cold_damage), NAMEOF(src, unsuitable_heat_damage))
+ apply_temperature_requirements()
+ if(NAMEOF(src, speed))
+ datum_flags |= DF_VAR_EDITED
+ set_varspeed(vval)
/mob/living/basic/proc/set_varspeed(var_value)
speed = var_value
@@ -260,3 +284,25 @@
else if(on_fire && !isnull(last_icon_state))
return last_icon_state
return null
+
+/mob/living/basic/put_in_hands(obj/item/I, del_on_fail = FALSE, merge_stacks = TRUE, ignore_animation = TRUE)
+ . = ..()
+ if (.)
+ update_held_items()
+
+/mob/living/basic/update_held_items()
+ . = ..()
+ if(isnull(client) || isnull(hud_used) || hud_used.hud_version == HUD_STYLE_NOHUD)
+ return
+ var/turf/our_turf = get_turf(src)
+ for(var/obj/item/held in held_items)
+ var/index = get_held_index_of_item(held)
+ SET_PLANE(held, ABOVE_HUD_PLANE, our_turf)
+ held.screen_loc = ui_hand_position(index)
+ client.screen |= held
+
+/mob/living/basic/get_body_temp_heat_damage_limit()
+ return maximum_survivable_temperature
+
+/mob/living/basic/get_body_temp_cold_damage_limit()
+ return minimum_survivable_temperature
diff --git a/code/modules/mob/living/basic/basic_defense.dm b/code/modules/mob/living/basic/basic_defense.dm
index cc128a9f5e9e75..87fe6f8fed1bb6 100644
--- a/code/modules/mob/living/basic/basic_defense.dm
+++ b/code/modules/mob/living/basic/basic_defense.dm
@@ -148,7 +148,7 @@
apply_damage(500, damagetype = BRUTE)
else
investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
if (EXPLODE_HEAVY)
var/bloss = 60
@@ -165,6 +165,9 @@
return TRUE
/mob/living/basic/blob_act(obj/structure/blob/attacking_blob)
+ . = ..()
+ if (!.)
+ return
apply_damage(20, damagetype = BRUTE)
/mob/living/basic/do_attack_animation(atom/attacked_atom, visual_effect_icon, used_item, no_effect)
diff --git a/code/modules/mob/living/basic/blob_minions/blob_ai.dm b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
new file mode 100644
index 00000000000000..6168b7ca83be72
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
@@ -0,0 +1,51 @@
+/**
+ * Extremely simple AI, this isn't a very smart boy
+ * Only notable quirk is that it uses JPS movement, simple avoidance would fail to realise it can path through blobs
+ */
+/datum/ai_controller/basic_controller/blobbernaut
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ 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,
+ )
+
+/**
+ * Move to a point designated by the overmind, otherwise just slap people nearby
+ */
+/datum/ai_controller/basic_controller/blob_zombie
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/**
+ * As blob zombie but will prioritise attacking corpses to zombify them
+ */
+/datum/ai_controller/basic_controller/blob_spore
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/blob_minions/blob_mob.dm b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
new file mode 100644
index 00000000000000..35e41f09058456
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
@@ -0,0 +1,37 @@
+/// Root of shared behaviour for mobs spawned by blobs, is abstract and should not be spawned
+/mob/living/basic/blob_minion
+ name = "Blob Error"
+ desc = "A nonfunctional fungal creature created by bad code or celestial mistake. Point and laugh."
+ icon = 'icons/mob/nonhuman-player/blob.dmi'
+ icon_state = "blob_head"
+ unique_name = TRUE
+ pass_flags = PASSBLOB
+ faction = list(ROLE_BLOB)
+ combat_mode = TRUE
+ bubble_icon = "blob"
+ speak_emote = null
+ 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)
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ lighting_cutoff_red = 20
+ lighting_cutoff_green = 40
+ lighting_cutoff_blue = 30
+ initial_language_holder = /datum/language_holder/empty
+
+/mob/living/basic/blob_minion/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_BLOB_ALLY, TRAIT_MUTE), INNATE_TRAIT)
+ AddComponent(/datum/component/blob_minion, on_strain_changed = CALLBACK(src, PROC_REF(on_strain_updated)))
+
+/// Called when our blob overmind changes their variant, update some of our mob properties
+/mob/living/basic/blob_minion/proc/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ return
+
+/// Associates this mob with a specific blob factory node
+/mob/living/basic/blob_minion/proc/link_to_factory(obj/structure/blob/special/factory/factory)
+ RegisterSignal(factory, COMSIG_QDELETING, PROC_REF(on_factory_destroyed))
+
+/// Called when our factory is destroyed
+/mob/living/basic/blob_minion/proc/on_factory_destroyed()
+ SIGNAL_HANDLER
+ to_chat(src, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
diff --git a/code/modules/mob/living/basic/blob_minions/blob_spore.dm b/code/modules/mob/living/basic/blob_minions/blob_spore.dm
new file mode 100644
index 00000000000000..e8c3acc8b97f0f
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_spore.dm
@@ -0,0 +1,123 @@
+/**
+ * A floating fungus which turns people into zombies and explodes into reagent clouds upon death.
+ */
+/mob/living/basic/blob_minion/spore
+ name = "blob spore"
+ desc = "A floating, fragile spore."
+ icon = 'icons/mob/nonhuman-player/blob.dmi'
+ icon_state = "blobpod"
+ icon_living = "blobpod"
+ health_doll_icon = "blobpod"
+ health = BLOBMOB_SPORE_HEALTH
+ maxHealth = BLOBMOB_SPORE_HEALTH
+ verb_say = "psychically pulses"
+ verb_ask = "psychically probes"
+ verb_exclaim = "psychically yells"
+ verb_yell = "psychically screams"
+ melee_damage_lower = BLOBMOB_SPORE_DMG_LOWER
+ melee_damage_upper = BLOBMOB_SPORE_DMG_UPPER
+ obj_damage = 0
+ attack_verb_continuous = "batters"
+ attack_verb_simple = "batter"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ death_message = "explodes into a cloud of gas!"
+ gold_core_spawnable = HOSTILE_SPAWN
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/blob_spore
+ /// Size of cloud produced from a dying spore
+ var/death_cloud_size = 1
+ /// Type of mob to create
+ var/mob/living/zombie_type = /mob/living/basic/blob_minion/zombie
+
+/mob/living/basic/blob_minion/spore/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/spore/death(gibbed)
+ . = ..()
+ death_burst()
+
+/mob/living/basic/blob_minion/spore/on_factory_destroyed()
+ death()
+
+/// Create an explosion of spores on death
+/mob/living/basic/blob_minion/spore/proc/death_burst()
+ do_chem_smoke(range = death_cloud_size, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+
+/mob/living/basic/blob_minion/spore/melee_attack(mob/living/carbon/human/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!ishuman(target) || target.stat != DEAD)
+ return
+ zombify(target)
+
+/// Become a zombie
+/mob/living/basic/blob_minion/spore/proc/zombify(mob/living/carbon/human/target)
+ visible_message(span_warning("The corpse of [target.name] suddenly rises!"))
+ var/mob/living/basic/blob_minion/zombie/blombie = change_mob_type(zombie_type, loc, new_name = initial(zombie_type.name))
+ blombie.set_name()
+ if (istype(blombie)) // In case of badmin
+ blombie.consume_corpse(target)
+ SEND_SIGNAL(src, COMSIG_BLOB_ZOMBIFIED, blombie)
+ qdel(src)
+
+/// Variant of the blob spore which is actually spawned by blob factories
+/mob/living/basic/blob_minion/spore/minion
+ gold_core_spawnable = NO_SPAWN
+ zombie_type = /mob/living/basic/blob_minion/zombie/controlled
+ /// We die if we leave the same turf as this z level
+ var/turf/z_turf
+
+/mob/living/basic/blob_minion/spore/minion/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_changed))
+
+/// When we z-move check that we're on the same z level as our factory was
+/mob/living/basic/blob_minion/spore/minion/proc/on_z_changed()
+ SIGNAL_HANDLER
+ if (isnull(z_turf))
+ return
+ if (!is_valid_z_level(get_turf(src), z_turf))
+ death()
+
+/// Mark the turf we need to track from our factory
+/mob/living/basic/blob_minion/spore/minion/link_to_factory(obj/structure/blob/special/factory/factory)
+ . = ..()
+ z_turf = get_turf(factory)
+
+/// If the blob changes to distributed neurons then you can control the spores
+/mob/living/basic/blob_minion/spore/minion/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ if (isnull(overmind))
+ REMOVE_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
+ else
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
+
+ if (istype(new_strain, /datum/blobstrain/reagent/distributed_neurons))
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB_INFECTION,\
+ poll_candidates = TRUE,\
+ poll_ignore_key = POLL_IGNORE_BLOB,\
+ )
+ else
+ qdel(GetComponent(/datum/component/ghost_direct_control))
+
+/mob/living/basic/blob_minion/spore/minion/death_burst()
+ return // This behaviour is superceded by the overmind's intervention
+
+
+/// Weakened spore spawned by distributed neurons, can't zombify people and makes a teeny explosion
+/mob/living/basic/blob_minion/spore/minion/weak
+ name = "fragile blob spore"
+ health = 15
+ maxHealth = 15
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ death_cloud_size = 0
+
+/mob/living/basic/blob_minion/spore/minion/weak/zombify()
+ return
+
+/mob/living/basic/blob_minion/spore/minion/weak/on_strain_updated()
+ return
diff --git a/code/modules/mob/living/basic/blob_minions/blob_zombie.dm b/code/modules/mob/living/basic/blob_minions/blob_zombie.dm
new file mode 100644
index 00000000000000..c9bf3b7346a989
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_zombie.dm
@@ -0,0 +1,99 @@
+/// A shambling mob made out of a crew member
+/mob/living/basic/blob_minion/zombie
+ name = "blob zombie"
+ desc = "A shambling corpse animated by the blob."
+ icon_state = "zombie"
+ icon_living = "zombie"
+ health_doll_icon = "blobpod"
+ mob_biotypes = MOB_ORGANIC | MOB_HUMANOID
+ health = 70
+ maxHealth = 70
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ melee_attack_cooldown = CLICK_CD_MELEE
+ obj_damage = 20
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ death_message = "collapses to the ground!"
+ gold_core_spawnable = NO_SPAWN
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/blob_zombie
+ /// The dead body we have inside
+ var/mob/living/carbon/human/corpse
+
+/mob/living/basic/blob_minion/zombie/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT) // This mob doesn't function visually without a corpse and wouldn't respawn with one
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/zombie/death(gibbed)
+ corpse?.forceMove(loc)
+ death_burst()
+ return ..()
+
+/mob/living/basic/blob_minion/zombie/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != corpse)
+ return
+ corpse = null
+ death()
+
+/mob/living/basic/blob_minion/zombie/Destroy()
+ QDEL_NULL(corpse)
+ return ..()
+
+/mob/living/basic/blob_minion/zombie/on_factory_destroyed()
+ . = ..()
+ death()
+
+/mob/living/basic/blob_minion/zombie/update_overlays()
+ . = ..()
+ copy_overlays(corpse, TRUE)
+ var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "blob_head")
+ blob_head_overlay.color = LAZYACCESS(atom_colours, FIXED_COLOUR_PRIORITY) || COLOR_WHITE
+ color = initial(color) // reversing what our component did lol, but we needed the value for the overlay
+ . += blob_head_overlay
+
+/// Create an explosion of spores on death
+/mob/living/basic/blob_minion/zombie/proc/death_burst()
+ do_chem_smoke(range = 0, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+/// Store a body so that we can drop it on death
+/mob/living/basic/blob_minion/zombie/proc/consume_corpse(mob/living/carbon/human/new_corpse)
+ if(new_corpse.wear_suit)
+ maxHealth += new_corpse.get_armor_rating(MELEE)
+ health = maxHealth
+ new_corpse.set_facial_hairstyle("Shaved", update = FALSE)
+ new_corpse.set_hairstyle("Bald", update = TRUE)
+ new_corpse.forceMove(src)
+ corpse = new_corpse
+ update_appearance(UPDATE_ICON)
+ RegisterSignal(corpse, COMSIG_LIVING_REVIVE, PROC_REF(on_corpse_revived))
+
+/// Dynamic changeling reentry
+/mob/living/basic/blob_minion/zombie/proc/on_corpse_revived()
+ SIGNAL_HANDLER
+ visible_message(span_boldwarning("[src] bursts from the inside!"))
+ death()
+
+/// Blob-created zombies will ping for player control when they make a zombie
+/mob/living/basic/blob_minion/zombie/controlled
+
+/mob/living/basic/blob_minion/zombie/controlled/consume_corpse(mob/living/carbon/human/new_corpse)
+ . = ..()
+ if (!isnull(client))
+ return
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB_INFECTION,\
+ poll_candidates = TRUE,\
+ poll_ignore_key = POLL_IGNORE_BLOB,\
+ )
+
+/mob/living/basic/blob_minion/zombie/controlled/death_burst()
+ return
diff --git a/code/modules/mob/living/basic/blob_minions/blobbernaut.dm b/code/modules/mob/living/basic/blob_minions/blobbernaut.dm
new file mode 100644
index 00000000000000..b483641993a7e0
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blobbernaut.dm
@@ -0,0 +1,109 @@
+/**
+ * Player-piloted brute mob. Mostly just a "move and click" kind of guy.
+ * Has a variant which takes damage when away from blob tiles
+ */
+/mob/living/basic/blob_minion/blobbernaut
+ name = "blobbernaut"
+ desc = "A hulking, mobile chunk of blobmass."
+ icon_state = "blobbernaut"
+ icon_living = "blobbernaut"
+ icon_dead = "blobbernaut_dead"
+ health = BLOBMOB_BLOBBERNAUT_HEALTH
+ maxHealth = BLOBMOB_BLOBBERNAUT_HEALTH
+ damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER
+ melee_attack_cooldown = CLICK_CD_MELEE
+ obj_damage = BLOBMOB_BLOBBERNAUT_DMG_OBJ
+ attack_verb_continuous = "slams"
+ attack_verb_simple = "slam"
+ attack_sound = 'sound/effects/blobattack.ogg'
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ force_threshold = 10
+ pressure_resistance = 50
+ mob_size = MOB_SIZE_LARGE
+ hud_type = /datum/hud/living/blobbernaut
+ gold_core_spawnable = HOSTILE_SPAWN
+ ai_controller = /datum/ai_controller/basic_controller/blobbernaut
+
+/mob/living/basic/blob_minion/blobbernaut/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/blobbernaut/death(gibbed)
+ flick("blobbernaut_death", src)
+ return ..()
+
+/// This variant is the one actually spawned by blob factories, takes damage when away from blob tiles
+/mob/living/basic/blob_minion/blobbernaut/minion
+ gold_core_spawnable = NO_SPAWN
+ /// Is our factory dead?
+ var/orphaned = FALSE
+
+/mob/living/basic/blob_minion/blobbernaut/minion/Life(seconds_per_tick, times_fired)
+ . = ..()
+ if (!.)
+ return FALSE
+ var/damage_sources = 0
+ var/list/blobs_in_area = range(2, src)
+
+ if (!(locate(/obj/structure/blob) in blobs_in_area))
+ damage_sources++
+
+ if (orphaned)
+ damage_sources++
+ else
+ var/particle_colour = atom_colours[FIXED_COLOUR_PRIORITY] || COLOR_BLACK
+ if (locate(/obj/structure/blob/special/core) in blobs_in_area)
+ heal_overall_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALING_CORE * seconds_per_tick)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
+ heal_effect.color = particle_colour
+
+ if (locate(/obj/structure/blob/special/node) in blobs_in_area)
+ heal_overall_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALING_NODE * seconds_per_tick)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
+ heal_effect.color = particle_colour
+
+ if (damage_sources == 0)
+ return FALSE
+
+ // take 2.5% of max health as damage when not near the blob or if the naut has no factory, 5% if both
+ apply_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALTH_DECAY * damage_sources * seconds_per_tick, damagetype = TOX) // We reduce brute damage
+ var/mutable_appearance/harming = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "nautdamage", MOB_LAYER + 0.01)
+ harming.appearance_flags = RESET_COLOR
+ harming.color = atom_colours[FIXED_COLOUR_PRIORITY] || COLOR_WHITE
+ harming.dir = dir
+ flick_overlay_view(harming, 0.8 SECONDS)
+ return TRUE
+
+/// Called by the blob creation power to give us a mind and a basic task orientation
+/mob/living/basic/blob_minion/blobbernaut/minion/proc/assign_key(ckey, datum/blobstrain/blobstrain)
+ key = ckey
+ flick("blobbernaut_produce", src)
+ health = maxHealth / 2 // Start out injured to encourage not beelining away from the blob
+ SEND_SOUND(src, sound('sound/effects/blobattack.ogg'))
+ SEND_SOUND(src, sound('sound/effects/attackblob.ogg'))
+ to_chat(src, span_infoplain("You are powerful, hard to kill, and slowly regenerate near nodes and cores, [span_cultlarge("but will slowly die if not near the blob")] or if the factory that made you is killed."))
+ to_chat(src, span_infoplain("You can communicate with other blobbernauts and overminds telepathically by attempting to speak normally"))
+ to_chat(src, span_infoplain("Your overmind's blob reagent is: [blobstrain.name]!"))
+ to_chat(src, span_infoplain("The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]"))
+
+/// Set our attack damage based on blob's properties
+/mob/living/basic/blob_minion/blobbernaut/minion/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ if (isnull(overmind))
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ attack_verb_continuous = initial(attack_verb_continuous)
+ return
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
+ attack_verb_continuous = new_strain.blobbernaut_message
+
+/// Called by our factory to inform us that it's not going to support us financially any more
+/mob/living/basic/blob_minion/blobbernaut/minion/on_factory_destroyed()
+ . = ..()
+ orphaned = TRUE
+ throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm
index 78715361356083..5682edf933907e 100644
--- a/code/modules/mob/living/basic/clown/clown.dm
+++ b/code/modules/mob/living/basic/clown/clown.dm
@@ -37,7 +37,7 @@
BB_EMOTE_SAY = list("HONK", "Honk!", "Welcome to clown planet!"),
BB_EMOTE_HEAR = list("honks", "squeaks"),
BB_EMOTE_SOUND = list('sound/items/bikehorn.ogg'), //WE LOVE TO PARTY
- BB_EMOTE_CHANCE = 5,
+ BB_SPEAK_CHANCE = 5,
)
///do we waddle (honk)
var/waddles = TRUE
@@ -150,9 +150,9 @@
),
BB_EMOTE_HEAR = list("honks", "contemplates its existence"),
BB_EMOTE_SEE = list("sweats", "jiggles"),
- BB_EMOTE_CHANCE = 5,
+ BB_SPEAK_CHANCE = 5,
)
-
+
/mob/living/basic/clown/fleshclown/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
@@ -185,7 +185,7 @@
emotes = list(
BB_EMOTE_SAY = list("YA-HONK!!!"),
BB_EMOTE_HEAR = list("honks", "squeaks"),
- BB_EMOTE_CHANCE = 60,
+ BB_SPEAK_CHANCE = 60,
)
/mob/living/basic/clown/clownhulk
@@ -221,7 +221,7 @@
BB_EMOTE_SAY = list("HONK", "Honk!", "HAUAUANK!!!", "GUUURRRRAAAHHH!!!"),
BB_EMOTE_HEAR = list("honks", "grunts"),
BB_EMOTE_SEE = list("sweats"),
- BB_EMOTE_CHANCE = 5,
+ BB_SPEAK_CHANCE = 5,
)
/mob/living/basic/clown/clownhulk/chlown
@@ -252,7 +252,7 @@
emotes = list(
BB_EMOTE_SAY = list("HONK", "Honk!", "Bruh", "cheeaaaahhh?"),
BB_EMOTE_SEE = list("asserts his dominance", "emasculates everyone implicitly"),
- BB_EMOTE_CHANCE = 5,
+ BB_SPEAK_CHANCE = 5,
)
/mob/living/basic/clown/clownhulk/honkmunculus
@@ -318,7 +318,7 @@
BB_EMOTE_SAY = list("HONK!!!", "The Honkmother is merciful, so I must act out her wrath.", "parce mihi ad beatus honkmother placet mihi ut peccata committere,", "DIE!!!"),
BB_EMOTE_HEAR = list("honks", "grunts"),
BB_EMOTE_SEE = list("sweats"),
- BB_EMOTE_CHANCE = 5,
+ BB_SPEAK_CHANCE = 5,
)
/mob/living/basic/clown/mutant
@@ -354,7 +354,7 @@
emotes = list(
BB_EMOTE_SAY = list("aaaaaahhhhuuhhhuhhhaaaaa", "AAAaaauuuaaAAAaauuhhh", "huuuuuh... hhhhuuuooooonnnnkk", "HuaUAAAnKKKK"),
BB_EMOTE_SEE = list("squirms", "writhes", "pulsates", "froths", "oozes"),
- BB_EMOTE_CHANCE = 10,
+ BB_SPEAK_CHANCE = 10,
)
/mob/living/basic/clown/mutant/slow
diff --git a/code/modules/mob/living/basic/constructs/_construct.dm b/code/modules/mob/living/basic/constructs/_construct.dm
new file mode 100644
index 00000000000000..f2e55cceb86b3b
--- /dev/null
+++ b/code/modules/mob/living/basic/constructs/_construct.dm
@@ -0,0 +1,156 @@
+/mob/living/basic/construct
+ icon = 'icons/mob/nonhuman-player/cult.dmi'
+ gender = NEUTER
+ basic_mob_flags = DEL_ON_DEATH
+ combat_mode = TRUE
+ mob_biotypes = MOB_MINERAL | MOB_SPECIAL
+ faction = list(FACTION_CULT)
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ pressure_resistance = 100
+ speed = 0
+ unique_name = TRUE
+ initial_language_holder = /datum/language_holder/construct
+ death_message = "collapses in a shattered heap."
+
+ 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"
+
+ // Vivid red, cause cult theme
+ lighting_cutoff_red = 30
+ lighting_cutoff_green = 5
+ lighting_cutoff_blue = 20
+
+ /// 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. Gets the healing_touch component if so.
+ 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
+ /// 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.
+ var/smashes_walls = FALSE
+
+/mob/living/basic/construct/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ if(length(remains))
+ AddElement(/datum/element/death_drops, remains)
+ if(smashes_walls)
+ AddElement(/datum/element/wall_smasher, strength_flag = ENVIRONMENT_SMASH_WALLS)
+ 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)),\
+ 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.",\
+ show_health = TRUE,\
+ heal_color = COLOR_CULT_RED,\
+ )
+ var/static/list/structure_types = typecacheof(list(/obj/structure/destructible/cult))
+ AddElement(\
+ /datum/element/structure_repair,\
+ 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)
+
+ 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/basic/construct/Login()
+ . = ..()
+ if(!. || !client)
+ return FALSE
+ to_chat(src, span_bold(playstyle_string))
+
+/mob/living/basic/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(span_bold("[p_They()] look[p_s()] severely dented!"))
+ . += ""
+ return .
+
+/mob/living/basic/construct/narsie_act()
+ return
+
+/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"
+ desc = "Has a pungent metallic smell."
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/harvester.dm b/code/modules/mob/living/basic/constructs/harvester.dm
similarity index 69%
rename from code/modules/mob/living/simple_animal/hostile/constructs/harvester.dm
rename to code/modules/mob/living/basic/constructs/harvester.dm
index 8c5fc8eae37b3b..30b3099487282d 100644
--- a/code/modules/mob/living/simple_animal/hostile/constructs/harvester.dm
+++ b/code/modules/mob/living/basic/constructs/harvester.dm
@@ -1,4 +1,4 @@
-/mob/living/simple_animal/hostile/construct/harvester
+/mob/living/basic/construct/harvester
name = "Harvester"
real_name = "Harvester"
desc = "A long, thin construct built to herald Nar'Sie's rise. It'll be all over soon."
@@ -24,58 +24,33 @@
can_repair = TRUE
slowed_by_drag = FALSE
-
-/mob/living/simple_animal/hostile/construct/harvester/Bump(atom/thing)
+/mob/living/basic/construct/harvester/Initialize(mapload)
. = ..()
- if(!istype(thing, /turf/closed/wall/mineral/cult) || thing == loc)
- return // we can go through cult walls
- var/atom/movable/stored_pulling = pulling
-
- if(stored_pulling)
- stored_pulling.setDir(get_dir(stored_pulling.loc, loc))
- stored_pulling.forceMove(loc)
- forceMove(thing)
-
- if(stored_pulling)
- start_pulling(stored_pulling, supress_message = TRUE) //drag anything we're pulling through the wall with us by magic
+ AddElement(\
+ /datum/element/amputating_limbs,\
+ surgery_time = 0,\
+ surgery_verb = "slicing",\
+ minimum_stat = CONSCIOUS,\
+ )
+ AddElement(/datum/element/wall_walker, /turf/closed/wall/mineral/cult)
+ var/datum/action/innate/seek_prey/seek = new(src)
+ seek.Grant(src)
+ seek.Activate()
-/mob/living/simple_animal/hostile/construct/harvester/AttackingTarget()
- if(!iscarbon(target))
+/// If the attack is a limbless carbon, abort the attack, paralyze them, and get a special message from Nar'Sie.
+/mob/living/basic/construct/harvester/resolve_unarmed_attack(atom/attack_target, list/modifiers)
+ if(!iscarbon(attack_target))
return ..()
+ var/mob/living/carbon/carbon_target = attack_target
- var/mob/living/carbon/victim = target
- if(HAS_TRAIT(victim, TRAIT_NODISMEMBER))
- return ..() //ATTACK!
-
- var/list/parts = list()
- var/strong_limbs = 0
-
- for(var/obj/item/bodypart/limb as anything in victim.bodyparts)
+ for(var/obj/item/bodypart/limb as anything in carbon_target.bodyparts)
if(limb.body_part == HEAD || limb.body_part == CHEST)
continue
- if(!(limb.bodypart_flags & BODYPART_UNREMOVABLE))
- parts += limb
- else
- strong_limbs++
-
- if(!LAZYLEN(parts))
- if(strong_limbs) // they have limbs we can't remove, and no parts we can, attack!
- return ..()
- victim.Paralyze(60)
- visible_message(span_danger("[src] knocks [victim] down!"))
- to_chat(src, span_cultlarge("\"Bring [victim.p_them()] to me.\""))
- return FALSE
-
- do_attack_animation(victim)
- var/obj/item/bodypart/limb = pick(parts)
- limb.dismember()
- return FALSE
-
-/mob/living/simple_animal/hostile/construct/harvester/Initialize(mapload)
- . = ..()
- var/datum/action/innate/seek_prey/seek = new()
- seek.Grant(src)
- seek.Activate()
+ return ..() //if any arms or legs exist, attack
+
+ carbon_target.Paralyze(6 SECONDS)
+ visible_message(span_danger("[src] knocks [carbon_target] down!"))
+ to_chat(src, span_cultlarge("\"Bring [carbon_target.p_them()] to me.\""))
/datum/action/innate/seek_master
name = "Seek your Master"
@@ -89,7 +64,7 @@
/// Where is nar nar? Are we even looking?
var/tracking = FALSE
/// The construct we're attached to
- var/mob/living/simple_animal/hostile/construct/the_construct
+ var/mob/living/basic/construct/the_construct
/datum/action/innate/seek_master/Grant(mob/living/player)
the_construct = player
@@ -132,7 +107,7 @@
/datum/action/innate/seek_prey/Activate()
if(GLOB.cult_narsie == null)
return
- var/mob/living/simple_animal/hostile/construct/harvester/the_construct = owner
+ var/mob/living/basic/construct/harvester/the_construct = owner
if(the_construct.seeking)
desc = "None can hide from Nar'Sie, activate to track a survivor attempting to flee the red harvest!"
diff --git a/code/modules/mob/living/basic/farm_animals/deer.dm b/code/modules/mob/living/basic/farm_animals/deer.dm
index 445ad831e59518..a948ec4d7a6e3d 100644
--- a/code/modules/mob/living/basic/farm_animals/deer.dm
+++ b/code/modules/mob/living/basic/farm_animals/deer.dm
@@ -34,7 +34,6 @@
/datum/ai_controller/basic_controller/deer
blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE,
BB_STATIONARY_MOVE_TO_TARGET = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
)
diff --git a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
new file mode 100644
index 00000000000000..f698e5015e8c70
--- /dev/null
+++ b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
@@ -0,0 +1,135 @@
+/// The Greatest (animal) Of All Time. Cud chewing, shin-kicking, kitchen-dwelling nuisance.
+/mob/living/basic/goat
+ name = "goat"
+ desc = "Not known for their pleasant disposition."
+ icon_state = "goat"
+ icon_living = "goat"
+ icon_dead = "goat_dead"
+
+ speak_emote = list("brays")
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes aside"
+ response_disarm_simple = "gently push aside"
+ response_harm_continuous = "kicks"
+ response_harm_simple = "kick"
+ attack_verb_continuous = "kicks"
+ attack_verb_simple = "kick"
+ attack_sound = 'sound/weapons/punch1.ogg'
+ attack_vis_effect = ATTACK_EFFECT_KICK
+
+ butcher_results = list(/obj/item/food/meat/slab/grassfed = 4)
+
+ faction = list(FACTION_NEUTRAL)
+ mob_biotypes = MOB_ORGANIC | MOB_BEAST
+
+ health = 40
+ maxHealth = 40
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ environment_smash = ENVIRONMENT_SMASH_NONE
+
+ minimum_survivable_temperature = COLD_ROOM_TEMP - 75 // enough so that they can survive the cold room spawn with plenty of room for comfort
+
+ blood_volume = BLOOD_VOLUME_NORMAL
+
+ ai_controller = /datum/ai_controller/basic_controller/goat
+
+ /// List of stuff (flora) that we want to eat
+ var/static/list/edibles = list(
+ /obj/structure/alien/resin/flower_bud,
+ /obj/structure/glowshroom,
+ /obj/structure/spacevine,
+ )
+
+/mob/living/basic/goat/Initialize(mapload)
+ . = ..()
+ add_udder()
+ AddElement(/datum/element/cliff_walking) //we walk the cliff
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_SHOE)
+ AddElement(/datum/element/ai_retaliate)
+
+ RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_pre_attack))
+ RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+ RegisterSignal(src, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_move))
+
+ ai_controller.set_blackboard_key(BB_BASIC_FOODS, edibles)
+
+/// Called when we attack something in order to piece together the intent of the AI/user and provide desired behavior. The element might be okay here but I'd rather the fluff.
+/// Goats are really good at beating up plants by taking bites out of them, but we use the default attack for everything else
+/mob/living/basic/goat/proc/on_pre_attack(datum/source, atom/target)
+ if(is_type_in_list(target, edibles))
+ eat_plant(list(target))
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ if(!isliving(target))
+ return
+
+ var/mob/living/living_target = target
+ if(!(living_target.mob_biotypes & MOB_PLANT))
+ return
+
+ living_target.adjustBruteLoss(20)
+ playsound(src, 'sound/items/eatfood.ogg', rand(30, 50), TRUE)
+ var/obj/item/bodypart/edible_bodypart
+
+ if(ishuman(living_target))
+ var/mob/living/carbon/human/plant_man = target
+ edible_bodypart = pick(plant_man.bodyparts)
+ edible_bodypart.dismember()
+
+ living_target.visible_message(
+ span_warning("[src] takes a big chomp out of [living_target]!"),
+ span_userdanger("[src] takes a big chomp out of your [edible_bodypart || "body"]!"),
+ )
+
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/// If we are being attacked by someone who we are already retaliating against, give a nice fluff message.
+/mob/living/basic/goat/proc/on_attacked(datum/source, atom/attacker, attack_flags)
+ var/is_attacker_shitlisted = locate(attacker) in ai_controller.blackboard[BB_BASIC_MOB_RETALIATE_LIST]
+ if(!is_attacker_shitlisted)
+ return
+
+ visible_message(
+ span_danger("[src] gets an evil-looking gleam in [p_their()] eye."),
+ )
+
+/// Handles automagically eating a plant when we move into a turf that has one.
+/mob/living/basic/goat/proc/on_move(datum/source, atom/entering_loc)
+ SIGNAL_HANDLER
+ if(!isturf(entering_loc))
+ return
+
+ var/list/edible_plants = list()
+ for(var/obj/target in entering_loc)
+ if(is_type_in_list(target, edibles))
+ edible_plants += target
+
+ INVOKE_ASYNC(src, PROC_REF(eat_plant), edible_plants)
+
+/// When invoked, adds an udder. Overridden on subtypes
+/mob/living/basic/goat/proc/add_udder()
+ AddComponent(/datum/component/udder)
+
+/// Proc that handles dealing with the various types of plants we might eat. Assumes that a valid list of type(s) will be passed in.
+/mob/living/basic/goat/proc/eat_plant(list/plants)
+ var/eaten = FALSE
+
+ for(var/atom/target as anything in plants)
+ if(istype(target, /obj/structure/spacevine))
+ var/obj/structure/spacevine/vine = target
+ vine.eat(src)
+ eaten = TRUE
+
+ if(istype(target, /obj/structure/alien/resin/flower_bud))
+ target.take_damage(rand(30, 50), BRUTE, 0)
+ eaten = TRUE
+
+ if(istype(target, /obj/structure/glowshroom))
+ qdel(target)
+ eaten = TRUE
+
+ if(eaten && prob(10))
+ say("Nom") // bon appetit
+ playsound(src, 'sound/items/eatfood.ogg', rand(30, 50), TRUE)
diff --git a/code/modules/mob/living/basic/farm_animals/goat/goat_ai.dm b/code/modules/mob/living/basic/farm_animals/goat/goat_ai.dm
new file mode 100644
index 00000000000000..41fc448a3b6ac6
--- /dev/null
+++ b/code/modules/mob/living/basic/farm_animals/goat/goat_ai.dm
@@ -0,0 +1,22 @@
+/// Goats are normally content to sorta hang around and crunch any plant in sight, but they will go ape on someone who attacks them.
+/datum/ai_controller/basic_controller/goat
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
+
+ ai_traits = STOP_MOVING_WHEN_PULLED
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/find_food,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/random_speech/goat,
+ )
+
+/datum/ai_planning_subtree/random_speech/goat
+ speech_chance = 3
+ emote_hear = list("brays.")
+ emote_see = list("shakes their head.", "stamps a foot.", "glares around.")
+ speak = list("EHEHEHEHEH", "eh?")
diff --git a/code/modules/mob/living/basic/farm_animals/goat/goat_subtypes.dm b/code/modules/mob/living/basic/farm_animals/goat/goat_subtypes.dm
new file mode 100644
index 00000000000000..19d50fb38097ad
--- /dev/null
+++ b/code/modules/mob/living/basic/farm_animals/goat/goat_subtypes.dm
@@ -0,0 +1,12 @@
+/mob/living/basic/goat/pete // Pete!
+ name = "Pete"
+ gender = MALE
+
+/mob/living/basic/goat/pete/examine()
+ . = ..()
+ var/area/goat_area = get_area(src)
+ if((bodytemperature < T20C) || istype(goat_area, /area/station/service/kitchen/coldroom))
+ . += span_notice("[p_They()] [p_do()]n't seem to be too bothered about the cold.") // special for pete
+
+/mob/living/basic/goat/pete/add_udder()
+ return //no thank you
diff --git a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
new file mode 100644
index 00000000000000..b0926b41811a1a
--- /dev/null
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
@@ -0,0 +1,181 @@
+/// Where do we draw gorilla held overlays?
+#define GORILLA_HANDS_LAYER 1
+
+/**
+ * Like a bigger monkey
+ * They make a lot of noise and punch limbs off unconscious folks
+ */
+/mob/living/basic/gorilla
+ name = "Gorilla"
+ desc = "A ground-dwelling, predominantly herbivorous ape which usually inhabits the forests of central Africa but today is quite far away from there."
+ icon = 'icons/mob/simple/gorilla.dmi'
+ icon_state = "crawling"
+ icon_living = "crawling"
+ icon_dead = "dead"
+ health_doll_icon = "crawling"
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ maxHealth = 220
+ health = 220
+ response_help_continuous = "prods"
+ response_help_simple = "prod"
+ response_disarm_continuous = "challenges"
+ response_disarm_simple = "challenge"
+ response_harm_continuous = "thumps"
+ response_harm_simple = "thump"
+ speed = 0.5
+ 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)
+ obj_damage = 20
+ attack_verb_continuous = "pummels"
+ attack_verb_simple = "pummel"
+ attack_sound = 'sound/weapons/punch1.ogg'
+ unique_name = TRUE
+ ai_controller = /datum/ai_controller/basic_controller/gorilla
+ faction = list(FACTION_MONKEY, FACTION_JUNGLE)
+ butcher_results = list(/obj/item/food/meat/slab/gorilla = 4, /obj/effect/gibspawner/generic/animal = 1)
+ /// How likely our meaty fist is to stun someone
+ var/paralyze_chance = 20
+ /// A counter for when we can scream again
+ var/oogas = 0
+ /// Types of things we want to find and eat
+ var/static/list/gorilla_food = list(
+ /obj/item/food/bread/banana,
+ /obj/item/food/breadslice/banana,
+ /obj/item/food/cnds/banana_honk,
+ /obj/item/food/grown/banana,
+ /obj/item/food/popsicle/topsicle/banana,
+ /obj/item/food/salad/fruit,
+ /obj/item/food/salad/jungle,
+ /obj/item/food/sundae,
+ )
+
+/mob/living/basic/gorilla/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
+ AddElement(/datum/element/wall_smasher)
+ AddElement(/datum/element/dextrous)
+ AddElement(/datum/element/footstep, FOOTSTEP_MOB_BAREFOOT)
+ AddElement(/datum/element/basic_eating, heal_amt = 10, food_types = gorilla_food)
+ AddElement(
+ /datum/element/amputating_limbs, \
+ surgery_time = 0 SECONDS, \
+ surgery_verb = "punches",\
+ )
+ AddComponent(/datum/component/personal_crafting)
+ AddComponent(/datum/component/basic_inhands, y_offset = -1)
+ ai_controller?.set_blackboard_key(BB_BASIC_FOODS, gorilla_food)
+
+/mob/living/basic/gorilla/update_overlays()
+ . = ..()
+ if (is_holding_items())
+ . += "standing_overlay"
+
+/mob/living/basic/gorilla/update_icon_state()
+ . = ..()
+ if (stat == DEAD)
+ return
+ icon_state = is_holding_items() ? "standing" : "crawling"
+
+/mob/living/basic/gorilla/update_held_items()
+ . = ..()
+ update_appearance(UPDATE_ICON)
+ if (is_holding_items())
+ add_movespeed_modifier(/datum/movespeed_modifier/gorilla_standing)
+ else
+ remove_movespeed_modifier(/datum/movespeed_modifier/gorilla_standing)
+
+/mob/living/basic/gorilla/melee_attack(mob/living/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!. || !isliving(target))
+ return
+ ooga_ooga()
+ if (prob(paralyze_chance))
+ target.Paralyze(2 SECONDS)
+ visible_message(span_danger("[src] knocks [target] down!"))
+ else
+ target.throw_at(get_edge_target_turf(target, dir), range = rand(1, 2), speed = 7, thrower = src)
+
+/mob/living/basic/gorilla/gib(drop_bitflags = DROP_BRAIN)
+ if(!(drop_bitflags & DROP_BRAIN))
+ return ..()
+ var/mob/living/brain/gorilla_brain = new(drop_location())
+ gorilla_brain.name = real_name
+ gorilla_brain.real_name = real_name
+ mind?.transfer_to(gorilla_brain)
+ return ..()
+
+/mob/living/basic/gorilla/can_use_guns(obj/item/gun)
+ to_chat(src, span_warning("Your meaty finger is much too large for the trigger guard!"))
+ return FALSE
+
+/// Assert your dominance with audio cues
+/mob/living/basic/gorilla/proc/ooga_ooga()
+ if (isnull(client))
+ return // Sorry NPCs
+ oogas -= 1
+ if(oogas > 0)
+ return
+ oogas = rand(2,6)
+ emote("ooga")
+
+/// Gorillas are slower when carrying something
+/datum/movespeed_modifier/gorilla_standing
+ blacklisted_movetypes = (FLYING|FLOATING)
+ multiplicative_slowdown = 0.5
+
+/// A smaller gorilla summoned via magic
+/mob/living/basic/gorilla/lesser
+ name = "lesser Gorilla"
+ desc = "An adolescent Gorilla. It may not be fully grown but, much like a banana, that just means it's sturdier and harder to chew!"
+ maxHealth = 120
+ health = 120
+ speed = 0.35
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ obj_damage = 15
+ ai_controller = /datum/ai_controller/basic_controller/gorilla/lesser
+ butcher_results = list(/obj/item/food/meat/slab/gorilla = 2)
+
+/mob/living/basic/gorilla/lesser/Initialize(mapload)
+ . = ..()
+ transform *= 0.75
+
+/// Cargo's wonderful mascot, the tranquil box-carrying ape
+/mob/living/basic/gorilla/cargorilla
+ name = "Cargorilla" // Overriden, normally
+ icon = 'icons/mob/simple/cargorillia.dmi'
+ desc = "Cargo's pet gorilla. They seem to have an 'I love Mom' tattoo."
+ maxHealth = 200
+ health = 200
+ faction = list(FACTION_NEUTRAL, FACTION_MONKEY, FACTION_JUNGLE)
+ unique_name = FALSE
+ ai_controller = null
+
+/mob/living/basic/gorilla/cargorilla/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_PACIFISM, INNATE_TRAIT)
+ AddComponent(/datum/component/crate_carrier)
+
+/**
+ * Poll ghosts for control of the gorilla. Not added in init because we only want to poll when the round starts.
+ * Preferably in future we can replace this with a popup on the lobby to queue to become a gorilla.
+ */
+/mob/living/basic/gorilla/cargorilla/proc/poll_for_gorilla()
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ poll_candidates = TRUE,\
+ poll_length = 30 SECONDS,\
+ role_name = "Cargorilla",\
+ assumed_control_message = "You are Cargorilla, a pacifist friend of the station and carrier of freight.",\
+ poll_ignore_key = POLL_IGNORE_CARGORILLA,\
+ after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\
+ )
+
+/// Called once a ghost assumes control
+/mob/living/basic/gorilla/cargorilla/proc/became_player_controlled()
+ mind.set_assigned_role(SSjob.GetJobType(/datum/job/cargo_technician))
+ mind.special_role = "Cargorilla"
+ to_chat(src, span_notice("You can pick up crates by clicking on them, and drop them by clicking on the ground."))
+
+#undef GORILLA_HANDS_LAYER
diff --git a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_accessories.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_accessories.dm
new file mode 100644
index 00000000000000..814e56487bf54d
--- /dev/null
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_accessories.dm
@@ -0,0 +1,5 @@
+/// Cargorilla's ID card
+/obj/item/card/id/advanced/cargo_gorilla
+ name = "cargorilla ID"
+ desc = "A card used to provide ID and determine access across the station. A gorilla-sized ID for a gorilla-sized cargo technician."
+ trim = /datum/id_trim/job/cargo_technician
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
new file mode 100644
index 00000000000000..c62307542777d3
--- /dev/null
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_ai.dm
@@ -0,0 +1,35 @@
+/// 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_EMOTE_KEY = "ooga",
+ BB_EMOTE_CHANCE = 40,
+ )
+
+ ai_traits = STOP_MOVING_WHEN_PULLED
+ 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/attack_obstacle_in_path/gorilla,
+ /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
+
+/datum/ai_behavior/attack_obstructions/gorilla
+ can_attack_turfs = TRUE
+
+/datum/ai_controller/basic_controller/gorilla/lesser
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
+ BB_EMOTE_KEY = "ooga",
+ BB_EMOTE_CHANCE = 60,
+ )
diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/emotes.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_emotes.dm
similarity index 78%
rename from code/modules/mob/living/simple_animal/hostile/gorilla/emotes.dm
rename to code/modules/mob/living/basic/farm_animals/gorilla/gorilla_emotes.dm
index 20166d81391910..94133336c4d495 100644
--- a/code/modules/mob/living/simple_animal/hostile/gorilla/emotes.dm
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla_emotes.dm
@@ -1,5 +1,5 @@
/datum/emote/gorilla
- mob_type_allowed_typecache = /mob/living/simple_animal/hostile/gorilla
+ mob_type_allowed_typecache = /mob/living/basic/gorilla
mob_type_blacklist_typecache = list()
/datum/emote/gorilla/ooga
@@ -9,4 +9,3 @@
message_param = "oogas at %t."
emote_type = EMOTE_AUDIBLE | EMOTE_VISIBLE
sound = 'sound/creatures/gorilla.ogg'
-
diff --git a/code/modules/mob/living/basic/farm_animals/pony.dm b/code/modules/mob/living/basic/farm_animals/pony.dm
index 4bc09391cb7183..7649068458d1cf 100644
--- a/code/modules/mob/living/basic/farm_animals/pony.dm
+++ b/code/modules/mob/living/basic/farm_animals/pony.dm
@@ -24,15 +24,24 @@
gold_core_spawnable = FRIENDLY_SPAWN
blood_volume = BLOOD_VOLUME_NORMAL
ai_controller = /datum/ai_controller/basic_controller/pony
+ /// Do we register a unique rider?
+ var/unique_tamer = FALSE
+ /// The person we've been tamed by
+ var/datum/weakref/my_owner
+
+ greyscale_config = /datum/greyscale_config/pony
+ /// Greyscale color config; 1st color is body, 2nd is mane
+ var/list/ponycolors = list("#cc8c5d", "#cc8c5d")
/mob/living/basic/pony/Initialize(mapload)
. = ..()
+ apply_colour()
AddElement(/datum/element/pet_bonus, "whickers.")
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/ai_flee_while_injured)
AddElement(/datum/element/waddling)
- AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/apple), tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, PROC_REF(tamed)))
+ AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/apple), tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, PROC_REF(tamed)), unique = unique_tamer)
/mob/living/basic/pony/proc/tamed(mob/living/tamer)
can_buckle = TRUE
@@ -40,6 +49,7 @@
playsound(src, 'sound/creatures/pony/snort.ogg', 50)
AddElement(/datum/element/ridable, /datum/component/riding/creature/pony)
visible_message(span_notice("[src] snorts happily."))
+ new /obj/effect/temp_visual/heart(loc)
ai_controller.replace_planning_subtrees(list(
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
@@ -47,6 +57,30 @@
/datum/ai_planning_subtree/random_speech/pony/tamed
))
+ if(unique_tamer)
+ my_owner = WEAKREF(tamer)
+ RegisterSignal(src, COMSIG_MOVABLE_PREBUCKLE, PROC_REF(on_prebuckle))
+
+/mob/living/basic/pony/Destroy()
+ UnregisterSignal(src, COMSIG_MOVABLE_PREBUCKLE)
+ my_owner = null
+ return ..()
+
+/// Only let us get ridden if the buckler is our owner, if we have a unique owner.
+/mob/living/basic/pony/proc/on_prebuckle(mob/source, mob/living/buckler, force, buckle_mob_flags)
+ SIGNAL_HANDLER
+ var/mob/living/tamer = my_owner?.resolve()
+ if(!unique_tamer || (isnull(tamer) && unique_tamer))
+ return
+ if(buckler != tamer)
+ whinny_angrily()
+ return COMPONENT_BLOCK_BUCKLE
+
+/mob/living/basic/pony/proc/apply_colour()
+ if(!greyscale_config)
+ return
+ set_greyscale(colors = ponycolors)
+
/mob/living/basic/pony/proc/whinny_angrily()
manual_emote("whinnies ANGRILY!")
@@ -86,3 +120,35 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/pony
)
+
+// A stronger horse is required for our strongest cowboys.
+/mob/living/basic/pony/syndicate
+ health = 300
+ maxHealth = 300
+ desc = "A special breed of horse engineered by the syndicate to be capable of surviving in the deep reaches of space. A modern outlaw's best friend."
+ faction = list(ROLE_SYNDICATE)
+ ponycolors = list("#5d566f", COLOR_RED)
+ pressure_resistance = 200
+ 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)
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = 1500
+ unique_tamer = TRUE
+
+/mob/living/basic/pony/syndicate/Initialize(mapload)
+ . = ..()
+ // Help discern your horse from your allies
+ var/mane_colors = list(
+ COLOR_RED=6,
+ COLOR_BLUE=6,
+ COLOR_PINK=3,
+ COLOR_GREEN=3,
+ COLOR_BLACK=3,
+ COLOR_YELLOW=2,
+ COLOR_ORANGE=1,
+ COLOR_WHITE=1,
+ COLOR_DARK_BROWN=1,
+ )
+ ponycolors = list("#5d566f", pick_weight(mane_colors))
+ name = pick("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
+ // Only one person can tame these fellas, and they only need one apple
+ AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/apple), tame_chance = 100, bonus_tame_chance = 15, after_tame = CALLBACK(src, PROC_REF(tamed)), unique = unique_tamer)
diff --git a/code/modules/mob/living/basic/farm_animals/rabbit.dm b/code/modules/mob/living/basic/farm_animals/rabbit.dm
index 92d40e0228e2d1..e837fc3bf8325b 100644
--- a/code/modules/mob/living/basic/farm_animals/rabbit.dm
+++ b/code/modules/mob/living/basic/farm_animals/rabbit.dm
@@ -49,7 +49,6 @@
/datum/ai_controller/basic_controller/rabbit
blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
)
ai_traits = STOP_MOVING_WHEN_PULLED
diff --git a/code/modules/mob/living/basic/farm_animals/sheep.dm b/code/modules/mob/living/basic/farm_animals/sheep.dm
index 691f1db14e3ce4..4452c5ae77cbae 100644
--- a/code/modules/mob/living/basic/farm_animals/sheep.dm
+++ b/code/modules/mob/living/basic/farm_animals/sheep.dm
@@ -81,7 +81,6 @@
/datum/ai_controller/basic_controller/sheep
blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
)
ai_traits = STOP_MOVING_WHEN_PULLED
diff --git a/code/modules/mob/living/basic/health_adjustment.dm b/code/modules/mob/living/basic/health_adjustment.dm
index 6355f809cf470e..9644a1a6299057 100644
--- a/code/modules/mob/living/basic/health_adjustment.dm
+++ b/code/modules/mob/living/basic/health_adjustment.dm
@@ -1,57 +1,70 @@
/**
- * Adjusts the health of a simple mob by a set amount and wakes AI if its idle to react
+ * Adjusts the health of a simple mob by a set amount
*
* Arguments:
* * amount The amount that will be used to adjust the mob's health
* * updating_health If the mob's health should be immediately updated to the new value
* * forced If we should force update the adjustment of the mob's health no matter the restrictions, like GODMODE
+ * returns the net change in bruteloss after applying the damage amount
*/
/mob/living/basic/proc/adjust_health(amount, updating_health = TRUE, forced = FALSE)
. = FALSE
- if(forced || !(status_flags & GODMODE))
- bruteloss = round(clamp(bruteloss + amount, 0, maxHealth * 2), DAMAGE_PRECISION)
- if(updating_health)
- updatehealth()
- . = amount
- if(ckey || stat)
- return
- //if(AIStatus == AI_IDLE)
- // toggle_ai(AI_ON)
+ if(!forced && (status_flags & GODMODE))
+ return 0
+ . = bruteloss // bruteloss value before applying damage
+ bruteloss = round(clamp(bruteloss + amount, 0, maxHealth * 2), DAMAGE_PRECISION)
+ if(updating_health)
+ updatehealth()
+ return . - bruteloss
/mob/living/basic/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+ if(!can_adjust_brute_loss(amount, forced, required_bodytype))
+ return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[BRUTE])
. = adjust_health(amount * damage_coeff[BRUTE] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/basic/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
+ return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[BURN])
. = adjust_health(amount * damage_coeff[BURN] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/basic/adjustOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype, required_respiration_type)
+ if(!can_adjust_oxy_loss(amount, forced, required_biotype, required_respiration_type))
+ return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[OXY])
. = adjust_health(amount * damage_coeff[OXY] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/basic/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+ if(!can_adjust_tox_loss(amount, forced, required_biotype))
+ return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[TOX])
. = adjust_health(amount * damage_coeff[TOX] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
-/mob/living/basic/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE)
+/mob/living/basic/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+ if(!can_adjust_clone_loss(amount, forced, required_biotype))
+ return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[CLONE])
. = adjust_health(amount * damage_coeff[CLONE] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/basic/adjustStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype)
+ if(!can_adjust_stamina_loss(amount, forced, required_biotype))
+ return 0
+ . = staminaloss
if(forced)
staminaloss = max(0, min(BASIC_MOB_MAX_STAMINALOSS, staminaloss + amount))
else
staminaloss = max(0, min(BASIC_MOB_MAX_STAMINALOSS, staminaloss + (amount * damage_coeff[STAMINA])))
if(updating_stamina)
update_stamina()
+ . -= staminaloss
diff --git a/code/modules/mob/living/basic/heretic/ash_spirit.dm b/code/modules/mob/living/basic/heretic/ash_spirit.dm
new file mode 100644
index 00000000000000..b2d4d8b4d29473
--- /dev/null
+++ b/code/modules/mob/living/basic/heretic/ash_spirit.dm
@@ -0,0 +1,25 @@
+/**
+ * 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"
+ real_name = "Ashy"
+ desc = "A manifestation of ash, trailing a perpetual cloud of short-lived cinders."
+ icon_state = "ash_walker"
+ icon_living = "ash_walker"
+ maxHealth = 75
+ health = 75
+ melee_damage_lower = 15
+ melee_damage_upper = 20
+ sight = SEE_TURFS
+
+/mob/living/basic/heretic_summon/ash_spirit/Initialize(mapload)
+ . = ..()
+ var/static/list/actions_to_add = list(
+ /datum/action/cooldown/spell/fire_sworn,
+ /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)
diff --git a/code/modules/mob/living/basic/heretic/flesh_stalker.dm b/code/modules/mob/living/basic/heretic/flesh_stalker.dm
new file mode 100644
index 00000000000000..6f31b3ce7c523a
--- /dev/null
+++ b/code/modules/mob/living/basic/heretic/flesh_stalker.dm
@@ -0,0 +1,46 @@
+/// Durable ambush mob with an EMP ability
+/mob/living/basic/heretic_summon/stalker
+ name = "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"
+ icon_living = "stalker"
+ maxHealth = 150
+ health = 150
+ melee_damage_lower = 15
+ 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
+ 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,
+ /datum/action/cooldown/spell/shapeshift/eldritch = BB_SHAPESHIFT_ACTION,
+ )
+
+/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)
+
+/// Changes shape and lies in wait when it has no target, uses EMP and attacks once it does
+/datum/ai_controller/basic_controller/stalker
+ ai_traits = CAN_ACT_IN_STASIS
+ 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
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/shapechange_ambush,
+ /datum/ai_planning_subtree/use_mob_ability,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/heretic/flesh_worm.dm b/code/modules/mob/living/basic/heretic/flesh_worm.dm
new file mode 100644
index 00000000000000..3c60a9b653c32a
--- /dev/null
+++ b/code/modules/mob/living/basic/heretic/flesh_worm.dm
@@ -0,0 +1,137 @@
+/// Armsy starts to look a bit funky if he's shorter than this
+#define MINIMUM_ARMSY_LENGTH 2
+
+// What if we took a linked list... But made it a mob?
+/// The "Terror of the Night" / Armsy, a large worm made of multiple bodyparts that occupies multiple tiles
+/mob/living/basic/heretic_summon/armsy
+ name = "Lord of the Night"
+ real_name = "Master of Decay"
+ desc = "An abomination made from dozens and dozens of severed and malformed limbs grasping onto each other."
+ icon_state = "armsy_start"
+ icon_living = "armsy_start"
+ base_icon_state = "armsy"
+ maxHealth = 400
+ health = 400
+ melee_damage_lower = 30
+ melee_damage_upper = 50
+ obj_damage = 200
+ move_force = MOVE_FORCE_OVERPOWERING
+ move_resist = MOVE_FORCE_OVERPOWERING
+ pull_force = MOVE_FORCE_OVERPOWERING
+ mob_size = MOB_SIZE_HUGE
+ sentience_type = SENTIENCE_BOSS
+ mob_biotypes = MOB_ORGANIC|MOB_SPECIAL
+ ///Previous segment in the chain, we hold onto this purely to keep track of how long we currently are and to attach new growth to the back
+ var/mob/living/basic/heretic_summon/armsy/back
+ ///How many arms do we have to eat to expand?
+ var/stacks_to_grow = 5
+ ///Currently eaten arms
+ var/current_stacks = 0
+/*
+ * Arguments
+ * * spawn_bodyparts - whether we spawn additional armsy bodies until we reach length.
+ * * worm_length - the length of the worm we're creating. Below 2 doesn't work very well.
+ */
+/mob/living/basic/heretic_summon/armsy/Initialize(mapload, spawn_bodyparts = TRUE, worm_length = 6)
+ . = ..()
+ AddElement(/datum/element/wall_smasher, ENVIRONMENT_SMASH_RWALLS)
+ AddElement(\
+ /datum/element/amputating_limbs,\
+ surgery_time = 0 SECONDS,\
+ surgery_verb = "tears",\
+ minimum_stat = CONSCIOUS,\
+ snip_chance = 10,\
+ target_zones = GLOB.arm_zones,\
+ )
+ AddComponent(\
+ /datum/component/blood_walk, \
+ blood_type = /obj/effect/decal/cleanable/blood/tracks, \
+ target_dir_change = TRUE,\
+ )
+
+ if(spawn_bodyparts)
+ build_tail(worm_length)
+
+// We are a vessel of otherworldly destruction, we bring our gravity with us
+/mob/living/basic/heretic_summon/armsy/has_gravity(turf/gravity_turf)
+ return TRUE
+
+/mob/living/basic/heretic_summon/armsy/can_be_pulled()
+ return FALSE // The component does this but not on the head. We don't want the head to be pulled either.
+
+/mob/living/basic/heretic_summon/armsy/proc/build_tail(worm_length)
+ worm_length = max(worm_length, MINIMUM_ARMSY_LENGTH)
+ // Sets the hp of the head to be exactly the (length * hp), so the head is de facto the hardest to destroy.
+ maxHealth = worm_length * maxHealth
+ health = maxHealth
+
+ AddComponent(/datum/component/mob_chain, vary_icon_state = TRUE) // We're the front
+
+ var/mob/living/basic/heretic_summon/armsy/prev = src
+ for(var/i in 1 to worm_length)
+ prev = new_segment(behind = prev)
+ update_appearance(UPDATE_ICON_STATE)
+
+/// Grows a new segment behind the passed mob
+/mob/living/basic/heretic_summon/armsy/proc/new_segment(mob/living/basic/heretic_summon/armsy/behind)
+ var/mob/living/segment = new type(drop_location(), FALSE)
+ ADD_TRAIT(segment, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
+ segment.AddComponent(/datum/component/mob_chain, front = behind, vary_icon_state = TRUE)
+ behind.register_behind(segment)
+ return segment
+
+/// Record that we got another guy on our ass
+/mob/living/basic/heretic_summon/armsy/proc/register_behind(mob/living/tail)
+ if(!isnull(back)) // Shouldn't happen but just in case
+ UnregisterSignal(back, COMSIG_QDELETING)
+ back = tail
+ update_appearance(UPDATE_ICON_STATE)
+ if(!isnull(back))
+ RegisterSignal(back, COMSIG_QDELETING, PROC_REF(tail_deleted))
+
+/// When our tail is gone stop holding a reference to it
+/mob/living/basic/heretic_summon/armsy/proc/tail_deleted()
+ SIGNAL_HANDLER
+ register_behind(null)
+
+/mob/living/basic/heretic_summon/armsy/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ if(!istype(target, /obj/item/bodypart/arm))
+ return ..()
+ visible_message(span_warning("[src] devours [target]!"))
+ playsound(src, 'sound/magic/demon_consume.ogg', 50, TRUE)
+ qdel(target)
+ on_arm_eaten()
+
+/*
+ * Handle healing our chain.
+ * Eating arms off the ground heals us, and if we eat enough arms while above a certain health threshold we get longer!
+ */
+/mob/living/basic/heretic_summon/armsy/proc/on_arm_eaten()
+ if(!isnull(back))
+ back.on_arm_eaten()
+ return
+
+ adjustBruteLoss(-maxHealth * 0.5, FALSE)
+ adjustFireLoss(-maxHealth * 0.5, FALSE)
+
+ if(health < maxHealth * 0.8)
+ return
+
+ current_stacks++
+ if(current_stacks < stacks_to_grow)
+ return
+
+ visible_message(span_boldwarning("[src] flexes and expands!"))
+ current_stacks = 0
+ new_segment(behind = src)
+
+/*
+ * Recursively get the length of our chain.
+ */
+/mob/living/basic/heretic_summon/armsy/proc/get_length()
+ . = 1
+ if(isnull(back))
+ return
+ . += back.get_length()
+
+#undef MINIMUM_ARMSY_LENGTH
diff --git a/code/modules/mob/living/basic/heretic/heretic_summon.dm b/code/modules/mob/living/basic/heretic/heretic_summon.dm
index 0f7d63f903cc5c..cf1bcf80aad830 100644
--- a/code/modules/mob/living/basic/heretic/heretic_summon.dm
+++ b/code/modules/mob/living/basic/heretic/heretic_summon.dm
@@ -1,26 +1,25 @@
/mob/living/basic/heretic_summon
name = "Eldritch Demon"
real_name = "Eldritch Demon"
- desc = "A horror from beyond this realm."
+ desc = "A horror from beyond this realm, summoned by bad code."
icon = 'icons/mob/nonhuman-player/eldritch_mobs.dmi'
faction = list(FACTION_HERETIC)
basic_mob_flags = DEL_ON_DEATH
gender = NEUTER
mob_biotypes = NONE
- unsuitable_atmos_damage = 0
- unsuitable_cold_damage = 0
- unsuitable_heat_damage = 0
+ 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)
damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
speed = 0
+ melee_attack_cooldown = CLICK_CD_MELEE
attack_sound = 'sound/weapons/punch1.ogg'
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 = "reaps"
- response_harm_simple = "tears"
+ response_harm_continuous = "rips"
+ response_harm_simple = "tear"
death_message = "implodes into itself."
combat_mode = TRUE
diff --git a/code/modules/antagonists/heretic/mobs/maid_in_mirror.dm b/code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm
similarity index 60%
rename from code/modules/antagonists/heretic/mobs/maid_in_mirror.dm
rename to code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm
index b53bbe147d3df9..edc5148b3ccce3 100644
--- a/code/modules/antagonists/heretic/mobs/maid_in_mirror.dm
+++ b/code/modules/mob/living/basic/heretic/maid_in_the_mirror.dm
@@ -1,11 +1,11 @@
-// A summon which floats around the station incorporeally, and can appear in any mirror
-/mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror
+/// 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"
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'
icon_state = "stand"
- icon_living = "stand" // Placeholder sprite
+ icon_living = "stand" // Placeholder sprite... still
speak_emote = list("whispers")
movement_type = FLOATING
status_flags = CANSTUN | CANPUSH
@@ -16,36 +16,34 @@
melee_damage_upper = 16
sight = SEE_MOBS | SEE_OBJS | SEE_TURFS
death_message = "shatters and vanishes, releasing a gust of cold air."
- loot = list(
- /obj/item/shard,
+ /// Whether we take damage when someone looks at us
+ var/harmed_by_examine = TRUE
+ /// How often being examined by a specific mob can hurt us
+ var/recent_examine_damage_cooldown = 10 SECONDS
+ /// A list of REFs to people who recently examined us
+ var/list/recent_examiner_refs = list()
+
+/mob/living/basic/heretic_summon/Initialize(mapload)
+ . = ..()
+ var/static/list/loot = list(
/obj/effect/decal/cleanable/ash,
/obj/item/clothing/suit/armor/vest,
/obj/item/organ/internal/lungs,
+ /obj/item/shard,
)
- actions_to_add = list(/datum/action/cooldown/spell/jaunt/mirror_walk)
+ AddElement(/datum/element/death_drops, loot)
+ var/datum/action/cooldown/spell/jaunt/mirror_walk/jaunt = new (src)
+ jaunt.Grant(src)
- /// Whether we take damage when we're examined
- var/weak_on_examine = TRUE
- /// The cooldown after being examined that the same mob cannot trigger it again
- var/recent_examine_damage_cooldown = 10 SECONDS
- /// A list of REFs to people who recently examined us
- var/list/recent_examiner_refs = list()
-
-/mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror/death(gibbed)
+/mob/living/basic/heretic_summon/maid_in_the_mirror/death(gibbed)
var/turf/death_turf = get_turf(src)
- death_turf.TakeTemperature(-40)
+ death_turf.TakeTemperature(-40) // Spooky
return ..()
// Examining them will harm them, on a cooldown.
-/mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror/examine(mob/user)
+/mob/living/basic/heretic_summon/maid_in_the_mirror/examine(mob/user)
. = ..()
- if(!weak_on_examine)
- return
-
- if(!isliving(user) || user.stat == DEAD)
- return
-
- if(IS_HERETIC_OR_MONSTER(user) || user == src)
+ if(!harmed_by_examine || user == src || user.stat == DEAD || !isliving(user) || IS_HERETIC_OR_MONSTER(user))
return
var/user_ref = REF(user)
@@ -62,7 +60,9 @@
recent_examiner_refs += user_ref
apply_damage(maxHealth * 0.1) // We take 10% of our health as damage upon being examined
playsound(src, 'sound/effects/ghost2.ogg', 40, TRUE)
- addtimer(CALLBACK(src, PROC_REF(clear_recent_examiner), user_ref), recent_examine_damage_cooldown)
+ addtimer(CALLBACK(src, PROC_REF(clear_recent_examiner), user_ref), recent_examine_damage_cooldown, TIMER_DELETE_ME)
+ animate(src, alpha = 120, time = 0.5 SECONDS, easing = ELASTIC_EASING, loop = 2, flags = ANIMATION_PARALLEL)
+ animate(alpha = 255, time = 0.5 SECONDS, easing = ELASTIC_EASING)
// If we're examined on low enough health we die straight up
else
@@ -73,7 +73,7 @@
death()
-/mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror/proc/clear_recent_examiner(mob_ref)
+/mob/living/basic/heretic_summon/maid_in_the_mirror/proc/clear_recent_examiner(mob_ref)
if(!(mob_ref in recent_examiner_refs))
return
diff --git a/code/modules/mob/living/basic/heretic/raw_prophet.dm b/code/modules/mob/living/basic/heretic/raw_prophet.dm
new file mode 100644
index 00000000000000..1a3b2d1aa923a2
--- /dev/null
+++ b/code/modules/mob/living/basic/heretic/raw_prophet.dm
@@ -0,0 +1,96 @@
+/**
+ * A funny little rolling guy who is great at scouting.
+ * It can see through walls, jaunt, and create a psychic network to report its findings.
+ * 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"
+ real_name = "Raw Prophet"
+ desc = "An abomination stitched together from a few severed arms and one swollen, orphaned eye."
+ icon_state = "raw_prophet"
+ icon_living = "raw_prophet"
+ status_flags = CANPUSH
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+ 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
+
+/mob/living/basic/heretic_summon/raw_prophet/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/wheel)
+ var/static/list/body_parts = list(/obj/effect/gibspawner/human, /obj/item/bodypart/arm/left, /obj/item/organ/internal/eyes)
+ AddElement(/datum/element/death_drops, body_parts)
+ AddComponent(/datum/component/focused_attacker)
+ var/on_link_message = "You feel something new enter your sphere of mind... \
+ You hear whispers of people far away, screeches of horror and a huming of welcome to [src]'s Mansus Link."
+ var/on_unlink_message = "Your mind shatters as [src]'s Mansus Link leaves your mind."
+ AddComponent( \
+ /datum/component/mind_linker/active_linking, \
+ network_name = "Mansus Link", \
+ chat_color = "#568b00", \
+ post_unlink_callback = CALLBACK(src, PROC_REF(after_unlink)), \
+ speech_action_background_icon_state = "bg_heretic", \
+ speech_action_overlay_state = "bg_heretic_border", \
+ linker_action_path = /datum/action/cooldown/spell/pointed/manse_link, \
+ link_message = on_link_message, \
+ 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)
+
+ var/datum/action/cooldown/blind = new blind_action_type(src)
+ blind.Grant(src)
+ ai_controller?.set_blackboard_key(BB_TARGETTED_ACTION, blind)
+
+/*
+ * Callback for the mind_linker component.
+ * Stuns people who are ejected from the network.
+ */
+/mob/living/basic/heretic_summon/raw_prophet/proc/after_unlink(mob/living/unlinked_mob)
+ if(QDELETED(unlinked_mob) || unlinked_mob.stat == DEAD)
+ return
+
+ INVOKE_ASYNC(unlinked_mob, TYPE_PROC_REF(/mob, emote), "scream")
+ unlinked_mob.AdjustParalyzed(0.5 SECONDS) //micro stun
+
+/mob/living/basic/heretic_summon/raw_prophet/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ SpinAnimation(speed = 5, loops = 1)
+ if (target == src)
+ return
+ return ..()
+
+/// Variant raw prophet used by eldritch transformation with more base attack power
+/mob/living/basic/heretic_summon/raw_prophet/ascended
+ melee_damage_lower = 15
+ melee_damage_upper = 20
+
+/// 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
+
+/// Walk and attack people, blind them when we can
+/datum/ai_controller/basic_controller/raw_prophet
+ 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
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/heretic/rust_walker.dm b/code/modules/mob/living/basic/heretic/rust_walker.dm
new file mode 100644
index 00000000000000..070326c0281aeb
--- /dev/null
+++ b/code/modules/mob/living/basic/heretic/rust_walker.dm
@@ -0,0 +1,87 @@
+/// 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"
+ real_name = "Rusty"
+ desc = "A grinding, clanking construct which leaches life from its surroundings with every armoured step."
+ icon_state = "rust_walker_s"
+ base_icon_state = "rust_walker"
+ icon_living = "rust_walker_s"
+ maxHealth = 75
+ health = 75
+ melee_damage_lower = 15
+ melee_damage_upper = 20
+ sight = SEE_TURFS
+ speed = 1
+ ai_controller = /datum/ai_controller/basic_controller/rust_walker
+
+/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)
+
+/mob/living/basic/heretic_summon/rust_walker/setDir(newdir)
+ . = ..()
+ update_appearance(UPDATE_ICON_STATE)
+
+/mob/living/basic/heretic_summon/rust_walker/update_icon_state()
+ . = ..()
+ if(stat == DEAD) // We usually delete on death but just in case
+ return
+ if(dir & NORTH)
+ icon_state = "[base_icon_state]_n"
+ else if(dir & SOUTH)
+ icon_state = "[base_icon_state]_s"
+ icon_living = icon_state
+
+/mob/living/basic/heretic_summon/rust_walker/Life(seconds_per_tick = SSMOBS_DT, times_fired)
+ if(stat == DEAD)
+ return ..()
+ var/turf/our_turf = get_turf(src)
+ if(HAS_TRAIT(our_turf, TRAIT_RUSTY))
+ adjustBruteLoss(-3 * seconds_per_tick)
+
+ return ..()
+
+/// Converts unconverted terrain, sprays pocket sand around
+/datum/ai_controller/basic_controller/rust_walker
+ 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/rust
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/use_mob_ability/rust_walker,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Moves a lot if healthy and on rust (to find more tiles to rust) or unhealthy and not on rust (to find healing rust)
+/// Still moving in random directions though we're not really seeking it out
+/datum/idle_behavior/idle_random_walk/rust
+
+/datum/idle_behavior/idle_random_walk/rust/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
+ var/mob/living/our_mob = controller.pawn
+ var/turf/our_turf = get_turf(our_mob)
+ if (HAS_TRAIT(our_turf, TRAIT_RUSTY))
+ walk_chance = (our_mob.health < our_mob.maxHealth) ? 10 : 50
+ else
+ walk_chance = (our_mob.health < our_mob.maxHealth) ? 50 : 10
+ return ..()
+
+/// Use if we're not stood on rust right now
+/datum/ai_planning_subtree/use_mob_ability/rust_walker
+
+/datum/ai_planning_subtree/use_mob_ability/rust_walker/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/turf/our_turf = get_turf(controller.pawn)
+ if (HAS_TRAIT(our_turf, TRAIT_RUSTY))
+ return
+ return ..()
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
new file mode 100644
index 00000000000000..960f875365bfa4
--- /dev/null
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm
@@ -0,0 +1,89 @@
+/mob/living/basic/mining/ice_demon
+ name = "demonic watcher"
+ desc = "A creature formed entirely out of ice, bluespace energy emanates from inside of it."
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "ice_demon"
+ icon_living = "ice_demon"
+ icon_gib = "syndicate_gib"
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ mouse_opacity = MOUSE_OPACITY_ICON
+ basic_mob_flags = DEL_ON_DEATH
+ speed = 2
+ maxHealth = 150
+ health = 150
+ obj_damage = 40
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ attack_verb_continuous = "slices"
+ attack_verb_simple = "slice"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
+ move_force = MOVE_FORCE_VERY_STRONG
+ move_resist = MOVE_FORCE_VERY_STRONG
+ pull_force = MOVE_FORCE_VERY_STRONG
+ crusher_loot = /obj/item/crusher_trophy/ice_demon_cube
+ ai_controller = /datum/ai_controller/basic_controller/ice_demon
+ death_message = "fades as the energies that tied it to this world dissipate."
+ death_sound = 'sound/magic/demon_dies.ogg'
+
+/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)
+ AddComponent(\
+ /datum/component/ranged_attacks,\
+ projectile_type = /obj/projectile/temp/ice_demon,\
+ projectile_sound = 'sound/weapons/pierce.ogg',\
+ )
+ var/static/list/death_loot = list(/obj/item/stack/ore/bluespace_crystal = 3)
+ AddElement(/datum/element/death_drops, death_loot)
+ AddElement(/datum/element/simple_flying)
+
+/mob/living/basic/mining/ice_demon/death(gibbed)
+ if(prob(5))
+ new /obj/item/raw_anomaly_core/bluespace(loc)
+ return ..()
+
+/mob/living/basic/mining/demon_afterimage
+ name = "afterimage demonic watcher"
+ desc = "Is this some sort of illusion?"
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "ice_demon"
+ icon_living = "ice_demon"
+ icon_gib = "syndicate_gib"
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ mouse_opacity = MOUSE_OPACITY_ICON
+ basic_mob_flags = DEL_ON_DEATH
+ speed = 5
+ maxHealth = 20
+ health = 20
+ melee_damage_lower = 5
+ melee_damage_upper = 5
+ attack_verb_continuous = "slices"
+ attack_verb_simple = "slice"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ alpha = 80
+ ai_controller = /datum/ai_controller/basic_controller/ice_demon/afterimage
+ ///how long do we exist for
+ var/existence_period = 15 SECONDS
+
+/mob/living/basic/mining/demon_afterimage/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ AddElement(/datum/element/temporary_atom, life_time = existence_period)
+
+///afterimage subtypes summoned by the crusher
+/mob/living/basic/mining/demon_afterimage/crusher
+ speed = 2
+ health = 60
+ maxHealth = 60
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ existence_period = 7 SECONDS
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
new file mode 100644
index 00000000000000..79c9ee9bd583eb
--- /dev/null
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
@@ -0,0 +1,117 @@
+/obj/projectile/temp/ice_demon
+ name = "ice blast"
+ icon_state = "ice_2"
+ damage = 5
+ damage_type = BURN
+ armor_flag = ENERGY
+ speed = 1
+ pixel_speed_multiplier = 0.25
+ temperature = -75
+
+/datum/action/cooldown/mob_cooldown/ice_demon_teleport
+ name = "Bluespace Teleport"
+ desc = "Teleport towards a destination target!"
+ button_icon = 'icons/obj/ore.dmi'
+ button_icon_state = "bluespace_crystal"
+ cooldown_time = 3 SECONDS
+ melee_cooldown_time = 0 SECONDS
+ ///time delay before teleport
+ var/time_delay = 0.5 SECONDS
+
+/datum/action/cooldown/mob_cooldown/ice_demon_teleport/Activate(atom/target_atom)
+ if(isclosedturf(get_turf(target_atom)))
+ owner.balloon_alert(owner, "blocked!")
+ return FALSE
+ animate(owner, transform = matrix().Scale(0.8), time = time_delay, easing = SINE_EASING)
+ addtimer(CALLBACK(src, PROC_REF(teleport_to_turf), target_atom), time_delay)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/ice_demon_teleport/proc/teleport_to_turf(atom/target)
+ animate(owner, transform = matrix(), time = 0.5 SECONDS, easing = SINE_EASING)
+ do_teleport(teleatom = owner, destination = target, channel = TELEPORT_CHANNEL_BLUESPACE, forced = TRUE)
+
+/datum/action/cooldown/mob_cooldown/slippery_ice_floors
+ name = "Iced Floors"
+ desc = "Summon slippery ice floors all around!"
+ button_icon = 'icons/turf/floors/ice_turf.dmi'
+ button_icon_state = "ice_turf-6"
+ cooldown_time = 2 SECONDS
+ click_to_activate = FALSE
+ melee_cooldown_time = 0 SECONDS
+ ///perimeter we will spawn the iced floors on
+ var/radius = 1
+ ///intervals we will spawn the ice floors in
+ var/spread_duration = 0.2 SECONDS
+
+/datum/action/cooldown/mob_cooldown/slippery_ice_floors/Activate(atom/target_atom)
+ for(var/i in 0 to radius)
+ var/list/list_of_turfs = border_diamond_range_turfs(owner, i)
+ addtimer(CALLBACK(src, PROC_REF(spawn_icy_floors), list_of_turfs), i * spread_duration)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/slippery_ice_floors/proc/spawn_icy_floors(list/list_of_turfs)
+ if(!length(list_of_turfs))
+ return
+ for(var/turf/location in list_of_turfs)
+ if(isnull(location))
+ continue
+ if(isclosedturf(location) || isspaceturf(location))
+ continue
+ new /obj/effect/temp_visual/slippery_ice(location)
+
+/obj/effect/temp_visual/slippery_ice
+ name = "slippery acid"
+ icon = 'icons/turf/floors/ice_turf.dmi'
+ icon_state = "ice_turf-6"
+ layer = BELOW_MOB_LAYER
+ plane = GAME_PLANE
+ anchored = TRUE
+ duration = 3 SECONDS
+ alpha = 100
+ /// how long does it take for the effect to phase in
+ var/phase_in_period = 2 SECONDS
+
+/obj/effect/temp_visual/slippery_ice/Initialize(mapload)
+ . = ..()
+ animate(src, alpha = 160, time = phase_in_period)
+ animate(alpha = 0, time = duration - phase_in_period) /// slowly fade out of existence
+ addtimer(CALLBACK(src, PROC_REF(add_slippery_component), phase_in_period)) //only become slippery after we phased in
+
+/obj/effect/temp_visual/slippery_ice/proc/add_slippery_component()
+ AddComponent(/datum/component/slippery, 2 SECONDS)
+
+/datum/action/cooldown/spell/conjure/create_afterimages
+ name = "Create After Images"
+ button_icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ button_icon_state = "ice_demon"
+ spell_requirements = NONE
+ cooldown_time = 1 MINUTES
+ 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
+
+/datum/action/cooldown/spell/conjure/create_afterimages/can_cast_spell(feedback = TRUE)
+ . = ..()
+ 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--
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
new file mode 100644
index 00000000000000..b17d5c1a30dd8b
--- /dev/null
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm
@@ -0,0 +1,118 @@
+/datum/ai_controller/basic_controller/ice_demon
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_LIST_SCARY_ITEMS = list(
+ /obj/item/weldingtool,
+ /obj/item/flashlight/flare,
+ ),
+ BB_BASIC_MOB_FLEEING = TRUE,
+ BB_MINIMUM_DISTANCE_RANGE = 3,
+ )
+
+ 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/flee_target/ice_demon,
+ /datum/ai_planning_subtree/ranged_skirmish/ice_demon,
+ /datum/ai_planning_subtree/maintain_distance/cover_minimum_distance/ice_demon,
+ /datum/ai_planning_subtree/teleport_away_from_target,
+ /datum/ai_planning_subtree/find_and_hunt_target/teleport_destination,
+ /datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages,
+ )
+
+
+/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance/ice_demon
+ maximum_distance = 7
+
+/datum/ai_planning_subtree/teleport_away_from_target
+ ability_key = BB_DEMON_TELEPORT_ABILITY
+
+/datum/ai_planning_subtree/find_and_hunt_target/teleport_destination
+ target_key = BB_TELEPORT_DESTINATION
+ hunting_behavior = /datum/ai_behavior/hunt_target/use_ability_on_target/demon_teleport
+ finding_behavior = /datum/ai_behavior/find_valid_teleport_location
+ hunt_targets = list(/turf/open)
+ hunt_range = 3
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/find_and_hunt_target/teleport_destination/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ return
+ if(controller.blackboard_key_exists(BB_ESCAPE_DESTINATION))
+ controller.clear_blackboard_key(BB_TELEPORT_DESTINATION)
+ return
+ var/datum/action/cooldown/ability = controller.blackboard[BB_DEMON_TELEPORT_ABILITY]
+ if(!ability?.IsAvailable())
+ return
+ return ..()
+
+/datum/ai_behavior/find_valid_teleport_location
+
+/datum/ai_behavior/find_valid_teleport_location/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range)
+ . = ..()
+ var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ var/list/possible_turfs = list()
+
+ if(QDELETED(target))
+ finish_action(controller, FALSE)
+ return
+
+ for(var/turf/open/potential_turf in oview(hunt_range, target)) //we check for turfs around the target
+ if(potential_turf.is_blocked_turf())
+ continue
+ if(!can_see(target, potential_turf, hunt_range))
+ continue
+ possible_turfs += potential_turf
+
+ if(!length(possible_turfs))
+ finish_action(controller, FALSE)
+ return
+
+ controller.set_blackboard_key(hunting_target_key, pick(possible_turfs))
+ finish_action(controller, TRUE)
+
+/datum/ai_behavior/hunt_target/use_ability_on_target/demon_teleport
+ hunt_cooldown = 2 SECONDS
+ ability_key = BB_DEMON_TELEPORT_ABILITY
+ behavior_flags = NONE
+
+/datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages
+ ability_key = BB_DEMON_CLONE_ABILITY
+
+/datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ if(living_pawn.health / living_pawn.maxHealth > 0.5) //only use this ability when under half health
+ return
+ return ..()
+
+/datum/ai_planning_subtree/flee_target/ice_demon
+
+/datum/ai_planning_subtree/flee_target/ice_demon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ if(QDELETED(target))
+ return
+ if(!iscarbon(target))
+ return
+ var/mob/living/carbon/human_target = target
+
+ for(var/obj/held_item in human_target.held_items)
+ if(!is_type_in_list(held_item, controller.blackboard[BB_LIST_SCARY_ITEMS]))
+ continue
+ if(!held_item.light_on)
+ continue
+ var/datum/action/cooldown/slip_ability = controller.blackboard[BB_DEMON_SLIP_ABILITY]
+ if(slip_ability?.IsAvailable())
+ controller.queue_behavior(/datum/ai_behavior/use_mob_ability, BB_DEMON_SLIP_ABILITY)
+ return ..()
+
+/datum/ai_planning_subtree/ranged_skirmish/ice_demon
+ min_range = 0
+
+/datum/ai_controller/basic_controller/ice_demon/afterimage
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/flee_target/ice_demon, //even the afterimages are afraid of flames!
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
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 292766be07bf90..f89009cc2d9a5c 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
@@ -87,5 +87,5 @@
balloon_alert(src, "devouring...")
if(!do_after(src, 5 SECONDS, target))
return
- target.gib()
+ target.gib(DROP_ALL_REMAINS)
adjustBruteLoss(-1 * heal_on_cannibalize)
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 5951bd6b7feb8e..47280af4281698 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
@@ -13,53 +13,33 @@
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/sculpt_statues,
- /datum/ai_planning_subtree/find_and_hunt_target/cannibalize,
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses/ice_whelp,
/datum/ai_planning_subtree/burn_trees,
)
-
-/datum/ai_planning_subtree/find_and_hunt_target/cannibalize
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/ice_whelp
target_key = BB_TARGET_CANNIBAL
- hunting_behavior = /datum/ai_behavior/cannibalize
- finding_behavior = /datum/ai_behavior/find_hunt_target/dragon_corpse
+ finding_behavior = /datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise
hunt_targets = list(/mob/living/basic/mining/ice_whelp)
hunt_range = 10
-/datum/ai_behavior/find_hunt_target/dragon_corpse
+/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
-/datum/ai_behavior/find_hunt_target/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
- if(dinner.stat != DEAD)
- return FALSE
+/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
if(dinner.pulledby) //someone already got him before us
return FALSE
+ return ..()
- return can_see(source, dinner, radius)
-
-/datum/ai_behavior/cannibalize
+/datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
-/datum/ai_behavior/cannibalize/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/cannibalize/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
- . = ..()
- var/mob/living/basic/living_pawn = controller.pawn
+/datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
var/mob/living/target = controller.blackboard[target_key]
-
- if(QDELETED(target))
+ if(QDELETED(target) || target.stat != DEAD || target.pulledby) //we were too slow
finish_action(controller, FALSE)
return
-
- if(target.stat != DEAD || target.pulledby) //we were too slow
- finish_action(controller, FALSE)
- return
-
- living_pawn.melee_attack(target)
- finish_action(controller, TRUE)
+ return ..()
/datum/ai_behavior/cannibalize/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
diff --git a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
index c88178135dc54c..8964ebf837ec18 100644
--- a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
+++ b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
@@ -1,7 +1,6 @@
/datum/ai_controller/basic_controller/mega_arachnid
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_BASIC_MOB_FLEEING = TRUE,
BB_BASIC_MOB_FLEE_DISTANCE = 5,
)
@@ -37,7 +36,7 @@
flee_behaviour = /datum/ai_behavior/run_away_from_target/mega_arachnid
/datum/ai_planning_subtree/flee_target/mega_arachnid/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- if(!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
var/datum/action/cooldown/slip_acid = controller.blackboard[BB_ARACHNID_SLIP]
diff --git a/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm b/code/modules/mob/living/basic/jungle/venus_human_trap.dm
similarity index 62%
rename from code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm
rename to code/modules/mob/living/basic/jungle/venus_human_trap.dm
index c489d6c888d4aa..a997a8d266b1a0 100644
--- a/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm
+++ b/code/modules/mob/living/basic/jungle/venus_human_trap.dm
@@ -120,13 +120,14 @@
* The result of a kudzu flower bud, these enemies use vines to drag prey close to them for attack.
*
* A carnivorious plant which uses vines to catch and ensnare prey. Spawns from kudzu flower buds.
- * Each one has a maximum of four vines, which can be attached to a variety of things. Carbons are stunned when a vine is attached to them, and movable entities are pulled closer over time.
+ * Each one can attach up to two temporary vines to objects or mobs and drag them around with it.
* Attempting to attach a vine to something with a vine already attached to it will pull all movable targets closer on command.
* Once the prey is in melee range, melee attacks from the venus human trap heals itself for 10% of its max health, assuming the target is alive.
* Akin to certain spiders, venus human traps can also be possessed and controlled by ghosts.
*
*/
-/mob/living/simple_animal/hostile/venus_human_trap
+
+/mob/living/basic/venus_human_trap
name = "venus human trap"
desc = "Now you know how the fly feels."
icon = 'icons/mob/spacevines.dmi'
@@ -135,24 +136,21 @@
mob_biotypes = MOB_ORGANIC | MOB_PLANT
layer = SPACEVINE_MOB_LAYER
plane = GAME_PLANE_UPPER_FOV_HIDDEN
- health = 50
- maxHealth = 50
- ranged = TRUE
- harm_intent_damage = 5
+ health = 100
+ maxHealth = 100
obj_damage = 60
- melee_damage_lower = 20
+ melee_damage_lower = 10
melee_damage_upper = 20
- minbodytemp = 100
+ minimum_survivable_temperature = 100
combat_mode = TRUE
- ranged_cooldown_time = 4 SECONDS
- del_on_death = TRUE
+ basic_mob_flags = DEL_ON_DEATH
death_message = "collapses into bits of plant matter."
attacked_sound = 'sound/creatures/venus_trap_hurt.ogg'
death_sound = 'sound/creatures/venus_trap_death.ogg'
attack_sound = 'sound/creatures/venus_trap_hit.ogg'
unsuitable_heat_damage = 5 // heat damage is different from cold damage since coldmos is significantly more common than plasmafires
unsuitable_cold_damage = 2 // they now do take cold damage, but this should be sufficiently small that it does not cause major issues
- 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)
+ 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_atmos_damage = 0
/// copied over from the code from eyeballs (the mob) to make it easier for venus human traps to see in kudzu that doesn't have the transparency mutation
sight = SEE_SELF|SEE_MOBS|SEE_OBJS|SEE_TURFS
@@ -163,74 +161,82 @@
faction = list(FACTION_HOSTILE,FACTION_VINES,FACTION_PLANTS)
initial_language_holder = /datum/language_holder/venus
unique_name = TRUE
- /// A list of all the plant's vines
- var/list/vines = list()
- /// The maximum amount of vines a plant can have at one time
- var/max_vines = 4
- /// How far away a plant can attach a vine to something
- var/vine_grab_distance = 5
- /// Whether or not this plant is ghost possessable
- var/playable_plant = TRUE
+ speed = 1.2
+ melee_attack_cooldown = 1.2 SECONDS
+ ai_controller = /datum/ai_controller/basic_controller/human_trap
+ ///how much damage we take out of weeds
+ var/no_weed_damage = 20
+ ///how much do we heal in weeds
+ var/weed_heal = 10
+ ///if the balloon alert was shown atleast once, reset after healing in weeds
+ var/alert_shown = FALSE
-/mob/living/simple_animal/hostile/venus_human_trap/Life(seconds_per_tick = SSMOBS_DT, times_fired)
+/mob/living/basic/venus_human_trap/Initialize(mapload)
. = ..()
- pull_vines()
+ 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)
-/mob/living/simple_animal/hostile/venus_human_trap/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- pixel_x = base_pixel_x + (dir & (NORTH|WEST) ? 2 : -2)
+/mob/living/basic/venus_human_trap/RangedAttack(atom/victim)
+ if(!combat_mode)
+ return
+ var/datum/action/cooldown/mob_cooldown/tangle_ability = ai_controller.blackboard[BB_TARGETTED_ACTION]
+ if(!istype(tangle_ability))
+ return
+ tangle_ability.Trigger(target = victim)
-/mob/living/simple_animal/hostile/venus_human_trap/AttackingTarget()
+/mob/living/basic/venus_human_trap/Life(seconds_per_tick = SSMOBS_DT, times_fired)
. = ..()
- if(isliving(target))
- var/mob/living/L = target
- if(L.stat != DEAD)
- adjustHealth(-maxHealth * 0.1)
-
-/mob/living/simple_animal/hostile/venus_human_trap/OpenFire(atom/the_target)
- for(var/datum/beam/B in vines)
- if(B.target == the_target)
- pull_vines()
- ranged_cooldown = world.time + (ranged_cooldown_time * 0.5)
- return
- if(get_dist(src,the_target) > vine_grab_distance || vines.len >= max_vines)
+ if(!.)
+ return FALSE
+
+ var/vines_in_range = locate(/obj/structure/spacevine) in range(2, src)
+ if(!vines_in_range && !alert_shown)
+ alert_shown = TRUE
+ balloon_alert(src, "do not leave vines!")
+ 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
+
+/datum/action/cooldown/vine_tangle
+ name = "Tangle"
+ button_icon = 'icons/mob/spacevines.dmi'
+ button_icon_state = "Light1"
+ desc = "Grabs a target with a sticky vine, allowing you to pull it alongside you."
+ cooldown_time = 8 SECONDS
+ ///how many vines can we handle
+ var/max_vines = 2
+ /// An assoc list of all the plant's vines (beam = leash)
+ var/list/datum/beam/vines = list()
+ /// How far away a plant can attach a vine to something
+ var/vine_grab_distance = 4
+ /// how long does a vine attached to something last (and its leash) (lasts twice as long on nonliving things)
+ var/vine_duration = 2 SECONDS
+
+/datum/action/cooldown/vine_tangle/Remove(mob/remove_from)
+ QDEL_LIST(vines)
+ return ..()
+
+/datum/action/cooldown/vine_tangle/Activate(atom/target_atom)
+ if(isturf(target_atom) || istype(target_atom, /obj/structure/spacevine))
+ return
+ if(length(vines) >= max_vines || get_dist(owner, target_atom) > vine_grab_distance)
return
- for(var/turf/T in get_line(src,target))
- if (T.density)
+ for(var/turf/blockage in get_line(owner, target_atom))
+ if(blockage.is_blocked_turf(exclude_mobs = TRUE))
return
- for(var/obj/O in T)
- if(O.density)
- return
-
- var/datum/beam/newVine = Beam(the_target, icon_state = "vine", maxdistance = vine_grab_distance, beam_type=/obj/effect/ebeam/vine, emissive = FALSE)
- RegisterSignal(newVine, COMSIG_QDELETING, PROC_REF(remove_vine), newVine)
- vines += newVine
- if(isliving(the_target))
- var/mob/living/L = the_target
- L.apply_damage(85, STAMINA, BODY_ZONE_CHEST)
- L.Knockdown(1 SECONDS)
- ranged_cooldown = world.time + ranged_cooldown_time
-
-/mob/living/simple_animal/hostile/venus_human_trap/Destroy()
- for(var/datum/beam/vine as anything in vines)
- qdel(vine) // reference is automatically deleted by remove_vine
- return ..()
-/**
- * Manages how the vines should affect the things they're attached to.
- *
- * Pulls all movable targets of the vines closer to the plant
- * If the target is on the same tile as the plant, destroy the vine
- * Removes any QDELETED vines from the vines list.
- */
-/mob/living/simple_animal/hostile/venus_human_trap/proc/pull_vines()
- for(var/datum/beam/B in vines)
- if(istype(B.target, /atom/movable))
- var/atom/movable/AM = B.target
- if(!AM.anchored)
- step(AM, get_dir(AM, src))
- if(get_dist(src, B.target) == 0)
- qdel(B)
+ var/datum/beam/new_vine = owner.Beam(target_atom, icon_state = "vine", time = vine_duration * (ismob(target_atom) ? 1 : 2), beam_type = /obj/effect/ebeam/vine, emissive = FALSE)
+ var/component = target_atom.AddComponent(/datum/component/leash, owner, vine_grab_distance)
+ RegisterSignal(new_vine, COMSIG_QDELETING, PROC_REF(remove_vine), new_vine)
+ vines[new_vine] = component
+ if(isliving(target_atom))
+ var/mob/living/victim = target_atom
+ victim.Knockdown(2 SECONDS)
+ StartCooldown()
+ return TRUE
/**
* Removes a vine from the list.
@@ -240,9 +246,24 @@
* Arguments:
* * datum/beam/vine - The vine to be removed from the list.
*/
-/mob/living/simple_animal/hostile/venus_human_trap/proc/remove_vine(datum/beam/vine)
+/datum/action/cooldown/vine_tangle/proc/remove_vine(datum/beam/vine)
SIGNAL_HANDLER
+ qdel(vines[vine])
vines -= vine
+/datum/ai_controller/basic_controller/human_trap
+ 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
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability/continue_planning,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
#undef FINAL_BUD_GROWTH_ICON
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 58efaf1f81b46f..fe1c4150315b04 100644
--- a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub_ai.dm
@@ -3,7 +3,6 @@
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
BB_ORE_IGNORE_TYPES = list(/obj/item/stack/ore/iron, /obj/item/stack/ore/glass),
- BB_BASIC_MOB_FLEEING = TRUE,
BB_STORM_APPROACHING = FALSE,
)
@@ -14,9 +13,9 @@
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/dig_away_from_danger,
/datum/ai_planning_subtree/flee_target,
- /datum/ai_planning_subtree/find_and_hunt_target/consume_ores,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores,
/datum/ai_planning_subtree/find_and_hunt_target/baby_egg,
- /datum/ai_planning_subtree/grub_mine,
+ /datum/ai_planning_subtree/mine_walls,
)
/datum/ai_controller/basic_controller/babygrub
@@ -25,7 +24,6 @@
BB_ORE_IGNORE_TYPES = list(/obj/item/stack/ore/glass),
BB_FIND_MOM_TYPES = list(/mob/living/basic/mining/goldgrub),
BB_IGNORE_MOM_TYPES = list(/mob/living/basic/mining/goldgrub/baby),
- BB_BASIC_MOB_FLEEING = TRUE,
BB_STORM_APPROACHING = FALSE,
)
@@ -34,29 +32,29 @@
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/dig_away_from_danger,
- /datum/ai_planning_subtree/find_and_hunt_target/consume_ores,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores,
/datum/ai_planning_subtree/flee_target,
/datum/ai_planning_subtree/look_for_adult,
)
///consume food!
-/datum/ai_planning_subtree/find_and_hunt_target/consume_ores
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores
target_key = BB_ORE_TARGET
- hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/consume_ores
- finding_behavior = /datum/ai_behavior/find_hunt_target/consume_ores
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/hunt_ores
+ finding_behavior = /datum/ai_behavior/find_hunt_target/hunt_ores
hunt_targets = list(/obj/item/stack/ore)
hunt_chance = 75
hunt_range = 9
-/datum/ai_behavior/find_hunt_target/consume_ores
+/datum/ai_behavior/find_hunt_target/hunt_ores
-/datum/ai_behavior/find_hunt_target/consume_ores/valid_dinner(mob/living/basic/source, obj/item/stack/ore/target, radius)
+/datum/ai_behavior/find_hunt_target/hunt_ores/valid_dinner(mob/living/basic/source, obj/item/stack/ore/target, radius)
var/list/forbidden_ore = source.ai_controller.blackboard[BB_ORE_IGNORE_TYPES]
if(is_type_in_list(target, forbidden_ore))
return FALSE
- if(target in source)
+ if(!isturf(target.loc))
return FALSE
var/obj/item/pet_target = source.ai_controller.blackboard[BB_CURRENT_PET_TARGET]
@@ -65,7 +63,7 @@
return can_see(source, target, radius)
-/datum/ai_behavior/hunt_target/unarmed_attack_target/consume_ores
+/datum/ai_behavior/hunt_target/unarmed_attack_target/hunt_ores
always_reset_target = TRUE
///find our child's egg and pull it!
@@ -123,45 +121,6 @@
/datum/ai_behavior/use_mob_ability/burrow
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
-///mine walls to look for food!
-/datum/ai_planning_subtree/grub_mine
-
-/datum/ai_planning_subtree/grub_mine/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- if(controller.blackboard_key_exists(BB_TARGET_MINERAL_WALL))
- controller.queue_behavior(/datum/ai_behavior/mine_wall, BB_TARGET_MINERAL_WALL)
- return SUBTREE_RETURN_FINISH_PLANNING
- controller.queue_behavior(/datum/ai_behavior/find_mineral_wall, BB_TARGET_MINERAL_WALL)
-
-/datum/ai_behavior/mine_wall
- behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
- action_cooldown = 15 SECONDS
-
-/datum/ai_behavior/mine_wall/setup(datum/ai_controller/controller, target_key)
- . = ..()
- var/turf/target = controller.blackboard[target_key]
- if(isnull(target))
- return FALSE
- set_movement_target(controller, target)
-
-/datum/ai_behavior/mine_wall/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
- . = ..()
- var/mob/living/basic/living_pawn = controller.pawn
- var/turf/closed/mineral/target = controller.blackboard[target_key]
- var/is_gibtonite_turf = istype(target, /turf/closed/mineral/gibtonite)
- if(QDELETED(target))
- finish_action(controller, FALSE, target_key)
- return
- living_pawn.melee_attack(target)
- if(is_gibtonite_turf)
- living_pawn.manual_emote("sighs...") //accept whats about to happen to us
-
- finish_action(controller, TRUE, target_key)
- return
-
-/datum/ai_behavior/mine_wall/finish_action(datum/ai_controller/controller, success, target_key)
- . = ..()
- controller.clear_blackboard_key(target_key)
-
/datum/pet_command/grub_spit
command_name = "Spit"
command_desc = "Ask your grub pet to spit out its ores."
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
index 256ab1fbd8f861..11043e58d11ead 100644
--- a/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
+++ b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
@@ -102,7 +102,7 @@
/mob/living/basic/hivelord_brood/Initialize(mapload)
. = ..()
- add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE), INNATE_TRAIT)
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_PERMANENTLY_MORTAL), INNATE_TRAIT)
AddElement(/datum/element/simple_flying)
AddComponent(/datum/component/swarming)
AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY)
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 6b3525cb32ab68..cbadd709047a31 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
@@ -2,7 +2,6 @@
/datum/ai_controller/basic_controller/legion
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
- BB_BASIC_MOB_FLEEING = TRUE,
BB_AGGRO_RANGE = 5, // Unobservant
BB_BASIC_MOB_FLEE_DISTANCE = 6,
)
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 bc21bd0e506c90..962d232c5ef842 100644
--- a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
@@ -34,7 +34,7 @@
/mob/living/basic/legion_brood/Initialize(mapload)
. = ..()
- add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE), INNATE_TRAIT)
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_PERMANENTLY_MORTAL), INNATE_TRAIT)
AddElement(/datum/element/simple_flying)
AddComponent(/datum/component/swarming)
AddComponent(/datum/component/clickbox, icon_state = "sphere", max_scale = 2)
diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
index 18cd73219362e9..a048fe77ab146a 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
@@ -28,8 +28,6 @@
ai_controller = /datum/ai_controller/basic_controller/lobstrosity
/// Charging ability
var/datum/action/cooldown/mob_cooldown/charge/basic_charge/lobster/charge
- /// Limbs we will cut off an unconscious man
- var/static/list/target_limbs = list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)
/// Things we will eat if we see them (arms, chiefly)
var/static/list/target_foods = list(/obj/item/bodypart/arm)
@@ -41,8 +39,8 @@
AddElement(/datum/element/basic_eating, food_types = target_foods)
AddElement(\
/datum/element/amputating_limbs,\
- surgery_verb = "snipping",\
- target_zones = target_limbs,\
+ surgery_verb = "begins snipping",\
+ target_zones = GLOB.arm_zones,\
)
charge = new(src)
charge.Grant(src)
@@ -76,7 +74,7 @@
var/mob/living/basic/basic_source = source
var/mob/living/living_target = target
basic_source.melee_attack(living_target, ignore_cooldown = TRUE)
- basic_source.ai_controller?.set_blackboard_key(BB_BASIC_MOB_FLEEING, FALSE)
+ basic_source.ai_controller?.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE)
basic_source.start_pulling(living_target)
/datum/action/cooldown/mob_cooldown/charge/basic_charge/lobster/do_charge(atom/movable/charger, atom/target_atom, delay, past)
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 8e4dfe9e29463f..7b6926fca04e7f 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm
@@ -2,7 +2,6 @@
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/lobster,
BB_LOBSTROSITY_EXPLOIT_TRAITS = list(TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT),
- BB_BASIC_MOB_FLEEING = TRUE,
BB_LOBSTROSITY_FINGER_LUST = 0
)
@@ -26,7 +25,7 @@
melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/lobster
/datum/ai_planning_subtree/basic_melee_attack_subtree/lobster/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- if (controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (!controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
if (!isnull(controller.blackboard[BB_LOBSTROSITY_TARGET_LIMB]))
return
@@ -48,8 +47,8 @@
is_vulnerable = TRUE
break
if (!is_vulnerable)
- controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, TRUE)
- if (controller.blackboard[BB_BASIC_MOB_FLEEING])
+ controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, FALSE)
+ if (!controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
finish_action(controller = controller, succeeded = TRUE, target_key = target_key) // We don't want to clear our target
return
return ..()
@@ -57,6 +56,12 @@
/datum/ai_planning_subtree/flee_target/lobster
flee_behaviour = /datum/ai_behavior/run_away_from_target/lobster
+/datum/ai_planning_subtree/flee_target/lobster/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/datum/action/cooldown/using_action = controller.blackboard[BB_TARGETTED_ACTION]
+ if (using_action?.IsAvailable())
+ return
+ return ..()
+
/datum/ai_behavior/run_away_from_target/lobster
clear_failed_targets = FALSE
@@ -64,10 +69,11 @@
var/atom/target = controller.blackboard[target_key]
if(isnull(target))
return ..()
+
for (var/trait in controller.blackboard[BB_LOBSTROSITY_EXPLOIT_TRAITS])
if (!HAS_TRAIT(target, trait))
continue
- controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, FALSE)
+ controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE)
finish_action(controller, succeeded = FALSE)
return
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook.dm b/code/modules/mob/living/basic/lavaland/mook/mook.dm
new file mode 100644
index 00000000000000..da833437715c64
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/mook/mook.dm
@@ -0,0 +1,273 @@
+//Fragile but highly aggressive wanderers that pose a large threat in numbers.
+//They'll attempt to leap at their target from afar using their hatchets.
+/mob/living/basic/mining/mook
+ name = "wanderer"
+ desc = "This unhealthy looking primitive seems to be talented at administiring health care."
+ icon = 'icons/mob/simple/jungle/mook.dmi'
+ icon_state = "mook"
+ icon_living = "mook"
+ icon_dead = "mook_dead"
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ gender = FEMALE
+ maxHealth = 150
+ faction = list(FACTION_MINING, FACTION_NEUTRAL)
+ health = 150
+ move_resist = MOVE_FORCE_OVERPOWERING
+ 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
+ base_pixel_y = -16
+
+ ///the state of combat we are in
+ var/attack_state = MOOK_ATTACK_NEUTRAL
+ ///are we a healer?
+ var/is_healer = TRUE
+ ///the ore we are holding if any
+ var/obj/held_ore
+ ///overlay for neutral stance
+ var/mutable_appearance/neutral_stance
+ ///overlay for attacking stance
+ var/mutable_appearance/attack_stance
+ ///overlay when we hold an ore
+ var/mutable_appearance/ore_overlay
+ ///commands we obey
+ var/list/pet_commands = list(
+ /datum/pet_command/idle,
+ /datum/pet_command/free,
+ /datum/pet_command/point_targetting/attack,
+ /datum/pet_command/point_targetting/fetch,
+ )
+
+/mob/living/basic/mining/mook/Initialize(mapload)
+ . = ..()
+ 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)
+
+ ore_overlay = mutable_appearance(icon, "mook_ore_overlay")
+
+ AddComponent(/datum/component/ai_listen_to_weather)
+ AddElement(/datum/element/wall_smasher)
+ RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
+ RegisterSignal(src, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(drop_ore))
+
+ if(is_healer)
+ grant_healer_abilities()
+
+ AddComponent(/datum/component/obeys_commands, pet_commands)
+
+/mob/living/basic/mining/mook/proc/grant_healer_abilities()
+ AddComponent(\
+ /datum/component/healing_touch,\
+ heal_brute = melee_damage_upper,\
+ heal_burn = melee_damage_upper,\
+ heal_time = 0,\
+ valid_targets_typecache = typecacheof(list(/mob/living/basic/mining/mook)),\
+ )
+
+/mob/living/basic/mining/mook/Entered(atom/movable/mover)
+ if(istype(mover, /obj/item/stack/ore))
+ held_ore = mover
+ update_appearance(UPDATE_OVERLAYS)
+
+ return ..()
+
+/mob/living/basic/mining/mook/Exited(atom/movable/mover)
+ . = ..()
+ if(held_ore != mover)
+ return
+ held_ore = null
+ update_appearance(UPDATE_OVERLAYS)
+
+/mob/living/basic/mining/mook/proc/pre_attack(mob/living/attacker, atom/target)
+ SIGNAL_HANDLER
+
+ return attack_sequence(target)
+
+/mob/living/basic/mining/mook/proc/attack_sequence(atom/target)
+ if(istype(target, /obj/item/stack/ore) && isnull(held_ore))
+ var/obj/item/ore_target = target
+ ore_target.forceMove(src)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ if(istype(target, /obj/structure/material_stand))
+ if(held_ore)
+ held_ore.forceMove(target)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ if(istype(target, /obj/structure/bonfire))
+ var/obj/structure/bonfire/fire_target = target
+ if(!fire_target.burning)
+ fire_target.start_burning()
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/mob/living/basic/mining/mook/proc/change_combatant_state(state)
+ attack_state = state
+ update_appearance()
+
+/mob/living/basic/mining/mook/Destroy()
+ QDEL_NULL(held_ore)
+ return ..()
+
+/mob/living/basic/mining/mook/update_icon_state()
+ . = ..()
+ if(stat == DEAD)
+ return
+ switch(attack_state)
+ if(MOOK_ATTACK_NEUTRAL)
+ icon_state = "mook"
+ if(MOOK_ATTACK_WARMUP)
+ icon_state = "mook_warmup"
+ if(MOOK_ATTACK_ACTIVE)
+ icon_state = "mook_leap"
+ if(MOOK_ATTACK_STRIKE)
+ icon_state = "mook_strike"
+
+/mob/living/basic/mining/mook/update_overlays()
+ . = ..()
+ if(stat == DEAD)
+ return
+
+ if(attack_state != MOOK_ATTACK_NEUTRAL || isnull(held_ore))
+ return
+
+ . += ore_overlay
+
+/mob/living/basic/mining/mook/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, gentle = FALSE, quickstart = TRUE)
+ change_combatant_state(state = MOOK_ATTACK_ACTIVE)
+ return ..()
+
+/mob/living/basic/mining/mook/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ . = ..()
+ change_combatant_state(state = MOOK_ATTACK_NEUTRAL)
+
+/mob/living/basic/mining/mook/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+
+ if(!istype(mover, /mob/living/basic/mining/mook))
+ return FALSE
+
+ var/mob/living/basic/mining/mook/mook_moover = mover
+ if(mook_moover.attack_state == MOOK_ATTACK_ACTIVE)
+ return TRUE
+
+/mob/living/basic/mining/mook/proc/drop_ore(mob/living/user)
+ SIGNAL_HANDLER
+
+ if(isnull(held_ore))
+ return
+ dropItemToGround(held_ore)
+ return COMSIG_KB_ACTIVATED
+
+/mob/living/basic/mining/mook/death()
+ desc = "A deceased primitive. Upon closer inspection, it was suffering from severe cellular degeneration and its garments are machine made..." //Can you guess the twist
+ return ..()
+
+/mob/living/basic/mining/mook/proc/attack_intruder(mob/living/intruder)
+ if(istype(intruder, /mob/living/basic/mining/mook))
+ return
+ for(var/mob/living/basic/mining/mook/villager in oview(src, 9))
+ villager.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, intruder)
+
+
+/mob/living/basic/mining/mook/worker
+ desc = "This unhealthy looking primitive is wielding a rudimentary hatchet, swinging it with wild abandon. One isn't much of a threat, but in numbers they can quickly overwhelm a superior opponent."
+ gender = MALE
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ ai_controller = /datum/ai_controller/basic_controller/mook
+ is_healer = FALSE
+
+/mob/living/basic/mining/mook/worker/Initialize(mapload)
+ . = ..()
+ 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/attack_sequence(atom/target)
+ . = ..()
+ if(. & COMPONENT_HOSTILE_NO_ATTACK)
+ return
+
+ if(attack_state == MOOK_ATTACK_STRIKE)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ change_combatant_state(state = MOOK_ATTACK_STRIKE)
+ addtimer(CALLBACK(src, PROC_REF(change_combatant_state), MOOK_ATTACK_NEUTRAL), 0.3 SECONDS)
+
+/mob/living/basic/mining/mook/worker/update_overlays()
+ . = ..()
+ if(stat == DEAD)
+ return
+
+ switch(attack_state)
+ if(MOOK_ATTACK_STRIKE)
+ . += attack_stance
+ if(MOOK_ATTACK_NEUTRAL)
+ . += neutral_stance
+
+/mob/living/basic/mining/mook/worker/bard
+ desc = "It's holding a guitar?"
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ gender = MALE
+ attack_sound = 'sound/weapons/stringsmash.ogg'
+ death_sound = 'sound/voice/mook_death.ogg'
+ ai_controller = /datum/ai_controller/basic_controller/mook/bard
+ ///our guitar
+ var/obj/item/instrument/guitar/held_guitar
+
+/mob/living/basic/mining/mook/worker/bard/Initialize(mapload)
+ . = ..()
+ neutral_stance = mutable_appearance(icon, "bard_overlay")
+ attack_stance = mutable_appearance(icon, "bard_strike")
+ held_guitar = new(src)
+ ai_controller.set_blackboard_key(BB_SONG_INSTRUMENT, held_guitar)
+ update_appearance()
+
+/mob/living/basic/mining/mook/worker/tribal_chief
+ name = "tribal chief"
+ desc = "Acknowledge him!"
+ gender = MALE
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ ai_controller = /datum/ai_controller/basic_controller/mook/tribal_chief
+ ///overlay in our neutral state
+ var/static/mutable_appearance/chief_neutral = mutable_appearance('icons/mob/simple/jungle/mook.dmi', "mook_chief")
+ ///overlay in our striking state
+ var/static/mutable_appearance/chief_strike = mutable_appearance('icons/mob/simple/jungle/mook.dmi', "mook_chief_strike")
+ ///overlay in our active state
+ var/static/mutable_appearance/chief_active = mutable_appearance('icons/mob/simple/jungle/mook.dmi', "mook_chief_leap")
+ ///overlay in our warmup state
+ var/static/mutable_appearance/chief_warmup = mutable_appearance('icons/mob/simple/jungle/mook.dmi', "mook_chief_warmup")
+
+/mob/living/basic/mining/mook/worker/tribal_chief/Initialize(mapload)
+ . = ..()
+ update_appearance()
+
+/mob/living/basic/mining/mook/worker/tribal_chief/update_overlays()
+ . = ..()
+ if(stat == DEAD)
+ return
+ switch(attack_state)
+ if(MOOK_ATTACK_NEUTRAL)
+ . += chief_neutral
+ if(MOOK_ATTACK_WARMUP)
+ . += chief_warmup
+ if(MOOK_ATTACK_ACTIVE)
+ . += chief_active
+ if(MOOK_ATTACK_STRIKE)
+ . += chief_strike
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm b/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm
new file mode 100644
index 00000000000000..cfc359bd54fccb
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/mook/mook_abilities.dm
@@ -0,0 +1,140 @@
+/datum/action/cooldown/mob_cooldown/mook_ability
+ ///are we a mook?
+ var/is_mook = FALSE
+
+/datum/action/cooldown/mob_cooldown/mook_ability/Grant(mob/grant_to)
+ . = ..()
+ if(isnull(owner))
+ return
+ is_mook = istype(owner, /mob/living/basic/mining/mook)
+
+/datum/action/cooldown/mob_cooldown/mook_ability/IsAvailable(feedback)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(!is_mook)
+ return TRUE
+
+ var/mob/living/basic/mining/mook/mook_owner = owner
+ if(mook_owner.attack_state != MOOK_ATTACK_NEUTRAL)
+ if(feedback)
+ mook_owner.balloon_alert(mook_owner, "still recovering!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap
+ name = "Mook leap"
+ desc = "Leap towards the enemy!"
+ cooldown_time = 7 SECONDS
+ shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
+ ///telegraph time before jumping
+ var/wind_up_time = 2 SECONDS
+ ///intervals between each of our attacks
+ var/attack_interval = 0.4 SECONDS
+ ///how many times do we attack if we reach the target?
+ var/times_to_attack = 4
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/Activate(atom/target)
+ if(owner.CanReach(target))
+ attack_combo(target)
+ StartCooldown()
+ return TRUE
+
+ if(is_mook)
+ var/mob/living/basic/mining/mook/mook_owner = owner
+ mook_owner.change_combatant_state(state = MOOK_ATTACK_WARMUP)
+
+ addtimer(CALLBACK(src, PROC_REF(launch_towards_target), target), wind_up_time)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/proc/launch_towards_target(atom/target)
+ new /obj/effect/temp_visual/mook_dust(get_turf(owner))
+ playsound(get_turf(owner), 'sound/weapons/thudswoosh.ogg', 25, TRUE)
+ playsound(owner, 'sound/voice/mook_leap_yell.ogg', 100, TRUE)
+ var/turf/target_turf = get_turf(target)
+
+ if(!target_turf.is_blocked_turf())
+ owner.throw_at(target = target_turf, range = 7, speed = 1, spin = FALSE, callback = CALLBACK(src, PROC_REF(attack_combo), target))
+ return
+
+ var/list/open_turfs = list()
+
+ for(var/turf/possible_turf in get_adjacent_open_turfs(target))
+ if(possible_turf.is_blocked_turf())
+ continue
+ open_turfs += possible_turf
+
+ if(!length(open_turfs))
+ return
+
+ var/turf/final_turf = get_closest_atom(/turf, open_turfs, owner)
+ owner.throw_at(target = final_turf, range = 7, speed = 1, spin = FALSE, callback = CALLBACK(src, PROC_REF(attack_combo), target))
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/proc/attack_combo(atom/target)
+ if(!owner.CanReach(target))
+ return FALSE
+
+ for(var/i in 0 to (times_to_attack - 1))
+ addtimer(CALLBACK(src, PROC_REF(attack_target), target), i * attack_interval)
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_leap/proc/attack_target(atom/target)
+ if(!owner.CanReach(target) || owner.stat == DEAD)
+ return
+ var/mob/living/basic/basic_owner = owner
+ basic_owner.melee_attack(target, ignore_cooldown = TRUE)
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_jump
+ name = "Mook Jump"
+ desc = "Soar high in the air!"
+ cooldown_time = 14 SECONDS
+ shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
+ click_to_activate = FALSE
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_jump/Activate(atom/target)
+ var/obj/effect/landmark/drop_zone = locate(/obj/effect/landmark/mook_village) in GLOB.landmarks_list
+ if(drop_zone?.z == owner.z)
+ var/turf/jump_destination = get_turf(drop_zone)
+ jump_to_turf(jump_destination)
+ StartCooldown()
+ return TRUE
+ var/list/potential_turfs = list()
+ for(var/turf/open_turf in oview(9, owner))
+ if(!open_turf.is_blocked_turf())
+ potential_turfs += open_turf
+ if(!length(potential_turfs))
+ return FALSE
+ jump_to_turf(pick(potential_turfs))
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_jump/proc/jump_to_turf(turf/target)
+ if(is_mook)
+ var/mob/living/basic/mining/mook/mook_owner = owner
+ mook_owner.change_combatant_state(state = MOOK_ATTACK_ACTIVE)
+ new /obj/effect/temp_visual/mook_dust(get_turf(owner))
+ playsound(get_turf(owner), 'sound/weapons/thudswoosh.ogg', 50, TRUE)
+ animate(owner, pixel_y = owner.base_pixel_y + 146, time = 0.5 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(land_on_turf), target), 0.5 SECONDS)
+
+/datum/action/cooldown/mob_cooldown/mook_ability/mook_jump/proc/land_on_turf(turf/target)
+ do_teleport(owner, target, precision = 3, no_effects = TRUE)
+ animate(owner, pixel_y = owner.base_pixel_y, time = 0.5 SECONDS)
+ new /obj/effect/temp_visual/mook_dust(get_turf(owner))
+ if(is_mook)
+ addtimer(CALLBACK(owner, TYPE_PROC_REF(/mob/living/basic/mining/mook, change_combatant_state), MOOK_ATTACK_NEUTRAL), 0.5 SECONDS)
+
+/obj/effect/temp_visual/mook_dust
+ name = "dust"
+ desc = "It's just a dust cloud!"
+ icon = 'icons/mob/simple/jungle/mook.dmi'
+ icon_state = "mook_leap_cloud"
+ layer = BELOW_MOB_LAYER
+ plane = GAME_PLANE
+ 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
new file mode 100644
index 00000000000000..14ed9eb2982936
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/mook/mook_ai.dm
@@ -0,0 +1,426 @@
+///commands the chief can pick from
+GLOBAL_LIST_INIT(mook_commands, list(
+ new /datum/pet_command/point_targetting/attack,
+ new /datum/pet_command/point_targetting/fetch,
+))
+
+/datum/ai_controller/basic_controller/mook
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/mook,
+ BB_BLACKLIST_MINERAL_TURFS = list(/turf/closed/mineral/gibtonite, /turf/closed/mineral/strong),
+ BB_MAXIMUM_DISTANCE_TO_VILLAGE = 7,
+ BB_STORM_APPROACHING = FALSE,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/look_for_village,
+ /datum/ai_planning_subtree/targeted_mob_ability/leap,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/material_stand,
+ /datum/ai_planning_subtree/use_mob_ability/mook_jump,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/mook,
+ /datum/ai_planning_subtree/mine_walls/mook,
+ /datum/ai_planning_subtree/wander_away_from_village,
+ )
+
+///check for faction if not a ash walker, otherwise just attack
+/datum/targetting_datum/basic/mook/faction_check(mob/living/living_mob, mob/living/the_target)
+ if(FACTION_ASHWALKER in living_mob.faction)
+ return FALSE
+
+ return ..()
+
+/datum/ai_planning_subtree/targeted_mob_ability/leap
+ ability_key = BB_MOOK_LEAP_ABILITY
+
+/datum/ai_planning_subtree/use_mob_ability/mook_jump
+ ability_key = BB_MOOK_JUMP_ABILITY
+
+///jump towards the village when we have found ore or there is a storm coming
+/datum/ai_planning_subtree/use_mob_ability/mook_jump/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/storm_approaching = controller.blackboard[BB_STORM_APPROACHING]
+ var/mob/living/living_pawn = controller.pawn
+ var/obj/effect/home = controller.blackboard[BB_HOME_VILLAGE]
+ if(QDELETED(home))
+ return
+ if(get_dist(living_pawn, home) < controller.blackboard[BB_MAXIMUM_DISTANCE_TO_VILLAGE])
+ return
+ if(home.z != living_pawn.z)
+ return
+ if(!storm_approaching && !(locate(/obj/item/stack/ore) in living_pawn))
+ return
+
+ controller.clear_blackboard_key(BB_TARGET_MINERAL_WALL)
+ return ..()
+
+///hunt ores that we will haul off back to the village
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/mook
+
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/mook/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ if(locate(/obj/item/stack/ore) in living_pawn)
+ return
+ return ..()
+
+///deposit ores into the stand!
+/datum/ai_planning_subtree/find_and_hunt_target/material_stand
+ target_key = BB_MATERIAL_STAND_TARGET
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/material_stand
+ finding_behavior = /datum/ai_behavior/find_hunt_target
+ hunt_targets = list(/obj/structure/material_stand)
+ hunt_range = 9
+
+/datum/ai_planning_subtree/find_and_hunt_target/material_stand/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ if(!locate(/obj/item/stack/ore) in living_pawn)
+ return
+ return ..()
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/material_stand
+ required_distance = 0
+ always_reset_target = TRUE
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+///try to face the counter when depositing ores
+/datum/ai_behavior/hunt_target/unarmed_attack_target/material_stand/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
+ . = ..()
+ var/atom/hunt_target = controller.blackboard[hunting_target_key]
+ if (QDELETED(hunt_target))
+ return FALSE
+ var/list/possible_turfs = list()
+ var/list/directions = list(SOUTH, SOUTHEAST)
+
+ for(var/direction in directions)
+ var/turf/bottom_turf = get_step(hunt_target, direction)
+ if(!bottom_turf.is_blocked_turf())
+ possible_turfs += bottom_turf
+
+ if(!length(possible_turfs))
+ return FALSE
+ set_movement_target(controller, pick(possible_turfs))
+
+///look for our village
+/datum/ai_planning_subtree/look_for_village
+
+/datum/ai_planning_subtree/look_for_village/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard_key_exists(BB_HOME_VILLAGE))
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/find_village, BB_HOME_VILLAGE)
+
+/datum/ai_behavior/find_village
+
+/datum/ai_behavior/find_village/perform(seconds_per_tick, datum/ai_controller/controller, village_key)
+ . = ..()
+
+ var/obj/effect/landmark/home_marker = locate(/obj/effect/landmark/mook_village) in GLOB.landmarks_list
+ if(isnull(home_marker))
+ finish_action(controller, FALSE)
+ return
+
+ controller.set_blackboard_key(village_key, home_marker)
+ finish_action(controller, TRUE)
+
+///explore the lands away from the village to look for ore
+/datum/ai_planning_subtree/wander_away_from_village
+
+/datum/ai_planning_subtree/wander_away_from_village/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ var/storm_approaching = controller.blackboard[BB_STORM_APPROACHING]
+ ///if we have ores to deposit or a storm is approaching, dont wander away
+ if(storm_approaching || (locate(/obj/item/stack/ore) in living_pawn))
+ return
+
+ if(controller.blackboard_key_exists(BB_HOME_VILLAGE))
+ controller.queue_behavior(/datum/ai_behavior/wander, BB_HOME_VILLAGE)
+
+/datum/ai_behavior/wander
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ required_distance = 0
+ /// distance we will wander away from the village
+ var/wander_distance = 9
+
+/datum/ai_behavior/wander/setup(datum/ai_controller/controller, village_key)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ var/obj/effect/target = controller.blackboard[village_key]
+ if(QDELETED(target))
+ return FALSE
+
+ if(target.z != living_pawn.z)
+ return FALSE
+
+ var/list/angle_directions = list()
+ for(var/direction in GLOB.alldirs)
+ angle_directions += dir2angle(direction)
+
+ var/angle_to_home = get_angle(living_pawn, target)
+ angle_directions -= angle_to_home
+ angle_directions -= (angle_to_home + 45)
+ angle_directions -= (angle_to_home - 45)
+ shuffle_inplace(angle_directions)
+
+ var/turf/wander_destination = get_turf(living_pawn)
+ for(var/angle in angle_directions)
+ var/turf/test_turf = get_furthest_turf(living_pawn, angle, target)
+ if(isnull(test_turf))
+ continue
+ var/distance_from_target = get_dist(target, test_turf)
+ if(distance_from_target <= get_dist(target, wander_destination))
+ continue
+ wander_destination = test_turf
+ if(distance_from_target == wander_distance)
+ break
+
+ set_movement_target(controller, wander_destination)
+
+/datum/ai_behavior/wander/proc/get_furthest_turf(atom/source, angle, atom/target)
+ var/turf/return_turf
+ for(var/i in 1 to wander_distance)
+ var/turf/test_destination = get_ranged_target_turf_direct(source, target, range = i, offset = angle)
+ if(test_destination.is_blocked_turf(source_atom = source))
+ break
+ return_turf = test_destination
+ return return_turf
+
+/datum/ai_behavior/wander/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
+ . = ..()
+ finish_action(controller, TRUE)
+
+/datum/ai_planning_subtree/mine_walls/mook
+ find_wall_behavior = /datum/ai_behavior/find_mineral_wall/mook
+
+/datum/ai_planning_subtree/mine_walls/mook/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ var/storm_approaching = controller.blackboard[BB_STORM_APPROACHING]
+ if(storm_approaching || locate(/obj/item/stack/ore) in living_pawn)
+ return
+ return ..()
+
+/datum/ai_behavior/find_mineral_wall/mook
+
+/datum/ai_behavior/find_mineral_wall/mook/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
+ var/list/forbidden_turfs = controller.blackboard[BB_BLACKLIST_MINERAL_TURFS]
+ if(is_type_in_list(target_wall, forbidden_turfs))
+ return FALSE
+ return ..()
+
+///bard mook plays nice music for the village
+/datum/ai_controller/basic_controller/mook/bard
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/mook,
+ BB_MAXIMUM_DISTANCE_TO_VILLAGE = 10,
+ BB_STORM_APPROACHING = FALSE,
+ BB_SONG_LINES = MOOK_SONG,
+ )
+ idle_behavior = /datum/idle_behavior/walk_near_target/mook_village
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/look_for_village,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/play_music_for_visitor,
+ /datum/ai_planning_subtree/use_mob_ability/mook_jump,
+ /datum/ai_planning_subtree/generic_play_instrument,
+ )
+
+
+///find an audience to follow and play music for!
+/datum/ai_planning_subtree/play_music_for_visitor
+
+/datum/ai_planning_subtree/play_music_for_visitor/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(BB_MOOK_MUSIC_AUDIENCE))
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/music_audience, BB_MOOK_MUSIC_AUDIENCE, /mob/living/carbon/human)
+ return
+ var/atom/home = controller.blackboard[BB_HOME_VILLAGE]
+ if(isnull(home))
+ return
+
+ var/atom/human_target = controller.blackboard[BB_MOOK_MUSIC_AUDIENCE]
+ if(get_dist(human_target, home) > controller.blackboard[BB_MAXIMUM_DISTANCE_TO_VILLAGE] || controller.blackboard[BB_STORM_APPROACHING])
+ controller.clear_blackboard_key(BB_MOOK_MUSIC_AUDIENCE)
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/travel_towards, BB_MOOK_MUSIC_AUDIENCE)
+
+/datum/ai_behavior/find_and_set/music_audience
+
+/datum/ai_behavior/find_and_set/music_audience/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/atom/home = controller.blackboard[BB_HOME_VILLAGE]
+ for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn))
+ if(target.stat > UNCONSCIOUS || !target.mind)
+ continue
+ if(isnull(home) || get_dist(target, home) > controller.blackboard[BB_MAXIMUM_DISTANCE_TO_VILLAGE])
+ continue
+ return target
+
+/datum/idle_behavior/walk_near_target/mook_village
+ target_key = BB_HOME_VILLAGE
+
+///healer mooks guard the village from intruders and heal the miner mooks when they come home
+/datum/ai_controller/basic_controller/mook/support
+ blackboard = 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,
+ )
+ idle_behavior = /datum/idle_behavior/walk_near_target/mook_village
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/look_for_village,
+ /datum/ai_planning_subtree/acknowledge_chief,
+ /datum/ai_planning_subtree/pet_planning,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/use_mob_ability/mook_jump,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/injured_mooks,
+ )
+
+///tree to find and register our leader
+/datum/ai_planning_subtree/acknowledge_chief/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard_key_exists(BB_MOOK_TRIBAL_CHIEF))
+ return
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/find_chief, BB_MOOK_TRIBAL_CHIEF, /mob/living/basic/mining/mook/worker/tribal_chief)
+
+/datum/ai_behavior/find_and_set/find_chief/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/mob/living/chief = locate(locate_path) in oview(search_range, controller.pawn)
+ if(isnull(chief))
+ return null
+ var/mob/living/living_pawn = controller.pawn
+ living_pawn.befriend(chief)
+ return chief
+
+///find injured miner mooks after they come home from a long day of work
+/datum/ai_planning_subtree/find_and_hunt_target/injured_mooks
+ target_key = BB_INJURED_MOOK
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/injured_mooks
+ finding_behavior = /datum/ai_behavior/find_hunt_target/injured_mooks
+ hunt_targets = list(/mob/living/basic/mining/mook/worker)
+ hunt_range = 9
+
+///we only heal when the mooks are home during a storm
+/datum/ai_planning_subtree/find_and_hunt_target/injured_mooks/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard[BB_STORM_APPROACHING])
+ return ..()
+
+
+/datum/ai_behavior/find_hunt_target/injured_mooks
+
+/datum/ai_behavior/find_hunt_target/injured_mooks/valid_dinner(mob/living/source, mob/living/injured_mook)
+ return (injured_mook.health < injured_mook.maxHealth)
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/injured_mooks
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/injured_mooks
+ always_reset_target = TRUE
+ hunt_cooldown = 10 SECONDS
+
+
+///the chief would rather command his mooks to attack people than attack them himself
+/datum/ai_controller/basic_controller/mook/tribal_chief
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/mook,
+ BB_STORM_APPROACHING = FALSE,
+ )
+ idle_behavior = /datum/idle_behavior/walk_near_target/mook_village
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/look_for_village,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability/leap,
+ /datum/ai_planning_subtree/issue_commands,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/material_stand,
+ /datum/ai_planning_subtree/use_mob_ability/mook_jump,
+ /datum/ai_planning_subtree/find_and_hunt_target/bonfire,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/tribal_chief,
+ )
+
+/datum/ai_planning_subtree/issue_commands
+ ///how far we look for a mook to command
+ var/command_distance = 5
+
+/datum/ai_planning_subtree/issue_commands/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!locate(/mob/living/basic/mining/mook) in oview(command_distance, controller.pawn))
+ return
+ if(controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ controller.queue_behavior(/datum/ai_behavior/issue_commands, BB_BASIC_MOB_CURRENT_TARGET, /datum/pet_command/point_targetting/attack)
+ return
+
+ var/atom/ore_target = controller.blackboard[BB_ORE_TARGET]
+ var/mob/living/living_pawn = controller.pawn
+ if(isnull(ore_target))
+ return
+ if(get_dist(ore_target, living_pawn) <= 1)
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/issue_commands, BB_ORE_TARGET, /datum/pet_command/point_targetting/fetch)
+
+/datum/ai_behavior/issue_commands
+ action_cooldown = 5 SECONDS
+
+/datum/ai_behavior/issue_commands/perform(seconds_per_tick, datum/ai_controller/controller, target_key, command_path)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/atom/target = controller.blackboard[target_key]
+
+ if(isnull(target))
+ finish_action(controller, FALSE)
+ return
+
+ var/datum/pet_command/to_command = locate(command_path) in GLOB.mook_commands
+ if(isnull(to_command))
+ finish_action(controller, FALSE)
+ return
+
+ var/issue_command = pick(to_command.speech_commands)
+ living_pawn.say(issue_command, forced = "controller")
+ living_pawn._pointed(target)
+ finish_action(controller, TRUE)
+
+
+///find an ore, only pick it up when a mook brings it close to us
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/tribal_chief
+
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/tribal_chief/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ if(locate(/obj/item/stack/ore) in living_pawn)
+ return
+
+ var/atom/target_ore = controller.blackboard[BB_ORE_TARGET]
+
+ if(isnull(target_ore))
+ return ..()
+
+ if(!isturf(target_ore.loc)) //picked up by someone else
+ controller.clear_blackboard_key(BB_ORE_TARGET)
+ return
+
+ if(get_dist(target_ore, living_pawn) > 1)
+ return
+
+ return ..()
+
+/datum/ai_planning_subtree/find_and_hunt_target/bonfire
+ target_key = BB_MOOK_BONFIRE_TARGET
+ finding_behavior = /datum/ai_behavior/find_hunt_target/bonfire
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/bonfire
+ hunt_targets = list(/obj/structure/bonfire)
+ hunt_range = 9
+
+
+/datum/ai_behavior/find_hunt_target/bonfire
+
+/datum/ai_behavior/find_hunt_target/bonfire/valid_dinner(mob/living/source, obj/structure/bonfire/fire, radius)
+ if(fire.burning)
+ return FALSE
+
+ return can_see(source, fire, radius)
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/bonfire
+ always_reset_target = TRUE
diff --git a/code/modules/mob/living/basic/lavaland/mook/mook_village.dm b/code/modules/mob/living/basic/lavaland/mook/mook_village.dm
new file mode 100644
index 00000000000000..e3a091f6f0e49f
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/mook/mook_village.dm
@@ -0,0 +1,85 @@
+///unique items that spawn at the mook village
+/obj/structure/material_stand
+ name = "material stand"
+ desc = "Is everyone free to use this thing?"
+ icon = 'icons/mob/simple/jungle/mook.dmi'
+ icon_state = "material_stand"
+ density = TRUE
+ anchored = TRUE
+ resistance_flags = INDESTRUCTIBLE
+ bound_width = 64
+ bound_height = 64
+
+/obj/structure/material_stand/attackby(obj/item/ore, mob/living/carbon/human/user, list/modifiers)
+ if(istype(ore, /obj/item/stack/ore))
+ ore.forceMove(src)
+ return
+ return ..()
+
+/obj/structure/material_stand/Entered(atom/movable/mover)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/structure/material_stand/Exited(atom/movable/mover)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+///put ore icons on the counter!
+/obj/structure/material_stand/update_overlays()
+ . = ..()
+ for(var/obj/item/stack/ore/ore_item in contents)
+ var/image/ore_icon = image(icon = initial(ore_item.icon), icon_state = initial(ore_item.icon_state), layer = LOW_ITEM_LAYER)
+ ore_icon.transform = ore_icon.transform.Scale(0.6, 0.6)
+ ore_icon.pixel_x = rand(9, 17)
+ ore_icon.pixel_y = rand(2, 4)
+ . += ore_icon
+
+/obj/structure/material_stand/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "MaterialStand")
+ ui.open()
+
+/obj/structure/material_stand/ui_data(mob/user)
+ var/list/data = list()
+ data["ores"] = list()
+ for(var/obj/item/stack/ore/ore_item in contents)
+ data["ores"] += list(list(
+ "id" = REF(ore_item),
+ "name" = ore_item.name,
+ "amount" = ore_item.amount,
+ ))
+ return data
+
+/obj/structure/material_stand/ui_static_data(mob/user)
+ var/list/data = list()
+ data["ore_images"] = list()
+ for(var/obj/item/stack/ore_item as anything in subtypesof(/obj/item/stack/ore))
+ data["ore_images"] += list(list(
+ "name" = initial(ore_item.name),
+ "icon" = icon2base64(getFlatIcon(image(icon = initial(ore_item.icon), icon_state = initial(ore_item.icon_state)), no_anim=TRUE))
+ ))
+ return data
+
+/obj/structure/material_stand/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+
+ if(. || !isliving(usr))
+ return TRUE
+
+ var/mob/living/customer = usr
+ var/obj/item/stack_to_move
+ switch(action)
+ if("withdraw")
+ if(isnull(params["reference"]))
+ return TRUE
+ stack_to_move = locate(params["reference"]) in contents
+ if(isnull(stack_to_move))
+ return TRUE
+ stack_to_move.forceMove(get_turf(customer))
+ return TRUE
+
+/obj/effect/landmark/mook_village
+ name = "mook village landmark"
+ icon_state = "x"
diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
index a25234817f31e6..348bbcfcaa7d00 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
@@ -27,13 +27,10 @@
return ..()
/datum/ai_planning_subtree/ranged_skirmish/watcher
- attack_behavior = /datum/ai_behavior/ranged_skirmish/watcher
+ min_range = 0
/datum/ai_planning_subtree/ranged_skirmish/watcher/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCHED))
return // Don't bully people who are playing red light green light
return ..()
-
-/datum/ai_behavior/ranged_skirmish/watcher
- min_range = 0
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 9426db41cca60d..4b322c220ed3ec 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm
@@ -12,6 +12,7 @@
check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
click_to_activate = FALSE
shared_cooldown = NONE
+ melee_cooldown_time = 0 SECONDS
/// At what range do we check for vision?
var/effect_radius = 7
/// How long does it take to play our various animation stages
diff --git a/code/modules/mob/living/basic/minebots/minebot_ai.dm b/code/modules/mob/living/basic/minebots/minebot_ai.dm
index a4b082f5dd1bbb..33e9821dbc4bbe 100644
--- a/code/modules/mob/living/basic/minebots/minebot_ai.dm
+++ b/code/modules/mob/living/basic/minebots/minebot_ai.dm
@@ -12,7 +12,7 @@
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot,
- /datum/ai_planning_subtree/find_and_hunt_target/consume_ores/minebot,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot,
/datum/ai_planning_subtree/minebot_mining,
/datum/ai_planning_subtree/locate_dead_humans,
)
@@ -133,11 +133,11 @@
controller.clear_blackboard_key(target_key)
///store ores in our body
-/datum/ai_planning_subtree/find_and_hunt_target/consume_ores/minebot
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot
hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/consume_ores/minebot
hunt_chance = 100
-/datum/ai_planning_subtree/find_and_hunt_target/consume_ores/minebot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/automated_mining = controller.blackboard[BB_AUTOMATED_MINING]
var/mob/living/living_pawn = controller.pawn
diff --git a/code/modules/mob/living/basic/pets/dog/corgi.dm b/code/modules/mob/living/basic/pets/dog/corgi.dm
index cb0df08d198b9e..9e120c4e8c038a 100644
--- a/code/modules/mob/living/basic/pets/dog/corgi.dm
+++ b/code/modules/mob/living/basic/pets/dog/corgi.dm
@@ -482,7 +482,7 @@
prey.investigate_log("has been sacrificed by [src].", INVESTIGATE_DEATHS)
if (isliving(prey))
var/mob/living/living_sacrifice = prey
- living_sacrifice.gib()
+ living_sacrifice.gib(DROP_ALL_REMAINS)
else
qdel(prey)
diff --git a/code/modules/mob/living/basic/pets/fox.dm b/code/modules/mob/living/basic/pets/fox.dm
index 578a64ba08dd89..f7a86be1e5cfb6 100644
--- a/code/modules/mob/living/basic/pets/fox.dm
+++ b/code/modules/mob/living/basic/pets/fox.dm
@@ -38,7 +38,6 @@
/datum/ai_controller/basic_controller/fox
blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/ours_or_smaller/ignore_faction,
BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction
)
diff --git a/code/modules/mob/living/basic/pets/sloth.dm b/code/modules/mob/living/basic/pets/sloth.dm
new file mode 100644
index 00000000000000..5058e7d53344d3
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/sloth.dm
@@ -0,0 +1,97 @@
+GLOBAL_DATUM(cargo_sloth, /mob/living/basic/sloth)
+
+/mob/living/basic/sloth
+ name = "sloth"
+ desc = "An adorable, sleepy creature."
+ icon = 'icons/mob/simple/pets.dmi'
+ icon_state = "sloth"
+ icon_living = "sloth"
+ icon_dead = "sloth_dead"
+
+ speak_emote = list("yawns")
+
+ can_be_held = TRUE
+ held_state = "sloth"
+
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes aside"
+ response_disarm_simple = "gently push aside"
+ response_harm_continuous = "kicks"
+ response_harm_simple = "kick"
+
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ gold_core_spawnable = FRIENDLY_SPAWN
+
+ melee_damage_lower = 18
+ melee_damage_upper = 18
+ health = 50
+ maxHealth = 50
+ speed = 10 // speed is fucking weird man. they aren't fast though don't worry
+ butcher_results = list(/obj/item/food/meat/slab = 3)
+
+ ai_controller = /datum/ai_controller/basic_controller/sloth
+
+/mob/living/basic/sloth/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/pet_bonus, "slowly smiles!")
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_CLAW)
+ AddElement(/datum/element/ai_retaliate)
+ AddComponent(/datum/component/tree_climber)
+
+ if(!mapload || isnull(GLOB.cargo_sloth) || !is_station_level(z))
+ return
+
+ // If someone adds non-cargo sloths to maps we'll have a problem but we're fine for now
+ GLOB.cargo_sloth = src
+
+/mob/living/basic/sloth/Destroy()
+ if(GLOB.cargo_sloth == src)
+ GLOB.cargo_sloth = null
+
+ return ..()
+
+/mob/living/basic/sloth/paperwork
+ name = "Paperwork"
+ desc = "Cargo's pet sloth. About as useful as the rest of the techs."
+ gender = MALE
+ gold_core_spawnable = NO_SPAWN
+
+/mob/living/basic/sloth/citrus
+ name = "Citrus"
+ desc = "Cargo's pet sloth. She's dressed in a horrible sweater."
+ icon_state = "cool_sloth"
+ icon_living = "cool_sloth"
+ icon_dead = "cool_sloth_dead"
+ gender = FEMALE
+ butcher_results = list(/obj/item/toy/spinningtoy = 1)
+ gold_core_spawnable = NO_SPAWN
+
+/// They're really passive in game, so they just wanna get away if you start smacking them. No trees in space from them to use for clawing your eyes out, but they will try if desperate.
+/datum/ai_controller/basic_controller/sloth
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
+ BB_BASIC_MOB_FLEEING = TRUE,
+ BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
+ )
+
+ ai_traits = STOP_MOVING_WHEN_PULLED
+ 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/climb_trees,
+ /datum/ai_planning_subtree/random_speech/sloth,
+ )
+
+/datum/ai_planning_subtree/random_speech/sloth
+ speech_chance = 1
+ emote_hear = list("snores.", "yawns.")
+ emote_see = list("dozes off.", "looks around sleepily.")
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 414f28a1e9af55..924cf854276d18 100644
--- a/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
+++ b/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
@@ -42,7 +42,7 @@
/mob/living/basic/bear/Initialize(mapload)
. = ..()
- ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
+ add_traits(list(TRAIT_SPACEWALK, TRAIT_FENCE_CLIMBER), INNATE_TRAIT)
AddElement(/datum/element/ai_retaliate)
AddComponent(/datum/component/tree_climber, climbing_distance = 15)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_BEAR, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm b/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm
index d220325ca15f51..fc6997896b0d27 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm
@@ -31,7 +31,7 @@
finish_planning = TRUE
/datum/ai_planning_subtree/make_carp_rift/panic_teleport/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
return ..()
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 b30970145352bc..503264c3dc2a68 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,6 +9,7 @@
*/
/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()
)
@@ -35,6 +36,7 @@
*/
/datum/ai_controller/basic_controller/carp/pet
blackboard = list(
+ BB_BASIC_MOB_STOP_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends()
)
@@ -78,6 +80,7 @@
*/
/datum/ai_controller/basic_controller/carp/passive
blackboard = list(
+ BB_BASIC_MOB_STOP_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends()
)
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
new file mode 100644
index 00000000000000..c73b008d6b48b6
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
@@ -0,0 +1,75 @@
+/**
+ * Spider-esque mob summoned by changelings. Exclusively player-controlled.
+ * An independent hit-and-run antagonist which can make webs and heals itself if left undamaged for a few seconds.
+ * Not a spider subtype because it keeps getting hit by unrelated balance changes intended for the Giant Spiders gamemode.
+ */
+/mob/living/basic/flesh_spider
+ name = "flesh spider"
+ desc = "A odd fleshy creature in the shape of a spider. Its eyes are pitch black and soulless."
+ icon = 'icons/mob/simple/arachnoid.dmi'
+ icon_state = "flesh"
+ icon_living = "flesh"
+ icon_dead = "flesh_dead"
+ mob_biotypes = MOB_ORGANIC|MOB_BUG
+ speak_emote = list("chitters")
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes aside"
+ response_disarm_simple = "gently push aside"
+ damage_coeff = list(BRUTE = 1, BURN = 1.25, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
+ basic_mob_flags = FLAMMABLE_MOB
+ status_flags = NONE
+ speed = -0.1
+ maxHealth = 90
+ health = 90
+ melee_damage_lower = 15
+ melee_damage_upper = 20
+ obj_damage = 30
+ melee_attack_cooldown = CLICK_CD_MELEE
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ unsuitable_cold_damage = 4
+ unsuitable_heat_damage = 4
+ combat_mode = TRUE
+ faction = list() // No allies but yourself
+ pass_flags = PASSTABLE
+ unique_name = TRUE
+ lighting_cutoff_red = 22
+ lighting_cutoff_green = 5
+ lighting_cutoff_blue = 5
+ butcher_results = list(/obj/item/food/meat/slab/spider = 2, /obj/item/food/spiderleg = 8)
+ ai_controller = /datum/ai_controller/basic_controller/giant_spider
+
+/mob/living/basic/flesh_spider/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_WEB_SURFER, INNATE_TRAIT)
+ AddElement(/datum/element/cliff_walking)
+ AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
+ AddElement(/datum/element/venomous, /datum/reagent/toxin/hunterspider, 5)
+ AddElement(/datum/element/web_walker, /datum/movespeed_modifier/fast_web)
+ 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!")
+ AddComponent(\
+ /datum/component/blood_walk,\
+ blood_type = /obj/effect/decal/cleanable/blood/bubblegum,\
+ blood_spawn_chance = 5,\
+ )
+ AddComponent(\
+ /datum/component/regenerator,\
+ regeneration_delay = 4 SECONDS,\
+ health_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/datum/action/cooldown/mob_cooldown/lay_web/sticky_web/web_sticky = new(src)
+ web_sticky.Grant(src)
diff --git a/code/modules/mob/living/basic/space_fauna/headslug.dm b/code/modules/mob/living/basic/space_fauna/changeling/headslug.dm
similarity index 100%
rename from code/modules/mob/living/basic/space_fauna/headslug.dm
rename to code/modules/mob/living/basic/space_fauna/changeling/headslug.dm
diff --git a/code/modules/mob/living/basic/space_fauna/demon/demon.dm b/code/modules/mob/living/basic/space_fauna/demon/demon.dm
index c2d8c751cde901..741ac27712f8f4 100644
--- a/code/modules/mob/living/basic/space_fauna/demon/demon.dm
+++ b/code/modules/mob/living/basic/space_fauna/demon/demon.dm
@@ -32,6 +32,7 @@
obj_damage = 40
melee_damage_lower = 10
melee_damage_upper = 15
+ melee_attack_cooldown = CLICK_CD_MELEE
death_message = "screams in agony as it sublimates into a sulfurous smoke."
death_sound = 'sound/magic/demon_dies.ogg'
diff --git a/code/modules/mob/living/basic/space_fauna/morph.dm b/code/modules/mob/living/basic/space_fauna/morph.dm
index 3f31f7f3735ac8..32115d05602551 100644
--- a/code/modules/mob/living/basic/space_fauna/morph.dm
+++ b/code/modules/mob/living/basic/space_fauna/morph.dm
@@ -21,6 +21,7 @@
obj_damage = 50
melee_damage_lower = 20
melee_damage_upper = 20
+ melee_attack_cooldown = CLICK_CD_MELEE
// Oh you KNOW it's gonna be real green
lighting_cutoff_red = 10
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 85369b72eb8cbd..164c25fb896d2f 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
@@ -24,6 +24,7 @@
obj_damage = 10
melee_damage_lower = 13
melee_damage_upper = 15
+ melee_attack_cooldown = CLICK_CD_MELEE
attack_verb_continuous = "slashes"
attack_verb_simple = "slash"
attack_sound = 'sound/weapons/bladeslice.ogg'
diff --git a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_actions.dm b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_actions.dm
index 7a30f88b4c27b4..e617ae0a670af5 100644
--- a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_actions.dm
+++ b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_actions.dm
@@ -221,31 +221,31 @@
taste_description = "something funny"
overdose_threshold = 20
-/datum/reagent/rat_spit/on_mob_metabolize(mob/living/L)
- ..()
- if(HAS_TRAIT(L, TRAIT_AGEUSIA))
+/datum/reagent/rat_spit/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ if(HAS_TRAIT(affected_mob, TRAIT_AGEUSIA))
return
- to_chat(L, span_notice("This food has a funny taste!"))
+ to_chat(affected_mob, span_notice("This food has a funny taste!"))
-/datum/reagent/rat_spit/overdose_start(mob/living/M)
- ..()
- var/mob/living/carbon/victim = M
+/datum/reagent/rat_spit/overdose_start(mob/living/affected_mob)
+ . = ..()
+ var/mob/living/carbon/victim = affected_mob
if (istype(victim) && !(FACTION_RAT in victim.faction))
to_chat(victim, span_userdanger("With this last sip, you feel your body convulsing horribly from the contents you've ingested. As you contemplate your actions, you sense an awakened kinship with rat-kind and their newly risen leader!"))
victim.faction |= FACTION_RAT
victim.vomit(VOMIT_CATEGORY_DEFAULT)
metabolization_rate = 10 * REAGENTS_METABOLISM
-/datum/reagent/rat_spit/on_mob_life(mob/living/carbon/C)
+/datum/reagent/rat_spit/on_mob_life(mob/living/carbon/affected_mob)
+ . = ..()
if(prob(15))
- to_chat(C, span_notice("You feel queasy!"))
- C.adjust_disgust(3)
+ to_chat(affected_mob, span_notice("You feel queasy!"))
+ affected_mob.adjust_disgust(3)
else if(prob(10))
- to_chat(C, span_warning("That food does not sit up well!"))
- C.adjust_disgust(5)
+ to_chat(affected_mob, span_warning("That food does not sit up well!"))
+ affected_mob.adjust_disgust(5)
else if(prob(5))
- C.vomit(VOMIT_CATEGORY_DEFAULT)
- return ..()
+ affected_mob.vomit(VOMIT_CATEGORY_DEFAULT)
/datum/pet_command/protect_owner/glockroach
protect_behavior = /datum/ai_behavior/basic_ranged_attack/glockroach
diff --git a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_ai.dm b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_ai.dm
index ef92c7b3b76b1e..97dbdbd0a7d047 100644
--- a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_ai.dm
+++ b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat_ai.dm
@@ -1,7 +1,6 @@
/datum/ai_controller/basic_controller/regal_rat
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- BB_BASIC_MOB_FLEEING = TRUE,
BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
)
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
new file mode 100644
index 00000000000000..b3c6935c92efe4
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
@@ -0,0 +1,444 @@
+/// Source for a trait we get when we're stunned
+#define REVENANT_STUNNED_TRAIT "revenant_got_stunned"
+
+/// Revenants: "Ghosts" that are invisible and move like ghosts, cannot take damage while invisible
+/// Can hear deadchat, but are NOT normal ghosts and do NOT have x-ray vision
+/// Admin-spawn or random event
+/mob/living/basic/revenant
+ name = "revenant"
+ desc = "A malevolent spirit."
+ icon = 'icons/mob/simple/mob.dmi'
+ icon_state = "revenant_idle"
+ mob_biotypes = MOB_SPIRIT
+ incorporeal_move = INCORPOREAL_MOVE_JAUNT
+ invisibility = INVISIBILITY_REVENANT
+ health = INFINITY //Revenants don't use health, they use essence instead
+ maxHealth = INFINITY
+ plane = GHOST_PLANE
+ sight = SEE_SELF
+ throwforce = 0
+
+ // Going for faint purple spoopy ghost
+ lighting_cutoff_red = 20
+ lighting_cutoff_green = 15
+ lighting_cutoff_blue = 35
+
+ friendly_verb_continuous = "touches"
+ friendly_verb_simple = "touch"
+ response_help_continuous = "passes through"
+ response_help_simple = "pass through"
+ response_disarm_continuous = "swings through"
+ response_disarm_simple = "swing through"
+ response_harm_continuous = "punches through"
+ response_harm_simple = "punch through"
+ unsuitable_atmos_damage = 0
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) //I don't know how you'd apply those, but revenants no-sell them anyway.
+ 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)
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+
+ status_flags = NONE
+ density = FALSE
+ move_resist = MOVE_FORCE_OVERPOWERING
+ mob_size = MOB_SIZE_TINY
+ pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
+ speed = 1
+ unique_name = TRUE
+ hud_possible = list(ANTAG_HUD)
+ hud_type = /datum/hud/revenant
+
+ /// The icon we use while just floating around.
+ var/icon_idle = "revenant_idle"
+ /// The icon we use while in a revealed state.
+ var/icon_reveal = "revenant_revealed"
+ /// The icon we use when stunned (temporarily frozen)
+ var/icon_stun = "revenant_stun"
+ /// The icon we use while draining someone.
+ var/icon_drain = "revenant_draining"
+
+ /// Are we currently dormant (ectoplasm'd)?
+ var/dormant = FALSE
+ /// Are we currently draining someone?
+ var/draining = FALSE
+ /// Have we already given this revenant abilities?
+ var/generated_objectives_and_spells = FALSE
+
+ /// Lazylist of drained mobs to ensure that we don't steal a soul from someone twice
+ var/list/drained_mobs = null
+ /// List of action ability datums to grant on Initialize. Keep in mind that anything with the `/aoe/revenant` subtype starts locked by default.
+ var/static/list/datum/action/abilities = list(
+ /datum/action/cooldown/spell/aoe/revenant/blight,
+ /datum/action/cooldown/spell/aoe/revenant/defile,
+ /datum/action/cooldown/spell/aoe/revenant/haunt_object,
+ /datum/action/cooldown/spell/aoe/revenant/malfunction,
+ /datum/action/cooldown/spell/aoe/revenant/overload,
+ /datum/action/cooldown/spell/list_target/telepathy/revenant,
+ )
+
+ /// The resource, and health, of revenants.
+ var/essence = 75
+ /// The regeneration cap of essence (go figure); regenerates every Life() tick up to this amount.
+ var/max_essence = 75
+ /// If the revenant regenerates essence or not
+ var/essence_regenerating = TRUE
+ /// How much essence regenerates per second
+ var/essence_regen_amount = 2.5
+ /// How much essence the revenant has stolen
+ var/essence_accumulated = 0
+ /// How much stolen essence is available for unlocks
+ var/essence_excess = 0
+ /// How long the revenant is revealed for, is about 2 seconds times this var.
+ var/unreveal_time = 0
+ /// How many perfect, regen-cap increasing souls the revenant has. //TODO, add objective for getting a perfect soul(s?)
+ var/perfectsouls = 0
+
+/mob/living/basic/revenant/Initialize(mapload)
+ . = ..()
+ 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)
+
+ RegisterSignal(src, COMSIG_LIVING_BANED, PROC_REF(on_baned))
+ RegisterSignal(src, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_move))
+ RegisterSignal(src, COMSIG_LIVING_LIFE, PROC_REF(on_life))
+ set_random_revenant_name()
+
+ GLOB.revenant_relay_mobs |= src
+
+/mob/living/basic/revenant/Destroy()
+ GLOB.revenant_relay_mobs -= src
+ return ..()
+
+/mob/living/basic/revenant/Login()
+ . = ..()
+ if(!. || isnull(client))
+ return FALSE
+
+ var/static/cached_string = null
+ if(isnull(cached_string))
+ cached_string = examine_block(jointext(create_login_string(), "\n"))
+
+ to_chat(src, cached_string, type = MESSAGE_TYPE_INFO)
+
+ if(generated_objectives_and_spells)
+ return TRUE
+
+ generated_objectives_and_spells = TRUE
+ mind.set_assigned_role(SSjob.GetJobType(/datum/job/revenant))
+ mind.special_role = ROLE_REVENANT
+ SEND_SOUND(src, sound('sound/effects/ghost.ogg'))
+ mind.add_antag_datum(/datum/antagonist/revenant)
+ return TRUE
+
+/// Signal Handler Injection to handle Life() stuff for revenants
+/mob/living/basic/revenant/proc/on_life(seconds_per_tick = SSMOBS_DT, times_fired)
+ SIGNAL_HANDLER
+
+ if(dormant)
+ return COMPONENT_LIVING_CANCEL_LIFE_PROCESSING
+
+ if(HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) && essence <= 0)
+ death()
+ return COMPONENT_LIVING_CANCEL_LIFE_PROCESSING
+
+ if(essence_regenerating && !HAS_TRAIT(src, TRAIT_REVENANT_INHIBITED) && essence < max_essence) //While inhibited, essence will not regenerate
+ var/change_in_time = DELTA_WORLD_TIME(SSmobs)
+ essence = min(essence + (essence_regen_amount * change_in_time), max_essence)
+ update_mob_action_buttons() //because we update something required by our spells in life, we need to update our buttons
+
+ update_appearance(UPDATE_ICON)
+ update_health_hud()
+
+/mob/living/basic/revenant/get_status_tab_items()
+ . = ..()
+ . += "Current Essence: [essence >= max_essence ? essence : "[essence] / [max_essence]"] E"
+ . += "Total Essence Stolen: [essence_accumulated] SE"
+ . += "Unused Stolen Essence: [essence_excess] SE"
+ . += "Perfect Souls Stolen: [perfectsouls]"
+
+/mob/living/basic/revenant/update_health_hud()
+ if(isnull(hud_used))
+ return
+
+ var/essencecolor = "#8F48C6"
+ if(essence > max_essence)
+ essencecolor = "#9A5ACB" //oh boy you've got a lot of essence
+ else if(essence <= 0)
+ essencecolor = "#1D2953" //oh jeez you're dying
+ hud_used.healths.maptext = MAPTEXT("
[essence]E
")
+
+/mob/living/basic/revenant/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null, message_range = 7, datum/saymode/saymode = null)
+ if(!message)
+ return
+
+ if(client)
+ if(client.prefs.muted & MUTE_IC)
+ to_chat(src, span_boldwarning("You cannot send IC messages (muted)."))
+ return
+ if (!(ignore_spam || forced) && client.handle_spam_prevention(message, MUTE_IC))
+ return
+
+ if(sanitize)
+ message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
+
+ log_talk(message, LOG_SAY)
+ var/rendered = span_deadsay("UNDEAD: [src] says, \"[message]\"")
+ relay_to_list_and_observers(rendered, GLOB.revenant_relay_mobs, src)
+
+/mob/living/basic/revenant/ClickOn(atom/A, params) //revenants can't interact with the world directly, so we gotta do some wacky override stuff
+ var/list/modifiers = params2list(params)
+ if(LAZYACCESS(modifiers, SHIFT_CLICK))
+ ShiftClickOn(A)
+ return
+ if(LAZYACCESS(modifiers, ALT_CLICK))
+ AltClickNoInteract(src, A)
+ return
+ if(LAZYACCESS(modifiers, RIGHT_CLICK))
+ ranged_secondary_attack(A, modifiers)
+ return
+
+ if(ishuman(A) && in_range(src, A))
+ attempt_harvest(A)
+
+/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))
+ return
+
+ var/list/icon_dimensions = get_icon_dimensions(target.icon)
+ var/orbitsize = (icon_dimensions["width"] + icon_dimensions["height"]) * 0.5
+ orbitsize -= (orbitsize / world.icon_size) * (world.icon_size * 0.25)
+ orbit(target, orbitsize)
+
+/mob/living/basic/revenant/adjust_health(amount, updating_health = TRUE, forced = FALSE)
+ if(!forced && !HAS_TRAIT(src, TRAIT_REVENANT_REVEALED))
+ return 0
+
+ . = amount
+
+ essence = max(0, essence - amount)
+ if(updating_health)
+ update_health_hud()
+ if(essence == 0)
+ death()
+
+ return .
+
+/mob/living/basic/revenant/orbit(atom/target)
+ setDir(SOUTH) // reset dir so the right directional sprites show up
+ return ..()
+
+/mob/living/basic/revenant/update_icon_state()
+ . = ..()
+
+ if(HAS_TRAIT(src, TRAIT_REVENANT_REVEALED))
+ icon_state = icon_reveal
+ return
+
+ if(HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
+ if(draining)
+ icon_state = icon_drain
+ else
+ icon_state = icon_stun
+
+ return
+
+ icon_state = icon_idle
+
+/mob/living/basic/revenant/med_hud_set_health()
+ return //we use no hud
+
+/mob/living/basic/revenant/med_hud_set_status()
+ return //we use no hud
+
+/mob/living/basic/revenant/dust(just_ash, drop_items, force)
+ death()
+
+/mob/living/basic/revenant/gib()
+ death()
+
+/mob/living/basic/revenant/can_perform_action(atom/movable/target, action_bitflags)
+ return FALSE
+
+/mob/living/basic/revenant/ex_act(severity, target)
+ return FALSE //Immune to the effects of explosions.
+
+/mob/living/basic/revenant/blob_act(obj/structure/blob/attacking_blob)
+ return //blah blah blobs aren't in tune with the spirit world, or something.
+
+/mob/living/basic/revenant/singularity_act()
+ return //don't walk into the singularity expecting to find corpses, okay?
+
+/mob/living/basic/revenant/narsie_act()
+ return //most humans will now be either bones or harvesters, but we're still un-alive.
+
+/mob/living/basic/revenant/bullet_act()
+ if(!HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) || dormant)
+ return BULLET_ACT_FORCE_PIERCE
+ return ..()
+
+/mob/living/basic/revenant/death()
+ if(!HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) || dormant) //Revenants cannot die if they aren't revealed //or are already dead
+ return
+ ADD_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
+ dormant = TRUE
+
+ visible_message(
+ span_warning("[src] lets out a waning screech as violet mist swirls around its dissolving body!"),
+ span_revendanger("NO! No... it's too late, you can feel your essence [pick("breaking apart", "drifting away")]..."),
+ )
+
+ invisibility = 0
+ icon_state = "revenant_draining"
+ playsound(src, 'sound/effects/screech.ogg', 100, TRUE)
+
+ animate(src, alpha = 0, time = 3 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(move_to_ectoplasm)), 3 SECONDS)
+
+/// Forces the mob, once dormant, to move inside ectoplasm until it can regenerate.
+/mob/living/basic/revenant/proc/move_to_ectoplasm()
+ if(QDELETED(src) || !dormant) // something fucky happened, abort. we MUST be dormant to go inside the ectoplasm.
+ return
+
+ visible_message(span_danger("[src]'s body breaks apart into a fine pile of blue dust."))
+
+ var/obj/item/ectoplasm/revenant/goop = new(get_turf(src)) // the ectoplasm will handle moving us out of dormancy
+ goop.old_ckey = client.ckey
+ goop.revenant = src
+ forceMove(goop)
+
+/mob/living/basic/revenant/proc/on_move(datum/source, atom/entering_loc)
+ SIGNAL_HANDLER
+ if(HAS_TRAIT(src, TRAIT_NO_TRANSFORM)) // just in case it occurs, need to provide some feedback
+ balloon_alert(src, "can't move!")
+ return
+
+ if(isnull(orbiting) || incorporeal_move_check(entering_loc))
+ return
+
+ // we're about to go somewhere we aren't meant to, end the orbit and block the move. feedback will be given in `incorporeal_move_check()`
+ orbiting.end_orbit(src)
+ return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
+
+/// Generates the information the player needs to know how to play their role, and returns it as a list.
+/mob/living/basic/revenant/proc/create_login_string()
+ RETURN_TYPE(/list)
+ var/list/returnable_list = list()
+ returnable_list += span_deadsay(span_boldbig("You are a revenant."))
+ returnable_list += span_bold("Your formerly mundane spirit has been infused with alien energies and empowered into a revenant.")
+ returnable_list += span_bold("You are not dead, not alive, but somewhere in between. You are capable of limited interaction with both worlds.")
+ returnable_list += span_bold("You are invincible and invisible to everyone but other ghosts. Most abilities will reveal you, rendering you vulnerable.")
+ returnable_list += span_bold("To function, you are to drain the life essence from humans. This essence is a resource, as well as your health, and will power all of your abilities.")
+ returnable_list += span_bold("You do not remember anything of your past lives, nor will you remember anything about this one after your death.")
+ returnable_list += span_bold("Be sure to read the wiki page to learn more.")
+ return returnable_list
+
+/mob/living/basic/revenant/proc/set_random_revenant_name()
+ var/list/built_name_strings = list()
+ built_name_strings += pick(strings(REVENANT_NAME_FILE, "spirit_type"))
+ built_name_strings += " of "
+ built_name_strings += pick(strings(REVENANT_NAME_FILE, "adverb"))
+ built_name_strings += pick(strings(REVENANT_NAME_FILE, "theme"))
+ name = built_name_strings.Join("")
+
+/mob/living/basic/revenant/proc/on_baned(obj/item/weapon, mob/living/user)
+ SIGNAL_HANDLER
+ visible_message(
+ span_warning("[src] violently flinches!"),
+ span_revendanger("As [weapon] passes through you, you feel your essence draining away!"),
+ )
+ apply_status_effect(/datum/status_effect/revenant/inhibited, 3 SECONDS)
+
+/// Incorporeal move check: blocked by holy-watered tiles and salt piles.
+/mob/living/basic/revenant/proc/incorporeal_move_check(atom/destination)
+ var/turf/open/floor/step_turf = get_turf(destination)
+ if(isnull(step_turf))
+ return TRUE // what? whatever let it happen
+
+ if(step_turf.turf_flags & NOJAUNT)
+ to_chat(src, span_warning("Some strange aura is blocking the way."))
+ return FALSE
+
+ if(locate(/obj/effect/decal/cleanable/food/salt) in step_turf)
+ balloon_alert(src, "blocked by salt!")
+ apply_status_effect(/datum/status_effect/revenant/revealed, 2 SECONDS)
+ apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, 2 SECONDS)
+ return FALSE
+
+ if(locate(/obj/effect/blessing) in step_turf)
+ to_chat(src, span_warning("Holy energies block your path!"))
+ return FALSE
+
+ return TRUE
+
+/mob/living/basic/revenant/proc/cast_check(essence_cost)
+ if(QDELETED(src))
+ return
+
+ var/turf/current = get_turf(src)
+
+ if(isclosedturf(current))
+ to_chat(src, span_revenwarning("You cannot use abilities from inside of a wall."))
+ return FALSE
+
+ for(var/obj/thing in current)
+ if(!thing.density || thing.CanPass(src, get_dir(current, src)))
+ continue
+ to_chat(src, span_revenwarning("You cannot use abilities inside of a dense object."))
+ return FALSE
+
+ if(HAS_TRAIT(src, TRAIT_REVENANT_INHIBITED))
+ to_chat(src, span_revenwarning("Your powers have been suppressed by a nullifying energy!"))
+ return FALSE
+
+ if(!change_essence_amount(essence_cost, TRUE))
+ to_chat(src, span_revenwarning("You lack the essence to use that ability."))
+ return FALSE
+
+ return TRUE
+
+/mob/living/basic/revenant/proc/unlock(essence_cost)
+ if(essence_excess < essence_cost)
+ return FALSE
+ essence_excess -= essence_cost
+ update_mob_action_buttons()
+ return TRUE
+
+/mob/living/basic/revenant/proc/death_reset()
+ REMOVE_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
+ forceMove(get_turf(src))
+ // clean slate, so no more debilitating effects
+ remove_status_effect(/datum/status_effect/revenant/revealed)
+ remove_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant)
+ remove_status_effect(/datum/status_effect/revenant/inhibited)
+ draining = FALSE
+ dormant = FALSE
+ incorporeal_move = INCORPOREAL_MOVE_JAUNT
+ invisibility = INVISIBILITY_REVENANT
+ alpha = 255
+
+/mob/living/basic/revenant/proc/change_essence_amount(essence_to_change_by, silent = FALSE, source = null)
+ if(QDELETED(src))
+ return FALSE
+
+ if((essence + essence_to_change_by) < 0)
+ return FALSE
+
+ essence = max(0, essence + essence_to_change_by)
+ update_health_hud()
+
+ if(essence_to_change_by > 0)
+ essence_accumulated = max(0, essence_accumulated + essence_to_change_by)
+ essence_excess = max(0, essence_excess + essence_to_change_by)
+
+ update_mob_action_buttons()
+ if(!silent)
+ if(essence_to_change_by > 0)
+ to_chat(src, span_revennotice("Gained [essence_to_change_by]E [source ? "from [source]":""]."))
+ else
+ to_chat(src, span_revenminor("Lost [essence_to_change_by]E [source ? "from [source]":""]."))
+ return TRUE
+
+#undef REVENANT_STUNNED_TRAIT
diff --git a/code/modules/antagonists/revenant/revenant_abilities.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
similarity index 63%
rename from code/modules/antagonists/revenant/revenant_abilities.dm
rename to code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
index ef00f51e45d947..63f4bbb9dbc3b9 100644
--- a/code/modules/antagonists/revenant/revenant_abilities.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
@@ -1,122 +1,6 @@
#define REVENANT_DEFILE_MIN_DAMAGE 30
#define REVENANT_DEFILE_MAX_DAMAGE 50
-/mob/living/simple_animal/revenant/ClickOn(atom/A, params) //revenants can't interact with the world directly.
- var/list/modifiers = params2list(params)
- if(LAZYACCESS(modifiers, SHIFT_CLICK))
- ShiftClickOn(A)
- return
- if(LAZYACCESS(modifiers, ALT_CLICK))
- AltClickNoInteract(src, A)
- return
- if(LAZYACCESS(modifiers, RIGHT_CLICK))
- ranged_secondary_attack(A, modifiers)
- return
-
- if(ishuman(A))
- //Humans are tagged, so this is fine
- if(REF(A) in drained_mobs)
- to_chat(src, span_revenwarning("[A]'s soul is dead and empty.") )
- else if(in_range(src, A))
- Harvest(A)
-
-/mob/living/simple_animal/revenant/ranged_secondary_attack(atom/target, modifiers)
- if(revealed || inhibited || HAS_TRAIT(src, TRAIT_NO_TRANSFORM) || !Adjacent(target) || !incorporeal_move_check(target))
- return
-
- var/list/icon_dimensions = get_icon_dimensions(target.icon)
- var/orbitsize = (icon_dimensions["width"] + icon_dimensions["height"]) * 0.5
- orbitsize -= (orbitsize / world.icon_size) * (world.icon_size * 0.25)
- orbit(target, orbitsize)
-
-//Harvest; activated by clicking the target, will try to drain their essence.
-/mob/living/simple_animal/revenant/proc/Harvest(mob/living/carbon/human/target)
- if(!castcheck(0))
- return
- if(draining)
- to_chat(src, span_revenwarning("You are already siphoning the essence of a soul!"))
- return
- if(!target.stat)
- to_chat(src, span_revennotice("[target.p_Their()] soul is too strong to harvest."))
- if(prob(10))
- to_chat(target, span_revennotice("You feel as if you are being watched."))
- return
- log_combat(src, target, "started to harvest")
- face_atom(target)
- draining = TRUE
- essence_drained += rand(15, 20)
- to_chat(src, span_revennotice("You search for the soul of [target]."))
- if(do_after(src, rand(10, 20), target, timed_action_flags = IGNORE_HELD_ITEM)) //did they get deleted in that second?
- if(target.ckey)
- to_chat(src, span_revennotice("[target.p_Their()] soul burns with intelligence."))
- essence_drained += rand(20, 30)
- if(target.stat != DEAD && !HAS_TRAIT(target, TRAIT_WEAK_SOUL))
- to_chat(src, span_revennotice("[target.p_Their()] soul blazes with life!"))
- essence_drained += rand(40, 50)
- if(HAS_TRAIT(target, TRAIT_WEAK_SOUL) && !target.ckey)
- to_chat(src, span_revennotice("[target.p_Their()] soul is weak and underdeveloped. They won't be worth very much."))
- essence_drained = 5
- else
- to_chat(src, span_revennotice("[target.p_Their()] soul is weak and faltering."))
- if(do_after(src, rand(15, 20), target, timed_action_flags = IGNORE_HELD_ITEM)) //did they get deleted NOW?
- switch(essence_drained)
- if(1 to 30)
- to_chat(src, span_revennotice("[target] will not yield much essence. Still, every bit counts."))
- if(30 to 70)
- to_chat(src, span_revennotice("[target] will yield an average amount of essence."))
- if(70 to 90)
- to_chat(src, span_revenboldnotice("Such a feast! [target] will yield much essence to you."))
- if(90 to INFINITY)
- to_chat(src, span_revenbignotice("Ah, the perfect soul. [target] will yield massive amounts of essence to you."))
- if(do_after(src, rand(15, 25), target, timed_action_flags = IGNORE_HELD_ITEM)) //how about now
- if(!target.stat)
- to_chat(src, span_revenwarning("[target.p_Theyre()] now powerful enough to fight off your draining."))
- to_chat(target, span_boldannounce("You feel something tugging across your body before subsiding."))
- draining = 0
- essence_drained = 0
- return //hey, wait a minute...
- to_chat(src, span_revenminor("You begin siphoning essence from [target]'s soul."))
- if(target.stat != DEAD)
- to_chat(target, span_warning("You feel a horribly unpleasant draining sensation as your grip on life weakens..."))
- if(target.stat == SOFT_CRIT)
- target.Stun(46)
- reveal(46)
- stun(46)
- target.visible_message(span_warning("[target] suddenly rises slightly into the air, [target.p_their()] skin turning an ashy gray."))
- if(target.can_block_magic(MAGIC_RESISTANCE_HOLY))
- to_chat(src, span_revenminor("Something's wrong! [target] seems to be resisting the siphoning, leaving you vulnerable!"))
- target.visible_message(span_warning("[target] slumps onto the ground."), \
- span_revenwarning("Violet lights, dancing in your vision, receding--"))
- draining = FALSE
- return
- var/datum/beam/B = Beam(target,icon_state="drain_life")
- if(do_after(src, 46, target, timed_action_flags = IGNORE_HELD_ITEM)) //As one cannot prove the existance of ghosts, ghosts cannot prove the existance of the target they were draining.
- change_essence_amount(essence_drained, FALSE, target)
- if(essence_drained <= 90 && target.stat != DEAD && !HAS_TRAIT(target, TRAIT_WEAK_SOUL))
- essence_regen_cap += 5
- to_chat(src, span_revenboldnotice("The absorption of [target]'s living soul has increased your maximum essence level. Your new maximum essence is [essence_regen_cap]."))
- if(essence_drained > 90)
- essence_regen_cap += 15
- perfectsouls++
- to_chat(src, span_revenboldnotice("The perfection of [target]'s soul has increased your maximum essence level. Your new maximum essence is [essence_regen_cap]."))
- to_chat(src, span_revennotice("[target]'s soul has been considerably weakened and will yield no more essence for the time being."))
- target.visible_message(span_warning("[target] slumps onto the ground."), \
- span_revenwarning("Violets lights, dancing in your vision, getting clo--"))
- drained_mobs += REF(target)
- if(target.stat != DEAD)
- target.investigate_log("has died from revenant harvest.", INVESTIGATE_DEATHS)
- target.death(FALSE)
- else
- to_chat(src, span_revenwarning("[target ? "[target] has":"[target.p_Theyve()]"] been drawn out of your grasp. The link has been broken."))
- if(target) //Wait, target is WHERE NOW?
- target.visible_message(span_warning("[target] slumps onto the ground."), \
- span_revenwarning("Violets lights, dancing in your vision, receding--"))
- qdel(B)
- else
- to_chat(src, span_revenwarning("You are not close enough to siphon [target ? "[target]'s":"[target.p_their()]"] soul. The link has been broken."))
- draining = FALSE
- essence_drained = 0
-
//Transmit: the revemant's only direct way to communicate. Sends a single message silently to a single mob
/datum/action/cooldown/spell/list_target/telepathy/revenant
name = "Revenant Transmit"
@@ -170,8 +54,8 @@
stack_trace("[type] was owned by a non-revenant mob, please don't.")
return FALSE
- var/mob/living/simple_animal/revenant/ghost = owner
- if(ghost.inhibited)
+ var/mob/living/basic/revenant/ghost = owner
+ if(ghost.dormant || HAS_TRAIT(ghost, TRAIT_REVENANT_INHIBITED))
return FALSE
if(locked && ghost.essence_excess <= unlock_amount)
return FALSE
@@ -183,7 +67,7 @@
/datum/action/cooldown/spell/aoe/revenant/get_things_to_cast_on(atom/center)
return RANGE_TURFS(aoe_radius, center)
-/datum/action/cooldown/spell/aoe/revenant/before_cast(mob/living/simple_animal/revenant/cast_on)
+/datum/action/cooldown/spell/aoe/revenant/before_cast(mob/living/basic/revenant/cast_on)
. = ..()
if(. & SPELL_CANCEL_CAST)
return
@@ -201,16 +85,16 @@
reset_spell_cooldown()
return . | SPELL_CANCEL_CAST
- if(!cast_on.castcheck(-cast_amount))
+ if(!cast_on.cast_check(-cast_amount))
reset_spell_cooldown()
return . | SPELL_CANCEL_CAST
-/datum/action/cooldown/spell/aoe/revenant/after_cast(mob/living/simple_animal/revenant/cast_on)
+/datum/action/cooldown/spell/aoe/revenant/after_cast(mob/living/basic/revenant/cast_on)
. = ..()
if(reveal_duration > 0 SECONDS)
- cast_on.reveal(reveal_duration)
+ cast_on.apply_status_effect(/datum/status_effect/revenant/revealed, reveal_duration)
if(stun_duration > 0 SECONDS)
- cast_on.stun(stun_duration)
+ cast_on.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, stun_duration)
//Overload Light: Breaks a light that's online and sends out lightning bolts to all nearby people.
/datum/action/cooldown/spell/aoe/revenant/overload
@@ -226,10 +110,10 @@
/// The range the shocks from the lights go
var/shock_range = 2
- /// The damage the shcoskf rom the lgihts do
+ /// The damage the shocks from the lights do
var/shock_damage = 15
-/datum/action/cooldown/spell/aoe/revenant/overload/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster)
+/datum/action/cooldown/spell/aoe/revenant/overload/cast_on_thing_in_aoe(turf/victim, mob/living/basic/revenant/caster)
for(var/obj/machinery/light/light in victim)
if(!light.on)
continue
@@ -241,7 +125,7 @@
new /obj/effect/temp_visual/revenant(get_turf(light))
addtimer(CALLBACK(src, PROC_REF(overload_shock), light, caster), 20)
-/datum/action/cooldown/spell/aoe/revenant/overload/proc/overload_shock(obj/machinery/light/to_shock, mob/living/simple_animal/revenant/caster)
+/datum/action/cooldown/spell/aoe/revenant/overload/proc/overload_shock(obj/machinery/light/to_shock, mob/living/basic/revenant/caster)
flick("[to_shock.base_state]2", to_shock)
for(var/mob/living/carbon/human/human_mob in view(shock_range, to_shock))
if(human_mob == caster)
@@ -266,7 +150,7 @@
reveal_duration = 4 SECONDS
stun_duration = 2 SECONDS
-/datum/action/cooldown/spell/aoe/revenant/defile/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster)
+/datum/action/cooldown/spell/aoe/revenant/defile/cast_on_thing_in_aoe(turf/victim, mob/living/basic/revenant/caster)
for(var/obj/effect/blessing/blessing in victim)
qdel(blessing)
new /obj/effect/temp_visual/revenant(victim)
@@ -315,7 +199,7 @@
unlock_amount = 125
// A note to future coders: do not replace this with an EMP because it will wreck malf AIs and everyone will hate you.
-/datum/action/cooldown/spell/aoe/revenant/malfunction/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster)
+/datum/action/cooldown/spell/aoe/revenant/malfunction/cast_on_thing_in_aoe(turf/victim, mob/living/basic/revenant/caster)
for(var/mob/living/simple_animal/bot/bot in victim)
if(!(bot.bot_cover_flags & BOT_COVER_EMAGGED))
new /obj/effect/temp_visual/revenant(bot.loc)
@@ -356,7 +240,7 @@
cast_amount = 50
unlock_amount = 75
-/datum/action/cooldown/spell/aoe/revenant/blight/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster)
+/datum/action/cooldown/spell/aoe/revenant/blight/cast_on_thing_in_aoe(turf/victim, mob/living/basic/revenant/caster)
for(var/mob/living/mob in victim)
if(mob == caster)
continue
@@ -430,7 +314,7 @@
return things
-/datum/action/cooldown/spell/aoe/revenant/haunt_object/cast_on_thing_in_aoe(obj/item/victim, mob/living/simple_animal/revenant/caster)
+/datum/action/cooldown/spell/aoe/revenant/haunt_object/cast_on_thing_in_aoe(obj/item/victim, mob/living/basic/revenant/caster)
var/distance_from_caster = get_dist(get_turf(victim), get_turf(caster))
var/chance_of_haunting = 150 * (1 / distance_from_caster)
if(!prob(chance_of_haunting))
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
new file mode 100644
index 00000000000000..0eeec231973ee1
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_effects.dm
@@ -0,0 +1,73 @@
+/// Parent type for all unique revenant status effects
+/datum/status_effect/revenant
+
+/datum/status_effect/revenant/on_creation(mob/living/new_owner, duration)
+ if(isnum(duration))
+ src.duration = duration
+ return ..()
+
+/datum/status_effect/revenant/revealed
+ id = "revenant_revealed"
+
+/datum/status_effect/revenant/revealed/on_apply()
+ . = ..()
+ if(!.)
+ return FALSE
+ owner.orbiting?.end_orbit(src)
+
+ ADD_TRAIT(owner, TRAIT_REVENANT_REVEALED, TRAIT_STATUS_EFFECT(id))
+ owner.invisibility = 0
+ owner.incorporeal_move = FALSE
+ owner.update_appearance(UPDATE_ICON)
+ owner.update_mob_action_buttons()
+
+/datum/status_effect/revenant/revealed/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_REVENANT_REVEALED, TRAIT_STATUS_EFFECT(id))
+
+ owner.incorporeal_move = INCORPOREAL_MOVE_JAUNT
+ owner.invisibility = INVISIBILITY_REVENANT
+ owner.update_appearance(UPDATE_ICON)
+ owner.update_mob_action_buttons()
+ return ..()
+
+/datum/status_effect/revenant/inhibited
+ id = "revenant_inhibited"
+
+/datum/status_effect/revenant/inhibited/on_apply()
+ . = ..()
+ if(!.)
+ return FALSE
+ owner.orbiting?.end_orbit(src)
+
+ ADD_TRAIT(owner, TRAIT_REVENANT_INHIBITED, TRAIT_STATUS_EFFECT(id))
+ owner.update_appearance(UPDATE_ICON)
+
+ owner.balloon_alert(owner, "inhibited!")
+
+/datum/status_effect/revenant/inhibited/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_REVENANT_INHIBITED, TRAIT_STATUS_EFFECT(id))
+ owner.update_appearance(UPDATE_ICON)
+
+ owner.balloon_alert(owner, "uninhibited")
+ return ..()
+
+/datum/status_effect/incapacitating/paralyzed/revenant
+ id = "revenant_paralyzed"
+
+/datum/status_effect/incapacitating/paralyzed/revenant/on_apply()
+ . = ..()
+ if(!.)
+ return FALSE
+ owner.orbiting?.end_orbit(src)
+
+ ADD_TRAIT(owner, TRAIT_NO_TRANSFORM, TRAIT_STATUS_EFFECT(id))
+ owner.balloon_alert(owner, "can't move!")
+ owner.update_mob_action_buttons()
+ owner.update_appearance(UPDATE_ICON)
+
+/datum/status_effect/incapacitating/paralyzed/revenant/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_NO_TRANSFORM, TRAIT_STATUS_EFFECT(id))
+ owner.update_mob_action_buttons()
+ owner.balloon_alert(owner, "can move again")
+
+ 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
new file mode 100644
index 00000000000000..b8bb05db484142
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_harvest.dm
@@ -0,0 +1,143 @@
+// This file contains the proc we use for revenant harvesting because it is a very long and bulky proc that takes up a lot of space elsewhere
+
+/// Container proc for `harvest()`, handles the pre-checks as well as potential early-exits for any reason.
+/// Will return FALSE if we can't execute `harvest()`, or will otherwise the result of `harvest()`: a boolean value.
+/mob/living/basic/revenant/proc/attempt_harvest(mob/living/carbon/human/target)
+ if(LAZYFIND(drained_mobs, REF(target)))
+ to_chat(src, span_revenwarning("[target]'s soul is dead and empty."))
+ return FALSE
+
+ if(!cast_check(0))
+ return FALSE
+
+ if(draining)
+ to_chat(src, span_revenwarning("You are already siphoning the essence of a soul!"))
+ return FALSE
+
+ draining = TRUE
+ var/value_to_return = harvest_soul(target)
+ if(!value_to_return)
+ log_combat(src, target, "stopped the harvest of")
+ draining = FALSE
+
+ return value_to_return
+
+/// Harvest; activated by clicking a target, will try to drain their essence. Handles all messages and handling of the target.
+/// Returns FALSE if we exit out of the harvest, TRUE if it is fully done.
+/mob/living/basic/revenant/proc/harvest_soul(mob/living/carbon/human/target) // this isn't in the main revenant code file because holyyyy shit it's long
+ if(QDELETED(target)) // what
+ return FALSE
+
+ // cache pronouns in case they get deleted as well as be a nice micro-opt due to the multiple times we use them
+ var/target_their = target.p_their()
+ var/target_Their = target.p_Their()
+ var/target_Theyre = target.p_Theyre()
+ var/target_They_have = "[target.p_They()] [target.p_have()]"
+
+ if(target.stat == CONSCIOUS)
+ to_chat(src, span_revennotice("[target_Their] soul is too strong to harvest."))
+ if(prob(10))
+ to_chat(target, span_revennotice("You feel as if you are being watched."))
+ return FALSE
+
+ log_combat(src, target, "started to harvest")
+ face_atom(target)
+ var/essence_drained = rand(15, 20)
+
+ to_chat(src, span_revennotice("You search for the soul of [target]."))
+
+ if(!do_after(src, (rand(10, 20) DECISECONDS), target, timed_action_flags = IGNORE_HELD_ITEM)) //did they get deleted in that second?
+ return FALSE
+
+ var/target_has_client = !isnull(target.client)
+ if(target_has_client || target.ckey) // any target that has been occupied with a ckey is considered "intelligent"
+ to_chat(src, span_revennotice("[target_Their] soul burns with intelligence."))
+ essence_drained += rand(20, 30)
+
+ if(target.stat != DEAD && !HAS_TRAIT(target, TRAIT_WEAK_SOUL))
+ to_chat(src, span_revennotice("[target_Their] soul blazes with life!"))
+ essence_drained += rand(40, 50)
+
+ if(!target_has_client && HAS_TRAIT(target, TRAIT_WEAK_SOUL))
+ to_chat(src, span_revennotice("[target_Their] soul is weak and underdeveloped. They won't be worth very much."))
+ essence_drained = 5
+
+ to_chat(src, span_revennotice("[target_Their] soul is weak and faltering. It's time to harvest."))
+
+ if(!do_after(src, (rand(15, 20) DECISECONDS), target, timed_action_flags = IGNORE_HELD_ITEM))
+ to_chat(src, span_revennotice("The harvest is abandoned."))
+ return FALSE
+
+ switch(essence_drained)
+ if(1 to 30)
+ to_chat(src, span_revennotice("[target] will not yield much essence. Still, every bit counts."))
+ if(30 to 70)
+ to_chat(src, span_revennotice("[target] will yield an average amount of essence."))
+ if(70 to 90)
+ to_chat(src, span_revenboldnotice("Such a feast! [target] will yield much essence to you."))
+ if(90 to INFINITY)
+ to_chat(src, span_revenbignotice("Ah, the perfect soul. [target] will yield massive amounts of essence to you."))
+
+ if(!do_after(src, (rand(15, 25) DECISECONDS), target, timed_action_flags = IGNORE_HELD_ITEM)) //how about now
+ to_chat(src, span_revenwarning("You are not close enough to siphon [target ? "[target]'s" : "[target_their]"] soul. The link has been broken."))
+ return FALSE
+
+ if(target.stat == CONSCIOUS)
+ to_chat(src, span_revenwarning("[target_Theyre] now powerful enough to fight off your draining!"))
+ to_chat(target, span_boldannounce("You feel something tugging across your body before subsiding.")) //hey, wait a minute...
+ return FALSE
+
+ to_chat(src, span_revenminor("You begin siphoning essence from [target]'s soul."))
+ if(target.stat != DEAD)
+ to_chat(target, span_warning("You feel a horribly unpleasant draining sensation as your grip on life weakens..."))
+ if(target.stat == SOFT_CRIT)
+ target.Stun(46)
+
+ apply_status_effect(/datum/status_effect/revenant/revealed, 5 SECONDS)
+ apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, 5 SECONDS)
+
+ target.visible_message(span_warning("[target] suddenly rises slightly into the air, [target_their] skin turning an ashy gray."))
+
+ if(target.can_block_magic(MAGIC_RESISTANCE_HOLY))
+ to_chat(src, span_revenminor("Something's wrong! [target] seems to be resisting the siphoning, leaving you vulnerable!"))
+ target.visible_message(
+ span_warning("[target] slumps onto the ground."),
+ span_revenwarning("Violet lights, dancing in your vision, receding--"),
+ )
+ return FALSE
+
+ var/datum/beam/draining_beam = Beam(target, icon_state = "drain_life")
+ if(!do_after(src, 4.6 SECONDS, target, timed_action_flags = (IGNORE_HELD_ITEM | IGNORE_INCAPACITATED))) //As one cannot prove the existance of ghosts, ghosts cannot prove the existance of the target they were draining.
+ to_chat(src, span_revenwarning("[target ? "[target]'s soul has" : "[target_They_have]"] been drawn out of your grasp. The link has been broken."))
+ if(target)
+ target.visible_message(
+ span_warning("[target] slumps onto the ground."),
+ span_revenwarning("Violet lights, dancing in your vision, receding--"),
+ )
+ qdel(draining_beam)
+ return FALSE
+
+ change_essence_amount(essence_drained, FALSE, target)
+
+ if(essence_drained <= 90 && target.stat != DEAD && !HAS_TRAIT(target, TRAIT_WEAK_SOUL))
+ max_essence += 5
+ to_chat(src, span_revenboldnotice("The absorption of [target]'s living soul has increased your maximum essence level. Your new maximum essence is [max_essence]."))
+
+ if(essence_drained > 90)
+ max_essence += 15
+ perfectsouls++
+ to_chat(src, span_revenboldnotice("The perfection of [target]'s soul has increased your maximum essence level. Your new maximum essence is [max_essence]."))
+
+ to_chat(src, span_revennotice("[target]'s soul has been considerably weakened and will yield no more essence for the time being."))
+ target.visible_message(
+ span_warning("[target] slumps onto the ground."),
+ span_revenwarning("Violet lights, dancing in your vision, getting clo--"),
+ )
+
+ LAZYADD(drained_mobs, REF(target))
+ if(target.stat != DEAD)
+ target.investigate_log("has died from revenant harvest.", INVESTIGATE_DEATHS)
+ target.death(FALSE)
+
+ qdel(draining_beam)
+ return TRUE
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm
new file mode 100644
index 00000000000000..a9e17a9b305ff2
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_items.dm
@@ -0,0 +1,106 @@
+//reforming
+/obj/item/ectoplasm/revenant
+ name = "glimmering residue"
+ desc = "A pile of fine blue dust. Small tendrils of violet mist swirl around it."
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "revenantEctoplasm"
+ w_class = WEIGHT_CLASS_SMALL
+ /// Are we currently reforming?
+ var/reforming = TRUE
+ /// Are we inert (aka distorted such that we can't reform)?
+ var/inert = FALSE
+ /// The key of the revenant that we started the reform as
+ var/old_ckey
+ /// The revenant we're currently storing
+ var/mob/living/basic/revenant/revenant
+
+/obj/item/ectoplasm/revenant/Initialize(mapload)
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(try_reform)), 1 MINUTES)
+
+/obj/item/ectoplasm/revenant/Destroy()
+ if(!QDELETED(revenant))
+ qdel(revenant)
+ return ..()
+
+/obj/item/ectoplasm/revenant/attack_self(mob/user)
+ if(!reforming || inert)
+ return ..()
+ user.visible_message(
+ span_notice("[user] scatters [src] in all directions."),
+ span_notice("You scatter [src] across the area. The particles slowly fade away."),
+ )
+ user.dropItemToGround(src)
+ qdel(src)
+
+/obj/item/ectoplasm/revenant/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ . = ..()
+ if(inert)
+ return
+ visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
+ qdel(src)
+
+/obj/item/ectoplasm/revenant/examine(mob/user)
+ . = ..()
+ if(inert)
+ . += span_revennotice("It seems inert.")
+ else if(reforming)
+ . += span_revenwarning("It is shifting and distorted. It would be wise to destroy this.")
+
+/obj/item/ectoplasm/revenant/suicide_act(mob/living/user)
+ user.visible_message(span_suicide("[user] is inhaling [src]! It looks like [user.p_theyre()] trying to visit the shadow realm!"))
+ qdel(src)
+ return OXYLOSS
+
+/obj/item/ectoplasm/revenant/proc/try_reform()
+ if(reforming)
+ reforming = FALSE
+ reform()
+ else
+ inert = TRUE
+ visible_message(span_warning("[src] settles down and seems lifeless."))
+
+/// Actually moves the revenant out of ourself
+/obj/item/ectoplasm/revenant/proc/reform()
+ if(QDELETED(src) || QDELETED(revenant) || inert)
+ return
+
+ message_admins("Revenant ectoplasm was left undestroyed for 1 minute and is reforming into a new revenant.")
+ forceMove(drop_location()) //In case it's in a backpack or someone's hand
+
+ var/user_name = old_ckey
+ if(isnull(revenant.client))
+ var/mob/potential_user = get_new_user()
+ revenant.key = potential_user.key
+ user_name = potential_user.ckey
+ qdel(potential_user)
+
+ message_admins("[user_name] has been [old_ckey == user_name ? "re":""]made into a revenant by reforming ectoplasm.")
+ revenant.log_message("was [old_ckey == user_name ? "re":""]made as a revenant by reforming ectoplasm.", LOG_GAME)
+ visible_message(span_revenboldnotice("[src] suddenly rises into the air before fading away."))
+
+ revenant.death_reset()
+ revenant = null
+ qdel(src)
+
+/// Handles giving the revenant a new client to control it
+/obj/item/ectoplasm/revenant/proc/get_new_user()
+ message_admins("The new revenant's old client either could not be found or is in a new, living mob - grabbing a random candidate instead...")
+ var/list/candidates = poll_candidates_for_mob("Do you want to be [revenant.name] (reforming)?", ROLE_REVENANT, ROLE_REVENANT, 5 SECONDS, revenant)
+
+ if(!LAZYLEN(candidates))
+ message_admins("No candidates were found for the new revenant.")
+ inert = TRUE
+ visible_message(span_revenwarning("[src] settles down and seems lifeless."))
+ qdel(revenant)
+ return null
+
+ var/mob/dead/observer/potential_client = pick(candidates)
+ if(isnull(potential_client))
+ qdel(revenant)
+ message_admins("No candidate was found for the new revenant. Oh well!")
+ inert = TRUE
+ visible_message(span_revenwarning("[src] settles down and seems lifeless."))
+ return null
+
+ return potential_client
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_objectives.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_objectives.dm
new file mode 100644
index 00000000000000..7dd391c17e477e
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_objectives.dm
@@ -0,0 +1,37 @@
+/datum/objective/revenant
+
+/datum/objective/revenant/New()
+ target_amount = rand(350, 600)
+ explanation_text = "Absorb [target_amount] points of essence from humans."
+ return ..()
+
+/datum/objective/revenant/check_completion()
+ if(!isrevenant(owner.current))
+ return FALSE
+ var/mob/living/basic/revenant/owner_mob = owner.current
+ if(QDELETED(owner_mob) || owner_mob.stat == DEAD)
+ return FALSE
+ var/essence_stolen = owner_mob.essence_accumulated
+ return essence_stolen >= target_amount
+
+/datum/objective/revenant_fluff
+
+/datum/objective/revenant_fluff/New()
+ var/list/explanation_texts = list(
+ "Assist and exacerbate existing threats at critical moments.",
+ "Cause as much chaos and anger as you can without being killed.",
+ "Damage and render as much of the station rusted and unusable as possible.",
+ "Disable and cause malfunctions in as many machines as possible.",
+ "Ensure that any holy weapons are rendered unusable.",
+ "Heed and obey the requests of the dead, provided that carrying them out wouldn't be too inconvenient or self-destructive.",
+ "Impersonate or be worshipped as a God.",
+ "Make the captain as miserable as possible.",
+ "Make the clown as miserable as possible.",
+ "Make the crew as miserable as possible.",
+ "Prevent the use of energy weapons where possible.",
+ )
+ explanation_text = pick(explanation_texts)
+ return ..()
+
+/datum/objective/revenant_fluff/check_completion()
+ return TRUE
diff --git a/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spider_ai.dm b/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spider_ai.dm
index f998b7b53cd908..9894d15fdfba7b 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spider_ai.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/giant_spider/giant_spider_ai.dm
@@ -45,7 +45,6 @@
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/ours_or_smaller(), // Hunt mobs our size
BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/larger(), // Run away from mobs bigger than we are
- BB_BASIC_MOB_FLEEING = TRUE,
)
idle_behavior = /datum/idle_behavior/idle_random_walk
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 6df2eb427f4117..8cb7d8398bf366 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
@@ -478,55 +478,6 @@
menu_description = "Weaker version of the nurse spider, specializing in healing their brethren and placing webbings very swiftly, but has very low amount of health and deals low damage."
ai_controller = /datum/ai_controller/basic_controller/giant_spider/weak
-/**
- * ### Flesh Spider
- *
- * A subtype of giant spider which only occurs from changelings.
- * Has the base stats of a hunter, but they can heal themselves and spin webs faster.
- * They also occasionally leave puddles of blood when they walk around. Flavorful!
- */
-/mob/living/basic/spider/giant/hunter/flesh
- name = "flesh spider"
- desc = "A odd fleshy creature in the shape of a spider. Its eyes are pitch black and soulless."
- icon = 'icons/mob/simple/arachnoid.dmi'
- icon_state = "flesh"
- icon_living = "flesh"
- icon_dead = "flesh_dead"
- web_speed = 0.7
- maxHealth = 90
- health = 90
- menu_description = "Self-sufficient spider variant capable of healing themselves and producing webbbing fast."
-
-/mob/living/basic/spider/giant/hunter/flesh/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/blood_walk, \
- blood_type = /obj/effect/decal/cleanable/blood/bubblegum, \
- blood_spawn_chance = 5)
- // It might be easier and more fitting to just replace this with Regenerator
- AddComponent(/datum/component/healing_touch,\
- heal_brute = 45,\
- heal_burn = 45,\
- self_targetting = HEALING_TOUCH_SELF_ONLY,\
- interaction_key = DOAFTER_SOURCE_SPIDER,\
- valid_targets_typecache = typecacheof(list(/mob/living/basic/spider/giant/hunter/flesh)),\
- extra_checks = CALLBACK(src, PROC_REF(can_mend)),\
- action_text = "%SOURCE% begins mending themselves...",\
- complete_text = "%SOURCE%'s wounds mend together.",\
- )
-
- 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)
-
-/// Prevent you from healing other flesh spiders, or healing when on fire
-/mob/living/basic/spider/giant/hunter/flesh/proc/can_mend(mob/living/source, mob/living/target)
- if (on_fire)
- balloon_alert(src, "on fire!")
- return FALSE
- return TRUE
-
/**
* ### Viper Spider (Wizard)
*
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 e96482439f9825..53b48129e2ed49 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider.dm
@@ -13,6 +13,7 @@
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently push aside"
initial_language_holder = /datum/language_holder/spider
+ melee_attack_cooldown = CLICK_CD_MELEE
damage_coeff = list(BRUTE = 1, BURN = 1.25, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
basic_mob_flags = FLAMMABLE_MOB
status_flags = NONE
@@ -48,7 +49,7 @@
/mob/living/basic/spider/Initialize(mapload)
. = ..()
- ADD_TRAIT(src, TRAIT_WEB_SURFER, INNATE_TRAIT)
+ add_traits(list(TRAIT_WEB_SURFER, TRAIT_FENCE_CLIMBER), INNATE_TRAIT)
AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
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!")
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 7108983c310511..c949b438683cb6 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
@@ -61,7 +61,6 @@
/datum/ai_controller/basic_controller/spiderling
blackboard = list(
BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/larger, // Run away from mobs bigger than we are
- BB_BASIC_MOB_FLEEING = TRUE,
BB_VENTCRAWL_COOLDOWN = 20 SECONDS, // enough time to get splatted while we're out in the open.
BB_TIME_TO_GIVE_UP_ON_VENT_PATHING = 30 SECONDS,
)
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 bce35146ecfc69..d2ea5e8a831d05 100644
--- a/code/modules/mob/living/basic/space_fauna/statue/statue.dm
+++ b/code/modules/mob/living/basic/space_fauna/statue/statue.dm
@@ -162,7 +162,7 @@
maxHealth = 5000
melee_damage_lower = 65
melee_damage_upper = 65
- faction = list("statue","mining")
+ faction = list(FACTION_STATUE,FACTION_MINING)
/mob/living/basic/statue/frosty/Initialize(mapload)
. = ..()
diff --git a/code/modules/mob/living/basic/space_fauna/supermatter_spider.dm b/code/modules/mob/living/basic/space_fauna/supermatter_spider.dm
new file mode 100644
index 00000000000000..2a3ba326eaca95
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/supermatter_spider.dm
@@ -0,0 +1,102 @@
+/// A nasty little robotic bug that dusts people on attack. Jeepers. This should be a very, very, very rare spawn.
+/mob/living/basic/supermatter_spider
+ name = "supermatter spider"
+ desc= "A sliver of supermatter placed upon a robotically enhanced pedestal."
+
+ icon = 'icons/mob/simple/smspider.dmi'
+ icon_state = "smspider"
+ icon_living = "smspider"
+ icon_dead = "smspider_dead"
+
+ gender = NEUTER
+ mob_biotypes = MOB_BUG|MOB_ROBOTIC
+ speak_emote = list("vibrates")
+
+
+ attack_verb_continuous = "slices"
+ attack_verb_simple = "slice"
+ attack_sound = 'sound/effects/supermatter.ogg'
+ attack_vis_effect = ATTACK_EFFECT_CLAW
+
+ maxHealth = 10
+ health = 10
+ minimum_survivable_temperature = TCMB
+ maximum_survivable_temperature = T0C + 1250
+ 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)
+ death_message = "falls to the ground, its shard dulling to a miserable grey!"
+
+ faction = list(FACTION_HOSTILE)
+
+ // Gold, supermatter tinted
+ lighting_cutoff_red = 30
+ lighting_cutoff_green = 30
+ lighting_cutoff_blue = 10
+
+ ai_controller = /datum/ai_controller/basic_controller/supermatter_spider
+
+ /// If we successfully dust something, should we die?
+ var/single_use = TRUE
+
+/mob/living/basic/supermatter_spider/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/swarming)
+
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
+
+ RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_attack))
+
+/// Proc that we call on attacking something to dust 'em.
+/mob/living/basic/supermatter_spider/proc/on_attack(mob/living/basic/source, atom/target)
+ SIGNAL_HANDLER
+
+ if(isliving(target))
+ var/mob/living/victim = target
+ victim.investigate_log("has been dusted by [src].", INVESTIGATE_DEATHS)
+ dust_feedback(target)
+ victim.dust()
+ if(single_use)
+ death()
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ if(!isturf(target))
+ dust_feedback(target)
+ qdel(target)
+ if(single_use)
+ death()
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/// Simple proc that plays the supermatter dusting sound and sends a visible message.
+/mob/living/basic/supermatter_spider/proc/dust_feedback(atom/target)
+ playsound(get_turf(src), 'sound/effects/supermatter.ogg', 10, TRUE)
+ visible_message(span_danger("[src] knocks into [target], turning [target.p_them()] to dust in a brilliant flash of light!"))
+
+/mob/living/basic/supermatter_spider/overcharged
+ name = "overcharged supermatter spider"
+ desc = "A sliver of overcharged supermatter placed upon a robotically enhanced pedestal. This one seems especially dangerous."
+ icon_state = "smspideroc"
+ icon_living = "smspideroc"
+ maxHealth = 25
+ health = 25
+ single_use = FALSE
+
+/datum/ai_controller/basic_controller/supermatter_spider
+ 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
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/random_speech/supermatter_spider,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/datum/ai_planning_subtree/random_speech/supermatter_spider
+ speech_chance = 7
+ emote_hear = list("clinks", "clanks")
+ emote_see = list("vibrates")
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 d459648892b018..a9e2b538bdd741 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
@@ -65,7 +65,7 @@
fugu.melee_damage_upper = 20
fugu.status_flags |= GODMODE
fugu.obj_damage = 60
- fugu.ai_controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, FALSE)
+ fugu.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE)
fugu.ai_controller.CancelActions()
/datum/status_effect/inflated/on_remove()
@@ -84,7 +84,7 @@
if (fugu.stat != DEAD)
fugu.icon_state = "Fugu0"
fugu.obj_damage = 0
- fugu.ai_controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, TRUE)
+ fugu.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, FALSE)
fugu.ai_controller.CancelActions()
/// Remove status effect if we die
diff --git a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/wumborian_ai.dm b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/wumborian_ai.dm
index 9d3a09c5348f2c..e405ee3755abf1 100644
--- a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/wumborian_ai.dm
+++ b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/wumborian_ai.dm
@@ -2,7 +2,6 @@
/datum/ai_controller/basic_controller/wumborian_fugu
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- BB_BASIC_MOB_FLEEING = TRUE,
)
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/vermin/crab.dm b/code/modules/mob/living/basic/vermin/crab.dm
index abe5c25117b6db..4843c6e6f7b11d 100644
--- a/code/modules/mob/living/basic/vermin/crab.dm
+++ b/code/modules/mob/living/basic/vermin/crab.dm
@@ -77,7 +77,7 @@
///The basic ai controller for crabs
/datum/ai_controller/basic_controller/crab
blackboard = list(
- BB_BASIC_MOB_FLEEING = FALSE,
+ BB_BASIC_MOB_STOP_FLEEING = TRUE,
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/ours_or_smaller,
BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
)
diff --git a/code/modules/mob/living/basic/vermin/frog.dm b/code/modules/mob/living/basic/vermin/frog.dm
index 191ea12b4df33c..64dbafb45f85d2 100644
--- a/code/modules/mob/living/basic/vermin/frog.dm
+++ b/code/modules/mob/living/basic/vermin/frog.dm
@@ -26,7 +26,7 @@
response_harm_simple = "splat"
density = FALSE
faction = list(FACTION_HOSTILE, FACTION_MAINT_CREATURES)
- attack_sound = 'sound/effects/reee.ogg'
+ attack_sound = null // SKYRAT EDIT - No more frog ear-rape - ORIGINAL: attack_sound = 'sound/effects/reee.ogg'
butcher_results = list(/obj/item/food/nugget = 1)
pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
mob_size = MOB_SIZE_TINY
@@ -40,7 +40,7 @@
ai_controller = /datum/ai_controller/basic_controller/frog
- var/stepped_sound = 'sound/effects/huuu.ogg'
+ var/stepped_sound = null // SKYRAT EDIT - No more frog ear-rape - ORIGINA: var/stepped_sound = 'sound/effects/huuu.ogg'
///How much of a reagent the mob injects on attack
var/poison_per_bite = 3
///What reagent the mob injects targets with
diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm
index 46e175c5323bb4..87d549ac5234e1 100644
--- a/code/modules/mob/living/basic/vermin/mouse.dm
+++ b/code/modules/mob/living/basic/vermin/mouse.dm
@@ -376,8 +376,7 @@
/// The mouse AI controller
/datum/ai_controller/basic_controller/mouse
- blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE, // Always cowardly
+ blackboard = list( // Always cowardly
BB_CURRENT_HUNTING_TARGET = null, // cheese
BB_LOW_PRIORITY_HUNTING_TARGET = null, // cable
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), // Use this to find people to run away from
diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm
index 9354f02e2b2df9..bc0d2e3d8e81ae 100644
--- a/code/modules/mob/living/blood.dm
+++ b/code/modules/mob/living/blood.dm
@@ -41,11 +41,14 @@
//Effects of bloodloss
var/word = pick("dizzy","woozy","faint")
switch(blood_volume)
- if(BLOOD_VOLUME_EXCESS to BLOOD_VOLUME_MAX_LETHAL)
+ if(BLOOD_VOLUME_MAX_LETHAL to INFINITY)
if(SPT_PROB(7.5, seconds_per_tick))
to_chat(src, span_userdanger("Blood starts to tear your skin apart. You're going to burst!"))
investigate_log("has been gibbed by having too much blood.", INVESTIGATE_DEATHS)
inflate_gib()
+ if(BLOOD_VOLUME_EXCESS to BLOOD_VOLUME_MAX_LETHAL)
+ if(SPT_PROB(5, seconds_per_tick))
+ to_chat(src, span_warning("You feel your skin swelling."))
if(BLOOD_VOLUME_MAXIMUM to BLOOD_VOLUME_EXCESS)
if(SPT_PROB(5, seconds_per_tick))
to_chat(src, span_warning("You feel terribly bloated."))
@@ -94,7 +97,7 @@
//Blood loss still happens in locker, floor stays clean
if(isturf(loc) && prob(sqrt(amt)*BLOOD_DRIP_RATE_MOD))
- add_splatter_floor(loc, (amt >= 10))
+ add_splatter_floor(loc, (amt <= 10))
/mob/living/carbon/human/bleed(amt)
amt *= physiology.bleed_mod
diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm
index 10cff8c19acd1f..9918a21b4f6d40 100644
--- a/code/modules/mob/living/brain/brain_item.dm
+++ b/code/modules/mob/living/brain/brain_item.dm
@@ -552,7 +552,7 @@
/obj/item/organ/internal/brain/apply_organ_damage(damage_amount, maximum = maxHealth, required_organ_flag = NONE)
. = ..()
if(!owner)
- return
+ return FALSE
if(damage >= 60)
owner.add_mood_event("brain_damage", /datum/mood_event/brain_damage)
else
diff --git a/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm b/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm
index 8c2111511236bc..bbfd68f8186b8b 100644
--- a/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm
+++ b/code/modules/mob/living/carbon/alien/adult/adult_update_icons.dm
@@ -76,7 +76,7 @@
//Royals have bigger sprites, so inhand things must be handled differently.
/mob/living/carbon/alien/adult/royal/update_held_items()
- ..()
+ . = ..()
remove_overlay(HANDS_LAYER)
var/list/hands = list()
diff --git a/code/modules/mob/living/carbon/alien/alien_defense.dm b/code/modules/mob/living/carbon/alien/alien_defense.dm
index bc7a663206ae52..63ef8830435e12 100644
--- a/code/modules/mob/living/carbon/alien/alien_defense.dm
+++ b/code/modules/mob/living/carbon/alien/alien_defense.dm
@@ -103,7 +103,7 @@ In all, this is a lot like the monkey code. /N
var/obj/item/organ/internal/ears/ears = get_organ_slot(ORGAN_SLOT_EARS)
switch (severity)
if (EXPLODE_DEVASTATE)
- gib()
+ gib(DROP_ALL_REMAINS)
if (EXPLODE_HEAVY)
take_overall_damage(60, 60)
diff --git a/code/modules/mob/living/carbon/alien/damage_procs.dm b/code/modules/mob/living/carbon/alien/damage_procs.dm
index 8861a55d9931c6..5dbffda96739e0 100644
--- a/code/modules/mob/living/carbon/alien/damage_procs.dm
+++ b/code/modules/mob/living/carbon/alien/damage_procs.dm
@@ -11,5 +11,5 @@
return FALSE
///aliens are immune to stamina damage.
-/mob/living/carbon/alien/setStaminaLoss(amount, updating_stamina = 1)
+/mob/living/carbon/alien/setStaminaLoss(amount, updating_stamina = 1, forced = FALSE, required_biotype)
return FALSE
diff --git a/code/modules/mob/living/carbon/alien/death.dm b/code/modules/mob/living/carbon/alien/death.dm
index 718186c9078e00..f5a0b7ace1b47f 100644
--- a/code/modules/mob/living/carbon/alien/death.dm
+++ b/code/modules/mob/living/carbon/alien/death.dm
@@ -1,5 +1,5 @@
-/mob/living/carbon/alien/spawn_gibs(with_bodyparts)
- if(with_bodyparts)
+/mob/living/carbon/alien/spawn_gibs(drop_bitflags=NONE)
+ if(drop_bitflags & DROP_BODYPARTS)
new /obj/effect/gibspawner/xeno(drop_location(), src)
else
new /obj/effect/gibspawner/xeno/bodypartless(drop_location(), src)
diff --git a/code/modules/mob/living/carbon/alien/larva/death.dm b/code/modules/mob/living/carbon/alien/larva/death.dm
index 8fd6329a0c1de1..4b7f9f93218456 100644
--- a/code/modules/mob/living/carbon/alien/larva/death.dm
+++ b/code/modules/mob/living/carbon/alien/larva/death.dm
@@ -6,8 +6,8 @@
update_icons()
-/mob/living/carbon/alien/larva/spawn_gibs(with_bodyparts)
- if(with_bodyparts)
+/mob/living/carbon/alien/larva/spawn_gibs(drop_bitflags=NONE)
+ if(drop_bitflags & DROP_BODYPARTS)
new /obj/effect/gibspawner/larva(drop_location(), src)
else
new /obj/effect/gibspawner/larva/bodypartless(drop_location(), src)
diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm
index 5ef9b23c418212..af796c9ff685f7 100644
--- a/code/modules/mob/living/carbon/alien/organs.dm
+++ b/code/modules/mob/living/carbon/alien/organs.dm
@@ -336,7 +336,7 @@
if(owner)
shake_camera(owner, 2, 5)
owner.investigate_log("has been gibbed by something inside [owner.p_their()] stomach.", INVESTIGATE_DEATHS)
- owner.gib()
+ owner.gib(DROP_ALL_REMAINS)
qdel(src)
/obj/item/organ/internal/stomach/alien/proc/eject_stomach(list/turf/targets, spit_range, content_speed, particle_delay, particle_count=4)
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 37fb827f3a5d5c..5a6746ec6fa8b2 100644
--- a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
+++ b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
@@ -126,8 +126,8 @@
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!"))
- // owner.investigate_log("has been gibbed by an alien larva.", INVESTIGATE_DEATHS) SKYRAT EDIT REMOVAL - ALIEN QOL - don't ever gib host.
- // owner.gib(TRUE) SKYRAT EDIT REMOVAL - ALIEN QOL - don't ever gib host.
+ //owner.investigate_log("has been gibbed by an alien larva.", INVESTIGATE_DEATHS) // SKYRAT EDIT REMOVAL - ALIEN QOL - don't ever gib host.
+ //owner.gib(DROP_ORGANS|DROP_BODYPARTS)
// SKYRAT EDIT ADDITION BEGIN - ALIEN QOL - You aren't getting gibbed but you aren't going to be having fun
owner.apply_damage(150, BRUTE, BODY_ZONE_CHEST, wound_bonus = 30, sharpness = SHARP_POINTY)
owner.spawn_gibs()
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index de77200f940dbe..35dffd7c27ea93 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -10,6 +10,7 @@
COMSIG_CARBON_DISARM_COLLIDE = PROC_REF(disarm_collision),
)
AddElement(/datum/element/connect_loc, loc_connections)
+ ADD_TRAIT(src, TRAIT_CAN_HOLD_ITEMS, INNATE_TRAIT) // Carbons are assumed to be innately capable of having arms, we check their arms count instead
/mob/living/carbon/Destroy()
//This must be done first, so the mob ghosts correctly before DNA etc is nulled
@@ -27,45 +28,6 @@
QDEL_NULL(dna)
GLOB.carbon_list -= src
-/mob/living/carbon/perform_hand_swap(held_index)
- . = ..()
- if(!.)
- return
-
- if(!held_index)
- held_index = (active_hand_index % held_items.len)+1
-
- if(!isnum(held_index))
- CRASH("You passed [held_index] into swap_hand instead of a number. WTF man")
-
- var/oindex = active_hand_index
- active_hand_index = held_index
- if(hud_used)
- var/atom/movable/screen/inventory/hand/H
- H = hud_used.hand_slots["[oindex]"]
- if(H)
- H.update_appearance()
- H = hud_used.hand_slots["[held_index]"]
- if(H)
- H.update_appearance()
-
-
-/mob/living/carbon/activate_hand(selhand) //l/r OR 1-held_items.len
- if(!selhand)
- selhand = (active_hand_index % held_items.len)+1
-
- if(istext(selhand))
- selhand = lowertext(selhand)
- if(selhand == "right" || selhand == "r")
- selhand = 2
- if(selhand == "left" || selhand == "l")
- selhand = 1
-
- if(selhand != active_hand_index)
- swap_hand(selhand)
- else
- mode() // Activate held item
-
/mob/living/carbon/attackby(obj/item/item, mob/living/user, params)
if(!all_wounds || !(!user.combat_mode || user == src))
return ..()
@@ -467,17 +429,18 @@
playsound(get_turf(src), 'sound/effects/splat.ogg', 50, TRUE)
+ var/need_mob_update = FALSE
var/turf/location = get_turf(src)
if(!blood)
adjust_nutrition(-lost_nutrition)
- adjustToxLoss(-3)
+ need_mob_update += adjustToxLoss(-3, updating_health = FALSE)
for(var/i = 0 to distance)
if(blood)
if(location)
add_splatter_floor(location)
if(vomit_flags & MOB_VOMIT_HARM)
- adjustBruteLoss(3)
+ need_mob_update += adjustBruteLoss(3, updating_health = FALSE)
else
if(location)
location.add_vomit_floor(src, vomit_type, vomit_flags, purge_ratio) // call purge when doing detoxicfication to pump more chems out of the stomach.
@@ -485,6 +448,8 @@
location = get_step(location, starting_dir)
if (location?.is_blocked_turf())
break
+ if(need_mob_update) // so we only have to call updatehealth() once as opposed to n times
+ updatehealth()
return TRUE
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 789a459b49287c..a51e9c3430e447 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -479,8 +479,7 @@
var/immediately_stun = should_stun && !(flags & SHOCK_DELAY_STUN)
if (immediately_stun)
if (paralyze)
- //Paralyze(40) - SKYRAT EDIT REMOVAL
- StaminaKnockdown(10, TRUE) // SKYRAT EDIT ADDITION
+ StaminaKnockdown(stun_duration / 4) // SKYRAT EDIT CHANGE - ORIGINAL: Paralyze(40)
else
Knockdown(stun_duration)
//Jitter and other fluff.
@@ -494,8 +493,7 @@
///Called slightly after electrocute act to apply a secondary stun.
/mob/living/carbon/proc/secondary_shock(paralyze, stun_duration)
if (paralyze)
- //Paralyze(60) - SKYRAT EDIT REMOVAL
- StaminaKnockdown(10, TRUE) //SKYRAT EDIT ADDITION
+ StaminaKnockdown(stun_duration / 6) // SKYRAT EDIT CHANGE - ORIGINAL: Paralyze(60)
else
Knockdown(stun_duration)
@@ -768,7 +766,7 @@
amount = min(amount, 0) //Prevents oxy damage but not healing
. = ..()
- check_passout(.)
+ check_passout()
/mob/living/carbon/proc/get_interaction_efficiency(zone)
var/obj/item/bodypart/limb = get_bodypart(zone)
@@ -777,12 +775,12 @@
/mob/living/carbon/setOxyLoss(amount, updating_health = TRUE, forced, required_biotype, required_respiration_type)
. = ..()
- check_passout(.)
+ check_passout()
/**
* Check to see if we should be passed out from oyxloss
*/
-/mob/living/carbon/proc/check_passout(oxyloss)
+/mob/living/carbon/proc/check_passout()
if(!isnum(oxyloss))
return
if(oxyloss <= 50)
diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm
index bcf5f495715a48..c467cd2f0017a0 100644
--- a/code/modules/mob/living/carbon/carbon_defines.dm
+++ b/code/modules/mob/living/carbon/carbon_defines.dm
@@ -2,7 +2,7 @@
blood_volume = BLOOD_VOLUME_NORMAL
gender = MALE
pressure_resistance = 15
- hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD,GLAND_HUD)
+ hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD,GLAND_HUD, DNR_HUD) // SKYRAT EDIT ADDITION - DNR ICON
has_limbs = TRUE
held_items = list(null, null)
num_legs = 0 //Populated on init through list/bodyparts
diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm
index 2aab0a5e2bb6c1..d4fa006957e717 100644
--- a/code/modules/mob/living/carbon/carbon_update_icons.dm
+++ b/code/modules/mob/living/carbon/carbon_update_icons.dm
@@ -282,11 +282,17 @@
update_body()
/mob/living/carbon/update_held_items()
+ . = ..()
remove_overlay(HANDS_LAYER)
if (handcuffed)
drop_all_held_items()
return
+ overlays_standing[HANDS_LAYER] = get_held_overlays()
+ apply_overlay(HANDS_LAYER)
+
+/// Generate held item overlays
+/mob/living/carbon/proc/get_held_overlays()
var/list/hands = list()
for(var/obj/item/I in held_items)
if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
@@ -307,9 +313,7 @@
icon_file = I.righthand_file
hands += I.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE)
-
- overlays_standing[HANDS_LAYER] = hands
- apply_overlay(HANDS_LAYER)
+ return hands
/mob/living/carbon/update_fire_overlay(stacks, on_fire, last_icon_state, suffix = "")
var/fire_icon = "[dna?.species.fire_overlay || "human"]_[stacks > MOB_BIG_FIRE_STACK_THRESHOLD ? "big_fire" : "small_fire"][suffix]"
diff --git a/code/modules/mob/living/carbon/damage_procs.dm b/code/modules/mob/living/carbon/damage_procs.dm
index 6e0c7cef8670c5..040bf76a3db027 100644
--- a/code/modules/mob/living/carbon/damage_procs.dm
+++ b/code/modules/mob/living/carbon/damage_procs.dm
@@ -56,40 +56,42 @@
return amount
/mob/living/carbon/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- if(!forced && (status_flags & GODMODE))
- return FALSE
+ if(!can_adjust_brute_loss(amount, forced, required_bodytype))
+ return 0
if(amount > 0)
- take_overall_damage(brute = amount, updating_health = updating_health, required_bodytype = required_bodytype)
+ . = take_overall_damage(brute = amount, updating_health = updating_health, forced = forced, required_bodytype = required_bodytype)
else
- heal_overall_damage(brute = abs(amount), required_bodytype = required_bodytype, updating_health = updating_health)
- return amount
+ . = heal_overall_damage(brute = abs(amount), required_bodytype = required_bodytype, updating_health = updating_health, forced = forced)
/mob/living/carbon/setBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+ if(!forced && (status_flags & GODMODE))
+ return FALSE
var/current = getBruteLoss()
var/diff = amount - current
if(!diff)
- return
- adjustBruteLoss(diff, updating_health, forced, required_bodytype)
+ return FALSE
+ return adjustBruteLoss(diff, updating_health, forced, required_bodytype)
/mob/living/carbon/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- if(!forced && (status_flags & GODMODE))
- return FALSE
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
+ return 0
if(amount > 0)
- take_overall_damage(burn = amount, updating_health = updating_health, required_bodytype = required_bodytype)
+ . = take_overall_damage(burn = amount, updating_health = updating_health, forced = forced, required_bodytype = required_bodytype)
else
- heal_overall_damage(burn = abs(amount), required_bodytype = required_bodytype, updating_health = updating_health)
- return amount
+ . = heal_overall_damage(burn = abs(amount), required_bodytype = required_bodytype, updating_health = updating_health, forced = forced)
/mob/living/carbon/setFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+ if(!forced && (status_flags & GODMODE))
+ return FALSE
var/current = getFireLoss()
var/diff = amount - current
if(!diff)
- return
- adjustFireLoss(diff, updating_health, forced, required_bodytype)
+ return FALSE
+ return adjustFireLoss(diff, updating_health, forced, required_bodytype)
-/mob/living/carbon/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = MOB_ORGANIC)
- if(!forced && !(mob_biotypes & required_biotype))
- return
+/mob/living/carbon/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
+ if(!can_adjust_tox_loss(amount, forced, required_biotype))
+ return 0
if(!forced && HAS_TRAIT(src, TRAIT_TOXINLOVER)) //damage becomes healing and healing becomes damage
amount = -amount
if(HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing
@@ -98,11 +100,11 @@
blood_volume = max(blood_volume - (5*amount), 0)
else
blood_volume = max(blood_volume - amount, 0)
- else if(HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing
+ else if(!forced && HAS_TRAIT(src, TRAIT_TOXIMMUNE)) //Prevents toxin damage, but not healing
amount = min(amount, 0)
return ..()
-/mob/living/carbon/adjustStaminaLoss(amount, updating_stamina, forced, required_biotype)
+/mob/living/carbon/adjustStaminaLoss(amount, updating_stamina, forced, required_biotype = ALL)
. = ..()
if(amount > 0)
stam_regen_start_time = world.time + STAMINA_REGEN_BLOCK_TIME
@@ -115,14 +117,16 @@
* * amount - damage to be done
* * maximum - currently an arbitrarily large number, can be set so as to limit damage
* * required_organ_flag - targets only a specific organ type if set to ORGAN_ORGANIC or ORGAN_ROBOTIC
+ *
+ * Returns: The net change in damage from apply_organ_damage()
*/
/mob/living/carbon/adjustOrganLoss(slot, amount, maximum, required_organ_flag = NONE)
var/obj/item/organ/affected_organ = get_organ_slot(slot)
if(!affected_organ || (status_flags & GODMODE))
- return
+ return FALSE
if(required_organ_flag && !(affected_organ.organ_flags & required_organ_flag))
- return
- affected_organ.apply_organ_damage(amount, maximum)
+ return FALSE
+ return affected_organ.apply_organ_damage(amount, maximum)
/**
* If an organ exists in the slot requested, and we are capable of taking damage (we don't have [GODMODE] on), call the set damage proc on that organ, which can
@@ -132,16 +136,18 @@
* * slot - organ slot, like [ORGAN_SLOT_HEART]
* * amount - damage to be set to
* * required_organ_flag - targets only a specific organ type if set to ORGAN_ORGANIC or ORGAN_ROBOTIC
+ *
+ * Returns: The net change in damage from set_organ_damage()
*/
/mob/living/carbon/setOrganLoss(slot, amount, required_organ_flag = NONE)
var/obj/item/organ/affected_organ = get_organ_slot(slot)
if(!affected_organ || (status_flags & GODMODE))
- return
+ return FALSE
if(required_organ_flag && !(affected_organ.organ_flags & required_organ_flag))
- return
+ return FALSE
if(affected_organ.damage == amount)
- return
- affected_organ.set_organ_damage(amount)
+ return FALSE
+ return affected_organ.set_organ_damage(amount)
/**
* If an organ exists in the slot requested, return the amount of damage that organ has
@@ -200,14 +206,16 @@
* It automatically updates health status
*/
/mob/living/carbon/heal_bodypart_damage(brute = 0, burn = 0, updating_health = TRUE, required_bodytype = NONE, target_zone = null)
+ . = FALSE
var/list/obj/item/bodypart/parts = get_damaged_bodyparts(brute, burn, required_bodytype, target_zone)
if(!parts.len)
return
+
var/obj/item/bodypart/picked = pick(parts)
- var/damage_calculator = picked.get_damage(TRUE) //heal_damage returns update status T/F instead of amount healed so we dance gracefully around this
- if(picked.heal_damage(brute, burn, required_bodytype))
+ var/damage_calculator = picked.get_damage() //heal_damage returns update status T/F instead of amount healed so we dance gracefully around this
+ if(picked.heal_damage(abs(brute), abs(burn), required_bodytype = required_bodytype))
update_damage_overlays()
- return max(damage_calculator - picked.get_damage(TRUE), 0)
+ return (damage_calculator - picked.get_damage())
/**
@@ -218,15 +226,25 @@
* It automatically updates health status
*/
/mob/living/carbon/take_bodypart_damage(brute = 0, burn = 0, updating_health = TRUE, required_bodytype, check_armor = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE)
+ . = FALSE
+ if(status_flags & GODMODE)
+ return
var/list/obj/item/bodypart/parts = get_damageable_bodyparts(required_bodytype)
if(!parts.len)
return
+
var/obj/item/bodypart/picked = pick(parts)
- if(picked.receive_damage(brute, burn, check_armor ? run_armor_check(picked, (brute ? MELEE : burn ? FIRE : null)) : FALSE, wound_bonus = wound_bonus, bare_wound_bonus = bare_wound_bonus, sharpness = sharpness))
+ var/damage_calculator = picked.get_damage()
+ if(picked.receive_damage(abs(brute), abs(burn), check_armor ? run_armor_check(picked, (brute ? MELEE : burn ? FIRE : null)) : FALSE, wound_bonus = wound_bonus, bare_wound_bonus = bare_wound_bonus, sharpness = sharpness))
update_damage_overlays()
+ return (damage_calculator - picked.get_damage())
+
+/mob/living/carbon/heal_overall_damage(brute = 0, burn = 0, stamina = 0, required_bodytype, updating_health = TRUE, forced = FALSE)
+ . = FALSE
+ // treat negative args as positive
+ brute = abs(brute)
+ burn = abs(burn)
-///Heal MANY bodyparts, in random order
-/mob/living/carbon/heal_overall_damage(brute = 0, burn = 0, stamina = 0, required_bodytype, updating_health = TRUE)
var/list/obj/item/bodypart/parts = get_damaged_bodyparts(brute, burn, required_bodytype)
var/update = NONE
@@ -235,25 +253,35 @@
var/brute_was = picked.brute_dam
var/burn_was = picked.burn_dam
+ . += picked.get_damage()
- update |= picked.heal_damage(brute, burn, required_bodytype, FALSE)
+ update |= picked.heal_damage(brute, burn, updating_health = FALSE, forced = forced, required_bodytype = required_bodytype)
+
+ . -= picked.get_damage() // return the net amount of damage healed
brute = round(brute - (brute_was - picked.brute_dam), DAMAGE_PRECISION)
burn = round(burn - (burn_was - picked.burn_dam), DAMAGE_PRECISION)
parts -= picked
+
+ if(!.) // no change? no need to update anything
+ return
+
if(updating_health)
updatehealth()
if(update)
update_damage_overlays()
-/// damage MANY bodyparts, in random order
-/mob/living/carbon/take_overall_damage(brute = 0, burn = 0, stamina = 0, updating_health = TRUE, required_bodytype)
- if(status_flags & GODMODE)
- return //godmode
+/mob/living/carbon/take_overall_damage(brute = 0, burn = 0, stamina = 0, updating_health = TRUE, forced = FALSE, required_bodytype)
+ . = FALSE
+ if(!forced && (status_flags & GODMODE))
+ return
+ // treat negative args as positive
+ brute = abs(brute)
+ burn = abs(burn)
var/list/obj/item/bodypart/parts = get_damageable_bodyparts(required_bodytype)
- var/update = 0
+ var/update = NONE
while(parts.len && (brute > 0 || burn > 0))
var/obj/item/bodypart/picked = pick(parts)
var/brute_per_part = round(brute/parts.len, DAMAGE_PRECISION)
@@ -261,14 +289,21 @@
var/brute_was = picked.brute_dam
var/burn_was = picked.burn_dam
+ . += picked.get_damage()
+ // disabling wounds from these for now cuz your entire body snapping cause your heart stopped would suck
+ update |= picked.receive_damage(brute_per_part, burn_per_part, blocked = FALSE, updating_health = FALSE, forced = forced, required_bodytype = required_bodytype, wound_bonus = CANT_WOUND)
- update |= picked.receive_damage(brute_per_part, burn_per_part, FALSE, updating_health, required_bodytype, wound_bonus = CANT_WOUND) // disabling wounds from these for now cuz your entire body snapping cause your heart stopped would suck
+ . -= picked.get_damage() // return the net amount of damage healed
brute = round(brute - (picked.brute_dam - brute_was), DAMAGE_PRECISION)
burn = round(burn - (picked.burn_dam - burn_was), DAMAGE_PRECISION)
parts -= picked
+
+ if(!.) // no change? no need to update anything
+ return
+
if(updating_health)
updatehealth()
if(update)
diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm
index 4d14fb7df669aa..bbf82ccefc56a7 100644
--- a/code/modules/mob/living/carbon/death.dm
+++ b/code/modules/mob/living/carbon/death.dm
@@ -24,9 +24,9 @@
M.Scale(1.8, 1.2)
animate(src, time = 40, transform = M, easing = SINE_EASING)
-/mob/living/carbon/gib(no_brain, no_organs, no_bodyparts, safe_gib = FALSE)
+/mob/living/carbon/gib(drop_bitflags=NONE)
add_memory_in_range(src, 7, /datum/memory/witness_gib, protagonist = src)
- if(safe_gib) // If you want to keep all the mob's items and not have them deleted
+ if(drop_bitflags & DROP_ITEMS)
for(var/obj/item/W in src)
dropItemToGround(W)
if(prob(50))
@@ -37,39 +37,33 @@
visible_message(span_danger("[M] bursts out of [src]!"))
return ..()
-/mob/living/carbon/spill_organs(no_brain, no_organs, no_bodyparts)
+/mob/living/carbon/spill_organs(drop_bitflags=NONE)
var/atom/Tsec = drop_location()
- if(!no_bodyparts)
- if(no_organs)//so the organs don't get transferred inside the bodyparts we'll drop.
- for(var/organ in organs)
- if(no_brain || !istype(organ, /obj/item/organ/internal/brain))
- qdel(organ)
- else //we're going to drop all bodyparts except chest, so the only organs that needs spilling are those inside it.
- for(var/obj/item/organ/organ as anything in organs)
- if(no_brain && istype(organ, /obj/item/organ/internal/brain))
- qdel(organ) //so the brain isn't transferred to the head when the head drops.
- continue
- var/org_zone = check_zone(organ.zone) //both groin and chest organs.
- if(org_zone == BODY_ZONE_CHEST)
- organ.Remove(src)
- organ.forceMove(Tsec)
- organ.throw_at(get_edge_target_turf(src,pick(GLOB.alldirs)),rand(1,3),5)
- else
- for(var/obj/item/organ/organ as anything in organs)
- if(no_brain && istype(organ, /obj/item/organ/internal/brain))
- qdel(organ)
- continue
- if(no_organs && !istype(organ, /obj/item/organ/internal/brain))
- qdel(organ)
- continue
+
+ for(var/obj/item/organ/organ as anything in organs)
+ if((drop_bitflags & DROP_BRAIN) && istype(organ, /obj/item/organ/internal/brain))
+ if(drop_bitflags & DROP_BODYPARTS)
+ continue // the head will drop, so the brain should stay inside
+
organ.Remove(src)
organ.forceMove(Tsec)
- organ.throw_at(get_edge_target_turf(src,pick(GLOB.alldirs)),rand(1,3),5)
+ organ.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), rand(1,3), 5)
+ continue
+
+ if((drop_bitflags & DROP_ORGANS) && !istype(organ, /obj/item/organ/internal/brain))
+ if((drop_bitflags & DROP_BODYPARTS) && (check_zone(organ.zone) != BODY_ZONE_CHEST))
+ continue // only chest & groin organs will be ejected
+
+ organ.Remove(src)
+ organ.forceMove(Tsec)
+ organ.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), rand(1,3), 5)
+ continue
+
+ qdel(organ)
-/// Launches all bodyparts away from the mob. skip_head will keep the head attached.
-/mob/living/carbon/spread_bodyparts(skip_head = FALSE)
+/mob/living/carbon/spread_bodyparts(drop_bitflags=NONE)
for(var/obj/item/bodypart/part as anything in bodyparts)
- if(skip_head && part.body_zone == BODY_ZONE_HEAD)
+ if(!(drop_bitflags & DROP_BRAIN) && part.body_zone == BODY_ZONE_HEAD)
continue
else if(part.body_zone == BODY_ZONE_CHEST)
continue
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index 50817e54f8f535..22589f0944078b 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -449,6 +449,13 @@ GLOBAL_LIST_EMPTY(features_by_species)
wearer.hud_used?.update_locked_slots()
worn_items_fit_body_check(wearer)
+/**
+ * Normalizes blood in a human if it is excessive. If it is above BLOOD_VOLUME_NORMAL, this will clamp it to that value. It will not give the human more blodo than they have less than this value.
+ */
+/datum/species/proc/normalize_blood(mob/living/carbon/human/blood_possessing_human)
+ var/normalized_blood_values = max(blood_possessing_human.blood_volume, 0, BLOOD_VOLUME_NORMAL)
+ blood_possessing_human.blood_volume = normalized_blood_values
+
/**
* Proc called when a carbon becomes this species.
*
@@ -459,34 +466,37 @@ GLOBAL_LIST_EMPTY(features_by_species)
* * old_species - The species that the carbon used to be before becoming this race, used for regenerating organs.
* * pref_load - Preferences to be loaded from character setup, loads in preferred mutant things like bodyparts, digilegs, skin color, etc.
*/
-/datum/species/proc/on_species_gain(mob/living/carbon/human/C, datum/species/old_species, pref_load)
+/datum/species/proc/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load)
SHOULD_CALL_PARENT(TRUE)
// Drop the items the new species can't wear
- if(C.hud_used)
- C.hud_used.update_locked_slots()
+ if(human_who_gained_species.hud_used)
+ human_who_gained_species.hud_used.update_locked_slots()
- C.mob_biotypes = inherent_biotypes
- C.mob_respiration_type = inherent_respiration_type
- C.butcher_results = knife_butcher_results?.Copy()
+ human_who_gained_species.mob_biotypes = inherent_biotypes
+ human_who_gained_species.mob_respiration_type = inherent_respiration_type
+ human_who_gained_species.butcher_results = knife_butcher_results?.Copy()
if(old_species.type != type)
- replace_body(C, src)
+ replace_body(human_who_gained_species, src)
- regenerate_organs(C, old_species, visual_only = C.visual_only_organs)
+ regenerate_organs(human_who_gained_species, old_species, visual_only = human_who_gained_species.visual_only_organs)
// Drop the items the new species can't wear
- INVOKE_ASYNC(src, PROC_REF(worn_items_fit_body_check), C, TRUE)
+ INVOKE_ASYNC(src, PROC_REF(worn_items_fit_body_check), human_who_gained_species, TRUE)
//Assigns exotic blood type if the species has one
- if(exotic_bloodtype && C.dna.blood_type != exotic_bloodtype)
- C.dna.blood_type = exotic_bloodtype
+ if(exotic_bloodtype && human_who_gained_species.dna.blood_type != exotic_bloodtype)
+ human_who_gained_species.dna.blood_type = exotic_bloodtype
//Otherwise, check if the previous species had an exotic bloodtype and we do not have one and assign a random blood type
//(why the fuck is blood type not tied to a fucking DNA block?)
else if(old_species.exotic_bloodtype && !exotic_bloodtype)
- C.dna.blood_type = random_blood_type()
+ human_who_gained_species.dna.blood_type = random_blood_type()
+
+ //Resets blood if it is excessively high so they don't gib
+ normalize_blood(human_who_gained_species)
- if(ishuman(C))
- var/mob/living/carbon/human/human = C
+ if(ishuman(human_who_gained_species))
+ var/mob/living/carbon/human/human = human_who_gained_species
for(var/obj/item/organ/external/organ_path as anything in external_organs)
if(!should_external_organ_apply_to(organ_path, human))
continue
@@ -495,25 +505,27 @@ GLOBAL_LIST_EMPTY(features_by_species)
var/obj/item/organ/external/new_organ = SSwardrobe.provide_type(organ_path)
new_organ.Insert(human, special=TRUE, drop_if_replaced=FALSE)
+
+
if(length(inherent_traits))
- C.add_traits(inherent_traits, SPECIES_TRAIT)
+ human_who_gained_species.add_traits(inherent_traits, SPECIES_TRAIT)
if(inherent_factions)
for(var/i in inherent_factions)
- C.faction += i //Using +=/-= for this in case you also gain the faction from a different source.
+ human_who_gained_species.faction += i //Using +=/-= for this in case you also gain the faction from a different source.
// All languages associated with this language holder are added with source [LANGUAGE_SPECIES]
// rather than source [LANGUAGE_ATOM], so we can track what to remove if our species changes again
var/datum/language_holder/gaining_holder = GLOB.prototype_language_holders[species_language_holder]
for(var/language in gaining_holder.understood_languages)
- C.grant_language(language, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES)
+ human_who_gained_species.grant_language(language, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES)
for(var/language in gaining_holder.spoken_languages)
- C.grant_language(language, SPOKEN_LANGUAGE, LANGUAGE_SPECIES)
+ human_who_gained_species.grant_language(language, SPOKEN_LANGUAGE, LANGUAGE_SPECIES)
for(var/language in gaining_holder.blocked_languages)
- C.add_blocked_language(language, LANGUAGE_SPECIES)
- C.regenerate_icons()
+ human_who_gained_species.add_blocked_language(language, LANGUAGE_SPECIES)
+ human_who_gained_species.regenerate_icons()
- SEND_SIGNAL(C, COMSIG_SPECIES_GAIN, src, old_species)
+ SEND_SIGNAL(human_who_gained_species, COMSIG_SPECIES_GAIN, src, old_species)
properly_gained = TRUE
@@ -1444,7 +1456,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
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 1
+ return TRUE
/datum/species/proc/on_hit(obj/projectile/P, mob/living/carbon/human/H)
// called when hit by a projectile
@@ -1893,12 +1905,21 @@ GLOBAL_LIST_EMPTY(features_by_species)
/datum/species/proc/on_owner_login(mob/living/carbon/human/owner)
return
+/**
+ * Gets a description of the species' *physical* attributes. What makes playing as one different. Used in magic mirrors.
+ *
+ * Returns a string.
+ */
+
+/datum/species/proc/get_physical_attributes()
+ return "An unremarkable species."
/**
* Gets a short description for the specices. Should be relatively succinct.
* Used in the preference menu.
*
* Returns a string.
*/
+
/datum/species/proc/get_species_description()
SHOULD_CALL_PARENT(FALSE)
diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm
index 90b94689796c16..3c0ffb2536b790 100644
--- a/code/modules/mob/living/carbon/human/death.dm
+++ b/code/modules/mob/living/carbon/human/death.dm
@@ -5,8 +5,8 @@ GLOBAL_LIST_EMPTY(dead_players_during_shift)
/mob/living/carbon/human/dust_animation()
new /obj/effect/temp_visual/dust_animation(loc, dna.species.dust_anim)
-/mob/living/carbon/human/spawn_gibs(with_bodyparts)
- if(with_bodyparts)
+/mob/living/carbon/human/spawn_gibs(drop_bitflags=NONE)
+ if(drop_bitflags & DROP_BODYPARTS)
new /obj/effect/gibspawner/human(drop_location(), src, get_static_viruses())
else
new /obj/effect/gibspawner/human/bodypartless(drop_location(), src, get_static_viruses())
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 680e5c14f7ea6e..0905a4ac40ff83 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -733,7 +733,7 @@
if(heal_flags & HEAL_NEGATIVE_MUTATIONS)
for(var/datum/mutation/human/existing_mutation in dna.mutations)
if(existing_mutation.quality != POSITIVE)
- dna.remove_mutation(existing_mutation.name)
+ dna.remove_mutation(existing_mutation)
if(heal_flags & HEAL_TEMP)
set_coretemperature(get_body_temp_normal(apply_change = FALSE))
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index b02aaf03b17bf4..233f9fe25fd29e 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -414,7 +414,7 @@
if(EXPLODE_LIGHT)
SSexplosions.low_mov_atom += thing
investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
return TRUE
else
brute_loss = 500
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index fb015a237d04d0..cfda7721da0c97 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -5,7 +5,7 @@
icon = 'icons/mob/human/human.dmi'
icon_state = "human_basic"
appearance_flags = KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE
- hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPCHEM_HUD,IMPTRACK_HUD,ANTAG_HUD,GLAND_HUD,SENTIENT_DISEASE_HUD,FAN_HUD,PERMIT_HUD) //SKYRAT EDIT: ADD PERMIT_HUD
+ hud_possible = list(HEALTH_HUD,STATUS_HUD,ID_HUD,WANTED_HUD,IMPLOYAL_HUD,IMPCHEM_HUD,IMPTRACK_HUD,ANTAG_HUD,GLAND_HUD,SENTIENT_DISEASE_HUD,FAN_HUD,PERMIT_HUD, DNR_HUD) //SKYRAT EDIT ADDITION - PERMIT_HUD, DNR_HUD
hud_type = /datum/hud/human
pressure_resistance = 25
can_buckle = TRUE
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 bb112144447fd5..42eb727a83d331 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -87,7 +87,6 @@ There are several things that need to be remembered:
if(check_obscured_slots(transparent_protection = TRUE) & ITEM_SLOT_ICLOTHING)
return
-
var/target_overlay = uniform.icon_state
if(uniform.adjusted == ALT_STYLE)
target_overlay = "[target_overlay]_d"
@@ -102,19 +101,29 @@ There are several things that need to be remembered:
var/handled_by_bodytype = TRUE
var/icon_file
var/woman
+ var/digi // SKYRAT EDIT ADDITION - Digi female gender shaping
+ var/female_sprite_flags = uniform.female_sprite_flags // SKYRAT EDIT ADDITION - Digi female gender shaping
var/mutant_styles = NONE // SKYRAT EDIT ADDITON - mutant styles to pass down to build_worn_icon.
//BEGIN SPECIES HANDLING
if((bodytype & BODYTYPE_MONKEY) && (uniform.supports_variations_flags & CLOTHING_MONKEY_VARIATION))
- icon_file = dna.species.generate_custom_worn_icon(LOADOUT_ITEM_UNIFORM, w_uniform, src) // SKYRAT EDIT CHANGE
+ icon_file = dna.species.generate_custom_worn_icon(LOADOUT_ITEM_UNIFORM, w_uniform, src) // SKYRAT EDIT CHANGE - ORIGINAL: icon_file = MONKEY_UNIFORM_FILE
else if((bodytype & BODYTYPE_DIGITIGRADE) && (uniform.supports_variations_flags & CLOTHING_DIGITIGRADE_VARIATION))
- icon_file = uniform.worn_icon_digi || DIGITIGRADE_UNIFORM_FILE // SKYRAT EDIT CHANGE
+ icon_file = uniform.worn_icon_digi || DIGITIGRADE_UNIFORM_FILE // SKYRAT EDIT CHANGE - ORIGINAL: icon_file = DIGITIGRADE_UNIFORM_FILE
+ digi = TRUE // SKYRAT EDIT ADDITION - Digi female gender shaping
// SKYRAT EDIT ADDITION - birbs
else if(bodytype & BODYTYPE_CUSTOM)
icon_file = dna.species.generate_custom_worn_icon(LOADOUT_ITEM_UNIFORM, w_uniform, src) // Might have to refactor how this works eventually, maybe.
// SKYRAT EDIT END
//Female sprites have lower priority than digitigrade sprites
- else if(dna.species.sexes && (bodytype & BODYTYPE_HUMANOID) && physique == FEMALE && !(uniform.female_sprite_flags & NO_FEMALE_UNIFORM)) //Agggggggghhhhh
+ if(dna.species.sexes && (bodytype & BODYTYPE_HUMANOID) && physique == FEMALE && !(female_sprite_flags & NO_FEMALE_UNIFORM)) //Agggggggghhhhh // SKYRAT EDIT CHANGE - ORIGINAL: else if(dna.species.sexes && (bodytype & BODYTYPE_HUMANOID) && physique == FEMALE && !(uniform.female_sprite_flags & NO_FEMALE_UNIFORM))
woman = TRUE
+ // SKYRAT EDIT ADDITION START - Digi female gender shaping
+ if(digi)
+ mutant_styles |= STYLE_DIGI // for passing to wear_female_version
+ if(!(female_sprite_flags & FEMALE_UNIFORM_DIGI_FULL))
+ female_sprite_flags &= ~FEMALE_UNIFORM_FULL // clear the FEMALE_UNIFORM_DIGI_FULL bit if it was set, we don't want that.
+ female_sprite_flags |= FEMALE_UNIFORM_TOP_ONLY // And set the FEMALE_UNIFORM_TOP bit if it is unset.
+ // SKYRAT EDIT ADDITION END
if(!icon_exists(icon_file, RESOLVE_ICON_STATE(uniform)))
icon_file = DEFAULT_UNIFORM_FILE
@@ -131,7 +140,7 @@ There are several things that need to be remembered:
default_layer = UNIFORM_LAYER,
default_icon_file = icon_file,
isinhands = FALSE,
- female_uniform = woman ? uniform.female_sprite_flags : null,
+ female_uniform = woman ? female_sprite_flags : null, // SKYRAT EDIT CHANGE - Digi female gender shaping - ORIGINAL: female_uniform = woman ? uniform.female_sprite_flags : null,
override_state = target_overlay,
override_file = handled_by_bodytype ? icon_file : null,
mutant_styles = mutant_styles, // SKYRAT EDIT ADDITION - Taur-friendly uniforms!
@@ -157,7 +166,9 @@ There are several things that need to be remembered:
var/obj/item/worn_item = wear_id
update_hud_id(worn_item)
var/icon_file = 'icons/mob/clothing/id.dmi'
+
id_overlay = wear_id.build_worn_icon(default_layer = ID_LAYER, default_icon_file = icon_file)
+
if(!id_overlay)
return
@@ -167,6 +178,7 @@ There are several things that need to be remembered:
apply_overlay(ID_LAYER)
+
/mob/living/carbon/human/update_worn_gloves()
remove_overlay(GLOVES_LAYER)
@@ -644,12 +656,7 @@ There are several things that need to be remembered:
apply_overlay(LEGCUFF_LAYER)
throw_alert("legcuffed", /atom/movable/screen/alert/restrained/legcuffed, new_master = src.legcuffed)
-/mob/living/carbon/human/update_held_items()
- remove_overlay(HANDS_LAYER)
- if (handcuffed)
- drop_all_held_items()
- return
-
+/mob/living/carbon/human/get_held_overlays()
var/list/hands = list()
for(var/obj/item/worn_item in held_items)
var/held_index = get_held_index_of_item(worn_item)
@@ -678,11 +685,10 @@ There are several things that need to be remembered:
held_in_hand?.held_hand_offset?.apply_offset(hand_overlay)
hands += hand_overlay
- overlays_standing[HANDS_LAYER] = hands
- apply_overlay(HANDS_LAYER)
+ return hands
-/proc/wear_female_version(t_color, icon, layer, type, greyscale_colors)
- var/index = "[t_color]-[greyscale_colors]"
+/proc/wear_female_version(t_color, icon, layer, type, greyscale_colors, mutant_styles) // SKYRAT EDIT CHANGE - Digi female gender shaping - ORIGINAL: /proc/wear_female_version(t_color, icon, layer, type, greyscale_colors)
+ var/index = "[t_color]-[greyscale_colors][(mutant_styles & STYLE_DIGI) ? "-d" : ""]" // SKYRAT EDIT CHANGE - Digi female gender shaping - Original: var/index = "[t_color]-[greyscale_colors]]"
var/icon/female_clothing_icon = GLOB.female_clothing_icons[index]
if(!female_clothing_icon) //Create standing/laying icons if they don't exist
generate_female_clothing(index, t_color, icon, type)
@@ -834,7 +840,7 @@ mutant_styles: The mutant style - taur bodytype, STYLE_TESHARI, etc. // SKYRAT E
var/mutable_appearance/standing
if(female_uniform)
- standing = wear_female_version(t_state, file2use, layer2use, female_uniform, greyscale_colors) //should layer2use be in sync with the adjusted value below? needs testing - shiz
+ standing = wear_female_version(t_state, file2use, layer2use, female_uniform, greyscale_colors, mutant_styles) //should layer2use be in sync with the adjusted value below? needs testing - shiz // SKYRAT EDIT CHANGE - ORIGINAL: standing = wear_female_version(t_state, file2use, layer2use, female_uniform, greyscale_colors)
if(!standing)
standing = mutable_appearance(file2use, t_state, -layer2use)
// SKYRAT EDIT ADDITION START - Taur-friendly uniforms and suits
@@ -879,7 +885,6 @@ mutant_styles: The mutant style - taur bodytype, STYLE_TESHARI, etc. // SKYRAT E
return standing
-
/// Returns offsets used for equipped item overlays in list(px_offset,py_offset) form.
/obj/item/proc/get_worn_offsets(isinhands)
. = list(0,0) //(px,py)
diff --git a/code/modules/mob/living/carbon/human/species_types/abductors.dm b/code/modules/mob/living/carbon/human/species_types/abductors.dm
index 58a46da81a4882..349742a15e5368 100644
--- a/code/modules/mob/living/carbon/human/species_types/abductors.dm
+++ b/code/modules/mob/living/carbon/human/species_types/abductors.dm
@@ -28,6 +28,11 @@
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/abductor,
)
+
+/datum/species/abductor/get_physical_attributes()
+ return "Abductors do not need to breathe, eat, do not have blood, a heart, stomach, or lungs and cannot be infected by human viruses. \
+ Their hardy physique prevents their skin from being wounded or dismembered, but their chunky tridactyl hands make it hard to operate human equipment."
+
/datum/species/abductor/on_species_gain(mob/living/carbon/C, datum/species/old_species)
. = ..()
var/datum/atom_hud/abductor_hud = GLOB.huds[DATA_HUD_ABDUCTOR]
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 e0da3a4ecbf3e6..ed8163a153889b 100644
--- a/code/modules/mob/living/carbon/human/species_types/android.dm
+++ b/code/modules/mob/living/carbon/human/species_types/android.dm
@@ -51,3 +51,8 @@
. = ..()
// Androids don't eat, hunger or metabolise foods. Let's do some cleanup.
C.set_safe_hunger_level()
+
+/datum/species/android/get_physical_attributes()
+ return "Androids are almost, but not quite, identical to fully augmented humans. \
+ Unlike those, though, they're completely immune to toxin damage, don't have blood or organs (besides their head), don't get hungry, and can reattach their limbs! \
+ That said, an EMP will devastate them and they cannot process any chemicals."
diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm
index 8b429bbeb8f6bd..c9609f2d43baff 100644
--- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm
+++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm
@@ -74,7 +74,7 @@
if(QDELETED(my_head))
my_head = null
human.investigate_log("has been gibbed by the loss of [human.p_their()] head.", INVESTIGATE_DEATHS)
- human.gib()
+ human.gib(DROP_ALL_REMAINS)
return
if(my_head.loc.name != human.real_name && istype(my_head.loc, /obj/item/bodypart/head))
@@ -87,7 +87,7 @@
if(illegal_head)
my_head = null
human.investigate_log("has been gibbed by having an illegal head put on [human.p_their()] shoulders.", INVESTIGATE_DEATHS)
- human.gib() // Yeah so giving them a head on their body is really not a good idea, so their original head will remain but uh, good luck fixing it after that.
+ human.gib(DROP_ALL_REMAINS) // Yeah so giving them a head on their body is really not a good idea, so their original head will remain but uh, good luck fixing it after that.
/datum/species/dullahan/proc/update_vision_perspective(mob/living/carbon/human/human)
var/obj/item/organ/internal/eyes/eyes = human.get_organ_slot(ORGAN_SLOT_EYES)
@@ -114,6 +114,8 @@
eyes_toggle_perspective_action?.Trigger()
owner_first_client_connection_handled = TRUE
+/datum/species/dullahan/get_physical_attributes()
+ return "A dullahan is much like a human, but their head is detached from their body and must be carried around."
/datum/species/dullahan/get_species_description()
return "An angry spirit, hanging onto the land of the living for \
@@ -265,7 +267,7 @@
if(isdullahan(human))
var/datum/species/dullahan/dullahan_species = human.dna.species
dullahan_species.my_head = null
- owner.gib()
+ owner.gib(DROP_ALL_REMAINS)
owner = null
return ..()
diff --git a/code/modules/mob/living/carbon/human/species_types/ethereal.dm b/code/modules/mob/living/carbon/human/species_types/ethereal.dm
index 42c718477bce25..4c28d190efff4a 100644
--- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm
+++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm
@@ -209,6 +209,11 @@
'sound/voice/ethereal/ethereal_scream_3.ogg',
)
+/datum/species/ethereal/get_physical_attributes()
+ return "Ethereals process electricity as their power supply, not food, and are somewhat resistant to it.\
+ They do so via their crystal core, their equivalent of a human heart, which will also encase them in a reviving crystal if they die.\
+ However, their skin is very thin and easy to pierce with brute weaponry."
+
/datum/species/ethereal/get_species_description()
return "Coming from the planet of Sprout, the theocratic ethereals are \
separated socially by caste, and espouse a dogma of aiding the weak and \
@@ -272,7 +277,7 @@
TRAIT_FIXED_MUTANT_COLORS,
TRAIT_FIXED_HAIRCOLOR,
TRAIT_AGENDER,
- TRAIT_TENACIOUS,
+ TRAIT_TENACIOUS, // this doesn't work. tenacity is an element
TRAIT_NOBREATH,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
@@ -287,6 +292,10 @@
BODY_ZONE_CHEST = /obj/item/bodypart/chest/ethereal,
)
+/datum/species/ethereal/lustrous/get_physical_attributes()
+ return "Lustrous are what remains of an Ethereal after freebasing esoteric drugs. \
+ They are pressure immune, virus immune, can see bluespace tears in reality, and have a really weird scream. They remain vulnerable to physical damage."
+
/datum/species/ethereal/lustrous/get_scream_sound(mob/living/carbon/human/ethereal)
return pick(
'sound/voice/ethereal/lustrous_scream_1.ogg',
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 c8e14a6afd12d5..61647c5aba23cf 100644
--- a/code/modules/mob/living/carbon/human/species_types/felinid.dm
+++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm
@@ -147,6 +147,10 @@
human_for_preview.update_body(TRUE)
// SKYRAT EDIT END
+/datum/species/human/felinid/get_physical_attributes()
+ return "Felinids are very similar to humans in almost all respects, with their biggest differences being the ability to lick their wounds, \
+ and an increased sensitivity to noise, which is often detrimental. They are also rather fond of eating oranges."
+
/datum/species/human/felinid/get_species_description()
return "Felinids are one of the many types of bespoke genetic \
modifications to come of humanity's mastery of genetic science, and are \
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 7f1d1112115693..c36252bbcb2dc9 100644
--- a/code/modules/mob/living/carbon/human/species_types/flypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
@@ -37,6 +37,9 @@
return 30 //Flyswatters deal 30x damage to flypeople.
return 1
+/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."
+
/datum/species/fly/get_species_description()
return "With no official documentation or knowledge of the origin of \
this species, they remain a mystery to most. Any and all rumours among \
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 3f5f46edf6c3ac..91ec9dfe0c5c57 100644
--- a/code/modules/mob/living/carbon/human/species_types/golems.dm
+++ b/code/modules/mob/living/carbon/human/species_types/golems.dm
@@ -58,6 +58,10 @@
name += " [pick(GLOB.last_names)]"
return name
+/datum/species/golem/get_physical_attributes()
+ return "Golems are hardy creatures made out of stone, which are thus naturally resistant to many dangers, including asphyxiation, fire, radiation, electricity, and viruses.\
+ They gain special abilities depending on the type of material consumed, but they need to consume material to keep their body animated."
+
/datum/species/golem/create_pref_unique_perks()
var/list/to_add = list()
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 2c997274ffb9e8..9db5a3253eb60d 100644
--- a/code/modules/mob/living/carbon/human/species_types/humans.dm
+++ b/code/modules/mob/living/carbon/human/species_types/humans.dm
@@ -18,7 +18,7 @@
human_mob.skin_tone = random_skin_tone()
/datum/species/human/get_scream_sound(mob/living/carbon/human/human)
- if(human.gender == MALE)
+ if(human.physique == MALE)
if(prob(1))
return 'sound/voice/human/wilhelm_scream.ogg'
return pick(
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 82f0e88c2bd790..2f5c977110a351 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -201,6 +201,11 @@
BODY_ZONE_CHEST = /obj/item/bodypart/chest/slime,
)
+/datum/species/jelly/slime/get_physical_attributes()
+ return "Slimepeople have jelly for blood and their vacuoles can extremely quickly convert plasma to it if they're breathing it in.\
+ They can then use the excess blood to split off an excess body, which their consciousness can transfer to at will or on death.\
+ Most things that are toxic heal them, but most things that prevent toxicity damage them!"
+
/datum/species/jelly/slime/on_species_loss(mob/living/carbon/C)
if(slime_split)
slime_split.Remove(C)
@@ -501,6 +506,10 @@
/// The cooldown of us using exteracts
COOLDOWN_DECLARE(extract_cooldown)
+/datum/species/jelly/luminescent/get_physical_attributes()
+ return "Luminescent are able to integrate slime extracts into themselves for wondrous effects. \
+ Most things that are toxic heal them, but most things that prevent toxicity damage them!"
+
//Species datums don't normally implement destroy, but JELLIES SUCK ASS OUT OF A STEEL STRAW and have to i guess
/datum/species/jelly/luminescent/Destroy(force)
current_extract = null
@@ -670,6 +679,10 @@
/// Special "project thought" telepathy action for stargazers.
var/datum/action/innate/project_thought/project_action
+/datum/species/jelly/stargazer/get_physical_attributes()
+ return "Stargazers can link others' minds with their own, creating a private communication channel. \
+ Most things that are toxic heal them, but most things that prevent toxicity damage them!"
+
/datum/species/jelly/stargazer/on_species_gain(mob/living/carbon/grant_to, datum/species/old_species)
. = ..()
project_action = new(src)
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 68420fe460fc14..020cd7b4ebf89b 100644
--- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
@@ -89,6 +89,10 @@
'sound/voice/lizard/lizard_scream_3.ogg',
)
+/datum/species/lizard/get_physical_attributes()
+ return "Lizardpeople can withstand slightly higher temperatures than most species, but they are very vulnerable to the cold \
+ and can't regulate their body-temperature internally, making the vacuum of space extremely deadly to them."
+
/datum/species/lizard/get_species_description()
return "The militaristic Lizardpeople hail originally from Tizira, but have grown \
throughout their centuries in the stars to possess a large spacefaring \
@@ -155,6 +159,10 @@ Lizard subspecies: ASHWALKERS
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/lizard,
)
+/datum/species/lizard/get_physical_attributes()
+ return "Ash Walkers are identical to lizardpeople in almost all aspects. \
+ Unlike them, they're always digitigrade, they can breathe Lavaland's often noxious atmosphere and resist viruses. They are usually illiterate."
+
/*
Lizard subspecies: SILVER SCALED
*/
@@ -184,6 +192,11 @@ Lizard subspecies: SILVER SCALED
///See above
var/old_eye_color_right
+/datum/species/lizard/silverscale/get_physical_attributes()
+ return "Silver Scales are to lizardpeople what angels are to humans. \
+ Mostly identical, they are holy, don't breathe, don't get viruses, their hide cannot be pierced, love the taste of wine, \
+ and their tongue allows them to turn into a statue, for some reason."
+
/datum/species/lizard/silverscale/on_species_gain(mob/living/carbon/human/new_silverscale, datum/species/old_species, pref_load)
old_mutcolor = new_silverscale.dna.features["mcolor"]
old_eye_color_left = new_silverscale.eye_color_left
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 243c1be83b3587..2d054c67c71208 100644
--- a/code/modules/mob/living/carbon/human/species_types/monkeys.dm
+++ b/code/modules/mob/living/carbon/human/species_types/monkeys.dm
@@ -133,6 +133,10 @@
'sound/creatures/monkey/monkey_screech_7.ogg',
)
+/datum/species/monkey/get_physical_attributes()
+ return "Monkeys are slippery, can crawl into vents, and are more dextrous than humans.. but only when stealing things. \
+ Natural monkeys cannot operate machinery or most tools with their paws, but unusually clever monkeys or those that were once something else can."
+
/datum/species/monkey/get_species_description()
return "Monkeys are a type of primate that exist between humans and animals on the evolutionary chain. \
Every year, on Monkey Day, Nanotrasen shows their respect for the little guys by allowing them to roam the station freely."
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 9f576e03f0721c..adb5ffa62aab87 100644
--- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
@@ -16,6 +16,7 @@
mutanteyes = /obj/item/organ/internal/eyes/moth
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)
@@ -58,6 +59,10 @@
/datum/species/moth/get_scream_sound(mob/living/carbon/human/human)
return 'sound/voice/moth/scream_moth.ogg'
+/datum/species/moth/get_physical_attributes()
+ return "Moths have large and fluffy wings, which help them navigate the station if gravity is offline by pushing the air around them. \
+ Due to that, it isn't of much use out in space. Their eyes are very sensitive."
+
/datum/species/moth/get_species_description()
return "Hailing from a planet that was lost long ago, the moths travel \
the galaxy as a nomadic people aboard a colossal fleet of ships, seeking a new homeland."
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 910c10a099a81b..67f57f75a5ad25 100644
--- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
@@ -145,6 +145,10 @@
'sound/voice/plasmaman/plasmeme_scream_3.ogg',
)
+/datum/species/plasmaman/get_physical_attributes()
+ return "Plasmamen literally breathe and live plasma. They spontaneously combust on contact with oxygen, and besides all the quirks that go with that, \
+ they're very vulnerable to all kinds of physical damage due to their brittle structure."
+
/datum/species/plasmaman/get_species_description()
return "Found on the Icemoon of Freyja, plasmamen consist of colonial \
fungal organisms which together form a sentient being. In human space, \
diff --git a/code/modules/mob/living/carbon/human/species_types/podpeople.dm b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
index 3703f2527ac95d..0b645fd120a1c4 100644
--- a/code/modules/mob/living/carbon/human/species_types/podpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
@@ -43,26 +43,29 @@
)
return ..()
-/datum/species/pod/spec_life(mob/living/carbon/human/H, seconds_per_tick, times_fired)
+/datum/species/pod/spec_life(mob/living/carbon/human/podperson, seconds_per_tick, times_fired)
. = ..()
- if(H.stat == DEAD)
+ if(podperson.stat == DEAD)
return
var/light_amount = 0 //how much light there is in the place, affects receiving nutrition and healing
- if(isturf(H.loc)) //else, there's considered to be no light
- var/turf/T = H.loc
- light_amount = min(1, T.get_lumcount()) - 0.5
- H.adjust_nutrition(5 * light_amount * seconds_per_tick)
+ if(isturf(podperson.loc)) //else, there's considered to be no light
+ var/turf/turf_loc = podperson.loc
+ light_amount = min(1, turf_loc.get_lumcount()) - 0.5
+ podperson.adjust_nutrition(5 * light_amount * seconds_per_tick)
if(light_amount > 0.2) //if there's enough light, heal
- H.heal_overall_damage(brute = 0.5 * seconds_per_tick, burn = 0.5 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
- H.adjustToxLoss(-0.5 * seconds_per_tick)
- H.adjustOxyLoss(-0.5 * seconds_per_tick)
+ var/need_mob_update = FALSE
+ need_mob_update += podperson.heal_overall_damage(brute = 0.5 * seconds_per_tick, burn = 0.5 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += podperson.adjustToxLoss(-0.5 * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += podperson.adjustOxyLoss(-0.5 * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ podperson.updatehealth()
- if(H.nutrition > NUTRITION_LEVEL_ALMOST_FULL) //don't make podpeople fat because they stood in the sun for too long
- H.set_nutrition(NUTRITION_LEVEL_ALMOST_FULL)
+ if(podperson.nutrition > NUTRITION_LEVEL_ALMOST_FULL) //don't make podpeople fat because they stood in the sun for too long
+ podperson.set_nutrition(NUTRITION_LEVEL_ALMOST_FULL)
- if(H.nutrition < NUTRITION_LEVEL_STARVING + 50)
- H.take_overall_damage(brute = 1 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
+ if(podperson.nutrition < NUTRITION_LEVEL_STARVING + 50)
+ podperson.take_overall_damage(brute = 1 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
/datum/species/pod/handle_chemical(datum/reagent/chem, mob/living/carbon/human/affected, seconds_per_tick, times_fired)
. = ..()
@@ -71,13 +74,9 @@
if(chem.type == /datum/reagent/toxin/plantbgone)
affected.adjustToxLoss(3 * REM * seconds_per_tick)
-// SKYRAT EDIT ADDITION
-/datum/species/pod/get_species_description()
- return "Plant lore!"
-
-/datum/species/pod/get_species_lore()
- return list("You're a plant!")
-// SKYRAT EDIT END
+/datum/species/pod/get_physical_attributes()
+ return "Podpeople are in many ways the inverse of shadows, healing in light and starving with the dark. \
+ Their bodies are like tinder and easy to char."
/datum/species/pod/create_pref_unique_perks()
var/list/to_add = list()
diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
index a8b1be203a3b36..1bd77a4356f08a 100644
--- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
@@ -37,6 +37,9 @@
return TRUE
return ..()
+/datum/species/shadow/get_physical_attributes()
+ return "These cursed creatures heal in the dark, but suffer in the light much more heavily. Their eyes let them see in the dark as though it were day."
+
/datum/species/shadow/get_species_description()
return "Victims of a long extinct space alien. Their flesh is a sickly \
seethrough filament, their tangled insides in clear view. Their form \
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 3ac0483f8ce031..67051c20607252 100644
--- a/code/modules/mob/living/carbon/human/species_types/skeletons.dm
+++ b/code/modules/mob/living/carbon/human/species_types/skeletons.dm
@@ -56,6 +56,11 @@
return TRUE
return ..()
+/datum/species/skeleton/get_physical_attributes()
+ return "These humerus folk lack any fleshy biology, which allows them to resist pressure, temperature, radiation, asphyxiation and even toxins. \
+ However, due to that same fact, it is quite hard to heal them as well. The calcium found in common space milk is highly effective at treating their wounds. \
+ Their limbs are easy to pop off their joints, but they can somehow just slot them back in."
+
/datum/species/skeleton/get_species_description()
return "A rattling skeleton! They descend upon Space Station 13 \
Every year to spook the crew! \"I've got a BONE to pick with you!\""
diff --git a/code/modules/mob/living/carbon/human/species_types/snail.dm b/code/modules/mob/living/carbon/human/species_types/snail.dm
index 689a02cfbec000..036b5ad5c24746 100644
--- a/code/modules/mob/living/carbon/human/species_types/snail.dm
+++ b/code/modules/mob/living/carbon/human/species_types/snail.dm
@@ -28,6 +28,11 @@
BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/snail
)
+
+/datum/species/snail/get_physical_attributes()
+ return "Snailpeople emit a viscous, slippery ooze when crawling along the ground, which they are somewhat faster at than other species. \
+ They are almost purely made of water, making them extremely susceptible to shocks, and salt will scour them heavily."
+
/datum/species/snail/handle_chemical(datum/reagent/chem, mob/living/carbon/human/affected, seconds_per_tick, times_fired)
. = ..()
if(. & COMSIG_MOB_STOP_REAGENT_CHECK)
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 d24e5f3d2727e9..c07f0478c03ea5 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -44,10 +44,13 @@
/datum/species/vampire/spec_life(mob/living/carbon/human/vampire, seconds_per_tick, times_fired)
. = ..()
if(istype(vampire.loc, /obj/structure/closet/crate/coffin))
- vampire.heal_overall_damage(brute = 2 * seconds_per_tick, burn = 2 * seconds_per_tick, required_bodytype = BODYTYPE_ORGANIC)
- vampire.adjustToxLoss(-2 * seconds_per_tick)
- vampire.adjustOxyLoss(-2 * seconds_per_tick)
- vampire.adjustCloneLoss(-2 * seconds_per_tick)
+ var/need_mob_update = FALSE
+ need_mob_update += vampire.heal_overall_damage(brute = 2 * seconds_per_tick, burn = 2 * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ need_mob_update += vampire.adjustToxLoss(-2 * seconds_per_tick, updating_health = FALSE,)
+ need_mob_update += vampire.adjustOxyLoss(-2 * seconds_per_tick, updating_health = FALSE,)
+ need_mob_update += vampire.adjustCloneLoss(-2 * seconds_per_tick, updating_health = FALSE,)
+ if(need_mob_update)
+ vampire.updatehealth()
return
vampire.blood_volume -= 0.125 * seconds_per_tick
if(vampire.blood_volume <= BLOOD_VOLUME_SURVIVE)
@@ -66,6 +69,10 @@
return 2 //Whips deal 2x damage to vampires. Vampire killer.
return 1
+/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. \
+ They will instantly turn into dust if they run out of blood or enter a holy area. However, coffins stabilize and heal them, and they can transform into bats!"
+
/datum/species/vampire/get_species_description()
return "A classy Vampire! They descend upon Space Station Thirteen Every year to spook the crew! \"Bleeg!!\""
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 d8b2125aa64e43..4f9b6f1b4af0bd 100644
--- a/code/modules/mob/living/carbon/human/species_types/zombies.dm
+++ b/code/modules/mob/living/carbon/human/species_types/zombies.dm
@@ -66,6 +66,10 @@
return TRUE
return ..()
+/datum/species/zombie/get_physical_attributes()
+ return "Zombies are undead, and thus completely immune to any enviromental hazard, or any physical threat besides blunt force trauma and burns. \
+ Their limbs are easy to pop off their joints, but they can somehow just slot them back in."
+
/datum/species/zombie/get_species_description()
return "A rotting zombie! They descend upon Space Station Thirteen Every year to spook the crew! \"Sincerely, the Zombies!\""
@@ -151,24 +155,27 @@
if(.)
COOLDOWN_START(src, regen_cooldown, REGENERATION_DELAY)
-/datum/species/zombie/infectious/spec_life(mob/living/carbon/C, seconds_per_tick, times_fired)
+/datum/species/zombie/infectious/spec_life(mob/living/carbon/carbon_mob, seconds_per_tick, times_fired)
. = ..()
- C.set_combat_mode(TRUE) // THE SUFFERING MUST FLOW
+ 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(C, TRAIT_CRITICAL_CONDITION))
+ if(HAS_TRAIT(carbon_mob, TRAIT_CRITICAL_CONDITION))
heal_amt *= 2
- C.heal_overall_damage(heal_amt * seconds_per_tick, heal_amt * seconds_per_tick)
- C.adjustToxLoss(-heal_amt * seconds_per_tick)
- for(var/i in C.all_wounds)
+ 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(C, TRAIT_CRITICAL_CONDITION) && SPT_PROB(2, seconds_per_tick))
- playsound(C, pick(spooks), 50, TRUE, 10)
+ 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)
@@ -192,7 +199,7 @@
// Your skin falls off
/datum/species/human/krokodil_addict
- name = "\improper Human"
+ name = "\improper Krokodil Human"
id = SPECIES_ZOMBIE_KROKODIL
examine_limb_id = SPECIES_HUMAN
changesource_flags = MIRROR_BADMIN | WABBAJACK | ERT_SPAWN
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 990f27f07070b8..4da9129b1fecd7 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -37,9 +37,8 @@
if(getStaminaLoss() > 0 && stam_regen_start_time <= world.time)
adjustStaminaLoss(-INFINITY)
- var/bprv = handle_bodyparts(seconds_per_tick, times_fired)
- if(bprv & BODYPART_LIFE_UPDATE_HEALTH)
- updatehealth()
+
+ handle_bodyparts(seconds_per_tick, times_fired)
if(. && mind) //. == not dead
for(var/key in mind.addiction_points)
@@ -410,10 +409,13 @@
//-- NITRIUM --//
if(nitrium_pp)
+ var/need_mob_update = FALSE
if(nitrium_pp > 0.5)
- adjustFireLoss(nitrium_pp * 0.15)
+ need_mob_update += adjustFireLoss(nitrium_pp * 0.15, updating_health = FALSE)
if(nitrium_pp > 5)
- adjustToxLoss(nitrium_pp * 0.05)
+ need_mob_update += adjustToxLoss(nitrium_pp * 0.05, updating_health = FALSE)
+ if(need_mob_update)
+ updatehealth()
// Handle chemical euphoria mood event, caused by N2O.
if (n2o_euphoria == EUPHORIA_ACTIVE)
@@ -492,7 +494,8 @@
return
for(var/obj/item/organ/internal/organ in organs)
// On-death is where organ decay is handled
- organ?.on_death(seconds_per_tick, times_fired) // organ can be null due to reagent metabolization causing organ shuffling
+ if(organ?.owner) // organ + owner can be null due to reagent metabolization causing organ shuffling
+ organ.on_death(seconds_per_tick, times_fired)
// We need to re-check the stat every organ, as one of our others may have revived us
if(stat != DEAD)
break
@@ -757,7 +760,7 @@
if(HAS_TRAIT(src, TRAIT_STABLELIVER) || HAS_TRAIT(src, TRAIT_LIVERLESS_METABOLISM))
return
- adjustToxLoss(0.6 * seconds_per_tick, TRUE, TRUE)
+ adjustToxLoss(0.6 * seconds_per_tick, forced = TRUE)
adjustOrganLoss(pick(ORGAN_SLOT_HEART, ORGAN_SLOT_LUNGS, ORGAN_SLOT_STOMACH, ORGAN_SLOT_EYES, ORGAN_SLOT_EARS), 0.5* seconds_per_tick)
/mob/living/carbon/proc/undergoing_liver_failure()
@@ -814,6 +817,7 @@
var/obj/item/organ/internal/heart/heart = get_organ_slot(ORGAN_SLOT_HEART)
if(!istype(heart))
- return
+ return FALSE
heart.beating = !status
+ return TRUE
diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm
index dfae5d2baf1d8e..45a35bb6e158b1 100644
--- a/code/modules/mob/living/damage_procs.dm
+++ b/code/modules/mob/living/damage_procs.dm
@@ -53,7 +53,7 @@
return adjustStaminaLoss(damage)
/// return the damage amount for the type given
-/mob/living/proc/get_damage_amount(damagetype = BRUTE)
+/mob/living/proc/get_current_damage_of_type(damagetype = BRUTE)
switch(damagetype)
if(BRUTE)
return getBruteLoss()
@@ -159,136 +159,191 @@
return TRUE
-
/mob/living/proc/getBruteLoss()
return bruteloss
-/mob/living/proc/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_BRUTE, amount) //SKYRAT EDIT ADDITION
+/mob/living/proc/can_adjust_brute_loss(amount, forced, required_bodytype)
if(!forced && (status_flags & GODMODE))
return FALSE
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_BRUTE_DAMAGE, BRUTE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ return FALSE
+ return TRUE
+
+/mob/living/proc/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
+ if (!can_adjust_brute_loss(amount, forced, required_bodytype))
+ return 0
+ . = bruteloss
bruteloss = clamp((bruteloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, maxHealth * 2)
+ . -= bruteloss
+ if(!.) // no change, no need to update
+ return 0
if(updating_health)
updatehealth()
- return amount
-/mob/living/proc/setBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+
+/mob/living/proc/setBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
if(!forced && (status_flags & GODMODE))
- return
+ return FALSE
. = bruteloss
bruteloss = amount
+
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
+ . -= bruteloss
/mob/living/proc/getOxyLoss()
return oxyloss
-/mob/living/proc/adjustOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype, required_respiration_type = ALL)
-
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_OXY, amount) //SKYRAT EDIT ADDITION
+/mob/living/proc/can_adjust_oxy_loss(amount, forced, required_biotype, required_respiration_type)
if(!forced)
if(status_flags & GODMODE)
- return
-
- var/obj/item/organ/internal/lungs/affected_lungs = get_organ_slot(ORGAN_SLOT_LUNGS)
- if(isnull(affected_lungs))
- if(!(mob_respiration_type & required_respiration_type)) // if the mob has no lungs, use mob_respiration_type
- return
- else
- if(!(affected_lungs.respiration_type & required_respiration_type)) // otherwise use the lungs' respiration_type
- return
+ return FALSE
+ if (required_respiration_type)
+ var/obj/item/organ/internal/lungs/affected_lungs = get_organ_slot(ORGAN_SLOT_LUNGS)
+ if(isnull(affected_lungs))
+ if(!(mob_respiration_type & required_respiration_type)) // if the mob has no lungs, use mob_respiration_type
+ return FALSE
+ else
+ if(!(affected_lungs.respiration_type & required_respiration_type)) // otherwise use the lungs' respiration_type
+ return FALSE
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_OXY_DAMAGE, OXY, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ return FALSE
+ return TRUE
+/mob/living/proc/adjustOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL, required_respiration_type = ALL)
+ if(!can_adjust_oxy_loss(amount, forced, required_biotype, required_respiration_type))
+ return 0
. = oxyloss
oxyloss = clamp((oxyloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, maxHealth * 2)
+ . -= oxyloss
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
-
-/mob/living/proc/setOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype, required_respiration_type = ALL)
+/mob/living/proc/setOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL, required_respiration_type = ALL)
if(!forced)
if(status_flags & GODMODE)
- return
+ return FALSE
var/obj/item/organ/internal/lungs/affected_lungs = get_organ_slot(ORGAN_SLOT_LUNGS)
if(isnull(affected_lungs))
if(!(mob_respiration_type & required_respiration_type))
- return
+ return FALSE
else
if(!(affected_lungs.respiration_type & required_respiration_type))
- return
-
+ return FALSE
. = oxyloss
oxyloss = amount
+ . -= oxyloss
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
-
/mob/living/proc/getToxLoss()
return toxloss
-/mob/living/proc/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_TOX, amount) //SKYRAT EDIT ADDITION
- if(!forced && (status_flags & GODMODE))
+/mob/living/proc/can_adjust_tox_loss(amount, forced, required_biotype)
+ if(!forced && ((status_flags & GODMODE) || !(mob_biotypes & required_biotype)))
return FALSE
- if(!forced && !(mob_biotypes & required_biotype))
- return
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_TOX_DAMAGE, TOX, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ return FALSE
+ return TRUE
+
+/mob/living/proc/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
+ if(!can_adjust_tox_loss(amount, forced, required_biotype))
+ return 0
+ . = toxloss
toxloss = clamp((toxloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, maxHealth * 2)
+ . -= toxloss
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
- return amount
-/mob/living/proc/setToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+/mob/living/proc/setToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
if(!forced && (status_flags & GODMODE))
return FALSE
if(!forced && !(mob_biotypes & required_biotype))
- return
+ return FALSE
+ . = toxloss
toxloss = amount
+ . -= toxloss
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
- return amount
/mob/living/proc/getFireLoss()
return fireloss
-/mob/living/proc/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_FIRE, amount) //SKYRAT EDIT ADDITION
+/mob/living/proc/can_adjust_fire_loss(amount, forced, required_bodytype)
if(!forced && (status_flags & GODMODE))
return FALSE
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_BURN_DAMAGE, BURN, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ return FALSE
+ return TRUE
+
+/mob/living/proc/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
+ return 0
+ . = fireloss
fireloss = clamp((fireloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, maxHealth * 2)
+ . -= fireloss
+ if(. == 0) // no change, no need to update
+ return
if(updating_health)
updatehealth()
- return amount
-/mob/living/proc/setFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+/mob/living/proc/setFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
if(!forced && (status_flags & GODMODE))
- return
+ return 0
. = fireloss
fireloss = amount
+ . -= fireloss
+ if(. == 0) // no change, no need to update
+ return 0
if(updating_health)
updatehealth()
/mob/living/proc/getCloneLoss()
return cloneloss
-/mob/living/proc/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_CLONE, amount) //SKYRAT EDIT ADDITION
- if(!forced && ( (status_flags & GODMODE) || HAS_TRAIT(src, TRAIT_NOCLONELOSS)) )
+/mob/living/proc/can_adjust_clone_loss(amount, forced, required_biotype)
+ 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)
return FALSE
+ return TRUE
+
+/mob/living/proc/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
+ if(!can_adjust_clone_loss(amount, forced, required_biotype))
+ return 0
+ . = cloneloss
cloneloss = clamp((cloneloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, maxHealth * 2)
+ . -= cloneloss
+ if(. == 0) // no change, no need to update
+ return 0
if(updating_health)
updatehealth()
- return amount
-/mob/living/proc/setCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+/mob/living/proc/setCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
if(!forced && ( (status_flags & GODMODE) || HAS_TRAIT(src, TRAIT_NOCLONELOSS)) )
return FALSE
+ if(!forced && !(mob_biotypes & required_biotype))
+ return FALSE
+ . = cloneloss
cloneloss = amount
+ . -= cloneloss
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
- return amount
/mob/living/proc/adjustOrganLoss(slot, amount, maximum, required_organ_flag)
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_ORGAN, slot, amount) //SKYRAT EDIT ADDITION
return
/mob/living/proc/setOrganLoss(slot, amount, maximum, required_organ_flag)
@@ -300,65 +355,84 @@
/mob/living/proc/getStaminaLoss()
return staminaloss
-/mob/living/proc/adjustStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype)
- if(!forced && (status_flags & GODMODE))
+/mob/living/proc/can_adjust_stamina_loss(amount, forced, required_biotype)
+ if(!forced && (!(mob_biotypes & required_biotype) || status_flags & GODMODE))
return FALSE
- if(required_biotype && !(mob_biotypes & required_biotype))
- return
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_STAMINA_DAMAGE, STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ return FALSE
+ return TRUE
+
+/mob/living/proc/adjustStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype = ALL)
+ if(!can_adjust_stamina_loss(amount, forced, required_biotype))
+ return 0
+ . = staminaloss
staminaloss = clamp((staminaloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, max_stamina)
+ . -= staminaloss
+ if(. == 0) // no change, no need to update
+ return 0
if(updating_stamina)
updatehealth()
- SEND_SIGNAL(src, COMSIG_MOB_LOSS_STAMINA, amount) //SKYRAT EDIT ADDITION
- return
-/mob/living/proc/setStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype)
- if(!forced && ( (status_flags & GODMODE) || HAS_TRAIT(src, TRAIT_NOCLONELOSS)) )
+/mob/living/proc/setStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype = ALL)
+ if(!forced && (status_flags & GODMODE))
+ return FALSE
+ if(!forced && !(mob_biotypes & required_biotype))
return FALSE
+ . = staminaloss
staminaloss = amount
+ . -= staminaloss
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_stamina)
updatehealth()
/**
* heal ONE external organ, organ gets randomly selected from damaged ones.
*
- * needs to return amount healed in order to calculate things like tend wounds xp gain
+ * returns the net change in damage
*/
/mob/living/proc/heal_bodypart_damage(brute = 0, burn = 0, updating_health = TRUE, required_bodytype = NONE, target_zone = null)
- . = (adjustBruteLoss(-brute, FALSE) + adjustFireLoss(-burn, FALSE)) //zero as argument for no instant health update
+ . = (adjustBruteLoss(-abs(brute), updating_health = FALSE) + adjustFireLoss(-abs(burn), updating_health = FALSE))
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
/// damage ONE external organ, organ gets randomly selected from damaged ones.
/mob/living/proc/take_bodypart_damage(brute = 0, burn = 0, updating_health = TRUE, required_bodytype, check_armor = FALSE, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE)
- adjustBruteLoss(brute, FALSE) //zero as argument for no instant health update
- adjustFireLoss(burn, FALSE)
+ . = (adjustBruteLoss(abs(brute), updating_health = FALSE) + adjustFireLoss(abs(burn), updating_health = FALSE))
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
-/// heal MANY bodyparts, in random order
-/mob/living/proc/heal_overall_damage(brute = 0, burn = 0, stamina = 0, required_bodytype, updating_health = TRUE)
- adjustBruteLoss(-brute, FALSE) //zero as argument for no instant health update
- adjustFireLoss(-burn, FALSE)
- adjustStaminaLoss(-stamina, FALSE)
+/// heal MANY bodyparts, in random order. note: stamina arg nonfunctional for carbon mobs
+/mob/living/proc/heal_overall_damage(brute = 0, burn = 0, stamina = 0, required_bodytype, updating_health = TRUE, forced = FALSE)
+ . = (adjustBruteLoss(-abs(brute), updating_health = FALSE, forced = forced) + \
+ adjustFireLoss(-abs(burn), updating_health = FALSE, forced = forced) + \
+ adjustStaminaLoss(-abs(stamina), updating_stamina = FALSE, forced = forced))
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
-/// damage MANY bodyparts, in random order
-/mob/living/proc/take_overall_damage(brute = 0, burn = 0, stamina = 0, updating_health = TRUE, required_bodytype)
- adjustBruteLoss(brute, FALSE) //zero as argument for no instant health update
- adjustFireLoss(burn, FALSE)
- adjustStaminaLoss(stamina, FALSE)
+/// damage MANY bodyparts, in random order. note: stamina arg nonfunctional for carbon mobs
+/mob/living/proc/take_overall_damage(brute = 0, burn = 0, stamina = 0, updating_health = TRUE, forced = FALSE, required_bodytype)
+ . = (adjustBruteLoss(abs(brute), updating_health = FALSE, forced = forced) + \
+ adjustFireLoss(abs(burn), updating_health = FALSE, forced = forced) + \
+ adjustStaminaLoss(abs(stamina), updating_stamina = FALSE, forced = forced))
+ if(!.) // no change, no need to update
+ return FALSE
if(updating_health)
updatehealth()
///heal up to amount damage, in a given order
/mob/living/proc/heal_ordered_damage(amount, list/damage_types)
- . = amount //we'll return the amount of damage healed
- for(var/i in damage_types)
- var/amount_to_heal = min(amount, get_damage_amount(i)) //heal only up to the amount of damage we have
+ . = FALSE //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, i)
+ . += apply_damage_type(-amount_to_heal, damagetype)
amount -= amount_to_heal //remove what we healed from our current amount
if(!amount)
break
- . -= amount //if there's leftover healing, remove it from what we return
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index 53a8d259393091..6174a5793bc90b 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -1,12 +1,14 @@
/**
* Blow up the mob into giblets
*
- * Arguments:
- * * no_brain - Should the mob NOT drop a brain?
- * * no_organs - Should the mob NOT drop organs?
- * * no_bodyparts - Should the mob NOT drop bodyparts?
-*/
-/mob/living/proc/gib(no_brain, no_organs, no_bodyparts)
+ * drop_bitflags: (see code/__DEFINES/blood.dm)
+ * * DROP_BRAIN - Gibbed mob will drop a brain
+ * * DROP_ORGANS - Gibbed mob will drop organs
+ * * DROP_BODYPARTS - Gibbed mob will drop bodyparts (arms, legs, etc.)
+ * * DROP_ITEMS - Gibbed mob will drop carried items (otherwise they get deleted)
+ * * DROP_ALL_REMAINS - Gibbed mob will drop everything
+**/
+/mob/living/proc/gib(drop_bitflags=NONE)
var/prev_lying = lying_angle
if(stat != DEAD)
death(TRUE)
@@ -14,25 +16,47 @@
if(!prev_lying)
gib_animation()
- spill_organs(no_brain, no_organs, no_bodyparts, TRUE) //SKYRAT EDIT CHANGE - ORIGINAL: spill_organs(no_brain, no_organs, no_bodyparts)
+ ghostize()
+ spill_organs(drop_bitflags)
- if(!no_bodyparts)
- spread_bodyparts(no_brain, no_organs)
+ if(drop_bitflags & DROP_BODYPARTS)
+ spread_bodyparts(drop_bitflags)
- spawn_gibs(no_bodyparts)
- SEND_SIGNAL(src, COMSIG_LIVING_GIBBED, no_brain, no_organs, no_bodyparts)
+ spawn_gibs(drop_bitflags)
+ SEND_SIGNAL(src, COMSIG_LIVING_GIBBED, drop_bitflags)
qdel(src)
/mob/living/proc/gib_animation()
return
-/mob/living/proc/spawn_gibs()
+/**
+ * Spawn bloody gib mess on the floor
+ *
+ * drop_bitflags: (see code/__DEFINES/blood.dm)
+ * * DROP_BODYPARTS - Gibs will spawn with bodypart limbs present
+**/
+/mob/living/proc/spawn_gibs(drop_bitflags=NONE)
new /obj/effect/gibspawner/generic(drop_location(), src, get_static_viruses())
-/mob/living/proc/spill_organs()
+/**
+ * Drops a mob's organs on the floor
+ *
+ * drop_bitflags: (see code/__DEFINES/blood.dm)
+ * * DROP_BRAIN - Mob will drop a brain
+ * * DROP_ORGANS - Mob will drop organs
+ * * DROP_BODYPARTS - Mob will drop bodyparts (arms, legs, etc.)
+ * * DROP_ALL_REMAINS - Mob will drop everything
+**/
+/mob/living/proc/spill_organs(drop_bitflags=NONE)
return
-/mob/living/proc/spread_bodyparts()
+/**
+ * Launches all bodyparts away from the mob
+ *
+ * drop_bitflags: (see code/__DEFINES/blood.dm)
+ * * DROP_BRAIN - Detaches the head from the mob and launches it away from the body
+**/
+/mob/living/proc/spread_bodyparts(drop_bitflags=NONE)
return
/**
@@ -58,6 +82,7 @@
dust_animation()
spawn_dust(just_ash)
+ ghostize()
QDEL_IN(src,5) // since this is sometimes called in the middle of movement, allow half a second for movement to finish, ghosting to happen and animation to play. Looks much nicer and doesn't cause multiple runtimes.
/mob/living/proc/dust_animation()
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index cb4d4a8241a6a2..dbb602aa2be53f 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -143,6 +143,8 @@
var/mob/living/carbon/human/H = user
var/open = FALSE
var/obj/item/organ/external/wings/functional/wings = H.get_organ_slot(ORGAN_SLOT_EXTERNAL_WINGS)
+
+ // open/close functional wings
if(istype(wings))
if(wings.wings_open)
open = TRUE
@@ -150,6 +152,10 @@
else
H.OpenWings()
addtimer(CALLBACK(wings, open ? TYPE_PROC_REF(/obj/item/organ/external/wings/functional, open_wings) : TYPE_PROC_REF(/obj/item/organ/external/wings/functional, close_wings)), wing_time)
+
+ // play moth flutter noise if moth wing
+ if(istype(wings, /obj/item/organ/external/wings/moth))
+ playsound(H, 'sound/voice/moth/moth_flutter.ogg', 50, TRUE)
*/
//SKYRAT EDIT REMOVAL END
@@ -193,7 +199,7 @@
return
var/mob/living/carbon/human/human_user = user
if(human_user.dna.species.id == SPECIES_HUMAN && !HAS_MIND_TRAIT(human_user, TRAIT_MIMING))
- if(human_user.gender == FEMALE)
+ if(human_user.physique == FEMALE)
return pick('sound/voice/human/gasp_female1.ogg', 'sound/voice/human/gasp_female2.ogg', 'sound/voice/human/gasp_female3.ogg')
else
return pick('sound/voice/human/gasp_male1.ogg', 'sound/voice/human/gasp_male2.ogg')
@@ -487,7 +493,7 @@
/datum/emote/living/tremble
key = "tremble"
key_third_person = "trembles"
- message = "trembles in fear!"
+ message = "trembles!"
#define TREMBLE_LOOP_DURATION (4.4 SECONDS)
/datum/emote/living/tremble/run_emote(mob/living/user, params, type_override, intentional)
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index aacdc60b56a17c..37a1253ad495ac 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -12,7 +12,10 @@
/mob/living/proc/Life(seconds_per_tick = SSMOBS_DT, times_fired)
set waitfor = FALSE
- SEND_SIGNAL(src, COMSIG_LIVING_LIFE, seconds_per_tick, times_fired)
+ var/signal_result = SEND_SIGNAL(src, COMSIG_LIVING_LIFE, seconds_per_tick, times_fired)
+
+ if(signal_result & COMPONENT_LIVING_CANCEL_LIFE_PROCESSING) // mmm less work
+ return
if (client)
var/turf/T = get_turf(src)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index e4fe206cf63926..c409b84c4e2130 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -807,8 +807,8 @@
*/
/mob/living/proc/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
if(excess_healing)
- adjustOxyLoss(-excess_healing, FALSE)
- adjustToxLoss(-excess_healing, FALSE, TRUE) //slime friendly
+ adjustOxyLoss(-excess_healing, updating_health = FALSE)
+ adjustToxLoss(-excess_healing, updating_health = FALSE, forced = TRUE) //slime friendly
updatehealth()
grab_ghost(force_grab_ghost)
@@ -857,13 +857,13 @@
var/oxy_to_heal = heal_to - getOxyLoss()
var/tox_to_heal = heal_to - getToxLoss()
if(brute_to_heal < 0)
- adjustBruteLoss(brute_to_heal, FALSE)
+ adjustBruteLoss(brute_to_heal, updating_health = FALSE)
if(burn_to_heal < 0)
- adjustFireLoss(burn_to_heal, FALSE)
+ adjustFireLoss(burn_to_heal, updating_health = FALSE)
if(oxy_to_heal < 0)
- adjustOxyLoss(oxy_to_heal, FALSE)
+ adjustOxyLoss(oxy_to_heal, updating_health = FALSE)
if(tox_to_heal < 0)
- adjustToxLoss(tox_to_heal, FALSE, TRUE)
+ adjustToxLoss(tox_to_heal, updating_health = FALSE, forced = TRUE)
// Run updatehealth once to set health for the revival check
updatehealth()
@@ -894,17 +894,17 @@
SHOULD_CALL_PARENT(TRUE)
if(heal_flags & HEAL_TOX)
- setToxLoss(0, FALSE, TRUE)
+ setToxLoss(0, updating_health = FALSE, forced = TRUE)
if(heal_flags & HEAL_OXY)
- setOxyLoss(0, FALSE, TRUE)
+ setOxyLoss(0, updating_health = FALSE, forced = TRUE)
if(heal_flags & HEAL_CLONE)
- setCloneLoss(0, FALSE, TRUE)
+ setCloneLoss(0, updating_health = FALSE, forced = TRUE)
if(heal_flags & HEAL_BRUTE)
- setBruteLoss(0, FALSE, TRUE)
+ setBruteLoss(0, updating_health = FALSE, forced = TRUE)
if(heal_flags & HEAL_BURN)
- setFireLoss(0, FALSE, TRUE)
+ setFireLoss(0, updating_health = FALSE, forced = TRUE)
if(heal_flags & HEAL_STAM)
- setStaminaLoss(0, FALSE, TRUE)
+ setStaminaLoss(0, updating_stamina = FALSE, forced = TRUE)
// I don't really care to keep this under a flag
set_nutrition(NUTRITION_LEVEL_FED + 50)
@@ -1308,7 +1308,7 @@
return
/mob/living/can_hold_items(obj/item/I)
- return usable_hands && ..()
+ return ..() && HAS_TRAIT(src, TRAIT_CAN_HOLD_ITEMS) && usable_hands
/mob/living/can_perform_action(atom/movable/target, action_bitflags)
if(!istype(target))
@@ -1479,6 +1479,7 @@
var/picked_animal = pick(
/mob/living/basic/bat,
/mob/living/basic/bear,
+ /mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
@@ -1487,6 +1488,8 @@
/mob/living/basic/chicken,
/mob/living/basic/cow,
/mob/living/basic/crab,
+ /mob/living/basic/goat,
+ /mob/living/basic/gorilla,
/mob/living/basic/headslug,
/mob/living/basic/killer_tomato,
/mob/living/basic/lizard,
@@ -1504,10 +1507,7 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
- /mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
- /mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet/cat,
/mob/living/simple_animal/pet/cat/cak,
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 0d309ff10c9334..01362dfb1ba55d 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -181,6 +181,7 @@
return ..()
/mob/living/fire_act()
+ . = ..()
adjust_fire_stacks(3)
ignite_mob()
@@ -463,7 +464,7 @@
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(cult_ending_helper), CULT_VICTORY_MASS_CONVERSION), 120)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(ending_helper)), 270)
if(client)
- makeNewConstruct(/mob/living/simple_animal/hostile/construct/harvester, src, cultoverride = TRUE)
+ makeNewConstruct(/mob/living/basic/construct/harvester, src, cultoverride = TRUE)
else
switch(rand(1, 4))
if(1)
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index 766a7ef89a8144..5b2f0fc020bf7d 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -1,6 +1,6 @@
/mob/living
see_invisible = SEE_INVISIBLE_LIVING
- hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD)
+ hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD, DNR_HUD) // SKYRAT EDIT ADDITION - DNR_HUD
pressure_resistance = 10
hud_type = /datum/hud/living
@@ -225,3 +225,6 @@
/// 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 3a09256ee69b3f..52daa3c2f0ab92 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -387,8 +387,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
if(!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES)))
speech_bubble_recipients.Add(M.client)
found_client = TRUE
-
- if(voice && found_client && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN))
+ if(SStts.tts_enabled && voice && found_client && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN))
var/tts_message_to_use = tts_message
if(!tts_message_to_use)
tts_message_to_use = message_raw
@@ -404,7 +403,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
filter += tts_filter.Join(",")
if(ishuman(src))
var/mob/living/carbon/human/human_speaker = src
- if(human_speaker.wear_mask)
+ if(istype(human_speaker.wear_mask, /obj/item/clothing/mask))
var/obj/item/clothing/mask/worn_mask = human_speaker.wear_mask
if(worn_mask.voice_override)
voice_to_use = worn_mask.voice_override
diff --git a/code/modules/mob/living/silicon/ai/ai_defense.dm b/code/modules/mob/living/silicon/ai/ai_defense.dm
index ae17e13f269acc..7445815c9d77b4 100644
--- a/code/modules/mob/living/silicon/ai/ai_defense.dm
+++ b/code/modules/mob/living/silicon/ai/ai_defense.dm
@@ -43,7 +43,7 @@
switch(severity)
if(EXPLODE_DEVASTATE)
investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
if(EXPLODE_HEAVY)
if (stat != DEAD)
adjustBruteLoss(60)
diff --git a/code/modules/mob/living/silicon/damage_procs.dm b/code/modules/mob/living/silicon/damage_procs.dm
index 8ee146508a4149..4fe6e688632cb1 100644
--- a/code/modules/mob/living/silicon/damage_procs.dm
+++ b/code/modules/mob/living/silicon/damage_procs.dm
@@ -18,19 +18,19 @@
/mob/living/silicon/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype) //immune to tox damage
return FALSE
-/mob/living/silicon/setToxLoss(amount, updating_health = TRUE, forced = FALSE)
+/mob/living/silicon/setToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
return FALSE
-/mob/living/silicon/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE) //immune to clone damage
+/mob/living/silicon/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype) //immune to clone damage
return FALSE
-/mob/living/silicon/setCloneLoss(amount, updating_health = TRUE, forced = FALSE)
+/mob/living/silicon/setCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
return FALSE
/mob/living/silicon/adjustStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype) //immune to stamina damage.
return FALSE
-/mob/living/silicon/setStaminaLoss(amount, updating_health = TRUE)
+/mob/living/silicon/setStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype)
return FALSE
/mob/living/silicon/adjustOrganLoss(slot, amount, maximum = 500, required_organ_flag) //immune to organ damage (no organs, duh)
@@ -45,7 +45,7 @@
return FALSE
-/mob/living/silicon/setOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+/mob/living/silicon/setOxyLoss(amount, updating_health = TRUE, forced = FALSE, forced = FALSE, required_biotype)
if(isAI(src)) //ditto
return ..()
diff --git a/code/modules/mob/living/silicon/robot/inventory.dm b/code/modules/mob/living/silicon/robot/inventory.dm
index 7df2e7d339065c..be713b429a6b51 100644
--- a/code/modules/mob/living/silicon/robot/inventory.dm
+++ b/code/modules/mob/living/silicon/robot/inventory.dm
@@ -401,6 +401,7 @@
/mob/living/silicon/robot/perform_hand_swap()
cycle_modules()
+ return TRUE
/mob/living/silicon/robot/can_hold_items(obj/item/I)
return (I && (I in model.modules)) //Only if it's part of our model.
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index 30559e616b569d..fe28d41fe846fe 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -385,7 +385,7 @@
else
explosion(src, devastation_range = -1, light_impact_range = 2)
investigate_log("has self-destructed.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
/mob/living/silicon/robot/proc/UnlinkSelf()
set_connected_ai(null)
diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm
index f820d3ca1cfc53..09440ec22a3f05 100644
--- a/code/modules/mob/living/silicon/robot/robot_defense.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defense.dm
@@ -414,14 +414,14 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
adjustBruteLoss(30)
else
investigate_log("has been gibbed a blob.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
return TRUE
/mob/living/silicon/robot/ex_act(severity, target)
switch(severity)
if(EXPLODE_DEVASTATE)
investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS)
- gib()
+ gib(DROP_ALL_REMAINS)
return
if(EXPLODE_HEAVY)
if (stat != DEAD)
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index fbe9986c3ca3c6..8a1f20d28c4b75 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -21,7 +21,7 @@
var/designation = ""
var/radiomod = "" //Radio character used before state laws/arrivals announce to allow department transmissions, default, or none at all.
var/obj/item/camera/siliconcam/aicamera = null //photography
- hud_possible = list(ANTAG_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_TRACK_HUD)
+ hud_possible = list(ANTAG_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_TRACK_HUD, DNR_HUD) // SKYRAT EDIT ADDITION - DNR HUD
var/obj/item/radio/borg/radio = null ///If this is a path, this gets created as an object in Initialize.
diff --git a/code/modules/mob/living/simple_animal/damage_procs.dm b/code/modules/mob/living/simple_animal/damage_procs.dm
index 6345320d9d9966..9640dbb9de4403 100644
--- a/code/modules/mob/living/simple_animal/damage_procs.dm
+++ b/code/modules/mob/living/simple_animal/damage_procs.dm
@@ -19,36 +19,48 @@
toggle_ai(AI_ON)
/mob/living/simple_animal/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+ if(!can_adjust_brute_loss(amount, forced, required_bodytype))
+ return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[BRUTE])
. = adjustHealth(amount * damage_coeff[BRUTE] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/simple_animal/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
+ return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[BURN])
. = adjustHealth(amount * damage_coeff[BURN] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/simple_animal/adjustOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype, required_respiration_type)
+ if(!can_adjust_oxy_loss(amount, forced, required_biotype, required_respiration_type))
+ return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[OXY])
. = adjustHealth(amount * damage_coeff[OXY] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/simple_animal/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+ if(!can_adjust_tox_loss(amount, forced, required_biotype))
+ return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[TOX])
. = adjustHealth(amount * damage_coeff[TOX] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
-/mob/living/simple_animal/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE)
+/mob/living/simple_animal/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
+ if(!can_adjust_clone_loss(amount, forced, required_biotype))
+ return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
else if(damage_coeff[CLONE])
. = adjustHealth(amount * damage_coeff[CLONE] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/simple_animal/adjustStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype)
+ if(!can_adjust_stamina_loss(amount, forced, required_biotype))
+ return 0
if(forced)
staminaloss = max(0, min(max_staminaloss, staminaloss + amount))
else
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
index a0f078acd92e85..75471c3cc30874 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
@@ -173,6 +173,7 @@
. = ..()
GLOB.drones_list += src
access_card = new /obj/item/card/id/advanced/simple_bot(src)
+ 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]
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm b/code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm
index 69c2ac3e8cf73c..90391dff58546c 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/visuals_icons.dm
@@ -27,45 +27,6 @@
if(slot_flags & (ITEM_SLOT_HANDS|ITEM_SLOT_BACKPACK|ITEM_SLOT_DEX_STORAGE))
update_inv_internal_storage()
-/mob/living/simple_animal/drone/update_held_items()
- remove_overlay(DRONE_HANDS_LAYER)
- var/list/hands_overlays = list()
-
- var/obj/item/l_hand = get_item_for_held_index(1)
- var/obj/item/r_hand = get_item_for_held_index(2)
-
- var/y_shift = getItemPixelShiftY()
-
- if(r_hand)
- var/mutable_appearance/r_hand_overlay = r_hand.build_worn_icon(default_layer = DRONE_HANDS_LAYER, default_icon_file = r_hand.righthand_file, isinhands = TRUE)
- if(y_shift)
- r_hand_overlay.pixel_y += y_shift
-
- hands_overlays += r_hand_overlay
-
- if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- SET_PLANE_EXPLICIT(r_hand, ABOVE_HUD_PLANE, src)
- r_hand.screen_loc = ui_hand_position(get_held_index_of_item(r_hand))
- client.screen |= r_hand
-
- if(l_hand)
- var/mutable_appearance/l_hand_overlay = l_hand.build_worn_icon(default_layer = DRONE_HANDS_LAYER, default_icon_file = l_hand.lefthand_file, isinhands = TRUE)
- if(y_shift)
- l_hand_overlay.pixel_y += y_shift
-
- hands_overlays += l_hand_overlay
-
- if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- SET_PLANE_EXPLICIT(l_hand, ABOVE_HUD_PLANE, src)
- l_hand.screen_loc = ui_hand_position(get_held_index_of_item(l_hand))
- client.screen |= l_hand
-
-
- if(hands_overlays.len)
- drone_overlays[DRONE_HANDS_LAYER] = hands_overlays
- apply_overlay(DRONE_HANDS_LAYER)
-
-
/mob/living/simple_animal/drone/proc/update_inv_internal_storage()
if(internal_storage && client && hud_used?.hud_shown)
internal_storage.screen_loc = ui_drone_storage
diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
deleted file mode 100644
index af1b4eb13705e5..00000000000000
--- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
+++ /dev/null
@@ -1,121 +0,0 @@
-//goat
-/mob/living/simple_animal/hostile/retaliate/goat
- name = "goat"
- desc = "Not known for their pleasant disposition."
- icon_state = "goat"
- icon_living = "goat"
- icon_dead = "goat_dead"
- speak = list("EHEHEHEHEH","eh?")
- speak_emote = list("brays")
- emote_hear = list("brays.")
- emote_see = list("shakes their head.", "stamps a foot.", "glares around.")
- speak_chance = 1
- turns_per_move = 5
- butcher_results = list(/obj/item/food/meat/slab/grassfed = 4)
- response_help_continuous = "pets"
- response_help_simple = "pet"
- response_disarm_continuous = "gently pushes aside"
- response_disarm_simple = "gently push aside"
- response_harm_continuous = "kicks"
- response_harm_simple = "kick"
- faction = list(FACTION_NEUTRAL)
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- attack_same = 1
- attack_verb_continuous = "kicks"
- attack_verb_simple = "kick"
- attack_sound = 'sound/weapons/punch1.ogg'
- attack_vis_effect = ATTACK_EFFECT_KICK
- health = 40
- maxHealth = 40
- minbodytemp = 180
- melee_damage_lower = 1
- melee_damage_upper = 2
- environment_smash = ENVIRONMENT_SMASH_NONE
- stop_automated_movement_when_pulled = 1
- blood_volume = BLOOD_VOLUME_NORMAL
-
- footstep_type = FOOTSTEP_MOB_SHOE
-
-/mob/living/simple_animal/hostile/retaliate/goat/Initialize(mapload)
- AddComponent(/datum/component/udder)
- AddElement(/datum/element/cliff_walking) //we walk the cliff
- . = ..()
-
-/mob/living/simple_animal/hostile/retaliate/goat/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- . = ..()
- if(.)
- //chance to go crazy and start wacking stuff
- if(!enemies.len && SPT_PROB(0.5, seconds_per_tick))
- Retaliate()
-
- if(enemies.len && SPT_PROB(5, seconds_per_tick))
- enemies.Cut()
- LoseTarget()
- src.visible_message(span_notice("[src] calms down."))
- if(stat != CONSCIOUS)
- return
-
- eat_plants()
- if(pulledby)
- return
-
- for(var/direction in shuffle(list(1,2,4,8,5,6,9,10)))
- var/turf/step = get_step(src, direction)
-
- if(!istype(step))
- return
-
- var/vine = locate(/obj/structure/spacevine) in step
- var/mushroom = locate(/obj/structure/glowshroom) in step
- var/flower = locate(/obj/structure/alien/resin/flower_bud) in step
-
- if(vine || mushroom || flower)
- Move(step, get_dir(src, step))
-
-/mob/living/simple_animal/hostile/retaliate/goat/Retaliate()
- ..()
- src.visible_message(span_danger("[src] gets an evil-looking gleam in [p_their()] eye."))
-
-/mob/living/simple_animal/hostile/retaliate/goat/Move()
- . = ..()
- if(!stat)
- eat_plants()
-
-/mob/living/simple_animal/hostile/retaliate/goat/proc/eat_plants()
- var/obj/structure/spacevine/vine = locate(/obj/structure/spacevine) in loc
- if(vine)
- vine.eat(src)
-
- var/obj/structure/alien/resin/flower_bud/flower = locate(/obj/structure/alien/resin/flower_bud) in loc
- if(flower)
- flower.take_damage(rand(30, 50), BRUTE, 0)
-
- var/obj/structure/glowshroom/mushroom = locate(/obj/structure/glowshroom) in loc
- if(mushroom)
- qdel(mushroom)
-
- if((vine || flower || mushroom) && prob(10))
- say("Nom") // bon appetit
- playsound(src, 'sound/items/eatfood.ogg', rand(30, 50), TRUE)
-
-/mob/living/simple_animal/hostile/retaliate/goat/AttackingTarget()
- . = ..()
-
- if(!. || !isliving(target))
- return
-
- var/mob/living/plant_target = target
- if(!(plant_target.mob_biotypes & MOB_PLANT))
- return
-
- plant_target.adjustBruteLoss(20)
- playsound(src, 'sound/items/eatfood.ogg', rand(30, 50), TRUE)
- var/obj/item/bodypart/edible_bodypart
-
- if(ishuman(plant_target))
- var/mob/living/carbon/human/plant_man = target
- edible_bodypart = pick(plant_man.bodyparts)
- edible_bodypart.dismember()
-
- plant_target.visible_message(span_warning("[src] takes a big chomp out of [plant_target]!"), \
- span_userdanger("[src] takes a big chomp out of your [edible_bodypart || "body"]!"))
diff --git a/code/modules/mob/living/simple_animal/friendly/sloth.dm b/code/modules/mob/living/simple_animal/friendly/sloth.dm
deleted file mode 100644
index 8e308c58d12d2a..00000000000000
--- a/code/modules/mob/living/simple_animal/friendly/sloth.dm
+++ /dev/null
@@ -1,68 +0,0 @@
-GLOBAL_DATUM(cargo_sloth, /mob/living/simple_animal/sloth)
-
-/mob/living/simple_animal/sloth
- name = "sloth"
- desc = "An adorable, sleepy creature."
- icon = 'icons/mob/simple/pets.dmi'
- icon_state = "sloth"
- icon_living = "sloth"
- icon_dead = "sloth_dead"
- speak_emote = list("yawns")
- emote_hear = list("snores.","yawns.")
- emote_see = list("dozes off.", "looks around sleepily.")
- speak_chance = 1
- turns_per_move = 5
- can_be_held = TRUE
- butcher_results = list(/obj/item/food/meat/slab = 3)
- response_help_continuous = "pets"
- response_help_simple = "pet"
- response_disarm_continuous = "gently pushes aside"
- response_disarm_simple = "gently push aside"
- response_harm_continuous = "kicks"
- response_harm_simple = "kick"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- gold_core_spawnable = FRIENDLY_SPAWN
- melee_damage_lower = 18
- melee_damage_upper = 18
- health = 50
- maxHealth = 50
- speed = 10
- held_state = "sloth"
- ///In the case 'melee_damage_upper' is somehow raised above 0
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- attack_sound = 'sound/weapons/bite.ogg'
- attack_vis_effect = ATTACK_EFFECT_BITE
-
- footstep_type = FOOTSTEP_MOB_CLAW
-
-/mob/living/simple_animal/sloth/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/pet_bonus, "slowly smiles!")
- // If someone adds non-cargo sloths to maps we'll have a problem but we're fine for now
- if(!GLOB.cargo_sloth && mapload && is_station_level(z))
- GLOB.cargo_sloth = src
-
-/mob/living/simple_animal/sloth/Destroy()
- if(GLOB.cargo_sloth == src)
- GLOB.cargo_sloth = null
-
- return ..()
-
-//Cargo Sloth
-/mob/living/simple_animal/sloth/paperwork
- name = "Paperwork"
- desc = "Cargo's pet sloth. About as useful as the rest of the techs."
- gold_core_spawnable = NO_SPAWN
-
-//Cargo Sloth 2
-
-/mob/living/simple_animal/sloth/citrus
- name = "Citrus"
- desc = "Cargo's pet sloth. She's dressed in a horrible sweater."
- icon_state = "cool_sloth"
- icon_living = "cool_sloth"
- icon_dead = "cool_sloth_dead"
- gender = FEMALE
- butcher_results = list(/obj/item/toy/spinningtoy = 1)
- gold_core_spawnable = NO_SPAWN
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
index ea5e0685b53fea..4ca35a673577ed 100644
--- a/code/modules/mob/living/simple_animal/guardian/guardian.dm
+++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm
@@ -6,7 +6,8 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
desc = "A mysterious being that stands by its charge, ever vigilant."
speak_emote = list("hisses")
gender = NEUTER
- mob_biotypes = NONE
+ mob_biotypes = MOB_SPECIAL
+ sentience_type = SENTIENCE_HUMANOID
bubble_icon = "guardian"
response_help_continuous = "passes through"
response_help_simple = "pass through"
@@ -89,6 +90,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
GLOB.parasites += src
update_theme(theme)
AddElement(/datum/element/simple_flying)
+ AddComponent(/datum/component/basic_inhands)
manifest_effects()
/mob/living/simple_animal/hostile/guardian/Destroy() //if deleted by admins or something random, cut from the summoner
@@ -461,32 +463,6 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
cut_overlay(overlay)
guardian_overlays[cache_index] = null
-/mob/living/simple_animal/hostile/guardian/update_held_items()
- remove_overlay(GUARDIAN_HANDS_LAYER)
- var/list/hands_overlays = list()
- var/obj/item/l_hand = get_item_for_held_index(1)
- var/obj/item/r_hand = get_item_for_held_index(2)
-
- if(r_hand)
- hands_overlays += r_hand.build_worn_icon(default_layer = GUARDIAN_HANDS_LAYER, default_icon_file = r_hand.righthand_file, isinhands = TRUE)
-
- if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- SET_PLANE_EXPLICIT(r_hand, ABOVE_HUD_PLANE, src)
- r_hand.screen_loc = ui_hand_position(get_held_index_of_item(r_hand))
- client.screen |= r_hand
-
- if(l_hand)
- hands_overlays += l_hand.build_worn_icon(default_layer = GUARDIAN_HANDS_LAYER, default_icon_file = l_hand.lefthand_file, isinhands = TRUE)
-
- if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- SET_PLANE_EXPLICIT(l_hand, ABOVE_HUD_PLANE, src)
- l_hand.screen_loc = ui_hand_position(get_held_index_of_item(l_hand))
- client.screen |= l_hand
-
- if(length(hands_overlays))
- guardian_overlays[GUARDIAN_HANDS_LAYER] = hands_overlays
- apply_overlay(GUARDIAN_HANDS_LAYER)
-
/mob/living/simple_animal/hostile/guardian/regenerate_icons()
update_held_items()
diff --git a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
index 798b2067c6334d..d2fbfc33c877d3 100644
--- a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
+++ b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
@@ -22,17 +22,9 @@
dropItemToGround(internal_storage)
/mob/living/simple_animal/hostile/guardian/dextrous/examine(mob/user)
- if(dextrous)
- . = list("This is [icon2html(src)] \a [src]!\n[desc]", EXAMINE_SECTION_BREAK) //SKYRAT EDIT CHANGE
- for(var/obj/item/held_item in held_items)
- if(held_item.item_flags & (ABSTRACT|EXAMINE_SKIP|HAND_ITEM))
- continue
- . += "It has [held_item.get_examine_string(user)] in its [get_held_index_name(get_held_index_of_item(held_item))]."
- if(internal_storage && !(internal_storage.item_flags & ABSTRACT))
- . += "It is holding [internal_storage.get_examine_string(user)] in its internal storage."
- . += ""
- else
- return ..()
+ . = ..()
+ if(internal_storage && !(internal_storage.item_flags & ABSTRACT))
+ . += span_info("It is holding [internal_storage.get_examine_string(user)] in its internal storage.")
/mob/living/simple_animal/hostile/guardian/dextrous/recall_effects()
drop_all_held_items()
diff --git a/code/modules/mob/living/simple_animal/guardian/types/support.dm b/code/modules/mob/living/simple_animal/guardian/types/support.dm
index 5347f049e13cb3..9afdf231ce7c41 100644
--- a/code/modules/mob/living/simple_animal/guardian/types/support.dm
+++ b/code/modules/mob/living/simple_animal/guardian/types/support.dm
@@ -47,10 +47,13 @@
span_userdanger("[src] heals you!"), null, COMBAT_MESSAGE_RANGE, src)
to_chat(src, span_notice("You heal [target]!"))
playsound(target, attack_sound, 50, TRUE, TRUE, frequency = -1) //play punch in REVERSE
- target.adjustBruteLoss(-healing_amount)
- target.adjustFireLoss(-healing_amount)
- target.adjustOxyLoss(-healing_amount)
- target.adjustToxLoss(-healing_amount, forced = TRUE)
+ var/need_mob_update
+ need_mob_update = target.adjustBruteLoss(-healing_amount, updating_health = FALSE)
+ need_mob_update += target.adjustFireLoss(-healing_amount, updating_health = FALSE)
+ need_mob_update += target.adjustOxyLoss(-healing_amount, updating_health = FALSE)
+ need_mob_update += target.adjustToxLoss(-healing_amount, updating_health = FALSE, forced = TRUE)
+ if(need_mob_update)
+ target.updatehealth()
var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(target))
heal_effect.color = guardian_color
diff --git a/code/modules/mob/living/simple_animal/hostile/blob.dm b/code/modules/mob/living/simple_animal/hostile/blob.dm
deleted file mode 100644
index 7a40a01239c89e..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/blob.dm
+++ /dev/null
@@ -1,101 +0,0 @@
-//Do not spawn
-/mob/living/simple_animal/hostile/blob
- icon = 'icons/mob/nonhuman-player/blob.dmi'
- pass_flags = PASSBLOB
- faction = list(ROLE_BLOB)
- bubble_icon = "blob"
- speak_emote = null //so we use verb_yell/verb_say/etc
- 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
- unique_name = 1
- combat_mode = TRUE
- // ... Blob colored lighting
- lighting_cutoff_red = 20
- lighting_cutoff_green = 40
- lighting_cutoff_blue = 30
- initial_language_holder = /datum/language_holder/empty
- retreat_distance = null //! retreat doesn't obey pass_flags, so won't work on blob mobs.
- /// Blob camera that controls the blob
- var/mob/camera/blob/overmind = null
- /// If this is related to anything else
- var/independent = FALSE
- /// The factory blob tile that generated this blob minion
- var/obj/structure/blob/special/factory/factory
-
-/mob/living/simple_animal/hostile/blob/update_icons()
- if(overmind)
- add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
- else
- remove_atom_colour(FIXED_COLOUR_PRIORITY)
-
-/mob/living/simple_animal/hostile/blob/Initialize(mapload)
- . = ..()
- if(!independent) //no pulling people deep into the blob
- remove_verb(src, /mob/living/verb/pulled)
- else
- pass_flags &= ~PASSBLOB
-
-/mob/living/simple_animal/hostile/blob/death()
- factory = null
- if(overmind)
- overmind.blob_mobs -= src
- overmind = null
- return ..()
-
-/mob/living/simple_animal/hostile/blob/get_status_tab_items()
- . = ..()
- if(overmind)
- . += "Blobs to Win: [overmind.blobs_legit.len]/[overmind.blobwincount]"
-
-/mob/living/simple_animal/hostile/blob/blob_act(obj/structure/blob/B)
- if(stat != DEAD && health < maxHealth)
- for(var/unused in 1 to 2)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
- adjustHealth(-maxHealth*BLOBMOB_HEALING_MULTIPLIER)
-
-/mob/living/simple_animal/hostile/blob/fire_act(exposed_temperature, exposed_volume)
- ..()
- if(exposed_temperature)
- adjustFireLoss(clamp(0.01 * exposed_temperature, 1, 5))
- else
- adjustFireLoss(5)
-
-/mob/living/simple_animal/hostile/blob/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(istype(mover, /obj/structure/blob))
- return TRUE
-
-///override to use astar/JPS instead of walk_to so we can take our blob pass_flags into account.
-/mob/living/simple_animal/hostile/blob/Goto(target, delay, minimum_distance)
- if(prevent_goto_movement)
- return FALSE
- if(target == src.target)
- approaching_target = TRUE
- else
- approaching_target = FALSE
-
- SSmove_manager.jps_move(moving = src, chasing = target, delay = delay, repath_delay = 2 SECONDS, minimum_distance = minimum_distance, simulated_only = FALSE, skip_first = TRUE, timeout = 5 SECONDS, flags = MOVEMENT_LOOP_IGNORE_GLIDE)
- return TRUE
-
-/mob/living/simple_animal/hostile/blob/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
- for(var/obj/structure/blob/blob in range(1, src))
- return 1
- return ..()
-
-/mob/living/simple_animal/hostile/blob/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null, message_range = 7, datum/saymode/saymode = null)
- if(sanitize)
- message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
- var/spanned_message = say_quote(message)
- var/rendered = "\[Blob Telepathy\] [real_name] [spanned_message]"
- for(var/creature in GLOB.mob_list)
- if(isovermind(creature) || isblobmonster(creature))
- to_chat(creature, rendered)
- if(isobserver(creature))
- var/link = FOLLOW_LINK(creature, src)
- to_chat(creature, "[link] [rendered]")
-
diff --git a/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm b/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm
deleted file mode 100644
index dc1d038795f3cd..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm
+++ /dev/null
@@ -1,109 +0,0 @@
-/mob/living/simple_animal/hostile/blob/blobbernaut
- name = "blobbernaut"
- desc = "A hulking, mobile chunk of blobmass."
- icon_state = "blobbernaut"
- icon_living = "blobbernaut"
- icon_dead = "blobbernaut_dead"
- health = BLOBMOB_BLOBBERNAUT_HEALTH
- maxHealth = BLOBMOB_BLOBBERNAUT_HEALTH
- damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
- melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER
- melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER
- obj_damage = BLOBMOB_BLOBBERNAUT_DMG_OBJ
- attack_verb_continuous = "slams"
- attack_verb_simple = "slam"
- attack_sound = 'sound/effects/blobattack.ogg'
- verb_say = "gurgles"
- verb_ask = "demands"
- verb_exclaim = "roars"
- verb_yell = "bellows"
- force_threshold = 10
- pressure_resistance = 50
- mob_size = MOB_SIZE_LARGE
- hud_type = /datum/hud/living/blobbernaut
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Initialize(mapload)
- . = ..()
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/mind_initialize()
- . = ..()
- if(independent | !overmind)
- return
- var/datum/antagonist/blob_minion/blobbernaut/naut = new(overmind)
- mind.add_antag_datum(naut)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!..())
- return FALSE
- var/list/blobs_in_area = range(2, src)
-
- if(independent)
- return FALSE // strong independent blobbernaut that don't need no blob
-
- var/damagesources = 0
-
- if(!(locate(/obj/structure/blob) in blobs_in_area))
- damagesources++
-
- if(!factory)
- damagesources++
- else
- if(locate(/obj/structure/blob/special/core) in blobs_in_area)
- adjustHealth(-maxHealth*BLOBMOB_BLOBBERNAUT_HEALING_CORE * seconds_per_tick)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
- if(locate(/obj/structure/blob/special/node) in blobs_in_area)
- adjustHealth(-maxHealth*BLOBMOB_BLOBBERNAUT_HEALING_NODE * seconds_per_tick)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
-
- if(!damagesources)
- return FALSE
-
- adjustHealth(maxHealth * BLOBMOB_BLOBBERNAUT_HEALTH_DECAY * damagesources * seconds_per_tick) //take 2.5% of max health as damage when not near the blob or if the naut has no factory, 5% if both
- var/mutable_appearance/healing = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "nautdamage", MOB_LAYER+0.01)
- healing.appearance_flags = RESET_COLOR
-
- if(overmind)
- healing.color = overmind.blobstrain.complementary_color
-
- flick_overlay_view(healing, 0.8 SECONDS)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/AttackingTarget()
- . = ..()
- if(. && isliving(target) && overmind)
- overmind.blobstrain.blobbernaut_attack(target, src)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/update_icons()
- ..()
- if(overmind) //if we have an overmind, we're doing chemical reactions instead of pure damage
- melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
- melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
- attack_verb_continuous = overmind.blobstrain.blobbernaut_message
- else
- melee_damage_lower = initial(melee_damage_lower)
- melee_damage_upper = initial(melee_damage_upper)
- attack_verb_continuous = initial(attack_verb_continuous)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/death(gibbed)
- if(factory)
- factory.blobbernaut = null //remove this blobbernaut from its factory
- factory.max_integrity = initial(factory.max_integrity)
- flick("blobbernaut_death", src)
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/independent
- independent = TRUE
- gold_core_spawnable = HOSTILE_SPAWN
-
-
diff --git a/code/modules/mob/living/simple_animal/hostile/blobspore.dm b/code/modules/mob/living/simple_animal/hostile/blobspore.dm
deleted file mode 100644
index 501afde41b091d..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/blobspore.dm
+++ /dev/null
@@ -1,168 +0,0 @@
-/mob/living/simple_animal/hostile/blob/blobspore
- name = "blob spore"
- desc = "A floating, fragile spore."
- icon_state = "blobpod"
- icon_living = "blobpod"
- health_doll_icon = "blobpod"
- health = BLOBMOB_SPORE_HEALTH
- maxHealth = BLOBMOB_SPORE_HEALTH
- verb_say = "psychically pulses"
- verb_ask = "psychically probes"
- verb_exclaim = "psychically yells"
- verb_yell = "psychically screams"
- melee_damage_lower = BLOBMOB_SPORE_DMG_LOWER
- melee_damage_upper = BLOBMOB_SPORE_DMG_UPPER
- environment_smash = ENVIRONMENT_SMASH_NONE
- obj_damage = 0
- attack_verb_continuous = "hits"
- attack_verb_simple = "hit"
- attack_sound = 'sound/weapons/genhit1.ogg'
- del_on_death = TRUE
- death_message = "explodes into a cloud of gas!"
- gold_core_spawnable = NO_SPAWN //gold slime cores should only spawn the independent subtype
- /// Size of cloud produced from a dying spore
- var/death_cloud_size = 1
- /// The attached person
- var/mob/living/carbon/human/corpse
- /// If this is attached to a person
- var/is_zombie = FALSE
- /// Whether or not this is a fragile spore from Distributed Neurons
- var/is_weak = FALSE
-
-/mob/living/simple_animal/hostile/blob/blobspore/Initialize(mapload, obj/structure/blob/special/linked_node)
- . = ..()
- AddElement(/datum/element/simple_flying)
-
- if(!istype(linked_node))
- return
-
- factory = linked_node
- factory.spores += src
- if(linked_node.overmind && istype(linked_node.overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons) && !istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak))
- notify_ghosts("A controllable spore has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Sentient Spore Created")
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/blob/blobspore/mind_initialize()
- . = ..()
- if(independent || !overmind)
- return FALSE
- var/datum/antagonist/blob_minion/blob_zombie/zombie = new(overmind)
- mind.add_antag_datum(zombie)
-
-/mob/living/simple_animal/hostile/blob/blobspore/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!is_zombie && isturf(loc))
- for(var/mob/living/carbon/human/target in view(src,1)) //Only for corpse right next to/on same tile
- if(!is_weak && target.stat == DEAD)
- zombify(target)
- break
- if(factory && !is_valid_z_level(get_turf(src), get_turf(factory)))
- death()
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/attack_ghost(mob/user)
- . = ..()
- if(.)
- return
- humanize_pod(user)
-
-/mob/living/simple_animal/hostile/blob/blobspore/death(gibbed)
- // On death, create a small smoke of harmful gas (s-Acid)
- var/datum/effect_system/fluid_spread/smoke/chem/spores = new
- var/turf/location = get_turf(src)
-
- // Create the reagents to put into the air
- create_reagents(10)
-
- if(overmind?.blobstrain)
- overmind.blobstrain.on_sporedeath(src)
- else
- reagents.add_reagent(/datum/reagent/toxin/spore, 10)
-
- // Attach the smoke spreader and setup/start it.
- spores.attach(location)
- spores.set_up(death_cloud_size, holder = src, location = location, carry = reagents, silent = TRUE)
- spores.start()
- if(factory)
- factory.spore_delay = world.time + factory.spore_cooldown //put the factory on cooldown
-
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/death()
- if(factory)
- factory.spores -= src
- corpse?.forceMove(loc)
- corpse = null
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/update_icons()
- if(overmind)
- add_atom_colour(overmind.blobstrain.complementary_color, FIXED_COLOUR_PRIORITY)
- else
- remove_atom_colour(FIXED_COLOUR_PRIORITY)
- if(!is_zombie)
- return FALSE
-
- copy_overlays(corpse, TRUE)
- var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "blob_head")
- if(overmind)
- blob_head_overlay.color = overmind.blobstrain.complementary_color
- color = initial(color) // looks better.
- add_overlay(blob_head_overlay)
-
-/mob/living/simple_animal/hostile/blob/blobspore/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/blob/blobspore/independent
- gold_core_spawnable = HOSTILE_SPAWN
- independent = TRUE
-
-/mob/living/simple_animal/hostile/blob/blobspore/weak
- name = "fragile blob spore"
- health = 15
- maxHealth = 15
- melee_damage_lower = 1
- melee_damage_upper = 2
- death_cloud_size = 0
- is_weak = TRUE
-
-/** Ghost control a blob zombie */
-/mob/living/simple_animal/hostile/blob/blobspore/proc/humanize_pod(mob/user)
- if((!overmind || istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak) || !istype(overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons)) && !is_zombie)
- return FALSE
- if(key || stat)
- return FALSE
- var/pod_ask = tgui_alert(usr, "Are you bulbous enough?", "Blob Spore", list("Yes", "No"))
- if(pod_ask != "Yes" || QDELETED(src))
- return FALSE
- if(key)
- to_chat(user, span_warning("Someone else already took this spore!"))
- return FALSE
- key = user.key
- log_message("took control of [name].", LOG_GAME)
-
-/** Zombifies a dead mob, turning it into a blob zombie */
-/mob/living/simple_animal/hostile/blob/blobspore/proc/zombify(mob/living/carbon/human/target)
- is_zombie = 1
- if(target.wear_suit)
- maxHealth += target.get_armor_rating(MELEE) // That zombie's got armor, I want armor!
- maxHealth += 40
- health = maxHealth
- name = "blob zombie"
- desc = "A shambling corpse animated by the blob."
- mob_biotypes |= MOB_HUMANOID
- melee_damage_lower += 8
- melee_damage_upper += 11
- obj_damage = 20 // now that it has a corpse to puppet, it can properly attack structures
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- movement_type = GROUND
- death_cloud_size = 0
- icon = target.icon
- icon_state = "zombie"
- target.set_facial_hairstyle("Shaved", update = FALSE)
- target.set_hairstyle("Bald", update = TRUE)
- target.forceMove(src)
- corpse = target
- update_icons()
- visible_message(span_warning("The corpse of [target.name] suddenly rises!"))
- if(!key)
- notify_ghosts("\A [src] has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Blob Zombie Created")
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm b/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
index 23f7590dc8e247..31150a4dc89c17 100644
--- a/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
+++ b/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
@@ -3,7 +3,7 @@
real_name = "Construct"
desc = ""
gender = NEUTER
- mob_biotypes = NONE
+ mob_biotypes = MOB_MINERAL | MOB_SPECIAL
speak_emote = list("hisses")
response_help_continuous = "thinks better of touching"
response_help_simple = "think better of touching"
diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm
deleted file mode 100644
index cceb0cdf58cb89..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm
+++ /dev/null
@@ -1,174 +0,0 @@
-#define GORILLA_TOTAL_LAYERS 1
-
-/mob/living/simple_animal/hostile/gorilla
- name = "Gorilla"
- desc = "A ground-dwelling, predominantly herbivorous ape that inhabits the forests of central Africa."
- icon = 'icons/mob/simple/gorilla.dmi'
- icon_state = "crawling"
- icon_living = "crawling"
- icon_dead = "dead"
- health_doll_icon = "crawling"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- speak_chance = 80
- maxHealth = 220
- health = 220
- loot = list(/obj/effect/gibspawner/generic/animal)
- butcher_results = list(/obj/item/food/meat/slab/gorilla = 4)
- response_help_continuous = "prods"
- response_help_simple = "prod"
- response_disarm_continuous = "challenges"
- response_disarm_simple = "challenge"
- response_harm_continuous = "thumps"
- response_harm_simple = "thump"
- speed = 0.5
- 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)
- obj_damage = 20
- environment_smash = ENVIRONMENT_SMASH_WALLS
- attack_verb_continuous = "pummels"
- attack_verb_simple = "pummel"
- attack_sound = 'sound/weapons/punch1.ogg'
- dextrous = TRUE
- hud_type = /datum/hud/dextrous
- held_items = list(null, null)
- faction = list(FACTION_MONKEY, FACTION_JUNGLE)
- robust_searching = TRUE
- stat_attack = HARD_CRIT
- minbodytemp = 270
- maxbodytemp = 350
- unique_name = TRUE
- footstep_type = FOOTSTEP_MOB_BAREFOOT
-
- var/list/gorilla_overlays[GORILLA_TOTAL_LAYERS]
- var/oogas = 0
-
-// Gorillas like to dismember limbs from unconscious mobs.
-// Returns null when the target is not an unconscious carbon mob; a list of limbs (possibly empty) otherwise.
-/mob/living/simple_animal/hostile/gorilla/proc/get_target_bodyparts(atom/hit_target)
- if(!iscarbon(hit_target))
- return
-
- var/mob/living/carbon/carbon_target = hit_target
- if(carbon_target.stat < UNCONSCIOUS)
- return
-
- var/list/parts = list()
- for(var/obj/item/bodypart/part as anything in carbon_target.bodyparts)
- if(part.body_part == HEAD || part.body_part == CHEST)
- continue
- if(part.bodypart_flags & BODYPART_UNREMOVABLE)
- continue
- parts += part
- return parts
-
-/mob/living/simple_animal/hostile/gorilla/AttackingTarget(atom/attacked_target)
- . = ..()
- if(!.)
- return
-
- if(client)
- oogaooga()
-
- var/list/parts = get_target_bodyparts(target)
- if(length(parts))
- var/obj/item/bodypart/to_dismember = pick(parts)
- to_dismember.dismember()
- return
-
- if(isliving(target))
- var/mob/living/living_target = target
- if(prob(80))
- living_target.throw_at(get_edge_target_turf(living_target, dir), rand(1, 2), 7, src)
-
- else
- living_target.Paralyze(2 SECONDS)
- visible_message(span_danger("[src] knocks [living_target] down!"))
-
-/mob/living/simple_animal/hostile/gorilla/CanAttack(atom/the_target)
- var/list/parts = get_target_bodyparts(target)
- return ..() && !ismonkey(the_target) && (!parts || length(parts) > 3)
-
-/mob/living/simple_animal/hostile/gorilla/CanSmashTurfs(turf/T)
- return iswallturf(T)
-
-/mob/living/simple_animal/hostile/gorilla/gib(no_brain)
- if(!no_brain)
- var/mob/living/brain/gorilla_brain = new(drop_location())
- gorilla_brain.name = real_name
- gorilla_brain.real_name = real_name
- mind?.transfer_to(gorilla_brain)
- return ..()
-
-/mob/living/simple_animal/hostile/gorilla/handle_automated_speech(override)
- if(speak_chance && (override || prob(speak_chance)))
- playsound(src, 'sound/creatures/gorilla.ogg', 50)
- return ..()
-
-/mob/living/simple_animal/hostile/gorilla/can_use_guns(obj/item/G)
- to_chat(src, span_warning("Your meaty finger is much too large for the trigger guard!"))
- return FALSE
-
-/mob/living/simple_animal/hostile/gorilla/proc/oogaooga()
- oogas -= 1
- if(oogas <= 0)
- oogas = rand(2,6)
- playsound(src, 'sound/creatures/gorilla.ogg', 50)
-
-/mob/living/simple_animal/hostile/gorilla/lesser
- name = "lesser Gorilla"
- desc = "An adolescent Gorilla. It may not be fully grown but, much like a banana, that just means it's sturdier and harder to chew!"
- speak_chance = 100 // compensating for something
- maxHealth = 120
- health = 120
- butcher_results = list(/obj/item/food/meat/slab/gorilla = 2)
- speed = 0.35
- melee_damage_lower = 10
- melee_damage_upper = 15
- obj_damage = 15
- stat_attack = SOFT_CRIT
- unique_name = TRUE
-
-/mob/living/simple_animal/hostile/gorilla/lesser/Initialize(mapload)
- . = ..()
- transform *= 0.75 // smolrilla
-
-/mob/living/simple_animal/hostile/gorilla/cargo_domestic
- name = "Cargorilla" // Overriden, normally
- icon = 'icons/mob/simple/cargorillia.dmi'
- desc = "Cargo's pet gorilla. They seem to have an 'I love Mom' tattoo."
- maxHealth = 200
- health = 200
- faction = list(FACTION_NEUTRAL, FACTION_MONKEY, FACTION_JUNGLE)
- gold_core_spawnable = NO_SPAWN
- unique_name = FALSE
-
-/mob/living/simple_animal/hostile/gorilla/cargo_domestic/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_PACIFISM, INNATE_TRAIT)
- AddComponent(/datum/component/crate_carrier)
-
-/// Poll ghosts for control of the gorilla.
-/mob/living/simple_animal/hostile/gorilla/cargo_domestic/proc/poll_for_gorilla()
- AddComponent(\
- /datum/component/ghost_direct_control,\
- poll_candidates = TRUE,\
- poll_length = 30 SECONDS,\
- role_name = "Cargorilla",\
- assumed_control_message = "You are Cargorilla, a pacifistic friend of the station and carrier of freight.",\
- poll_ignore_key = POLL_IGNORE_CARGORILLA,\
- after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\
- )
-
-/// Called once a ghost assumes control
-/mob/living/simple_animal/hostile/gorilla/cargo_domestic/proc/became_player_controlled()
- mind.set_assigned_role(SSjob.GetJobType(/datum/job/cargo_technician))
- mind.special_role = "Cargorilla"
- to_chat(src, span_notice("You can pick up crates by clicking on them, and drop them by clicking on the ground."))
-
-/obj/item/card/id/advanced/cargo_gorilla
- name = "cargorilla ID"
- desc = "A card used to provide ID and determine access across the station. A gorilla-sized ID for a gorilla-sized cargo technician."
- trim = /datum/id_trim/job/cargo_technician
-
-#undef GORILLA_TOTAL_LAYERS
diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm b/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm
deleted file mode 100644
index 39dfe8f7d899e5..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm
+++ /dev/null
@@ -1,56 +0,0 @@
-#define GORILLA_HANDS_LAYER 1
-
-/mob/living/simple_animal/hostile/gorilla/proc/apply_overlay(cache_index)
- . = gorilla_overlays[cache_index]
- if(.)
- add_overlay(.)
-
-/mob/living/simple_animal/hostile/gorilla/proc/remove_overlay(cache_index)
- var/I = gorilla_overlays[cache_index]
- if(I)
- cut_overlay(I)
- gorilla_overlays[cache_index] = null
-
-/mob/living/simple_animal/hostile/gorilla/update_held_items()
- cut_overlays("standing_overlay")
- remove_overlay(GORILLA_HANDS_LAYER)
-
- var/standing = FALSE
- for(var/I in held_items)
- if(I)
- standing = TRUE
- break
- if(!standing)
- if(stat != DEAD)
- icon_state = "crawling"
- set_varspeed(0.5)
- return ..()
- if(stat != DEAD)
- icon_state = "standing"
- set_varspeed(1) // Gorillas are slow when standing up.
-
- var/list/hands_overlays = list()
-
- var/obj/item/l_hand = get_item_for_held_index(1)
- var/obj/item/r_hand = get_item_for_held_index(2)
-
- if(r_hand)
- var/mutable_appearance/r_hand_overlay = r_hand.build_worn_icon(default_layer = GORILLA_HANDS_LAYER, default_icon_file = r_hand.righthand_file, isinhands = TRUE)
- r_hand_overlay.pixel_y -= 1
- hands_overlays += r_hand_overlay
-
- if(l_hand)
- var/mutable_appearance/l_hand_overlay = l_hand.build_worn_icon(default_layer = GORILLA_HANDS_LAYER, default_icon_file = l_hand.lefthand_file, isinhands = TRUE)
- l_hand_overlay.pixel_y -= 1
- hands_overlays += l_hand_overlay
-
- if(hands_overlays.len)
- gorilla_overlays[GORILLA_HANDS_LAYER] = hands_overlays
- apply_overlay(GORILLA_HANDS_LAYER)
- add_overlay("standing_overlay")
- return ..()
-
-/mob/living/simple_animal/hostile/gorilla/regenerate_icons()
- update_held_items()
-
-#undef GORILLA_HANDS_LAYER
diff --git a/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm b/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm
deleted file mode 100644
index 2c0b9ba983a09f..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm
+++ /dev/null
@@ -1,428 +0,0 @@
-/mob/living/simple_animal/hostile/heretic_summon
- name = "Eldritch Demon"
- real_name = "Eldritch Demon"
- desc = "A horror from beyond this realm."
- icon = 'icons/mob/nonhuman-player/eldritch_mobs.dmi'
- gender = NEUTER
- mob_biotypes = NONE
- attack_sound = 'sound/weapons/punch1.ogg'
- 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 = "reaps"
- response_harm_simple = "tears"
- speak_emote = list("screams")
- speak_chance = 1
- speed = 0
- combat_mode = TRUE
- stop_automated_movement = TRUE
- AIStatus = AI_OFF
- // Sort of greenish brown, to match the vibeTM
- lighting_cutoff_red = 20
- lighting_cutoff_green = 25
- lighting_cutoff_blue = 5
- 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
- movement_type = GROUND
- pressure_resistance = 100
- del_on_death = TRUE
- death_message = "implodes into itself."
- loot = list(/obj/effect/gibspawner/human)
- faction = list(FACTION_HERETIC)
- simple_mob_flags = SILENCE_RANGED_MESSAGE
-
- /// Innate spells that are added when a beast is created.
- var/list/actions_to_add
-
-/mob/living/simple_animal/hostile/heretic_summon/Initialize(mapload)
- . = ..()
- for(var/spell in actions_to_add)
- var/datum/action/cooldown/spell/new_spell = new spell(src)
- new_spell.Grant(src)
-
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet
- name = "Raw Prophet"
- real_name = "Raw Prophet"
- desc = "An abomination stitched together from a few severed arms and one lost eye."
- icon_state = "raw_prophet"
- icon_living = "raw_prophet"
- status_flags = CANPUSH
- melee_damage_lower = 5
- melee_damage_upper = 10
- maxHealth = 65
- health = 65
- sight = SEE_MOBS|SEE_OBJS|SEE_TURFS
- loot = list(/obj/effect/gibspawner/human, /obj/item/bodypart/arm/left, /obj/item/organ/internal/eyes)
- actions_to_add = list(
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/long,
- /datum/action/cooldown/spell/list_target/telepathy/eldritch,
- /datum/action/cooldown/spell/pointed/blind/eldritch,
- /datum/action/innate/expand_sight,
- )
- /// A weakref to the last target we smacked. Hitting targets consecutively does more damage.
- var/datum/weakref/last_target
-
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/Initialize(mapload)
- . = ..()
- var/on_link_message = "You feel something new enter your sphere of mind... \
- You hear whispers of people far away, screeches of horror and a huming of welcome to [src]'s Mansus Link."
-
- var/on_unlink_message = "Your mind shatters as [src]'s Mansus Link leaves your mind."
-
- AddComponent( \
- /datum/component/mind_linker/active_linking, \
- network_name = "Mansus Link", \
- chat_color = "#568b00", \
- post_unlink_callback = CALLBACK(src, PROC_REF(after_unlink)), \
- speech_action_background_icon_state = "bg_heretic", \
- speech_action_overlay_state = "bg_heretic_border", \
- linker_action_path = /datum/action/cooldown/spell/pointed/manse_link, \
- link_message = on_link_message, \
- unlink_message = on_unlink_message, \
- )
-
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/attack_animal(mob/living/simple_animal/user, list/modifiers)
- if(user == src) // Easy to hit yourself + very fragile = accidental suicide, prevent that
- return
-
- return ..()
-
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/AttackingTarget(atom/attacked_target)
- if(WEAKREF(attacked_target) == last_target)
- melee_damage_lower = min(melee_damage_lower + 5, 30)
- melee_damage_upper = min(melee_damage_upper + 5, 35)
- else
- melee_damage_lower = initial(melee_damage_lower)
- melee_damage_upper = initial(melee_damage_upper)
-
- . = ..()
- if(!.)
- return
-
- SpinAnimation(5, 1)
- last_target = WEAKREF(attacked_target)
-
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- var/rotation_degree = (360 / 3)
- if(movement_dir & WEST || movement_dir & SOUTH)
- rotation_degree *= -1
-
- var/matrix/to_turn = matrix(transform)
- to_turn = turn(transform, rotation_degree)
- animate(src, transform = to_turn, time = 0.1 SECONDS)
-
-/*
- * Callback for the mind_linker component.
- * Stuns people who are ejected from the network.
- */
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/proc/after_unlink(mob/living/unlinked_mob)
- if(QDELETED(unlinked_mob) || unlinked_mob.stat == DEAD)
- return
-
- INVOKE_ASYNC(unlinked_mob, TYPE_PROC_REF(/mob, emote), "scream")
- unlinked_mob.AdjustParalyzed(0.5 SECONDS) //micro stun
-
-// What if we took a linked list... But made it a mob?
-/// The "Terror of the Night" / Armsy, a large worm made of multiple bodyparts that occupies multiple tiles
-/mob/living/simple_animal/hostile/heretic_summon/armsy
- name = "Terror of the night"
- real_name = "Armsy"
- desc = "An abomination made from dozens and dozens of severed and malformed limbs piled onto each other."
- icon_state = "armsy_start"
- icon_living = "armsy_start"
- maxHealth = 200
- health = 200
- melee_damage_lower = 10
- melee_damage_upper = 15
- move_force = MOVE_FORCE_OVERPOWERING
- move_resist = MOVE_FORCE_OVERPOWERING
- pull_force = MOVE_FORCE_OVERPOWERING
- movement_type = GROUND
- mob_size = MOB_SIZE_HUGE
- sentience_type = SENTIENCE_BOSS
- environment_smash = ENVIRONMENT_SMASH_RWALLS
- mob_biotypes = MOB_ORGANIC|MOB_SPECIAL
- obj_damage = 200
- ranged_cooldown_time = 5
- ranged = TRUE
- rapid = 1
- actions_to_add = list(/datum/action/cooldown/spell/worm_contract)
- ///Previous segment in the chain
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/back
- ///Next segment in the chain
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/front
- ///Your old location
- var/oldloc
- ///Allow / disallow pulling
- var/allow_pulling = FALSE
- ///How many arms do we have to eat to expand?
- var/stacks_to_grow = 5
- ///Currently eaten arms
- var/current_stacks = 0
- ///Does this follow other pieces?
- var/follow = TRUE
-
-/*
- * Arguments
- * * spawn_bodyparts - whether we spawn additional armsy bodies until we reach length.
- * * worm_length - the length of the worm we're creating. Below 3 doesn't work very well.
- */
-/mob/living/simple_animal/hostile/heretic_summon/armsy/Initialize(mapload, spawn_bodyparts = TRUE, worm_length = 6)
- . = ..()
- if(worm_length < 3)
- stack_trace("[type] created with invalid len ([worm_length]). Reverting to 3.")
- worm_length = 3 //code breaks below 3, let's just not allow it.
-
- oldloc = loc
- RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(update_chain_links))
- if(!spawn_bodyparts)
- return
-
- AddComponent(/datum/component/blood_walk, \
- blood_type = /obj/effect/decal/cleanable/blood/tracks, \
- target_dir_change = TRUE)
-
- allow_pulling = TRUE
- // Sets the hp of the head to be exactly the (length * hp), so the head is de facto the hardest to destroy.
- maxHealth = worm_length * maxHealth
- health = maxHealth
-
- // The previous link in the chain
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/prev = src
- // The current link in the chain
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/current
-
- for(var/i in 1 to worm_length)
- current = new type(drop_location(), FALSE)
- current.icon_state = "armsy_mid"
- current.icon_living = "armsy_mid"
- current.AIStatus = AI_OFF
- current.front = prev
- prev.back = current
- prev = current
-
- prev.icon_state = "armsy_end"
- prev.icon_living = "armsy_end"
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/adjustBruteLoss(amount, updating_health, forced, required_bodytype)
- if(back)
- return back.adjustBruteLoss(amount, updating_health, forced)
-
- return ..()
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/adjustFireLoss(amount, updating_health, forced, required_bodytype)
- if(back)
- return back.adjustFireLoss(amount, updating_health, forced)
-
- return ..()
-
-// We are literally a vessel of otherworldly destruction, we bring our own gravity unto this plane
-/mob/living/simple_animal/hostile/heretic_summon/armsy/has_gravity(turf/gravity_turf)
- return TRUE
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/can_be_pulled()
- return FALSE
-
-/// Updates every body in the chain to force move onto a single tile.
-/mob/living/simple_animal/hostile/heretic_summon/armsy/proc/contract_next_chain_into_single_tile()
- if(!back)
- return
-
- back.forceMove(loc)
- back.contract_next_chain_into_single_tile()
-
-/*
- * Recursively get the length of our chain.
- */
-/mob/living/simple_animal/hostile/heretic_summon/armsy/proc/get_length()
- . = 1
- if(back)
- . += back.get_length()
-
-/// Updates the next mob in the chain to move to our last location. Fixes the chain if somehow broken.
-/mob/living/simple_animal/hostile/heretic_summon/armsy/proc/update_chain_links()
- SIGNAL_HANDLER
-
- if(!follow)
- return
-
- if(back && back.loc != oldloc)
- back.Move(oldloc)
-
- // self fixing properties if somehow broken
- if(front && loc != front.oldloc)
- forceMove(front.oldloc)
-
- oldloc = loc
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/Destroy()
- if(front)
- front.icon_state = "armsy_end"
- front.icon_living = "armsy_end"
- front.back = null
- front = null
- if(back)
- QDEL_NULL(back) // chain destruction baby
- return ..()
-
-/*
- * Handle healing our chain.
- *
- * Eating arms off the ground heals us,
- * and if we eat enough arms while above
- * a certain health threshold, we even gain back parts!
- */
-/mob/living/simple_animal/hostile/heretic_summon/armsy/proc/heal()
- if(back)
- back.heal()
- return
-
- adjustBruteLoss(-maxHealth * 0.5, FALSE)
- adjustFireLoss(-maxHealth * 0.5, FALSE)
-
- if(health < maxHealth * 0.8)
- return
-
- current_stacks++
- if(current_stacks < stacks_to_grow)
- return
-
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/prev = new type(drop_location(), FALSE)
- icon_state = "armsy_mid"
- icon_living = "armsy_mid"
- back = prev
- prev.icon_state = "armsy_end"
- prev.icon_living = "armsy_end"
- prev.front = src
- prev.AIStatus = AI_OFF
- current_stacks = 0
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/Shoot(atom/targeted_atom)
- GiveTarget(targeted_atom)
- AttackingTarget()
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/AttackingTarget()
- if(istype(target, /obj/item/bodypart/arm))
- playsound(src, 'sound/magic/demon_consume.ogg', 50, TRUE)
- qdel(target)
- heal()
- return
- if(target == back || target == front)
- return
- if(back)
- back.GiveTarget(target)
- back.AttackingTarget()
- if(!Adjacent(target))
- return
- do_attack_animation(target)
-
- if(iscarbon(target))
- var/mob/living/carbon/carbon_target = target
- if(HAS_TRAIT(carbon_target, TRAIT_NODISMEMBER))
- return ..()
-
- var/list/parts_to_remove = list()
- for(var/obj/item/bodypart/bodypart in carbon_target.bodyparts)
- if(bodypart.body_part != HEAD && bodypart.body_part != CHEST && bodypart.body_part != LEG_LEFT && bodypart.body_part != LEG_RIGHT)
- if(!(bodypart.bodypart_flags & BODYPART_UNREMOVABLE))
- parts_to_remove += bodypart
-
- if(parts_to_remove.len && prob(10))
- var/obj/item/bodypart/lost_arm = pick(parts_to_remove)
- lost_arm.dismember()
-
- return ..()
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/prime
- name = "Lord of the Night"
- real_name = "Master of Decay"
- maxHealth = 400
- health = 400
- melee_damage_lower = 30
- melee_damage_upper = 50
-
-/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/Initialize(mapload, spawn_bodyparts = TRUE, worm_length = 9)
- . = ..()
- var/matrix/matrix_transformation = matrix()
- matrix_transformation.Scale(1.4, 1.4)
- transform = matrix_transformation
-
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit
- name = "Rust Walker"
- real_name = "Rusty"
- desc = "An incomprehensible abomination. Everywhere it steps, it appears to be actively seeping life out of its surroundings."
- icon_state = "rust_walker_s"
- icon_living = "rust_walker_s"
- status_flags = CANPUSH
- maxHealth = 75
- health = 75
- melee_damage_lower = 15
- melee_damage_upper = 20
- sight = SEE_TURFS
- actions_to_add = list(
- /datum/action/cooldown/spell/aoe/rust_conversion/small,
- /datum/action/cooldown/spell/basic_projectile/rust_wave/short,
- )
-
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit/setDir(newdir)
- . = ..()
- if(newdir == NORTH)
- icon_state = "rust_walker_n"
- else if(newdir == SOUTH)
- icon_state = "rust_walker_s"
- update_appearance(UPDATE_ICON_STATE)
-
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- playsound(src, 'sound/effects/footstep/rustystep1.ogg', 100, TRUE)
-
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(stat == DEAD)
- return ..()
-
- var/turf/our_turf = get_turf(src)
- if(HAS_TRAIT(our_turf, TRAIT_RUSTY))
- adjustBruteLoss(-1.5 * seconds_per_tick, FALSE)
- adjustFireLoss(-1.5 * seconds_per_tick, FALSE)
-
- return ..()
-
-/mob/living/simple_animal/hostile/heretic_summon/ash_spirit
- name = "Ash Man"
- real_name = "Ashy"
- desc = "An incomprehensible abomination. As it moves, a thin trail of ash follows, appearing from seemingly nowhere."
- icon_state = "ash_walker"
- icon_living = "ash_walker"
- status_flags = CANPUSH
- maxHealth = 75
- health = 75
- melee_damage_lower = 15
- melee_damage_upper = 20
- sight = SEE_TURFS
- actions_to_add = list(
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash,
- /datum/action/cooldown/spell/pointed/cleave,
- /datum/action/cooldown/spell/fire_sworn,
- )
-
-/mob/living/simple_animal/hostile/heretic_summon/stalker
- name = "Flesh Stalker"
- real_name = "Flesh Stalker"
- desc = "An abomination made from several limbs and organs. Every moment you stare at it, it appears to shift and change unnaturally."
- icon_state = "stalker"
- icon_living = "stalker"
- status_flags = CANPUSH
- maxHealth = 150
- health = 150
- melee_damage_lower = 15
- melee_damage_upper = 20
- sight = SEE_MOBS
- actions_to_add = list(
- /datum/action/cooldown/spell/shapeshift/eldritch,
- /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash,
- /datum/action/cooldown/spell/emp/eldritch,
- )
diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm b/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
index 5ef5987942e2ec..04bb2f94cc9c69 100644
--- a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
+++ b/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm
@@ -132,9 +132,10 @@
taste_mult = 1.3
/datum/reagent/toxin/leaper_venom/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+ . = ..()
if(volume >= 10)
- M.adjustToxLoss(5 * REM * seconds_per_tick, 0)
- ..()
+ if(M.adjustToxLoss(5 * REM * seconds_per_tick, updating_health = FALSE))
+ . = UPDATE_MOB_HEALTH
/obj/effect/temp_visual/leaper_crush
name = "grim tidings"
diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/mook.dm b/code/modules/mob/living/simple_animal/hostile/jungle/mook.dm
deleted file mode 100644
index 444635f2dc344f..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/jungle/mook.dm
+++ /dev/null
@@ -1,229 +0,0 @@
-#define MOOK_ATTACK_NEUTRAL 0
-#define MOOK_ATTACK_WARMUP 1
-#define MOOK_ATTACK_ACTIVE 2
-#define MOOK_ATTACK_RECOVERY 3
-#define ATTACK_INTERMISSION_TIME 5
-
-//Fragile but highly aggressive wanderers that pose a large threat in numbers.
-//They'll attempt to leap at their target from afar using their hatchets.
-/mob/living/simple_animal/hostile/jungle/mook
- name = "wanderer"
- desc = "This unhealthy looking primitive is wielding a rudimentary hatchet, swinging it with wild abandon. One isn't much of a threat, but in numbers they can quickly overwhelm a superior opponent."
- icon = 'icons/mob/simple/jungle/mook.dmi'
- icon_state = "mook"
- icon_living = "mook"
- icon_dead = "mook_dead"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- SET_BASE_PIXEL(-16, -8)
-
- maxHealth = 45
- health = 45
- melee_damage_lower = 30
- melee_damage_upper = 30
- ranged = TRUE
- ranged_cooldown_time = 10
- pass_flags_self = LETPASSTHROW
- robust_searching = TRUE
- stat_attack = HARD_CRIT
- attack_sound = 'sound/weapons/rapierhit.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- death_sound = 'sound/voice/mook_death.ogg'
- aggro_vision_range = 15 //A little more aggressive once in combat to balance out their really low HP
- var/attack_state = MOOK_ATTACK_NEUTRAL
- var/struck_target_leap = FALSE
-
- footstep_type = FOOTSTEP_MOB_BAREFOOT
-
-/mob/living/simple_animal/hostile/jungle/mook/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(istype(mover, /mob/living/simple_animal/hostile/jungle/mook))
- var/mob/living/simple_animal/hostile/jungle/mook/mook_moover = mover
- if(mook_moover.attack_state == MOOK_ATTACK_ACTIVE && mook_moover.throwing)
- return TRUE
-
-/mob/living/simple_animal/hostile/jungle/mook/death()
- desc = "A deceased primitive. Upon closer inspection, it was suffering from severe cellular degeneration and its garments are machine made..."//Can you guess the twist
- return ..()
-
-/mob/living/simple_animal/hostile/jungle/mook/AttackingTarget()
- if(isliving(target))
- if(ranged_cooldown <= world.time && attack_state == MOOK_ATTACK_NEUTRAL)
- var/mob/living/L = target
- if(L.incapacitated())
- WarmupAttack(forced_slash_combo = TRUE)
- return
- WarmupAttack()
- return
- return ..()
-
-/mob/living/simple_animal/hostile/jungle/mook/Goto()
- if(attack_state != MOOK_ATTACK_NEUTRAL)
- return
- return ..()
-
-/mob/living/simple_animal/hostile/jungle/mook/Move()
- if(attack_state == MOOK_ATTACK_WARMUP || attack_state == MOOK_ATTACK_RECOVERY)
- return
- return ..()
-
-/mob/living/simple_animal/hostile/jungle/mook/proc/WarmupAttack(forced_slash_combo = FALSE)
- if(attack_state == MOOK_ATTACK_NEUTRAL && target)
- attack_state = MOOK_ATTACK_WARMUP
- SSmove_manager.stop_looping(src)
- update_icons()
- if(prob(50) && get_dist(src,target) <= 3 || forced_slash_combo)
- addtimer(CALLBACK(src, PROC_REF(SlashCombo)), ATTACK_INTERMISSION_TIME)
- return
- addtimer(CALLBACK(src, PROC_REF(LeapAttack)), ATTACK_INTERMISSION_TIME + rand(0,3))
- return
- attack_state = MOOK_ATTACK_RECOVERY
- ResetNeutral()
-
-/mob/living/simple_animal/hostile/jungle/mook/proc/SlashCombo()
- if(attack_state == MOOK_ATTACK_WARMUP && !stat)
- attack_state = MOOK_ATTACK_ACTIVE
- update_icons()
- SlashAttack()
- addtimer(CALLBACK(src, PROC_REF(SlashAttack)), 3)
- addtimer(CALLBACK(src, PROC_REF(SlashAttack)), 6)
- addtimer(CALLBACK(src, PROC_REF(AttackRecovery)), 9)
-
-/mob/living/simple_animal/hostile/jungle/mook/proc/SlashAttack()
- if(target && !stat && attack_state == MOOK_ATTACK_ACTIVE)
- melee_damage_lower = 15
- melee_damage_upper = 15
- var/mob_direction = get_dir(src,target)
- var/atom/target_from = GET_TARGETS_FROM(src)
- if(get_dist(src,target) > 1)
- step(src,mob_direction)
- if(isturf(target_from.loc) && target.Adjacent(target_from) && isliving(target))
- var/mob/living/L = target
- L.attack_animal(src)
- return
- var/swing_turf = get_step(src,mob_direction)
- new /obj/effect/temp_visual/kinetic_blast(swing_turf)
- playsound(src, 'sound/weapons/slashmiss.ogg', 50, TRUE)
-
-/mob/living/simple_animal/hostile/jungle/mook/proc/LeapAttack()
- if(target && !stat && attack_state == MOOK_ATTACK_WARMUP)
- attack_state = MOOK_ATTACK_ACTIVE
- ADD_TRAIT(src, TRAIT_UNDENSE, LEAPING_TRAIT)
- melee_damage_lower = 30
- melee_damage_upper = 30
- update_icons()
- new /obj/effect/temp_visual/mook_dust(get_turf(src))
- playsound(src, 'sound/weapons/thudswoosh.ogg', 25, TRUE)
- playsound(src, 'sound/voice/mook_leap_yell.ogg', 100, TRUE)
- var/target_turf = get_turf(target)
- throw_at(target_turf, 7, 1, src, FALSE, callback = CALLBACK(src, PROC_REF(AttackRecovery)))
- return
- attack_state = MOOK_ATTACK_RECOVERY
- ResetNeutral()
-
-/mob/living/simple_animal/hostile/jungle/mook/proc/AttackRecovery()
- if(attack_state == MOOK_ATTACK_ACTIVE && !stat)
- attack_state = MOOK_ATTACK_RECOVERY
- REMOVE_TRAIT(src, TRAIT_UNDENSE, LEAPING_TRAIT)
- face_atom(target)
- if(!struck_target_leap)
- update_icons()
- struck_target_leap = FALSE
- if(prob(40))
- attack_state = MOOK_ATTACK_NEUTRAL
- if(target)
- if(isliving(target))
- var/mob/living/L = target
- if(L.incapacitated() && L.stat != DEAD)
- addtimer(CALLBACK(src, PROC_REF(WarmupAttack), TRUE), ATTACK_INTERMISSION_TIME)
- return
- addtimer(CALLBACK(src, PROC_REF(WarmupAttack)), ATTACK_INTERMISSION_TIME)
- return
- addtimer(CALLBACK(src, PROC_REF(ResetNeutral)), ATTACK_INTERMISSION_TIME)
-
-/mob/living/simple_animal/hostile/jungle/mook/proc/ResetNeutral()
- if(attack_state == MOOK_ATTACK_RECOVERY)
- attack_state = MOOK_ATTACK_NEUTRAL
- ranged_cooldown = world.time + ranged_cooldown_time
- update_icons()
- if(target && !stat)
- update_icons()
- Goto(target, move_to_delay, minimum_distance)
-
-/mob/living/simple_animal/hostile/jungle/mook/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
- . = ..()
- if(isliving(hit_atom) && attack_state == MOOK_ATTACK_ACTIVE)
- var/mob/living/L = hit_atom
- if(CanAttack(L))
- L.attack_animal(src)
- struck_target_leap = TRUE
- REMOVE_TRAIT(src, TRAIT_UNDENSE, LEAPING_TRAIT)
- update_icons()
- var/mook_under_us = FALSE
- for(var/A in get_turf(src))
- if(struck_target_leap && mook_under_us)
- break
- if(A == src)
- continue
- if(isliving(A))
- var/mob/living/ML = A
- if(!struck_target_leap && CanAttack(ML))//Check if some joker is attempting to use rest to evade us
- struck_target_leap = TRUE
- ML.attack_animal(src)
- REMOVE_TRAIT(src, TRAIT_UNDENSE, LEAPING_TRAIT)
- struck_target_leap = TRUE
- update_icons()
- continue
- if(istype(ML, /mob/living/simple_animal/hostile/jungle/mook) && !mook_under_us)//If we land on the same tile as another mook, spread out so we don't stack our sprite on the same tile
- var/mob/living/simple_animal/hostile/jungle/mook/M = ML
- if(!M.stat)
- mook_under_us = TRUE
- var/anydir = pick(GLOB.cardinals)
- Move(get_step(src, anydir), anydir)
- continue
-
-/mob/living/simple_animal/hostile/jungle/mook/handle_automated_action()
- if(attack_state)
- return
- return ..()
-
-/mob/living/simple_animal/hostile/jungle/mook/OpenFire()
- if(isliving(target))
- var/mob/living/L = target
- if(L.incapacitated())
- return
- WarmupAttack()
-
-/mob/living/simple_animal/hostile/jungle/mook/update_icons()
- . = ..()
- if(!stat)
- switch(attack_state)
- if(MOOK_ATTACK_NEUTRAL)
- icon_state = "mook"
- if(MOOK_ATTACK_WARMUP)
- icon_state = "mook_warmup"
- if(MOOK_ATTACK_ACTIVE)
- if(!density)
- icon_state = "mook_leap"
- return
- if(struck_target_leap)
- icon_state = "mook_strike"
- return
- icon_state = "mook_slash_combo"
- if(MOOK_ATTACK_RECOVERY)
- icon_state = "mook"
-
-/obj/effect/temp_visual/mook_dust
- name = "dust"
- desc = "It's just a dust cloud!"
- icon = 'icons/mob/simple/jungle/mook.dmi'
- icon_state = "mook_leap_cloud"
- layer = BELOW_MOB_LAYER
- plane = GAME_PLANE
- SET_BASE_PIXEL(-16, -16)
- duration = 10
-
-#undef MOOK_ATTACK_NEUTRAL
-#undef MOOK_ATTACK_WARMUP
-#undef MOOK_ATTACK_ACTIVE
-#undef MOOK_ATTACK_RECOVERY
-#undef ATTACK_INTERMISSION_TIME
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 3484c27375da46..5bb9f314ed1560 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
@@ -70,7 +70,10 @@
return ..()
/mob/living/simple_animal/hostile/megafauna/death(gibbed, list/force_grant)
- if(health > 0)
+ if(gibbed) // in case they've been force dusted
+ return ..()
+
+ if(health > 0) // prevents instakills
return
var/datum/status_effect/crusher_damage/crusher_dmg = has_status_effect(/datum/status_effect/crusher_damage)
///Whether we killed the megafauna with primarily crusher damage or not
@@ -100,8 +103,8 @@
/mob/living/simple_animal/hostile/megafauna/gib()
if(health > 0)
return
- else
- ..()
+
+ return ..()
/mob/living/simple_animal/hostile/megafauna/singularity_act()
set_health(0)
@@ -110,8 +113,11 @@
/mob/living/simple_animal/hostile/megafauna/dust(just_ash, drop_items, force)
if(!force && health > 0)
return
- else
- ..()
+
+ crusher_loot.Cut()
+ loot.Cut()
+
+ return ..()
/mob/living/simple_animal/hostile/megafauna/AttackingTarget()
if(recovery_time >= world.time)
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 8a92ca680f77e2..5c63ca4e884005 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
@@ -607,7 +607,7 @@
holder_animal.mind.transfer_to(possessor)
possessor.mind.grab_ghost(force = TRUE)
holder_animal.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
- holder_animal.gib()
+ holder_animal.gib(DROP_ALL_REMAINS)
return ..()
return ..()
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 66d487c8697e04..84e2d9c049b8bf 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
@@ -272,7 +272,7 @@
if(!isclosedturf(T) && !islava(T))
var/lava_turf = /turf/open/lava/smooth
var/reset_turf = T.type
- T.ChangeTurf(lava_turf, flags = CHANGETURF_INHERIT_AIR)
+ T.TerraformTurf(lava_turf, flags = CHANGETURF_INHERIT_AIR)
addtimer(CALLBACK(T, TYPE_PROC_REF(/turf, ChangeTurf), reset_turf, null, CHANGETURF_INHERIT_AIR), reset_time, TIMER_OVERRIDE|TIMER_UNIQUE)
/obj/effect/temp_visual/drakewall
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
index e9cf04b9c21076..26f3690fef13cd 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
@@ -267,6 +267,10 @@ Difficulty: Hard
/mob/living/simple_animal/hostile/megafauna/wendigo/death(gibbed, list/force_grant)
if(health > 0)
return
+
+ if(!true_spawn)
+ return ..()
+
var/obj/effect/portal/permanent/one_way/exit = new /obj/effect/portal/permanent/one_way(starting)
exit.id = "wendigo arena exit"
exit.add_atom_colour(COLOR_RED_LIGHT, ADMIN_COLOUR_PRIORITY)
diff --git a/code/modules/mob/living/simple_animal/hostile/mimic.dm b/code/modules/mob/living/simple_animal/hostile/mimic.dm
index 2fec84385b2d2b..d07775b42bd538 100644
--- a/code/modules/mob/living/simple_animal/hostile/mimic.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mimic.dm
@@ -113,6 +113,7 @@ GLOBAL_LIST_INIT(animatable_blacklist, list(/obj/structure/table, /obj/structure
/mob/living/simple_animal/hostile/mimic/copy/Initialize(mapload, obj/copy, mob/living/creator, destroy_original = 0, no_googlies = FALSE)
. = ..()
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT) // They won't remember their original contents upon ressurection and would just be floating eyes
if (no_googlies)
overlay_googly_eyes = FALSE
CopyObject(copy, creator, destroy_original)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm
index 58d9988256f14c..d91f312b454d88 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/goliath_broodmother.dm
@@ -211,7 +211,7 @@
mother.children_list -= src
visible_message(span_warning("[src] explodes!"))
explosion(src, flame_range = 3, adminlog = FALSE)
- gib()
+ gib(DROP_ALL_REMAINS)
/obj/effect/goliath_tentacle/broodmother
grapple_time = 1 SECONDS
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm
deleted file mode 100644
index 25d1ea9da742e1..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm
+++ /dev/null
@@ -1,85 +0,0 @@
-/mob/living/simple_animal/hostile/asteroid/ice_demon
- name = "demonic watcher"
- desc = "A creature formed entirely out of ice, bluespace energy emanates from inside of it."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "ice_demon"
- icon_living = "ice_demon"
- icon_dead = "ice_demon_dead"
- icon_gib = "syndicate_gib"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- mouse_opacity = MOUSE_OPACITY_ICON
- speak_emote = list("telepathically cries")
- speed = 2
- move_to_delay = 2
- projectiletype = /obj/projectile/temp/ice_demon
- projectilesound = 'sound/weapons/pierce.ogg'
- ranged = TRUE
- ranged_message = "manifests ice"
- ranged_cooldown_time = 1.5 SECONDS
- minimum_distance = 3
- retreat_distance = 3
- maxHealth = 150
- health = 150
- obj_damage = 40
- melee_damage_lower = 15
- melee_damage_upper = 15
- attack_verb_continuous = "slices"
- attack_verb_simple = "slice"
- attack_sound = 'sound/weapons/bladeslice.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- vision_range = 9
- aggro_vision_range = 9
- move_force = MOVE_FORCE_VERY_STRONG
- move_resist = MOVE_FORCE_VERY_STRONG
- pull_force = MOVE_FORCE_VERY_STRONG
- del_on_death = TRUE
- loot = list()
- crusher_loot = /obj/item/crusher_trophy/demon_core /// SKYRAT EDIT CHANGE - ORIGINAL : crusher_loot = /obj/item/crusher_trophy/watcher_wing/ice_wing
- death_message = "fades as the energies that tied it to this world dissipate."
- death_sound = 'sound/magic/demon_dies.ogg'
- stat_attack = HARD_CRIT
- robust_searching = TRUE
- footstep_type = FOOTSTEP_MOB_CLAW
- /// Distance the demon will teleport from the target
- var/teleport_distance = 3
-
-/mob/living/simple_animal/hostile/asteroid/ice_demon/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/simple_flying)
-
-/obj/projectile/temp/ice_demon
- name = "ice blast"
- icon_state = "ice_2"
- damage = 5
- damage_type = BURN
- armor_flag = ENERGY
- speed = 1
- pixel_speed_multiplier = 0.25
- range = 200
- temperature = -75
-
-/mob/living/simple_animal/hostile/asteroid/ice_demon/OpenFire()
- ranged_cooldown = world.time + ranged_cooldown_time
- // Sentient ice demons teleporting has been linked to server crashes
- if(client)
- return ..()
- if(teleport_distance <= 0)
- return ..()
- var/list/possible_ends = view(teleport_distance, target.loc) - view(teleport_distance - 1, target.loc)
- for(var/turf/closed/turf_to_remove in possible_ends)
- possible_ends -= turf_to_remove
- if(!possible_ends.len)
- return ..()
- var/turf/end = pick(possible_ends)
- do_teleport(src, end, 0, channel=TELEPORT_CHANNEL_BLUESPACE, forced = TRUE)
- SLEEP_CHECK_DEATH(8, src)
- return ..()
-
-/mob/living/simple_animal/hostile/asteroid/ice_demon/death(gibbed)
- move_force = MOVE_FORCE_DEFAULT
- move_resist = MOVE_RESIST_DEFAULT
- pull_force = PULL_FORCE_DEFAULT
- new /obj/item/stack/ore/bluespace_crystal(loc, 3)
- if(prob(5))
- new /obj/item/raw_anomaly_core/bluespace(loc)
- return ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/smspider.dm b/code/modules/mob/living/simple_animal/hostile/smspider.dm
deleted file mode 100644
index 150180a11cff62..00000000000000
--- a/code/modules/mob/living/simple_animal/hostile/smspider.dm
+++ /dev/null
@@ -1,64 +0,0 @@
-/mob/living/simple_animal/hostile/smspider
- name = "supermatter spider"
- desc= "A sliver of supermatter placed upon a robotically enhanced pedestal."
- icon = 'icons/mob/simple/smspider.dmi'
- icon_state = "smspider"
- icon_living = "smspider"
- icon_dead = "smspider_dead"
- gender = NEUTER
- mob_biotypes = MOB_BUG|MOB_ROBOTIC
- turns_per_move = 2
- speak_emote = list("vibrates")
- emote_see = list("vibrates")
- emote_taunt = list("vibrates")
- taunt_chance = 40
- combat_mode = TRUE
- maxHealth = 10
- health = 10
- minbodytemp = 0
- maxbodytemp = 1500
- attack_verb_continuous = "slices"
- attack_verb_simple = "slice"
- attack_sound = 'sound/effects/supermatter.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)
- robust_searching = 1
- faction = list(FACTION_HOSTILE)
- // Gold, supermatter tinted
- lighting_cutoff_red = 30
- lighting_cutoff_green = 30
- lighting_cutoff_blue = 10
- death_message = "falls to the ground, its shard dulling to a miserable grey!"
- footstep_type = FOOTSTEP_MOB_CLAW
- var/overcharged = FALSE // if true, spider will not die if it dusts a limb
-
-/mob/living/simple_animal/hostile/smspider/AttackingTarget()
- . = ..()
- if(isliving(target))
- playsound(get_turf(src), 'sound/effects/supermatter.ogg', 10, TRUE)
- visible_message(span_danger("[src] knocks into [target], turning them to dust in a brilliant flash of light!"))
- var/mob/living/victim = target
- victim.investigate_log("has been dusted by [src].", INVESTIGATE_DEATHS)
- victim.dust()
- if(!overcharged)
- death()
- else if(!isturf(target))
- playsound(get_turf(src), 'sound/effects/supermatter.ogg', 10, TRUE)
- visible_message(span_danger("[src] knocks into [target], turning it to dust in a brilliant flash of light!"))
- qdel(target)
- if(!overcharged)
- death()
- return FALSE
-
-/mob/living/simple_animal/hostile/smspider/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/swarming)
-
-/mob/living/simple_animal/hostile/smspider/overcharged
- name = "overcharged supermatter spider"
- desc = "A sliver of overcharged supermatter placed upon a robotically enhanced pedestal. This one seems especially dangerous."
- icon_state = "smspideroc"
- icon_living = "smspideroc"
- maxHealth = 25
- health = 25
- overcharged = TRUE
diff --git a/code/modules/mob/living/simple_animal/revenant.dm b/code/modules/mob/living/simple_animal/revenant.dm
deleted file mode 100644
index 6e2ec11afeac8a..00000000000000
--- a/code/modules/mob/living/simple_animal/revenant.dm
+++ /dev/null
@@ -1,548 +0,0 @@
-//Revenants: based off of wraiths from Goon
-//"Ghosts" that are invisible and move like ghosts, cannot take damage while invisible
-//Can hear deadchat, but are NOT normal ghosts and do NOT have x-ray vision
-//Admin-spawn or random event
-
-/// Source for a trait we get when we're stunned
-#define REVENANT_STUNNED_TRAIT "revenant_got_stunned"
-
-/mob/living/simple_animal/revenant
- name = "revenant"
- desc = "A malevolent spirit."
- icon = 'icons/mob/simple/mob.dmi'
- icon_state = "revenant_idle"
- var/icon_idle = "revenant_idle"
- var/icon_reveal = "revenant_revealed"
- var/icon_stun = "revenant_stun"
- var/icon_drain = "revenant_draining"
- var/stasis = FALSE
- mob_biotypes = MOB_SPIRIT
- incorporeal_move = INCORPOREAL_MOVE_JAUNT
- invisibility = INVISIBILITY_REVENANT
- health = INFINITY //Revenants don't use health, they use essence instead
- maxHealth = INFINITY
- plane = GHOST_PLANE
- sight = SEE_SELF
- throwforce = 0
-
- // Going for faint purple spoopy ghost
- lighting_cutoff_red = 20
- lighting_cutoff_green = 15
- lighting_cutoff_blue = 35
- response_help_continuous = "passes through"
- response_help_simple = "pass through"
- response_disarm_continuous = "swings through"
- response_disarm_simple = "swing through"
- response_harm_continuous = "punches through"
- response_harm_simple = "punch through"
- unsuitable_atmos_damage = 0
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) //I don't know how you'd apply those, but revenants no-sell them anyway.
- 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
- harm_intent_damage = 0
- friendly_verb_continuous = "touches"
- friendly_verb_simple = "touch"
- status_flags = 0
- wander = FALSE
- density = FALSE
- move_resist = MOVE_FORCE_OVERPOWERING
- mob_size = MOB_SIZE_TINY
- pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
- speed = 1
- unique_name = TRUE
- hud_possible = list(ANTAG_HUD)
- hud_type = /datum/hud/revenant
-
- var/essence = 75 //The resource, and health, of revenants.
- var/essence_regen_cap = 75 //The regeneration cap of essence (go figure); regenerates every Life() tick up to this amount.
- var/essence_regenerating = TRUE //If the revenant regenerates essence or not
- var/essence_regen_amount = 2.5 //How much essence regenerates per second
- var/essence_accumulated = 0 //How much essence the revenant has stolen
- var/essence_excess = 0 //How much stolen essence avilable for unlocks
- var/revealed = FALSE //If the revenant can take damage from normal sources.
- var/unreveal_time = 0 //How long the revenant is revealed for, is about 2 seconds times this var.
- var/unstun_time = 0 //How long the revenant is stunned for, is about 2 seconds times this var.
- var/inhibited = FALSE //If the revenant's abilities are blocked by a chaplain's power.
- var/essence_drained = 0 //How much essence the revenant will drain from the corpse it's feasting on.
- var/draining = FALSE //If the revenant is draining someone.
- var/list/drained_mobs = list() //Cannot harvest the same mob twice
- var/perfectsouls = 0 //How many perfect, regen-cap increasing souls the revenant has. //TODO, add objective for getting a perfect soul(s?)
- var/generated_objectives_and_spells = FALSE
-
-/mob/living/simple_animal/revenant/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/simple_flying)
- add_traits(list(TRAIT_SPACEWALK, TRAIT_SIXTHSENSE, TRAIT_FREE_HYPERSPACE_MOVEMENT), INNATE_TRAIT)
-
- // Starting spells
-
- var/datum/action/cooldown/spell/list_target/telepathy/revenant/telepathy = new(src)
- telepathy.Grant(src)
-
- // Starting spells that start locked
- var/datum/action/cooldown/spell/aoe/revenant/overload/lights_go_zap = new(src)
- lights_go_zap.Grant(src)
-
- var/datum/action/cooldown/spell/aoe/revenant/defile/windows_go_smash = new(src)
- windows_go_smash.Grant(src)
-
- var/datum/action/cooldown/spell/aoe/revenant/blight/botany_go_mad = new(src)
- botany_go_mad.Grant(src)
-
- var/datum/action/cooldown/spell/aoe/revenant/malfunction/shuttle_go_emag = new(src)
- shuttle_go_emag.Grant(src)
-
- var/datum/action/cooldown/spell/aoe/revenant/haunt_object/toolbox_go_bonk = new(src)
- toolbox_go_bonk.Grant(src)
-
- RegisterSignal(src, COMSIG_LIVING_BANED, PROC_REF(on_baned))
- random_revenant_name()
-
-/mob/living/simple_animal/revenant/can_perform_action(atom/movable/target, action_bitflags)
- return FALSE
-
-/mob/living/simple_animal/revenant/proc/random_revenant_name()
- var/built_name = ""
- built_name += pick(strings(REVENANT_NAME_FILE, "spirit_type"))
- built_name += " of "
- built_name += pick(strings(REVENANT_NAME_FILE, "adverb"))
- built_name += pick(strings(REVENANT_NAME_FILE, "theme"))
- name = built_name
-
-/mob/living/simple_animal/revenant/Login()
- . = ..()
- if(!. || !client)
- return FALSE
- to_chat(src, span_deadsay("You are a revenant."))
- to_chat(src, "Your formerly mundane spirit has been infused with alien energies and empowered into a revenant.")
- to_chat(src, "You are not dead, not alive, but somewhere in between. You are capable of limited interaction with both worlds.")
- to_chat(src, "You are invincible and invisible to everyone but other ghosts. Most abilities will reveal you, rendering you vulnerable.")
- to_chat(src, "To function, you are to drain the life essence from humans. This essence is a resource, as well as your health, and will power all of your abilities.")
- to_chat(src, "You do not remember anything of your past lives, nor will you remember anything about this one after your death.")
- to_chat(src, "Be sure to read the wiki page to learn more.")
- if(!generated_objectives_and_spells)
- generated_objectives_and_spells = TRUE
- mind.set_assigned_role(SSjob.GetJobType(/datum/job/revenant))
- mind.special_role = ROLE_REVENANT
- SEND_SOUND(src, sound('sound/effects/ghost.ogg'))
- mind.add_antag_datum(/datum/antagonist/revenant)
-
-//Life, Stat, Hud Updates, and Say
-/mob/living/simple_animal/revenant/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(stasis)
- return
- var/delta_time = DELTA_WORLD_TIME(SSmobs)
- if(revealed && essence <= 0)
- death()
- if(unreveal_time && world.time >= unreveal_time)
- unreveal_time = 0
- revealed = FALSE
- incorporeal_move = INCORPOREAL_MOVE_JAUNT
- invisibility = INVISIBILITY_REVENANT
- to_chat(src, span_revenboldnotice("You are once more concealed."))
- if(unstun_time && world.time >= unstun_time)
- unstun_time = 0
- REMOVE_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
- to_chat(src, span_revenboldnotice("You can move again!"))
- if(essence_regenerating && !inhibited && essence < essence_regen_cap) //While inhibited, essence will not regenerate
- essence = min(essence + (essence_regen_amount * delta_time), essence_regen_cap)
- update_mob_action_buttons() //because we update something required by our spells in life, we need to update our buttons
- update_spooky_icon()
- update_health_hud()
- ..()
-
-/mob/living/simple_animal/revenant/get_status_tab_items()
- . = ..()
- . += "Current Essence: [essence >= essence_regen_cap ? essence : "[essence] / [essence_regen_cap]"]E"
- . += "Total Essence Stolen: [essence_accumulated]SE"
- . += "Unused Stolen Essence: [essence_excess]SE"
- . += "Perfect Souls Stolen: [perfectsouls]"
-
-/mob/living/simple_animal/revenant/update_health_hud()
- if(hud_used)
- var/essencecolor = "#8F48C6"
- if(essence > essence_regen_cap)
- essencecolor = "#9A5ACB" //oh boy you've got a lot of essence
- else if(!essence)
- essencecolor = "#1D2953" //oh jeez you're dying
- hud_used.healths.maptext = MAPTEXT("
[essence]E
")
-
-/mob/living/simple_animal/revenant/med_hud_set_health()
- return //we use no hud
-
-/mob/living/simple_animal/revenant/med_hud_set_status()
- return //we use no hud
-
-/mob/living/simple_animal/revenant/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null, message_range = 7, datum/saymode/saymode = null)
- if(!message)
- return
- if(sanitize)
- message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
- src.log_talk(message, LOG_SAY)
- var/rendered = span_deadsay("UNDEAD: [src] says, \"[message]\"")
- for(var/mob/M in GLOB.mob_list)
- if(isrevenant(M))
- to_chat(M, rendered)
- else if(isobserver(M))
- var/link = FOLLOW_LINK(M, src)
- to_chat(M, "[link] [rendered]")
- return
-
-
-//Immunities
-
-/mob/living/simple_animal/revenant/ex_act(severity, target)
- return FALSE //Immune to the effects of explosions.
-
-/mob/living/simple_animal/revenant/blob_act(obj/structure/blob/B)
- return //blah blah blobs aren't in tune with the spirit world, or something.
-
-/mob/living/simple_animal/revenant/singularity_act()
- return //don't walk into the singularity expecting to find corpses, okay?
-
-/mob/living/simple_animal/revenant/narsie_act()
- return //most humans will now be either bones or harvesters, but we're still un-alive.
-
-/mob/living/simple_animal/revenant/bullet_act()
- if(!revealed || stasis)
- return BULLET_ACT_FORCE_PIERCE
- return ..()
-
-//damage, gibbing, and dying
-/mob/living/simple_animal/revenant/proc/on_baned(obj/item/weapon, mob/living/user)
- SIGNAL_HANDLER
- visible_message(span_warning("[src] violently flinches!"), \
- span_revendanger("As [weapon] passes through you, you feel your essence draining away!"))
- inhibited = TRUE
- update_mob_action_buttons()
- addtimer(CALLBACK(src, PROC_REF(reset_inhibit)), 3 SECONDS)
-
-/mob/living/simple_animal/revenant/proc/reset_inhibit()
- inhibited = FALSE
- update_mob_action_buttons()
-
-/mob/living/simple_animal/revenant/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- if(!forced && !revealed)
- return FALSE
- . = amount
- essence = max(0, essence-amount)
- if(updating_health)
- update_health_hud()
- if(!essence)
- death()
-
-/mob/living/simple_animal/revenant/dust(just_ash, drop_items, force)
- death()
-
-/mob/living/simple_animal/revenant/gib()
- death()
-
-/mob/living/simple_animal/revenant/death()
- if(!revealed || stasis) //Revenants cannot die if they aren't revealed //or are already dead
- return
- stasis = TRUE
- to_chat(src, span_revendanger("NO! No... it's too late, you can feel your essence [pick("breaking apart", "drifting away")]..."))
- ADD_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
- revealed = TRUE
- invisibility = 0
- playsound(src, 'sound/effects/screech.ogg', 100, TRUE)
- visible_message(span_warning("[src] lets out a waning screech as violet mist swirls around its dissolving body!"))
- icon_state = "revenant_draining"
- for(var/i = alpha, i > 0, i -= 10)
- stoplag()
- alpha = i
- visible_message(span_danger("[src]'s body breaks apart into a fine pile of blue dust."))
- var/reforming_essence = essence_regen_cap //retain the gained essence capacity
- var/obj/item/ectoplasm/revenant/R = new(get_turf(src))
- R.essence = max(reforming_essence - 15 * perfectsouls, 75) //minus any perfect souls
- R.old_key = client.key //If the essence reforms, the old revenant is put back in the body
- R.revenant = src
- invisibility = INVISIBILITY_ABSTRACT
- revealed = FALSE
- ghostize(0)//Don't re-enter invisible corpse
-
-
-//reveal, stun, icon updates, cast checks, and essence changing
-/mob/living/simple_animal/revenant/proc/reveal(time)
- if(!src)
- return
- if(time <= 0)
- return
- revealed = TRUE
- invisibility = 0
- incorporeal_move = FALSE
- if(!unreveal_time)
- to_chat(src, span_revendanger("You have been revealed!"))
- unreveal_time = world.time + time
- else
- to_chat(src, span_revenwarning("You have been revealed!"))
- unreveal_time = unreveal_time + time
- update_spooky_icon()
- orbiting?.end_orbit(src)
-
-/mob/living/simple_animal/revenant/proc/stun(time)
- if(!src)
- return
- if(time <= 0)
- return
- ADD_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
- if(!unstun_time)
- to_chat(src, span_revendanger("You cannot move!"))
- unstun_time = world.time + time
- else
- to_chat(src, span_revenwarning("You cannot move!"))
- unstun_time = unstun_time + time
- update_spooky_icon()
- orbiting?.end_orbit(src)
-
-/mob/living/simple_animal/revenant/proc/update_spooky_icon()
- if(revealed)
- if(HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
- if(draining)
- icon_state = icon_drain
- else
- icon_state = icon_stun
- else
- icon_state = icon_reveal
- else
- icon_state = icon_idle
-
-/mob/living/simple_animal/revenant/proc/castcheck(essence_cost)
- if(!src)
- return
- var/turf/T = get_turf(src)
- if(isclosedturf(T))
- to_chat(src, span_revenwarning("You cannot use abilities from inside of a wall."))
- return FALSE
- for(var/obj/O in T)
- if(O.density && !O.CanPass(src, get_dir(T, src)))
- to_chat(src, span_revenwarning("You cannot use abilities inside of a dense object."))
- return FALSE
- if(inhibited)
- to_chat(src, span_revenwarning("Your powers have been suppressed by nulling energy!"))
- return FALSE
- if(!change_essence_amount(essence_cost, TRUE))
- to_chat(src, span_revenwarning("You lack the essence to use that ability."))
- return FALSE
- return TRUE
-
-/mob/living/simple_animal/revenant/proc/unlock(essence_cost)
- if(essence_excess < essence_cost)
- return FALSE
- essence_excess -= essence_cost
- update_mob_action_buttons()
- return TRUE
-
-/mob/living/simple_animal/revenant/proc/change_essence_amount(essence_amt, silent = FALSE, source = null)
- if(!src)
- return
- if(essence + essence_amt < 0)
- return
- essence = max(0, essence+essence_amt)
- update_health_hud()
- if(essence_amt > 0)
- essence_accumulated = max(0, essence_accumulated+essence_amt)
- essence_excess = max(0, essence_excess+essence_amt)
- update_mob_action_buttons()
- if(!silent)
- if(essence_amt > 0)
- to_chat(src, span_revennotice("Gained [essence_amt]E[source ? " from [source]":""]."))
- else
- to_chat(src, span_revenminor("Lost [essence_amt]E[source ? " from [source]":""]."))
- return 1
-
-/mob/living/simple_animal/revenant/proc/death_reset()
- revealed = FALSE
- unreveal_time = 0
- REMOVE_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT)
- unstun_time = 0
- inhibited = FALSE
- draining = FALSE
- incorporeal_move = INCORPOREAL_MOVE_JAUNT
- invisibility = INVISIBILITY_REVENANT
- alpha=255
- stasis = FALSE
-
-/mob/living/simple_animal/revenant/orbit(atom/target)
- setDir(SOUTH) // reset dir so the right directional sprites show up
- return ..()
-
-/mob/living/simple_animal/revenant/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- if(!orbiting) // only needed when orbiting
- return ..()
- if(incorporeal_move_check(src))
- return ..()
-
- // back back back it up, the orbitee went somewhere revenant cannot
- orbiting?.end_orbit(src)
- abstract_move(old_loc) // gross but maybe orbit component will be able to check pre move in the future
-
-/mob/living/simple_animal/revenant/stop_orbit(datum/component/orbiter/orbits)
- // reset the simple_flying animation
- animate(src, pixel_y = 2, time = 1 SECONDS, loop = -1, flags = ANIMATION_RELATIVE)
- animate(pixel_y = -2, time = 1 SECONDS, flags = ANIMATION_RELATIVE)
- return ..()
-
-/// Incorporeal move check: blocked by holy-watered tiles and salt piles.
-/mob/living/simple_animal/revenant/proc/incorporeal_move_check(atom/destination)
- var/turf/open/floor/stepTurf = get_turf(destination)
- if(stepTurf)
- var/obj/effect/decal/cleanable/food/salt/salt = locate() in stepTurf
- if(salt)
- to_chat(src, span_warning("[salt] bars your passage!"))
- reveal(20)
- stun(20)
- return
- if(stepTurf.turf_flags & NOJAUNT)
- to_chat(src, span_warning("Some strange aura is blocking the way."))
- return
- if(locate(/obj/effect/blessing) in stepTurf)
- to_chat(src, span_warning("Holy energies block your path!"))
- return
- return TRUE
-
-//reforming
-/obj/item/ectoplasm/revenant
- name = "glimmering residue"
- desc = "A pile of fine blue dust. Small tendrils of violet mist swirl around it."
- icon = 'icons/effects/effects.dmi'
- icon_state = "revenantEctoplasm"
- w_class = WEIGHT_CLASS_SMALL
- var/essence = 75 //the maximum essence of the reforming revenant
- var/reforming = TRUE
- var/inert = FALSE
- var/old_key //key of the previous revenant, will have first pick on reform.
- var/mob/living/simple_animal/revenant/revenant
-
-/obj/item/ectoplasm/revenant/Initialize(mapload)
- . = ..()
- addtimer(CALLBACK(src, PROC_REF(try_reform)), 600)
-
-/obj/item/ectoplasm/revenant/proc/scatter()
- qdel(src)
-
-/obj/item/ectoplasm/revenant/proc/try_reform()
- if(reforming)
- reforming = FALSE
- reform()
- else
- inert = TRUE
- visible_message(span_warning("[src] settles down and seems lifeless."))
-
-/obj/item/ectoplasm/revenant/attack_self(mob/user)
- if(!reforming || inert)
- return ..()
- user.visible_message(span_notice("[user] scatters [src] in all directions."), \
- span_notice("You scatter [src] across the area. The particles slowly fade away."))
- user.dropItemToGround(src)
- scatter()
-
-/obj/item/ectoplasm/revenant/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
- ..()
- if(inert)
- return
- visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
- scatter()
-
-/obj/item/ectoplasm/revenant/examine(mob/user)
- . = ..()
- if(inert)
- . += span_revennotice("It seems inert.")
- else if(reforming)
- . += span_revenwarning("It is shifting and distorted. It would be wise to destroy this.")
-
-/obj/item/ectoplasm/revenant/proc/reform()
- if(QDELETED(src) || QDELETED(revenant) || inert)
- return
- var/key_of_revenant
- message_admins("Revenant ectoplasm was left undestroyed for 1 minute and is reforming into a new revenant.")
- forceMove(drop_location()) //In case it's in a backpack or someone's hand
- revenant.forceMove(loc)
- if(old_key)
- for(var/mob/M in GLOB.dead_mob_list)
- if(M.client && M.client.key == old_key) //Only recreates the mob if the mob the client is in is dead
- key_of_revenant = old_key
- break
- if(!key_of_revenant)
- message_admins("The new revenant's old client either could not be found or is in a new, living mob - grabbing a random candidate instead...")
- var/list/candidates = poll_candidates_for_mob("Do you want to be [revenant.name] (reforming)?", ROLE_REVENANT, ROLE_REVENANT, 5 SECONDS, revenant)
- if(!LAZYLEN(candidates))
- qdel(revenant)
- message_admins("No candidates were found for the new revenant. Oh well!")
- inert = TRUE
- visible_message(span_revenwarning("[src] settles down and seems lifeless."))
- return
- var/mob/dead/observer/C = pick(candidates)
- key_of_revenant = C.key
- if(!key_of_revenant)
- qdel(revenant)
- message_admins("No ckey was found for the new revenant. Oh well!")
- inert = TRUE
- visible_message(span_revenwarning("[src] settles down and seems lifeless."))
- return
-
- message_admins("[key_of_revenant] has been [old_key == key_of_revenant ? "re":""]made into a revenant by reforming ectoplasm.")
- revenant.log_message("was [old_key == key_of_revenant ? "re":""]made as a revenant by reforming ectoplasm.", LOG_GAME)
- visible_message(span_revenboldnotice("[src] suddenly rises into the air before fading away."))
-
- revenant.essence = essence
- revenant.essence_regen_cap = essence
- revenant.death_reset()
- revenant.key = key_of_revenant
- revenant = null
- qdel(src)
-
-/obj/item/ectoplasm/revenant/suicide_act(mob/living/user)
- user.visible_message(span_suicide("[user] is inhaling [src]! It looks like [user.p_theyre()] trying to visit the shadow realm!"))
- scatter()
- return OXYLOSS
-
-/obj/item/ectoplasm/revenant/Destroy()
- if(!QDELETED(revenant))
- qdel(revenant)
- return ..()
-
-//objectives
-/datum/objective/revenant
- var/targetAmount = 100
-
-/datum/objective/revenant/New()
- targetAmount = rand(350,600)
- explanation_text = "Absorb [targetAmount] points of essence from humans."
- ..()
-
-/datum/objective/revenant/check_completion()
- if(!isrevenant(owner.current))
- return FALSE
- var/mob/living/simple_animal/revenant/R = owner.current
- if(!R || R.stat == DEAD)
- return FALSE
- var/essence_stolen = R.essence_accumulated
- if(essence_stolen < targetAmount)
- return FALSE
- return TRUE
-
-/datum/objective/revenant_fluff
-
-/datum/objective/revenant_fluff/New()
- var/list/explanation_texts = list(
- "Assist and exacerbate existing threats at critical moments.", \
- "Impersonate or be worshipped as a god.", \
- "Cause as much chaos and anger as you can without being killed.", \
- "Damage and render as much of the station rusted and unusable as possible.", \
- "Disable and cause malfunctions in as many machines as possible.", \
- "Ensure that any holy weapons are rendered unusable.", \
- "Heed and obey the requests of the dead, provided that carrying them out wouldn't be too inconvenient or self-destructive.", \
- "Make the crew as miserable as possible.", \
- "Make the clown as miserable as possible.", \
- "Make the captain as miserable as possible.", \
- "Prevent the use of energy weapons where possible.",
- )
- explanation_text = pick(explanation_texts)
- ..()
-
-/datum/objective/revenant_fluff/check_completion()
- return TRUE
-
-#undef REVENANT_STUNNED_TRAIT
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 041c76fac42f34..85c146b29c8278 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -177,6 +177,7 @@
stack_trace("Simple animal being instantiated in nullspace")
update_simplemob_varspeed()
if(dextrous)
+ AddElement(/datum/element/dextrous, hud_type = hud_type)
AddComponent(/datum/component/personal_crafting)
add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
ADD_TRAIT(src, TRAIT_NOFIRE_SPREAD, ROUNDSTART_TRAIT)
@@ -447,21 +448,20 @@
/mob/living/simple_animal/death(gibbed)
drop_loot()
- if(dextrous)
- drop_all_held_items()
if(del_on_death)
..()
//Prevent infinite loops if the mob Destroy() is overridden in such
//a manner as to cause a call to death() again //Pain
del_on_death = FALSE
qdel(src)
- else
- health = 0
- icon_state = icon_dead
- if(flip_on_death)
- transform = transform.Turn(180)
- ADD_TRAIT(src, TRAIT_UNDENSE, BASIC_MOB_DEATH_TRAIT)
- ..()
+ return
+
+ health = 0
+ icon_state = icon_dead
+ if(flip_on_death)
+ transform = transform.Turn(180)
+ ADD_TRAIT(src, TRAIT_UNDENSE, BASIC_MOB_DEATH_TRAIT)
+ return ..()
/mob/living/simple_animal/proc/CanAttack(atom/the_target)
if(!isatom(the_target)) // no
@@ -558,49 +558,12 @@
/mob/living/simple_animal/get_idcard(hand_first)
return (..() || access_card)
-/mob/living/simple_animal/can_hold_items(obj/item/I)
- return dextrous && ..()
-
-/mob/living/simple_animal/activate_hand(selhand)
- if(!dextrous)
- return ..()
- if(!selhand)
- selhand = (active_hand_index % held_items.len)+1
- if(istext(selhand))
- selhand = lowertext(selhand)
- if(selhand == "right" || selhand == "r")
- selhand = 2
- if(selhand == "left" || selhand == "l")
- selhand = 1
- if(selhand != active_hand_index)
- swap_hand(selhand)
- else
- mode()
-
-/mob/living/simple_animal/perform_hand_swap(hand_index)
- . = ..()
- if(!.)
- return
- if(!dextrous)
- return
- if(!hand_index)
- hand_index = (active_hand_index % held_items.len)+1
- var/oindex = active_hand_index
- active_hand_index = hand_index
- if(hud_used)
- var/atom/movable/screen/inventory/hand/H
- H = hud_used.hand_slots["[hand_index]"]
- if(H)
- H.update_appearance()
- H = hud_used.hand_slots["[oindex]"]
- if(H)
- H.update_appearance()
-
/mob/living/simple_animal/put_in_hands(obj/item/I, del_on_fail = FALSE, merge_stacks = TRUE, ignore_animation = TRUE)
. = ..()
update_held_items()
/mob/living/simple_animal/update_held_items()
+ . = ..()
if(!client || !hud_used || hud_used.hud_version == HUD_STYLE_NOHUD)
return
var/turf/our_turf = get_turf(src)
diff --git a/code/modules/mob/living/simple_animal/slime/life.dm b/code/modules/mob/living/simple_animal/slime/life.dm
index a103a55b996174..3c34a68d6a26e3 100644
--- a/code/modules/mob/living/simple_animal/slime/life.dm
+++ b/code/modules/mob/living/simple_animal/slime/life.dm
@@ -199,8 +199,11 @@
var/mob/living/animal_victim = prey
var/totaldamage = 0 //total damage done to this unfortunate animal
- totaldamage += animal_victim.adjustBruteLoss(rand(2, 4) * 0.5 * seconds_per_tick)
- totaldamage += animal_victim.adjustToxLoss(rand(1, 2) * 0.5 * seconds_per_tick)
+ var/need_mob_update
+ need_mob_update = totaldamage += animal_victim.adjustBruteLoss(rand(2, 4) * 0.5 * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += totaldamage += animal_victim.adjustToxLoss(rand(1, 2) * 0.5 * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ animal_victim.updatehealth()
if(totaldamage <= 0) //if we did no(or negative!) damage to it, stop
Feedstop(0, 0)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 2fdec7468993af..9002033a13e97b 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -808,18 +808,26 @@
*
* This sends you back to the lobby creating a new dead mob
*
- * Only works if flag/norespawn is allowed in config
+ * Only works if flag/allow_respawn is allowed in config
*/
/mob/verb/abandon_mob()
set name = "Respawn"
set category = "OOC"
- if (CONFIG_GET(flag/norespawn))
- if (!check_rights_for(usr.client, R_ADMIN))
- to_chat(usr, span_boldnotice("Respawning is not enabled!"))
- return
- else if (tgui_alert(usr, "Respawning is currently disabled, do you want to use your permissions to circumvent it?", "Respawn", list("Yes", "No")) != "Yes")
- return
+ switch(CONFIG_GET(flag/allow_respawn))
+ if(RESPAWN_FLAG_NEW_CHARACTER)
+ if(tgui_alert(usr, "Note, respawning is only allowed as another character. If you don't have another free slot you may not be able to respawn.", "Respawn", list("Ok", "Nevermind")) != "Ok")
+ return
+
+ if(RESPAWN_FLAG_FREE)
+ pass() // Normal respawn
+
+ if(RESPAWN_FLAG_DISABLED)
+ if (!check_rights_for(usr.client, R_ADMIN))
+ to_chat(usr, span_boldnotice("Respawning is not enabled!"))
+ return
+ if (tgui_alert(usr, "Respawning is currently disabled, do you want to use your permissions to circumvent it?", "Respawn", list("Yes", "No")) != "Yes")
+ return
if (stat != DEAD)
to_chat(usr, span_boldnotice("You must be dead to use this!"))
@@ -856,15 +864,14 @@
M.key = key
+/// Checks if the mob can respawn yet according to the respawn delay
/mob/proc/check_respawn_delay(override_delay = 0)
if(!override_delay && !CONFIG_GET(number/respawn_delay))
return TRUE
var/death_time = world.time - client.player_details.time_of_death
- var/required_delay = override_delay
- if(!required_delay)
- required_delay = CONFIG_GET(number/respawn_delay)
+ var/required_delay = override_delay || CONFIG_GET(number/respawn_delay)
if(death_time < required_delay)
if(!check_rights_for(usr.client, R_ADMIN))
@@ -977,10 +984,45 @@
/// Performs the actual ritual of swapping hands, such as setting the held index variables
/mob/proc/perform_hand_swap(held_index)
PROTECTED_PROC(TRUE)
+ if (!HAS_TRAIT(src, TRAIT_CAN_HOLD_ITEMS))
+ return FALSE
+
+ if(!held_index)
+ held_index = (active_hand_index % held_items.len) + 1
+
+ if(!isnum(held_index))
+ CRASH("You passed [held_index] into swap_hand instead of a number. WTF man")
+
+ var/previous_index = active_hand_index
+ active_hand_index = held_index
+ if(hud_used)
+ var/atom/movable/screen/inventory/hand/held_location
+ held_location = hud_used.hand_slots["[previous_index]"]
+ if(!isnull(held_location))
+ held_location.update_appearance()
+ held_location = hud_used.hand_slots["[held_index]"]
+ if(!isnull(held_location))
+ held_location.update_appearance()
return TRUE
-/mob/proc/activate_hand(selhand)
- return
+/mob/proc/activate_hand(selected_hand)
+ if (!HAS_TRAIT(src, TRAIT_CAN_HOLD_ITEMS))
+ return
+
+ if(!selected_hand)
+ selected_hand = (active_hand_index % held_items.len)+1
+
+ if(istext(selected_hand))
+ selected_hand = lowertext(selected_hand)
+ if(selected_hand == "right" || selected_hand == "r")
+ selected_hand = 2
+ if(selected_hand == "left" || selected_hand == "l")
+ selected_hand = 1
+
+ if(selected_hand != active_hand_index)
+ swap_hand(selected_hand)
+ else
+ mode()
/mob/proc/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null) //For sec bot threat assessment
return 0
@@ -1405,8 +1447,11 @@
. = ..()
VV_DROPDOWN_OPTION("", "---------")
VV_DROPDOWN_OPTION(VV_HK_GIB, "Gib")
+ VV_DROPDOWN_OPTION(VV_HK_REMOVE_SPELL, "Remove Spell")
VV_DROPDOWN_OPTION(VV_HK_GIVE_SPELL, "Give Spell")
VV_DROPDOWN_OPTION(VV_HK_REMOVE_SPELL, "Remove Spell")
+ VV_DROPDOWN_OPTION(VV_HK_GIVE_MOB_ACTION, "Give Mob Ability")
+ VV_DROPDOWN_OPTION(VV_HK_REMOVE_MOB_ACTION, "Remove Mob Ability")
VV_DROPDOWN_OPTION(VV_HK_GIVE_DISEASE, "Give Disease")
VV_DROPDOWN_OPTION(VV_HK_GODMODE, "Toggle Godmode")
VV_DROPDOWN_OPTION(VV_HK_DROP_ALL, "Drop Everything")
@@ -1433,6 +1478,14 @@
if(!check_rights(R_ADMIN))
return
usr.client.cmd_admin_godmode(src)
+ if(href_list[VV_HK_GIVE_MOB_ACTION])
+ if(!check_rights(NONE))
+ return
+ usr.client.give_mob_action(src)
+ if(href_list[VV_HK_REMOVE_MOB_ACTION])
+ if(!check_rights(NONE))
+ return
+ usr.client.remove_mob_action(src)
if(href_list[VV_HK_GIVE_SPELL])
if(!check_rights(NONE))
return
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 7df256d1873321..8b8a8cff943ad7 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -323,7 +323,7 @@
return FALSE
var/brute_damage = brute_heal > burn_heal //changes repair text based on how much brute/burn was supplied
if((brute_heal > 0 && affecting.brute_dam > 0) || (burn_heal > 0 && affecting.burn_dam > 0))
- if(affecting.heal_damage(brute_heal, burn_heal, BODYTYPE_ROBOTIC))
+ if(affecting.heal_damage(brute_heal, burn_heal, required_bodytype = BODYTYPE_ROBOTIC))
human.update_damage_overlays()
user.visible_message(span_notice("[user] fixes some of the [brute_damage ? "dents on" : "burnt wires in"] [human]'s [affecting.name]."), \
span_notice("You fix some of the [brute_damage ? "dents on" : "burnt wires in"] [human == user ? "your" : "[human]'s"] [affecting.name]."))
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index e9b0f532da5a3a..a5aa8c2c1453d9 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -132,7 +132,7 @@
//Basically an optional override for our glide size
//Sometimes you want to look like you're moving with a delay you don't actually have yet
visual_delay = 0
- var/old_dir = dir
+ var/old_dir = mob.dir
. = ..()
@@ -253,9 +253,9 @@
if(salt)
to_chat(L, span_warning("[salt] bars your passage!"))
if(isrevenant(L))
- var/mob/living/simple_animal/revenant/R = L
- R.reveal(20)
- R.stun(20)
+ var/mob/living/basic/revenant/ghostie = L
+ ghostie.apply_status_effect(/datum/status_effect/revenant/revealed, 2 SECONDS)
+ ghostie.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, 2 SECONDS)
return
if(stepTurf.turf_flags & NOJAUNT)
to_chat(L, span_warning("Some strange aura is blocking the way."))
diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm
index 9bc6a5b22dbc08..fe901b3ad9e2af 100644
--- a/code/modules/mob/mob_transformation_simple.dm
+++ b/code/modules/mob/mob_transformation_simple.dm
@@ -66,7 +66,7 @@
else
desired_mob.key = key
- SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE)
+ SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE, desired_mob)
if(delete_old_mob)
QDEL_IN(src, 1)
return desired_mob
diff --git a/code/modules/mob/mob_update_icons.dm b/code/modules/mob/mob_update_icons.dm
index 8a6464ee1825ac..b8b84f8782afe1 100644
--- a/code/modules/mob/mob_update_icons.dm
+++ b/code/modules/mob/mob_update_icons.dm
@@ -26,7 +26,8 @@
///Updates the held items overlay(s) & HUD element.
/mob/proc/update_held_items()
- return
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_MOB_UPDATE_HELD_ITEMS)
///Updates the mask overlay & HUD element.
/mob/proc/update_worn_mask()
diff --git a/code/modules/mob/status_procs.dm b/code/modules/mob/status_procs.dm
index 1dde25c7802f61..fccd64ab70540d 100644
--- a/code/modules/mob/status_procs.dm
+++ b/code/modules/mob/status_procs.dm
@@ -12,6 +12,7 @@
/mob/proc/adjust_bodytemperature(amount,min_temp=0,max_temp=INFINITY)
if(bodytemperature >= min_temp && bodytemperature <= max_temp)
bodytemperature = clamp(bodytemperature + amount,min_temp,max_temp)
+ return TRUE
/// Sight here is the mob.sight var, which tells byond what to actually show to our client
/// See [code\__DEFINES\sight.dm] for more details
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 09ce3b3c65c8ce..290177f5baf506 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -298,7 +298,7 @@
regenerate_icons()
icon = null
invisibility = INVISIBILITY_MAXIMUM
- var/mob/living/simple_animal/hostile/gorilla/new_gorilla = new (get_turf(src))
+ var/mob/living/basic/gorilla/new_gorilla = new (get_turf(src))
new_gorilla.set_combat_mode(TRUE)
if(mind)
mind.transfer_to(new_gorilla)
@@ -372,7 +372,7 @@
if(!MP)
return FALSE //Sanity, this should never happen.
- if(ispath(MP, /mob/living/simple_animal/hostile/construct))
+ if(ispath(MP, /mob/living/simple_animal/hostile/construct) || ispath(MP, /mob/living/basic/construct))
return FALSE //Verbs do not appear for players.
//Good mobs!
diff --git a/code/modules/mob_spawn/corpses/job_corpses.dm b/code/modules/mob_spawn/corpses/job_corpses.dm
index c8dd458f42db19..3893f3e1ba6965 100644
--- a/code/modules/mob_spawn/corpses/job_corpses.dm
+++ b/code/modules/mob_spawn/corpses/job_corpses.dm
@@ -83,4 +83,4 @@
name = JOB_ROBOTICIST
outfit = /datum/outfit/job/roboticist
icon_state = "corpseroboticist"
-
+
diff --git a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
index 060f7e178be2f6..ce02c6894aee88 100644
--- a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
+++ b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
@@ -46,6 +46,13 @@
pixel_x = -12
base_pixel_x = -12
+/obj/effect/mob_spawn/corpse/watcher
+ mob_type = /mob/living/basic/mining/watcher
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi'
+ icon_state = "watcher_dead_helper"
+ pixel_x = -12
+ base_pixel_x = -12
+
/// Dead headcrab for changeling-themed ruins
/obj/effect/mob_spawn/corpse/headcrab
mob_type = /mob/living/basic/headslug/beakless
diff --git a/code/modules/mob_spawn/ghost_roles/mining_roles.dm b/code/modules/mob_spawn/ghost_roles/mining_roles.dm
index de461380b543a9..9a1536a83d8521 100644
--- a/code/modules/mob_spawn/ghost_roles/mining_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/mining_roles.dm
@@ -207,7 +207,7 @@
yolk.underwear = "Nude"
yolk.equipOutfit(/datum/outfit/ashwalker)//this is an authentic mess we're making
yolk.update_body()
- yolk.gib()
+ yolk.gib(DROP_ALL_REMAINS)
QDEL_NULL(egg)
return ..()
@@ -236,7 +236,7 @@
return ..()
/obj/effect/mob_spawn/ghost_role/human/ash_walker/allow_spawn(mob/user, silent = FALSE)
- if(!(user.key in team.players_spawned))//one per person unless you get a bonus spawn
+ if(!(user.ckey in team.players_spawned))//one per person unless you get a bonus spawn
return TRUE
if(!silent)
to_chat(user, span_warning("You have exhausted your usefulness to the Necropolis."))
@@ -254,7 +254,7 @@
spawned_human.mind.add_antag_datum(/datum/antagonist/ashwalker, team)
spawned_human.remove_language(/datum/language/common)
- team.players_spawned += (spawned_human.key)
+ team.players_spawned += (spawned_human.ckey)
eggshell.egg = null
QDEL_NULL(eggshell)
@@ -316,7 +316,7 @@
ears = /obj/item/radio/headset/syndicate/alt
shoes = /obj/item/clothing/shoes/combat
r_pocket = /obj/item/gun/ballistic/automatic/pistol
- r_hand = /obj/item/gun/ballistic/rifle/sniper_rifle
+ r_hand = /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano/evil // SKYRAT EDIT - Original: /obj/item/gun/ballistic/rifle/sniper_rifle
implants = list(/obj/item/implant/weapons_auth)
id_trim = /datum/id_trim/syndicom/skyrat/interdyne //SKYRAT EDIT
diff --git a/code/modules/mob_spawn/ghost_roles/spider_roles.dm b/code/modules/mob_spawn/ghost_roles/spider_roles.dm
index e3dbea6b1ba7b7..fb3d470f5aa800 100644
--- a/code/modules/mob_spawn/ghost_roles/spider_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/spider_roles.dm
@@ -41,8 +41,9 @@
color = rgb(148, 0, 211)
/obj/structure/spider/eggcluster/bloody
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ icon_state = "eggs"
name = "bloody egg cluster"
- color = rgb(255, 0, 0)
/obj/structure/spider/eggcluster/midwife
name = "midwife egg cluster"
@@ -72,6 +73,8 @@
var/cluster_type = /obj/structure/spider/eggcluster
/// Physical structure housing the spawner
var/obj/structure/spider/eggcluster/egg
+ /// Which antag datum do we grant?
+ var/granted_datum = /datum/antagonist/spider
/// The types of spiders that the spawner can produce
var/list/potentialspawns = list(
/mob/living/basic/spider/growing/spiderling/nurse,
@@ -124,10 +127,11 @@
/obj/effect/mob_spawn/ghost_role/spider/special(mob/living/basic/spider/spawned_mob, mob/mob_possessor)
. = ..()
- spawned_mob.directive = directive
+ if (isspider(spawned_mob))
+ spawned_mob.directive = directive
egg.spawner = null
QDEL_NULL(egg)
- var/datum/antagonist/spider/spider_antag = new(directive)
+ 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/enriched
@@ -144,15 +148,18 @@
/obj/effect/mob_spawn/ghost_role/spider/bloody
name = "bloody egg cluster"
- color = rgb(255, 0, 0)
- you_are_text = "You are a bloody spider."
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ icon_state = "eggs"
+ you_are_text = "You are a flesh spider."
flavour_text = "An abomination of nature set upon the station by changelings. Your only goal is to kill, terrorize, and survive."
- directive = "You are the spawn of a vicious changeling. You have no ambitions except to wreak havoc and ensure your own survival. You are aggressive to all living beings outside of your species, including changelings."
+ faction = list()
+ directive = null
cluster_type = /obj/structure/spider/eggcluster/bloody
potentialspawns = list(
- /mob/living/basic/spider/giant/hunter/flesh,
+ /mob/living/basic/flesh_spider,
)
flash_window = TRUE
+ granted_datum = /datum/antagonist/spider/flesh
/obj/effect/mob_spawn/ghost_role/spider/midwife
name = "midwife egg cluster"
@@ -175,6 +182,14 @@
* * newname - If set, renames the mob to this name
*/
/obj/effect/mob_spawn/ghost_role/spider/create(mob/user, newname)
+ var/chosen_spider = length(potentialspawns) > 1 ? get_radial_choice(user) : potentialspawns[1]
+ if(QDELETED(src) || QDELETED(user) || isnull(chosen_spider))
+ return FALSE
+ mob_type = chosen_spider
+ return ..()
+
+/// Pick a spider type from a radial menu
+/obj/effect/mob_spawn/ghost_role/spider/proc/get_radial_choice(mob/user)
var/list/spider_list = list()
var/list/display_spiders = list()
for(var/choice in potentialspawns)
@@ -196,9 +211,6 @@
display_spiders[initial(spider.name)] = option
sort_list(display_spiders)
+
var/chosen_spider = show_radial_menu(user, egg, display_spiders, radius = 38)
- chosen_spider = spider_list[chosen_spider]
- if(QDELETED(src) || QDELETED(user) || !chosen_spider)
- return FALSE
- mob_type = chosen_spider
- return ..()
+ return spider_list[chosen_spider]
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 96a75842b1b61d..8ab475dce015d9 100644
--- a/code/modules/mob_spawn/ghost_roles/venus_human_trap.dm
+++ b/code/modules/mob_spawn/ghost_roles/venus_human_trap.dm
@@ -4,7 +4,7 @@
desc = "A large pulsating plant..."
icon = 'icons/mob/spacevines.dmi'
icon_state = "bud0"
- mob_type = /mob/living/simple_animal/hostile/venus_human_trap
+ mob_type = /mob/living/basic/venus_human_trap
density = FALSE
prompt_name = "venus human trap"
you_are_text = "You are a venus human trap."
@@ -23,7 +23,7 @@
flower_bud = null
return ..()
-/obj/effect/mob_spawn/ghost_role/venus_human_trap/equip(mob/living/simple_animal/hostile/venus_human_trap/spawned_human_trap)
+/obj/effect/mob_spawn/ghost_role/venus_human_trap/equip(mob/living/basic/venus_human_trap/spawned_human_trap)
if(spawned_human_trap && flower_bud)
if(flower_bud.trait_flags & SPACEVINE_HEAT_RESISTANT)
spawned_human_trap.unsuitable_heat_damage = 0
diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm
index a853d48c0eca88..02b3ea1866f87d 100644
--- a/code/modules/mob_spawn/mob_spawn.dm
+++ b/code/modules/mob_spawn/mob_spawn.dm
@@ -32,6 +32,8 @@
var/facial_haircolor
///sets a human's skin tone
var/skin_tone
+ /// Weakref to the mob this spawner created - just if you needed to do something with it.
+ var/datum/weakref/spawned_mob_ref
/obj/effect/mob_spawn/Initialize(mapload)
. = ..()
@@ -44,6 +46,7 @@
name_mob(spawned_mob, newname)
special(spawned_mob, mob_possessor)
equip(spawned_mob)
+ spawned_mob_ref = WEAKREF(spawned_mob)
return spawned_mob
/obj/effect/mob_spawn/proc/special(mob/living/spawned_mob)
@@ -250,6 +253,7 @@
if(isnull(created)) // If we explicitly return FALSE instead of just not returning a mob, we don't want to spam the admins
CRASH("An instance of [type] didn't return anything when creating a mob, this might be broken!")
+ SEND_SIGNAL(src, COMSIG_GHOSTROLE_SPAWNED, created)
check_uses() // Now we check if the spawner should delete itself or not
return created
@@ -304,6 +308,11 @@
///burn damage this corpse will spawn with
var/burn_damage = 0
+ ///what environmental storytelling script should this corpse have
+ var/corpse_description = ""
+ ///optionally different text to display if the target is a clown
+ var/naive_corpse_description = ""
+
/obj/effect/mob_spawn/corpse/Initialize(mapload, no_spawn)
. = ..()
if(no_spawn)
@@ -321,6 +330,8 @@
spawned_mob.adjustOxyLoss(oxy_damage)
spawned_mob.adjustBruteLoss(brute_damage)
spawned_mob.adjustFireLoss(burn_damage)
+ if (corpse_description)
+ spawned_mob.AddComponent(/datum/component/temporary_description, corpse_description, naive_corpse_description)
/obj/effect/mob_spawn/corpse/create(mob/mob_possessor, newname)
. = ..()
diff --git a/code/modules/mod/mod_link.dm b/code/modules/mod/mod_link.dm
index 8a3340fad3e153..12ce7fa48271e9 100644
--- a/code/modules/mod/mod_link.dm
+++ b/code/modules/mod/mod_link.dm
@@ -173,6 +173,10 @@
/obj/item/clothing/neck/link_scryer/examine(mob/user)
. = ..()
+ // SKYRAT EDIT NIFSOFT SCRYERS - START
+ if(custom_examine_controls)
+ return
+ // SKYRAT EDIT NIFSOFT SCRYERS - END
if(cell)
. += span_notice("The battery charge reads [cell.percent()]%. Right-click with an empty hand to remove it.")
else
diff --git a/code/modules/mod/mod_types.dm b/code/modules/mod/mod_types.dm
index f11c6b9b4b904b..f8daa9bab544f3 100644
--- a/code/modules/mod/mod_types.dm
+++ b/code/modules/mod/mod_types.dm
@@ -260,6 +260,12 @@
/obj/item/mod/module/jump_jet,
)
+/obj/item/mod/control/pre_equipped/nuclear/no_jetpack
+
+/obj/item/mod/control/pre_equipped/nuclear/no_jetpack/Initialize(mapload, new_theme, new_skin, new_core)
+ applied_modules -= list(/obj/item/mod/module/jetpack/advanced, /obj/item/mod/module/jump_jet)
+ return ..()
+
/obj/item/mod/control/pre_equipped/nuclear/plasmaman
/obj/item/mod/control/pre_equipped/nuclear/plasmaman/Initialize(mapload, new_theme, new_skin, new_core)
diff --git a/code/modules/mod/modules/modules_ninja.dm b/code/modules/mod/modules/modules_ninja.dm
index 90c731a7c367d3..7a38238594df36 100644
--- a/code/modules/mod/modules/modules_ninja.dm
+++ b/code/modules/mod/modules/modules_ninja.dm
@@ -275,7 +275,7 @@
var/mob/living/living_user = user
to_chat(living_user, span_danger("fATaL EERRoR: 382200-*#00CODE RED\nUNAUTHORIZED USE DETECteD\nCoMMENCING SUB-R0UTIN3 13...\nTERMInATING U-U-USER..."))
living_user.investigate_log("has been gibbed by using a MODsuit equipped with [src].", INVESTIGATE_DEATHS)
- living_user.gib()
+ living_user.gib(DROP_ALL_REMAINS)
/obj/item/mod/module/dna_lock/reinforced/on_emp(datum/source, severity)
return
diff --git a/code/modules/mod/modules/modules_security.dm b/code/modules/mod/modules/modules_security.dm
index 2c7ae6821fe8da..7e331433b4f275 100644
--- a/code/modules/mod/modules/modules_security.dm
+++ b/code/modules/mod/modules/modules_security.dm
@@ -361,13 +361,95 @@
/obj/item/mod/module/active_sonar
name = "MOD active sonar"
desc = "Ancient tech from the 20th century, this module uses sonic waves to detect living creatures within the user's radius. \
+ Its basic function slowly scans around the user for any bio-signatures, however it can be overclocked to scan everywhere at once.\
Its loud ping is much harder to hide in an indoor station than in the outdoor operations it was designed for."
icon_state = "active_sonar"
module_type = MODULE_USABLE
- use_power_cost = DEFAULT_CHARGE_DRAIN * 4
+ idle_power_cost = DEFAULT_CHARGE_DRAIN * 0.5
+ use_power_cost = DEFAULT_CHARGE_DRAIN * 3
complexity = 2
incompatible_modules = list(/obj/item/mod/module/active_sonar)
cooldown_time = 15 SECONDS
+ /// Time between us displaying radial scans
+ var/scan_cooldown_time = 0.5 SECONDS
+ /// The current slice we're going to scan
+ var/scanned_slice = 1
+ /// How many slices we make 360
+ var/radar_slices = 8 // 45 degrees each
+
+ /// A list of all creatures in range sorted by angle.
+ var/list/sorted_creatures = list()
+ /// A keyed list of all creatures
+ var/list/keyed_creatures = list()
+
+ /// Time between us displaying radial scans
+ COOLDOWN_DECLARE(scan_cooldown)
+
+/obj/item/mod/module/active_sonar/Initialize(mapload)
+ . = ..()
+ for(var/i in 1 to radar_slices)
+ sorted_creatures += list(list())
+
+/obj/item/mod/module/active_sonar/on_suit_activation()
+ RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(sort_all_creatures))
+
+/obj/item/mod/module/active_sonar/on_suit_deactivation(deleting = FALSE)
+ UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED)
+
+/// Detects all living creatures within world.view, and returns the amount.
+/obj/item/mod/module/active_sonar/proc/detect_living_creatures()
+ var/creatures_detected = 0
+ for(var/mob/living/creature in range(world.view, mod.wearer))
+ if(creature == mod.wearer || creature.stat == DEAD)
+ continue
+ if(keyed_creatures[creature])
+ creatures_detected++
+ continue
+ sort_creature_angle(creature)
+ RegisterSignal(creature, COMSIG_MOVABLE_MOVED, PROC_REF(sort_creature_angle))
+ creatures_detected++
+ return creatures_detected
+
+/// Swaps around where a creature is, when they move or when they're first detected
+/obj/item/mod/module/active_sonar/proc/sort_creature_angle(mob/living/creature, atom/old_loc, movement_dir, forced)
+ SIGNAL_HANDLER
+ var/oldgroup = keyed_creatures[creature]
+ var/newgroup = round(get_angle(mod.wearer, creature) / (360 / radar_slices)) + 1
+ if(oldgroup)
+ if(creature.stat == DEAD || get_dist(get_turf(mod.wearer), get_turf(creature)) > world.view)
+ sorted_creatures[oldgroup] -= creature
+ keyed_creatures -= creature
+ UnregisterSignal(creature, COMSIG_MOVABLE_MOVED)
+ return
+
+ if(oldgroup != newgroup)
+ sorted_creatures[oldgroup] -= creature
+
+ sorted_creatures[newgroup] += creature
+ keyed_creatures[creature] = newgroup
+
+/// Swaps all creatures when mod.wearer moves
+/obj/item/mod/module/active_sonar/proc/sort_all_creatures(mob/living/wearer, atom/old_loc, movement_dir, forced)
+ SIGNAL_HANDLER
+
+ for(var/mob/living/creature as anything in keyed_creatures)
+ sort_creature_angle(creature) // Kinda spaghetti but it honestly seems like the shortest path to the same result
+
+/obj/item/mod/module/active_sonar/on_process(seconds_per_tick)
+ . = ..()
+ if(!.)
+ return
+ if(!COOLDOWN_FINISHED(src, cooldown_timer) || !COOLDOWN_FINISHED(src, scan_cooldown))
+ return
+ detect_living_creatures()
+ for(var/mob/living/creature as anything in sorted_creatures[scanned_slice])
+ new /obj/effect/temp_visual/sonar_ping(mod.wearer.loc, mod.wearer, creature, "sonar_ping_small", FALSE)
+ // Next slice!
+ scanned_slice++
+ // IT'S ENOUGH SLICES
+ if(scanned_slice > radar_slices)
+ scanned_slice = 1
+ COOLDOWN_START(src, scan_cooldown, scan_cooldown_time)
/obj/item/mod/module/active_sonar/on_use()
. = ..()
@@ -377,14 +459,10 @@
playsound(mod.wearer, 'sound/mecha/skyfall_power_up.ogg', vol = 20, vary = TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
if(!do_after(mod.wearer, 1.1 SECONDS, target = mod))
return
- var/creatures_detected = 0
- for(var/mob/living/creature in range(9, mod.wearer))
- if(creature == mod.wearer || creature.stat == DEAD)
- continue
+ playsound(mod.wearer, 'sound/effects/ping_hit.ogg', vol = 75, vary = TRUE) // Should be audible for the radius of the sonar
+ to_chat(mod.wearer, span_notice("You slam your fist into the ground, sending out a sonic wave that detects [detect_living_creatures()] living beings nearby!"))
+ for(var/mob/living/creature as anything in keyed_creatures)
new /obj/effect/temp_visual/sonar_ping(mod.wearer.loc, mod.wearer, creature)
- creatures_detected++
- playsound(mod.wearer, 'sound/effects/ping_hit.ogg', vol = 75, vary = TRUE, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE) // Should be audible for the radius of the sonar
- to_chat(mod.wearer, span_notice("You slam your fist into the ground, sending out a sonic wave that detects [creatures_detected] living beings nearby!"))
#define SHOOTING_ASSISTANT_OFF "Currently Off"
#define STORMTROOPER_MODE "Quick Fire Stormtrooper"
diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm
index 6fe91c9575b669..0de25b903abeee 100644
--- a/code/modules/modular_computers/computers/item/computer.dm
+++ b/code/modules/modular_computers/computers/item/computer.dm
@@ -42,7 +42,7 @@
///The program currently active on the tablet.
var/datum/computer_file/program/active_program
///Idle programs on background. They still receive process calls but can't be interacted with.
- var/list/idle_threads = list()
+ var/list/datum/computer_file/program/idle_threads = list()
/// Amount of programs that can be ran at once
var/max_idle_programs = 2
@@ -72,12 +72,10 @@
/// The built-in light's color, editable by players.
var/comp_light_color = "#FFFFFF"
- ///The last recorded amount of power used.
- var/last_power_usage = 0
///Power usage when the computer is open (screen is active) and can be interacted with.
- var/base_active_power_usage = 75
- ///Power usage when the computer is idle and screen is off (currently only applies to laptops)
- var/base_idle_power_usage = 5
+ var/base_active_power_usage = 15 // SKYRAT EDIT CHANGE - Original: 125
+ ///Power usage when the computer is idle and screen is off.
+ var/base_idle_power_usage = 2 // SKYRAT EDIT CHANGE - Original: 5
// Modular computers can run on various devices. Each DEVICE (Laptop, Console & Tablet)
// must have it's own DMI file. Icon states must be called exactly the same in all files, but may look differently
@@ -369,6 +367,9 @@
/obj/item/modular_computer/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
. = ..()
+ if(held_item?.tool_behaviour == TOOL_SCREWDRIVER && internal_cell)
+ context[SCREENTIP_CONTEXT_RMB] = "Remove Cell"
+ . = CONTEXTUAL_SCREENTIP_SET
if(held_item?.tool_behaviour == TOOL_WRENCH)
context[SCREENTIP_CONTEXT_RMB] = "Deconstruct"
. = CONTEXTUAL_SCREENTIP_SET
@@ -440,7 +441,7 @@
to_chat(user, span_warning("You press the power button, but the computer fails to boot up, displaying variety of errors before shutting down again."))
return FALSE
- if(use_power()) // use_power() checks if the PC is powered
+ if(use_power()) // checks if the PC is powered
if(issynth)
to_chat(user, span_notice("You send an activation signal to \the [src], turning it on."))
else
@@ -462,7 +463,6 @@
// Process currently calls handle_power(), may be expanded in future if more things are added.
/obj/item/modular_computer/process(seconds_per_tick)
if(!enabled) // The computer is turned off
- last_power_usage = 0
return
if(atom_integrity <= integrity_failure * max_integrity)
@@ -736,7 +736,7 @@
return
if(istype(attacking_item, /obj/item/stock_parts/cell))
- if(ismachinery(loc))
+ if(ismachinery(physical))
return
if(internal_cell)
to_chat(user, span_warning("You try to connect \the [attacking_item] to \the [src], but its connectors are occupied."))
@@ -800,6 +800,16 @@
return ..()
+/obj/item/modular_computer/screwdriver_act_secondary(mob/living/user, obj/item/tool)
+ . = ..()
+ if(internal_cell)
+ user.balloon_alert(user, "cell removed")
+ internal_cell.forceMove(drop_location())
+ internal_cell = null
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+ else
+ user.balloon_alert(user, "no cell!")
+
/obj/item/modular_computer/wrench_act_secondary(mob/living/user, obj/item/tool)
. = ..()
tool.play_tool_sound(src, user, 20, volume=20)
diff --git a/code/modules/modular_computers/computers/item/computer_power.dm b/code/modules/modular_computers/computers/item/computer_power.dm
index 6b6230ed0d4ce8..24950621de6e28 100644
--- a/code/modules/modular_computers/computers/item/computer_power.dm
+++ b/code/modules/modular_computers/computers/item/computer_power.dm
@@ -1,9 +1,13 @@
+///The multiplier given to the base overtime charge drain value if its flashlight is on.
+#define FLASHLIGHT_DRAIN_MULTIPLIER 1.1
+
// Tries to draw power from charger or, if no operational charger is present, from power cell.
/obj/item/modular_computer/proc/use_power(amount = 0)
if(check_power_override())
return TRUE
- if(ismachinery(loc))
- var/obj/machinery/machine_holder = loc
+
+ if(ismachinery(physical))
+ var/obj/machinery/machine_holder = physical
if(machine_holder.powered())
machine_holder.use_power(amount)
return TRUE
@@ -27,20 +31,31 @@
return
if(active_program)
active_program.event_powerfailure()
+ if(light_on)
+ set_light_on(FALSE)
for(var/datum/computer_file/program/programs as anything in idle_threads)
programs.event_powerfailure()
shutdown_computer(loud = FALSE)
-// Handles power-related things, such as battery interaction, recharging, shutdown when it's discharged
+///Takes the charge necessary from the Computer, shutting it off if it's unable to provide it.
+///Charge depends on whether the PC is on, and what programs are running/idle on it.
/obj/item/modular_computer/proc/handle_power(seconds_per_tick)
var/power_usage = screen_on ? base_active_power_usage : base_idle_power_usage
- if(use_power(power_usage))
- last_power_usage = power_usage
+ if(light_on)
+ power_usage *= FLASHLIGHT_DRAIN_MULTIPLIER
+ if(active_program)
+ power_usage += active_program.power_cell_use
+ for(var/datum/computer_file/program/open_programs as anything in idle_threads)
+ if(!open_programs.power_cell_use)
+ continue
+ if(open_programs in idle_threads)
+ power_usage += (open_programs.power_cell_use / 2)
+
+ if(use_power(power_usage * seconds_per_tick))
return TRUE
- else
- power_failure()
- return FALSE
+ power_failure()
+ return FALSE
///Used by subtypes for special cases for power usage, returns TRUE if it should stop the use_power chain.
/obj/item/modular_computer/proc/check_power_override()
@@ -49,3 +64,5 @@
//Integrated (Silicon) tablets don't drain power, because the tablet is required to state laws, so it being disabled WILL cause problems.
/obj/item/modular_computer/pda/silicon/check_power_override()
return TRUE
+
+#undef FLASHLIGHT_DRAIN_MULTIPLIER
diff --git a/code/modules/modular_computers/computers/item/computer_ui.dm b/code/modules/modular_computers/computers/item/computer_ui.dm
index d5dcf98c623021..1b87a014f185b0 100644
--- a/code/modules/modular_computers/computers/item/computer_ui.dm
+++ b/code/modules/modular_computers/computers/item/computer_ui.dm
@@ -44,7 +44,7 @@
// Operates TGUI
/obj/item/modular_computer/ui_interact(mob/user, datum/tgui/ui)
- if(!enabled || !user.can_read(src, READING_CHECK_LITERACY) || !use_power())
+ if(!enabled || !user.can_read(src, READING_CHECK_LITERACY))
if(ui)
ui.close()
return
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 b96f61d44b4b91..e2f1b354eda283 100644
--- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm
+++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
@@ -270,6 +270,14 @@
/datum/computer_file/program/skill_tracker,
)
+/obj/item/modular_computer/pda/bitrunner
+ name = "bit runner PDA"
+ greyscale_colors = "#D6B328#6BC906"
+ starting_programs = list(
+ /datum/computer_file/program/arcade,
+ /datum/computer_file/program/skill_tracker,
+ )
+
/**
* Service
*/
diff --git a/code/modules/modular_computers/computers/machinery/modular_computer.dm b/code/modules/modular_computers/computers/machinery/modular_computer.dm
index 293cdd9c5f71a5..c54d3295fe389b 100644
--- a/code/modules/modular_computers/computers/machinery/modular_computer.dm
+++ b/code/modules/modular_computers/computers/machinery/modular_computer.dm
@@ -13,8 +13,6 @@
var/internal_cell = null
///A flag that describes this device type
var/hardware_flag = PROGRAM_CONSOLE
- ///Power usage during last tick
- var/last_power_usage = 0
/// Amount of programs that can be ran at once
var/max_idle_programs = 4
@@ -72,7 +70,7 @@
set_light(cpu?.enabled ? light_strength : 0)
/obj/machinery/modular_computer/update_icon_state()
- if(!cpu || !cpu.enabled || !cpu.use_power() || (machine_stat & NOPOWER))
+ if(!cpu || !cpu.enabled || (machine_stat & NOPOWER))
icon_state = icon_state_unpowered
else
icon_state = icon_state_powered
@@ -83,7 +81,7 @@
if(!cpu)
return .
- if(cpu.enabled && cpu.use_power())
+ if(cpu.enabled)
. += cpu.active_program?.program_icon_state || screen_icon_state_menu
else if(!(machine_stat & NOPOWER))
. += screen_icon_screensaver
diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm
index 1f36713380dff2..6f693b5bf998b2 100644
--- a/code/modules/modular_computers/file_system/program.dm
+++ b/code/modules/modular_computers/file_system/program.dm
@@ -1,8 +1,14 @@
+///The default amount a program should take in cell use.
+#define PROGRAM_BASIC_CELL_USE 15
+
// /program/ files are executable programs that do things.
/datum/computer_file/program
filetype = "PRG"
/// File name. FILE NAME MUST BE UNIQUE IF YOU WANT THE PROGRAM TO BE DOWNLOADABLE FROM NTNET!
filename = "UnknownProgram"
+
+ ///How much power running this program costs.
+ var/power_cell_use = PROGRAM_BASIC_CELL_USE
/// List of required accesses to *run* the program. Any match will do.
var/list/required_access = list()
/// List of required access to download or file host the program. Any match will do.
@@ -211,3 +217,5 @@
computer.update_tablet_open_uis(usr)
computer.update_appearance(UPDATE_ICON)
return TRUE
+
+#undef PROGRAM_BASIC_CELL_USE
diff --git a/code/modules/modular_computers/file_system/programs/budgetordering.dm b/code/modules/modular_computers/file_system/programs/budgetordering.dm
index c261f3a3d4357d..d2133697194335 100644
--- a/code/modules/modular_computers/file_system/programs/budgetordering.dm
+++ b/code/modules/modular_computers/file_system/programs/budgetordering.dm
@@ -118,9 +118,11 @@
if(SSshuttle.supply_blocked)
message = blockade_warning
data["message"] = message
+ var/list/amount_by_name = list()
var/cart_list = list()
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(cart_list[order.pack.name])
+ amount_by_name[order.pack.name] += 1
cart_list[order.pack.name][1]["amount"]++
cart_list[order.pack.name][1]["cost"] += order.get_final_cost()
if(order.department_destination)
@@ -145,15 +147,23 @@
data["cart"] += cart_list[item_id]
data["requests"] = list()
- for(var/datum/supply_order/SO in SSshuttle.request_list)
+ for(var/datum/supply_order/order in SSshuttle.request_list)
+ var/datum/supply_pack/pack = order.pack
+ amount_by_name[pack.name] += 1
data["requests"] += list(list(
- "object" = SO.pack.name,
- "cost" = SO.pack.get_cost(),
- "orderer" = SO.orderer,
- "reason" = SO.reason,
- "id" = SO.id
+ "object" = pack.name,
+ "cost" = pack.get_cost(),
+ "orderer" = order.orderer,
+ "reason" = order.reason,
+ "id" = order.id
))
+ data["amount_by_name"] = amount_by_name
+
+ return data
+/datum/computer_file/program/budgetorders/ui_static_data(mob/user)
+ var/list/data = list()
+ data["max_order"] = CARGO_MAX_ORDER
return data
/datum/computer_file/program/budgetorders/ui_act(action, params, datum/tgui/ui)
@@ -233,15 +243,20 @@
return
if(pack.goody && !self_paid)
- playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ playsound(computer, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
computer.say("ERROR: Small crates may only be purchased by private accounts.")
return
+ if(SSshuttle.supply.get_order_count(pack) == OVER_ORDER_LIMIT)
+ playsound(computer, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ computer.say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
+ return
+
if(!requestonly && !self_paid && ishuman(usr) && !account)
var/obj/item/card/id/id_card = computer.computer_id_slot?.GetID()
account = SSeconomy.get_dep_account(id_card?.registered_account?.account_job.paycheck_department)
- var/turf/T = get_turf(src)
+ var/turf/T = get_turf(computer)
var/datum/supply_order/SO = new(pack, name, rank, ckey, reason, account)
SO.generateRequisition(T)
if((requestonly && !self_paid) || !(computer.computer_id_slot?.GetID()))
diff --git a/code/modules/modular_computers/file_system/programs/crewmanifest.dm b/code/modules/modular_computers/file_system/programs/crewmanifest.dm
index 3215f62eef84cd..cdd05d6b4c64f0 100644
--- a/code/modules/modular_computers/file_system/programs/crewmanifest.dm
+++ b/code/modules/modular_computers/file_system/programs/crewmanifest.dm
@@ -4,7 +4,7 @@
category = PROGRAM_CATEGORY_CREW
program_icon_state = "id"
extended_desc = "Program for viewing and printing the current crew manifest"
- transfer_access = list(ACCESS_COMMAND)
+ transfer_access = list(ACCESS_SECURITY, ACCESS_COMMAND)
requires_ntnet = TRUE
size = 4
tgui_id = "NtosCrewManifest"
diff --git a/code/modules/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm
index cf6cc4b2bc273c..b724892da7e1c4 100644
--- a/code/modules/modular_computers/file_system/programs/frontier.dm
+++ b/code/modules/modular_computers/file_system/programs/frontier.dm
@@ -25,7 +25,7 @@
/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, src)
+ CONNECT_TO_RND_SERVER_ROUNDSTART(linked_techweb, computer)
/datum/computer_file/program/scipaper_program/application_attackby(obj/item/attacking_item, mob/living/user)
if(!istype(attacking_item, /obj/item/multitool))
diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm
index bba55b4474efb5..6e3e69cdccfc51 100644
--- a/code/modules/modular_computers/file_system/programs/secureye.dm
+++ b/code/modules/modular_computers/file_system/programs/secureye.dm
@@ -100,18 +100,19 @@
/datum/computer_file/program/secureye/ui_data()
var/list/data = list()
- data["network"] = network
data["activeCamera"] = null
var/obj/machinery/camera/active_camera = camera_ref?.resolve()
if(active_camera)
data["activeCamera"] = list(
name = active_camera.c_tag,
+ ref = REF(active_camera),
status = active_camera.status,
)
return data
/datum/computer_file/program/secureye/ui_static_data(mob/user)
var/list/data = list()
+ data["network"] = network
data["mapRef"] = cam_screen.assigned_map
data["can_spy"] = !!spying
var/list/cameras = get_camera_list(network)
@@ -120,6 +121,7 @@
var/obj/machinery/camera/C = cameras[i]
data["cameras"] += list(list(
name = C.c_tag,
+ ref = REF(C),
))
return data
@@ -130,13 +132,14 @@
return
switch(action)
if("switch_camera")
- var/c_tag = format_text(params["name"])
- var/list/cameras = get_camera_list(network)
- var/obj/machinery/camera/selected_camera = cameras[c_tag]
- camera_ref = WEAKREF(selected_camera)
+ var/obj/machinery/camera/selected_camera = locate(params["camera"]) in GLOB.cameranet.cameras
+ if(selected_camera)
+ camera_ref = WEAKREF(selected_camera)
+ else
+ camera_ref = null
if(!spying)
playsound(computer, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
- if(!selected_camera)
+ if(isnull(camera_ref))
return TRUE
if(internal_tracker && internal_tracker.tracking)
internal_tracker.set_tracking(FALSE)
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index 9c097b2fb9b024..dc9538cf3580d4 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -24,7 +24,7 @@
/datum/computer_file/program/science/on_start(mob/living/user)
. = ..()
if(!CONFIG_GET(flag/no_default_techweb_link) && !stored_research)
- CONNECT_TO_RND_SERVER_ROUNDSTART(stored_research, src)
+ CONNECT_TO_RND_SERVER_ROUNDSTART(stored_research, computer)
/datum/computer_file/program/science/application_attackby(obj/item/attacking_item, mob/living/user)
if(!istype(attacking_item, /obj/item/multitool))
diff --git a/code/modules/movespeed/modifiers/status_effects.dm b/code/modules/movespeed/modifiers/status_effects.dm
index e8aad88c50d3c7..65245880ef42ba 100644
--- a/code/modules/movespeed/modifiers/status_effects.dm
+++ b/code/modules/movespeed/modifiers/status_effects.dm
@@ -37,3 +37,19 @@
/datum/movespeed_modifier/status_effect/tired_post_charge
multiplicative_slowdown = 3
+
+/// Get slower the more gold is in your system.
+/datum/movespeed_modifier/status_effect/midas_blight
+ id = MOVESPEED_ID_MIDAS_BLIGHT
+
+/datum/movespeed_modifier/status_effect/midas_blight/soft
+ multiplicative_slowdown = 0.25
+
+/datum/movespeed_modifier/status_effect/midas_blight/medium
+ multiplicative_slowdown = 0.75
+
+/datum/movespeed_modifier/status_effect/midas_blight/hard
+ multiplicative_slowdown = 1.5
+
+/datum/movespeed_modifier/status_effect/midas_blight/gold
+ multiplicative_slowdown = 2
diff --git a/code/modules/pai/camera.dm b/code/modules/pai/camera.dm
index a091b208638f61..319f20e3699903 100644
--- a/code/modules/pai/camera.dm
+++ b/code/modules/pai/camera.dm
@@ -1,10 +1,3 @@
-/mob/living/silicon/pai/ClickOn(atom/target, params)
- . = ..()
- if(aicamera && aicamera.in_camera_mode)
- aicamera.toggle_camera_mode(sound = FALSE)
- aicamera.captureimage(target, usr)
- return TRUE
-
/obj/item/camera/siliconcam/pai_camera
name = "pAI photo camera"
light_color = COLOR_PAI_GREEN
@@ -13,7 +6,7 @@
var/number = length(stored)
picture.picture_name = "Image [number] (taken by [loc.name])"
stored[picture] = TRUE
- playsound(loc, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 75, TRUE, -3)
+ playsound(src, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 75, TRUE, -3)
balloon_alert(user, "image recorded")
/**
diff --git a/code/modules/pai/defense.dm b/code/modules/pai/defense.dm
index 3a888fa9c83a10..75c437c2546e06 100644
--- a/code/modules/pai/defense.dm
+++ b/code/modules/pai/defense.dm
@@ -73,17 +73,17 @@
to_chat(src, span_userdanger("The impact degrades your holochassis!"))
return amount
-/mob/living/silicon/pai/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- return take_holo_damage(amount)
+/// Called when we take burn or brute damage, pass it to the shell instead
+/mob/living/silicon/pai/proc/on_shell_damaged(datum/hurt, type, amount, forced)
+ SIGNAL_HANDLER
+ take_holo_damage(amount)
+ return COMPONENT_IGNORE_CHANGE
-/mob/living/silicon/pai/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- return take_holo_damage(amount)
-
-/mob/living/silicon/pai/adjustStaminaLoss(amount, updating_stamina, forced = FALSE, required_biotype)
- if(forced)
- take_holo_damage(amount)
- else
- take_holo_damage(amount * 0.25)
+/// Called when we take stamina damage, pass it to the shell instead
+/mob/living/silicon/pai/proc/on_shell_weakened(datum/hurt, type, amount, forced)
+ SIGNAL_HANDLER
+ take_holo_damage(amount * ((forced) ? 1 : 0.25))
+ return COMPONENT_IGNORE_CHANGE
/mob/living/silicon/pai/getBruteLoss()
return HOLOCHASSIS_MAX_HEALTH - holochassis_health
diff --git a/code/modules/pai/hud.dm b/code/modules/pai/hud.dm
index 530780758d2491..523d57d17b31c1 100644
--- a/code/modules/pai/hud.dm
+++ b/code/modules/pai/hud.dm
@@ -147,7 +147,8 @@
required_software = "Photography Module"
/atom/movable/screen/pai/image_take/Click()
- if(!..())
+ . = ..()
+ if(!.)
return
var/mob/living/silicon/pai/pAI = usr
pAI.aicamera.toggle_camera_mode(usr)
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index e1a4dfb0ae0b77..3c0a1bfb82cca6 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -227,6 +227,8 @@
update_appearance(UPDATE_DESC)
RegisterSignal(src, COMSIG_LIVING_CULT_SACRIFICED, PROC_REF(on_cult_sacrificed))
+ RegisterSignals(src, list(COMSIG_LIVING_ADJUST_BRUTE_DAMAGE, COMSIG_LIVING_ADJUST_BURN_DAMAGE), PROC_REF(on_shell_damaged))
+ RegisterSignal(src, COMSIG_LIVING_ADJUST_STAMINA_DAMAGE, PROC_REF(on_shell_weakened))
/mob/living/silicon/pai/make_laws()
laws = new /datum/ai_laws/pai()
diff --git a/code/modules/paperwork/paperbin.dm b/code/modules/paperwork/paperbin.dm
index 318fca21f57734..9971c0da7f7bbd 100644
--- a/code/modules/paperwork/paperbin.dm
+++ b/code/modules/paperwork/paperbin.dm
@@ -54,12 +54,26 @@
droppoint = drop_location()
if(collapse)
visible_message(span_warning("The stack of paper collapses!"))
- for(var/atom/movable/movable_atom in contents)
- movable_atom.forceMove(droppoint)
- if(!movable_atom.pixel_y)
- movable_atom.pixel_y = rand(-3,3)
- if(!movable_atom.pixel_x)
- movable_atom.pixel_x = rand(-3,3)
+ for(var/obj/item/paper/stacked_paper in paper_stack) //first, dump all of the paper that already exists
+ stacked_paper.forceMove(droppoint)
+ if(!stacked_paper.pixel_y)
+ stacked_paper.pixel_y = rand(-3,3)
+ if(!stacked_paper.pixel_x)
+ stacked_paper.pixel_x = rand(-3,3)
+ paper_stack -= stacked_paper
+ total_paper -= 1
+ for(var/i in 1 to total_paper) //second, generate new paper for the remainder
+ var/obj/item/paper/new_paper = generate_paper()
+ new_paper.forceMove(droppoint)
+ if(!new_paper.pixel_y)
+ new_paper.pixel_y = rand(-3,3)
+ if(!new_paper.pixel_x)
+ new_paper.pixel_x = rand(-3,3)
+ if(bin_pen)
+ var/obj/item/pen/pen = bin_pen
+ pen.forceMove(droppoint)
+ bin_pen = null
+ total_paper = 0
update_appearance()
/obj/item/paper_bin/fire_act(exposed_temperature, exposed_volume)
@@ -212,6 +226,8 @@
/obj/item/paper_bin/bundlenatural/dump_contents(atom/droppoint)
. = ..()
+ binding_cable.forceMove(droppoint)
+ binding_cable = null
qdel(src)
/obj/item/paper_bin/bundlenatural/update_overlays()
@@ -225,7 +241,7 @@
deconstruct(FALSE)
/obj/item/paper_bin/bundlenatural/deconstruct(disassembled)
- dump_contents()
+ dump_contents(drop_location())
return ..()
/obj/item/paper_bin/bundlenatural/fire_act(exposed_temperature, exposed_volume)
diff --git a/code/modules/photography/camera/other.dm b/code/modules/photography/camera/other.dm
index e9aa5d94e597a1..166517d055fbae 100644
--- a/code/modules/photography/camera/other.dm
+++ b/code/modules/photography/camera/other.dm
@@ -9,13 +9,15 @@
continue
// time to steal your soul
- if(istype(target, /mob/living/simple_animal/revenant))
- var/mob/living/simple_animal/revenant/peek_a_boo = target
- peek_a_boo.reveal(2 SECONDS) // no hiding
- if(!peek_a_boo.unstun_time)
- peek_a_boo.stun(2 SECONDS)
- target.visible_message(span_warning("[target] violently flinches!"), \
- span_revendanger("You feel your essence draining away from having your picture taken!"))
+ if(istype(target, /mob/living/basic/revenant))
+ var/mob/living/basic/revenant/peek_a_boo = target
+ peek_a_boo.apply_status_effect(/datum/status_effect/revenant/revealed, 2 SECONDS) // no hiding
+ peek_a_boo.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, 2 SECONDS)
+
+ target.visible_message(
+ span_warning("[target] violently flinches!"),
+ span_revendanger("You feel your essence draining away from having your picture taken!"),
+ )
target.apply_damage(rand(10, 15))
/obj/item/camera/spooky/badmin
diff --git a/code/modules/photography/camera/silicon_camera.dm b/code/modules/photography/camera/silicon_camera.dm
index 836ecc2690a161..9cdbee1bc2b7ae 100644
--- a/code/modules/photography/camera/silicon_camera.dm
+++ b/code/modules/photography/camera/silicon_camera.dm
@@ -1,23 +1,47 @@
/obj/item/camera/siliconcam
name = "silicon photo camera"
- var/in_camera_mode = FALSE
+ resistance_flags = INDESTRUCTIBLE
+ /// List of all pictures taken by this camera.
var/list/datum/picture/stored = list()
-/obj/item/camera/siliconcam/ai_camera
- name = "AI photo camera"
- flash_enabled = FALSE
+/// Checks if we can take a picture at this moment. Returns TRUE if we can, FALSE if we can't.
+/obj/item/camera/siliconcam/proc/can_take_picture(mob/living/silicon/clicker)
+ if(clicker.stat != CONSCIOUS || clicker.incapacitated())
+ return FALSE
+ return TRUE
+
+/obj/item/camera/siliconcam/proc/InterceptClickOn(mob/living/silicon/clicker, params, atom/clicked_on)
+ if(!can_take_picture(clicker))
+ return
+ clicker.face_atom(clicked_on)
+ captureimage(clicked_on, clicker)
+ toggle_camera_mode(clicker, sound = FALSE)
+/// Toggles the camera mode on or off.
+/// If sound is TRUE, plays a sound effect and displays a message on successful toggle
/obj/item/camera/siliconcam/proc/toggle_camera_mode(mob/user, sound = TRUE)
- in_camera_mode = !in_camera_mode
+ if(user.click_intercept == src)
+ user.click_intercept = null
+
+ else if(isnull(user.click_intercept))
+ user.click_intercept = src
+
+ else
+ // Trying to turn on camera mode while you have another click intercept active, such as malf abilities
+ if(sound)
+ balloon_alert(user, "can't enable camera mode!")
+ playsound(user, 'sound/machines/buzz-sigh.ogg', 25, TRUE)
+ return
+
if(sound)
- playsound(src, 'sound/items/wirecutter.ogg', 50, TRUE)
- to_chat(user, span_notice("Camera mode: [in_camera_mode ? "Activated" : "Deactivated"]."))
+ playsound(user, 'sound/items/wirecutter.ogg', 50, TRUE)
+ balloon_alert(user, "camera mode [user.click_intercept == src ? "activated" : "deactivated"]")
/obj/item/camera/siliconcam/proc/selectpicture(mob/user)
RETURN_TYPE(/datum/picture)
if(!length(stored))
- to_chat(user, span_notice("ERROR: No stored photos located."))
+ user.balloon_alert(user, "no stored photos!")
return
var/list/nametemp = list()
var/list/temp = list()
@@ -25,9 +49,7 @@
nametemp += stored_photo.picture_name
temp[stored_photo.picture_name] = stored_photo
var/find = tgui_input_list(user, "Select image", "Storage", nametemp)
- if(isnull(find))
- return
- if(isnull(temp[find]))
+ if(isnull(find) || isnull(temp[find]))
return
return temp[find]
@@ -36,48 +58,70 @@
if(istype(selection))
show_picture(user, selection)
+/obj/item/camera/siliconcam/ai_camera
+ name = "AI photo camera"
+ flash_enabled = FALSE
+
+/obj/item/camera/siliconcam/ai_camera/can_take_picture(mob/living/silicon/ai/clicker)
+ if(clicker.control_disabled)
+ return FALSE
+ return ..()
+
+/obj/item/camera/siliconcam/ai_camera/balloon_alert(mob/viewer, text)
+ if(isAI(loc))
+ // redirects balloon alerts on us to balloon alerts on our ai eye
+ var/mob/living/silicon/ai/ai = loc
+ return ai.eyeobj.balloon_alert(viewer, text)
+
+ return ..()
+
/obj/item/camera/siliconcam/ai_camera/after_picture(mob/user, datum/picture/picture)
var/number = length(stored)
picture.picture_name = "Image [number] (taken by [loc.name])"
stored[picture] = TRUE
- to_chat(user, span_notice("Image recorded."))
+ balloon_alert(user, "image recorded")
+ user.playsound_local(get_turf(user), pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, TRUE, -3)
/obj/item/camera/siliconcam/robot_camera
name = "Cyborg photo camera"
var/printcost = 2
-/obj/item/camera/siliconcam/robot_camera/after_picture(mob/user, datum/picture/picture)
- var/mob/living/silicon/robot/C = loc
- if(istype(C) && istype(C.connected_ai))
- var/number = C.connected_ai.aicamera.stored.len
+/obj/item/camera/siliconcam/robot_camera/can_take_picture(mob/living/silicon/robot/clicker)
+ if(clicker.lockcharge)
+ return FALSE
+ return ..()
+
+/obj/item/camera/siliconcam/robot_camera/after_picture(mob/living/silicon/robot/user, datum/picture/picture)
+ if(istype(user) && istype(user.connected_ai))
+ var/number = user.connected_ai.aicamera.stored.len
picture.picture_name = "Image [number] (taken by [loc.name])"
- C.connected_ai.aicamera.stored[picture] = TRUE
- to_chat(usr, span_notice("Image recorded and saved to remote database."))
+ user.connected_ai.aicamera.stored[picture] = TRUE
+ balloon_alert(user, "image recorded and uploaded")
else
var/number = stored.len
picture.picture_name = "Image [number] (taken by [loc.name])"
stored[picture] = TRUE
- to_chat(usr, span_notice("Image recorded and saved to local storage. Upload will happen automatically if unit is lawsynced."))
+ balloon_alert(user, "image recorded and saved locally")
+ playsound(src, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 75, TRUE, -3)
-/obj/item/camera/siliconcam/robot_camera/selectpicture(mob/user)
- var/mob/living/silicon/robot/R = loc
- if(istype(R) && R.connected_ai)
- R.picturesync()
- return R.connected_ai.aicamera.selectpicture(user)
- else
- return ..()
+/obj/item/camera/siliconcam/robot_camera/selectpicture(mob/living/silicon/robot/user)
+ if(istype(user) && user.connected_ai)
+ user.picturesync()
+ return user.connected_ai.aicamera.selectpicture(user)
+ return ..()
-/obj/item/camera/siliconcam/robot_camera/proc/borgprint(mob/user)
- var/mob/living/silicon/robot/C = loc
- if(!istype(C) || C.toner < 20)
- to_chat(user, span_warning("Insufficent toner to print image."))
+/obj/item/camera/siliconcam/robot_camera/proc/borgprint(mob/living/silicon/robot/user)
+ if(!istype(user) || user.toner < printcost)
+ balloon_alert(user, "not enough toner!")
return
var/datum/picture/selection = selectpicture(user)
if(!istype(selection))
- to_chat(user, span_warning("Invalid Image."))
+ balloon_alert(user, "invalid image!")
return
- var/obj/item/photo/p = new /obj/item/photo(C.loc, selection)
- p.pixel_x = p.base_pixel_x + rand(-10, 10)
- p.pixel_y = p.base_pixel_y + rand(-10, 10)
- C.toner -= printcost //All fun allowed.
- user.visible_message(span_notice("[C.name] spits out a photograph from a narrow slot on its chassis."), span_notice("You print a photograph."))
+ var/obj/item/photo/printed = new(user.drop_location(), selection)
+ printed.pixel_x = printed.base_pixel_x + rand(-10, 10)
+ printed.pixel_y = printed.base_pixel_y + rand(-10, 10)
+ user.toner -= printcost //All fun allowed.
+ user.visible_message(span_notice("[user.name] spits out a photograph from a narrow slot on its chassis."), span_notice("You print a photograph."))
+ balloon_alert(user, "photograph printed")
+ playsound(src, 'sound/items/taperecorder/taperecorder_print.ogg', 50, TRUE, -3)
diff --git a/code/modules/photography/photos/photo.dm b/code/modules/photography/photos/photo.dm
index 9be79a58266534..b34ff459c0075f 100644
--- a/code/modules/photography/photos/photo.dm
+++ b/code/modules/photography/photos/photo.dm
@@ -54,11 +54,11 @@
icon = I
return ..()
-/obj/item/photo/suicide_act(mob/living/carbon/user)
+/obj/item/photo/suicide_act(mob/living/carbon/human/user)
user.visible_message(span_suicide("[user] is taking one last look at \the [src]! It looks like [user.p_theyre()] giving in to death!"))//when you wanna look at photo of waifu one last time before you die...
- if (user.gender == MALE)
+ if (!ishuman(user) || user.physique == MALE)
playsound(user, 'sound/voice/human/manlaugh1.ogg', 50, TRUE)//EVERY TIME I DO IT MAKES ME LAUGH
- else if (user.gender == FEMALE)
+ else
playsound(user, 'sound/voice/human/womanlaugh.ogg', 50, TRUE)
return OXYLOSS
diff --git a/code/modules/plumbing/plumbers/acclimator.dm b/code/modules/plumbing/plumbers/acclimator.dm
index da5c4529a4230d..6b7a8caba4ac8c 100644
--- a/code/modules/plumbing/plumbers/acclimator.dm
+++ b/code/modules/plumbing/plumbers/acclimator.dm
@@ -23,9 +23,7 @@
var/enabled = TRUE
///COOLING, HEATING or NEUTRAL. We track this for change, so we dont needlessly update our icon
var/acclimate_state
- /**We can't take anything in, at least till we're emptied. Down side of the round robin chem transfer, otherwise while emptying 5u of an unreacted chem gets added,
- and you get nasty leftovers
- */
+ ///When conditions are met we send out the stored reagents
var/emptying = FALSE
/obj/machinery/plumbing/acclimator/Initialize(mapload, bolt, layer)
diff --git a/code/modules/plumbing/plumbers/pill_press.dm b/code/modules/plumbing/plumbers/pill_press.dm
index 3a9afca5e02642..e04215e94e26eb 100644
--- a/code/modules/plumbing/plumbers/pill_press.dm
+++ b/code/modules/plumbing/plumbers/pill_press.dm
@@ -3,7 +3,6 @@
name = "chemical press"
desc = "A press that makes pills, patches and bottles."
icon_state = "pill_press"
- buffer = 60 //SKYRAT EDIT HYPOVIALS. This is needed so it can completely fill the vials up.
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
///maximum size of a pill
@@ -12,8 +11,6 @@
var/max_patch_volume = 40
///maximum size of a bottle
var/max_bottle_volume = 50
- //SKYRAT EDIT HYPOVIALS maximum size of a vial
- var/max_vial_volume = 60
///current operating product (pills or patches)
var/product = "pill"
///the minimum size a pill or patch can be
@@ -72,13 +69,13 @@
reagents.trans_to(P, current_volume)
P.name = trim("[product_name] bottle")
stored_products += P
- //SKYRAT EDIT HYPOVIALS
+ //SKYRAT EDIT ADDITION BEGIN - HYPOVIALS
else if (product == "vial")
var/obj/item/reagent_containers/cup/vial/small/P = new(src)
reagents.trans_to(P, current_volume)
P.name = trim("[product_name] vial")
stored_products += P
- //SKYRAT EDIT HYPOVIALS END
+ //SKYRAT EDIT ADDITION END - HYPOVIALS
if(stored_products.len)
var/pill_amount = 0
for(var/thing in loc)
@@ -159,10 +156,10 @@
max_volume = max_patch_volume
else if (product == "bottle")
max_volume = max_bottle_volume
- //SKYRAT EDIT HYPOVIALS
+ //SKYRAT EDIT ADDITION BEGIN - HYPOVIALS
else if (product == "vial")
- max_volume = max_vial_volume
- //SKYRAT EDIT HPYOVIALS END
+ max_volume = max_bottle_volume
+ //SKYRAT EDIT ADDITION END - HYPOVIALS
current_volume = clamp(current_volume, min_volume, max_volume)
if("change_patch_style")
patch_style = params["patch_style"]
diff --git a/code/modules/plumbing/plumbers/reaction_chamber.dm b/code/modules/plumbing/plumbers/reaction_chamber.dm
index 689d043418b496..36320f18184fed 100644
--- a/code/modules/plumbing/plumbers/reaction_chamber.dm
+++ b/code/modules/plumbing/plumbers/reaction_chamber.dm
@@ -1,5 +1,8 @@
///a reaction chamber for plumbing. pretty much everything can react, but this one keeps the reagents separated and only reacts under your given terms
+/// coefficient to convert temperature to joules. same lvl as acclimator
+#define HEATER_COEFFICIENT 0.05
+
/obj/machinery/plumbing/reaction_chamber
name = "mixing chamber"
desc = "Keeps chemicals separated until given conditions are met."
@@ -19,9 +22,6 @@
///towards which temperature do we build (except during draining)?
var/target_temperature = 300
- ///cool/heat power
- var/heater_coefficient = 0.05 //same lvl as acclimator
-
/obj/machinery/plumbing/reaction_chamber/Initialize(mapload, bolt, layer)
. = ..()
@@ -35,30 +35,48 @@
/// Handles properly detaching signal hooks.
/obj/machinery/plumbing/reaction_chamber/proc/on_reagents_del(datum/reagents/reagents)
SIGNAL_HANDLER
+
UnregisterSignal(reagents, list(COMSIG_REAGENTS_REM_REAGENT, COMSIG_REAGENTS_DEL_REAGENT, COMSIG_REAGENTS_CLEAR_REAGENTS, COMSIG_REAGENTS_REACTED, COMSIG_QDELETING))
return NONE
/// Handles stopping the emptying process when the chamber empties.
/obj/machinery/plumbing/reaction_chamber/proc/on_reagent_change(datum/reagents/holder, ...)
SIGNAL_HANDLER
- if(holder.total_volume == 0 && emptying) //we were emptying, but now we aren't
+
+ if(!holder.total_volume && emptying) //we were emptying, but now we aren't
emptying = FALSE
holder.flags |= NO_REACT
return NONE
/obj/machinery/plumbing/reaction_chamber/process(seconds_per_tick)
- if(!emptying || reagents.is_reacting) //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
+ //half the power for getting reagents in
+ var/power_usage = active_power_usage * 0.5
+
+ if(!emptying || reagents.is_reacting)
+ //do reactions and stuff
reagents.handle_reactions()
- use_power(active_power_usage * seconds_per_tick)
+ //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
+ reagents.adjust_thermal_energy(temp_diff * HEATER_COEFFICIENT * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater
+
+ //do other stuff with final solution
+ handle_reagents(seconds_per_tick)
+
+ //full power for doing reactions
+ power_usage *= 2
+
+ use_power(power_usage * seconds_per_tick)
+
+///For subtypes that want to do additional reagent handling
+/obj/machinery/plumbing/reaction_chamber/proc/handle_reagents(seconds_per_tick)
+ return
/obj/machinery/plumbing/reaction_chamber/power_change()
. = ..()
- if(use_power != NO_POWER_USE)
- icon_state = initial(icon_state) + "_on"
- else
- icon_state = initial(icon_state)
+
+ icon_state = initial(icon_state) + "[use_power != NO_POWER_USE ? "_on" : ""]"
/obj/machinery/plumbing/reaction_chamber/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -67,31 +85,29 @@
ui.open()
/obj/machinery/plumbing/reaction_chamber/ui_data(mob/user)
- var/list/data = list()
+ . = list()
var/list/reagents_data = list()
for(var/datum/reagent/required_reagent as anything in required_reagents) //make a list where the key is text, because that looks alot better in the ui than a typepath
var/list/reagent_data = list()
reagent_data["name"] = initial(required_reagent.name)
- reagent_data["required_reagent"] = required_reagents[required_reagent]
+ reagent_data["volume"] = required_reagents[required_reagent]
reagents_data += list(reagent_data)
- data["reagents"] = reagents_data
- data["emptying"] = emptying
- data["temperature"] = round(reagents.chem_temp, 0.1)
- data["targetTemp"] = target_temperature
- data["isReacting"] = reagents.is_reacting
- return data
+ .["reagents"] = reagents_data
+ .["emptying"] = emptying
+ .["temperature"] = round(reagents.chem_temp, 0.1)
+ .["targetTemp"] = target_temperature
+ .["isReacting"] = reagents.is_reacting
-/obj/machinery/plumbing/reaction_chamber/ui_act(action, params)
+/obj/machinery/plumbing/reaction_chamber/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return TRUE
- . = FALSE
switch(action)
if("add")
- var/selected_reagent = tgui_input_list(usr, "Select reagent", "Reagent", GLOB.chemical_name_list)
+ var/selected_reagent = tgui_input_list(ui.user, "Select reagent", "Reagent", GLOB.chemical_name_list)
if(!selected_reagent)
return TRUE
@@ -104,32 +120,41 @@
if(input_amount)
required_reagents[input_reagent] = input_amount
- . = TRUE
+ return TRUE
if("remove")
var/reagent = get_chem_id(params["chem"])
if(reagent)
required_reagents.Remove(reagent)
- . = TRUE
+ return TRUE
if("temperature")
var/target = text2num(params["target"])
if(target != null)
- target_temperature=clamp(target, 0, 1000)
- .=TRUE
+ target_temperature = clamp(target, 0, 1000)
+ return TRUE
+
+ var/result = handle_ui_act(action, params, ui, state)
+ if(isnull(result))
+ result = FALSE
+ return result
+
+/// For custom handling of ui actions from inside a subtype
+/obj/machinery/plumbing/reaction_chamber/proc/handle_ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ return null
///Chemistry version of reaction chamber that allows for acid and base buffers to be used while reacting
/obj/machinery/plumbing/reaction_chamber/chem
name = "reaction chamber"
- ///If above this pH, we start dumping buffer into it
- var/acidic_limit = 9
///If below this pH, we start dumping buffer into it
- var/alkaline_limit = 5
+ var/acidic_limit = 5
+ ///If above this pH, we start dumping acid into it
+ var/alkaline_limit = 9
- ///Beaker that holds the acidic buffer. I don't want to deal with snowflaking so it's just a separate thing. It's a small (50u) beaker
+ ///beaker that holds the acidic buffer(50u)
var/obj/item/reagent_containers/cup/beaker/acidic_beaker
- ///beaker that holds the alkaline buffer.
+ ///beaker that holds the alkaline buffer(50u).
var/obj/item/reagent_containers/cup/beaker/alkaline_beaker
/obj/machinery/plumbing/reaction_chamber/chem/Initialize(mapload, bolt, layer)
@@ -147,13 +172,27 @@
QDEL_NULL(alkaline_beaker)
return ..()
-/obj/machinery/plumbing/reaction_chamber/chem/process(seconds_per_tick)
- //add acidic/alkaine buffer if over/under limit
- if(reagents.is_reacting && reagents.ph < alkaline_limit)
- alkaline_beaker.reagents.trans_to(reagents, 1 * seconds_per_tick)
- if(reagents.is_reacting && reagents.ph > acidic_limit)
- acidic_beaker.reagents.trans_to(reagents, 1 * seconds_per_tick)
- ..()
+/obj/machinery/plumbing/reaction_chamber/chem/handle_reagents(seconds_per_tick)
+ while(reagents.ph < acidic_limit || reagents.ph > alkaline_limit)
+ if(machine_stat & NOPOWER)
+ 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
+ * else if solution is getting too acidic(low ph) add some base to increase it's value
+ */
+ var/datum/reagents/buffer = reagents.ph > alkaline_limit ? acidic_beaker.reagents : alkaline_beaker.reagents
+ 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))
+ return
+
+ //some power for accurate ph balancing
+ use_power(active_power_usage * 0.2 * seconds_per_tick)
/obj/machinery/plumbing/reaction_chamber/chem/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -167,16 +206,16 @@
.["reagentAcidic"] = acidic_limit
.["reagentAlkaline"] = alkaline_limit
-/obj/machinery/plumbing/reaction_chamber/chem/ui_act(action, params)
- . = ..()
- if (.)
- return
+/obj/machinery/plumbing/reaction_chamber/chem/handle_ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = TRUE
switch(action)
if("acidic")
- acidic_limit = round(text2num(params["target"]))
+ acidic_limit = clamp(round(text2num(params["target"])), 0, alkaline_limit)
if("alkaline")
- alkaline_limit = round(text2num(params["target"]))
+ alkaline_limit = clamp(round(text2num(params["target"])), acidic_limit + 0.01, 14)
+ else
+ return FALSE
- return TRUE
+#undef HEATER_COEFFICIENT
diff --git a/code/modules/plumbing/plumbers/teleporter.dm b/code/modules/plumbing/plumbers/teleporter.dm
index a8e6e7ae3ac554..7bb098eae4e06a 100644
--- a/code/modules/plumbing/plumbers/teleporter.dm
+++ b/code/modules/plumbing/plumbers/teleporter.dm
@@ -45,7 +45,7 @@
///Transfer reagents and display a flashing icon
/obj/machinery/plumbing/sender/proc/teleport_chemicals(obj/machinery/plumbing/receiver/R, amount)
flick(initial(icon_state) + "_flash", src)
- reagents.trans_to(R, amount, round_robin = TRUE)
+ reagents.trans_to(R, amount)
///A bluespace output pipe for plumbing. Supports multiple recipients. Must be constructed with a circuit board
/obj/machinery/plumbing/receiver
diff --git a/code/modules/power/apc/apc_malf.dm b/code/modules/power/apc/apc_malf.dm
index f13b588842a8bc..3b0703688043d8 100644
--- a/code/modules/power/apc/apc_malf.dm
+++ b/code/modules/power/apc/apc_malf.dm
@@ -69,7 +69,7 @@
if(forced)
occupier.forceMove(drop_location())
INVOKE_ASYNC(occupier, TYPE_PROC_REF(/mob/living, death))
- occupier.gib()
+ occupier.gib(DROP_ALL_REMAINS)
if(!occupier.nuking) //Pinpointers go back to tracking the nuke disk, as long as the AI (somehow) isn't mid-nuking.
for(var/obj/item/pinpointer/nuke/disk_pinpointers in GLOB.pinpointer_list)
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index 1c83ca58e1d0bf..d11b57bcb93f12 100644
--- a/code/modules/power/singularity/narsie.dm
+++ b/code/modules/power/singularity/narsie.dm
@@ -112,7 +112,7 @@
return ..()
/obj/narsie/attack_ghost(mob/user)
- makeNewConstruct(/mob/living/simple_animal/hostile/construct/harvester, user, cultoverride = TRUE, loc_override = loc)
+ makeNewConstruct(/mob/living/basic/construct/harvester, user, cultoverride = TRUE, loc_override = loc)
/obj/narsie/process()
var/datum/component/singularity/singularity_component = singularity.resolve()
diff --git a/code/modules/projectiles/ammunition/ballistic/pistol.dm b/code/modules/projectiles/ammunition/ballistic/pistol.dm
index c61888b95259fd..a2f55f797bdb5f 100644
--- a/code/modules/projectiles/ammunition/ballistic/pistol.dm
+++ b/code/modules/projectiles/ammunition/ballistic/pistol.dm
@@ -21,6 +21,11 @@
desc = "A 10mm incendiary bullet casing."
projectile_type = /obj/projectile/bullet/incendiary/c10mm
+/obj/item/ammo_casing/c10mm/reaper
+ name = "10mm reaper bullet casing"
+ desc = "A 10mm reaper bullet casing."
+ projectile_type = /obj/projectile/bullet/c10mm/reaper
+
// 9mm (Makarov, Stechkin APS, PP-95)
/obj/item/ammo_casing/c9mm
diff --git a/code/modules/projectiles/ammunition/ballistic/shotgun.dm b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
index 53ff1f8a350419..81e2fe97931c3e 100644
--- a/code/modules/projectiles/ammunition/ballistic/shotgun.dm
+++ b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
@@ -136,6 +136,16 @@
icon_state = "cshell"
projectile_type = null
+/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)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/ammo_casing/shotgun/dart
name = "shotgun dart"
desc = "A dart for use in shotguns. Can be injected with up to 15 units of any chemical."
diff --git a/code/modules/projectiles/ammunition/energy/laser.dm b/code/modules/projectiles/ammunition/energy/laser.dm
index fbf9b289aa079e..081f4166236efb 100644
--- a/code/modules/projectiles/ammunition/energy/laser.dm
+++ b/code/modules/projectiles/ammunition/energy/laser.dm
@@ -16,6 +16,16 @@
e_cost = 62.5
select_name = "kill"
+/obj/item/ammo_casing/energy/lasergun/carbine
+ projectile_type = /obj/projectile/beam/laser/carbine
+ e_cost = 25 // 40 shots
+ select_name = "kill"
+
+/obj/item/ammo_casing/energy/lasergun/carbine/practice
+ projectile_type = /obj/projectile/beam/laser/carbine/practice
+ select_name = "practice"
+ harmful = FALSE
+
/obj/item/ammo_casing/energy/lasergun/old
projectile_type = /obj/projectile/beam/laser
e_cost = 200
diff --git a/code/modules/projectiles/boxes_magazines/external/pistol.dm b/code/modules/projectiles/boxes_magazines/external/pistol.dm
index 49ea0029f8f6db..8b0bc1da7e5b80 100644
--- a/code/modules/projectiles/boxes_magazines/external/pistol.dm
+++ b/code/modules/projectiles/boxes_magazines/external/pistol.dm
@@ -107,15 +107,11 @@
multiple_sprites = AMMO_BOX_PER_BULLET
/obj/item/ammo_box/magazine/r10mm
- name = "regal condor magazine (10mm)"
+ name = "regal condor magazine (10mm Reaper)"
icon_state = "r10mm-8"
base_icon_state = "r10mm"
- ammo_type = /obj/item/ammo_casing/c10mm
+ ammo_type = /obj/item/ammo_casing/c10mm/reaper
caliber = CALIBER_10MM
max_ammo = 8
multiple_sprites = AMMO_BOX_PER_BULLET
multiple_sprite_use_base = TRUE
-
-/obj/item/ammo_box/magazine/r10mm/empty
- icon_state = "r10mm-0"
- start_empty = TRUE
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index 576ba6395be72e..efff8c2fc6157c 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -723,7 +723,7 @@ GLOBAL_LIST_INIT(gun_saw_types, typecacheof(list(
/obj/item/suppressor
name = "suppressor"
- desc = "A Scarborough Arms small-arms suppressor for maximum espionage." //SKYRAT EDIT - ORIGINAL: desc = "A syndicate small-arms suppressor for maximum espionage."
+ desc = "A small-arms suppressor for maximum espionage." //SKYRAT EDIT - ORIGINAL: desc = "A syndicate small-arms suppressor for maximum espionage."
icon = 'icons/obj/weapons/guns/ballistic.dmi'
icon_state = "suppressor"
w_class = WEIGHT_CLASS_TINY
diff --git a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
index 5d33b3fce5170f..9f7ab7e354c94f 100644
--- a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
+++ b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
@@ -73,7 +73,7 @@
/obj/projectile/bullet/arrow/holy/Initialize(mapload)
. = ..()
//50 damage to revenants
- AddElement(/datum/element/bane, target_type = /mob/living/simple_animal/revenant, damage_multiplier = 0, added_damage = 30)
+ AddElement(/datum/element/bane, target_type = /mob/living/basic/revenant, damage_multiplier = 0, added_damage = 30)
/// special pyre sect arrow
/// in the future, this needs a special sprite, but bows don't support non-hardcoded arrow sprites
diff --git a/code/modules/projectiles/guns/ballistic/bows/bow_types.dm b/code/modules/projectiles/guns/ballistic/bows/bow_types.dm
index 355ed3575a8141..b9ac1af0cca12a 100644
--- a/code/modules/projectiles/guns/ballistic/bows/bow_types.dm
+++ b/code/modules/projectiles/guns/ballistic/bows/bow_types.dm
@@ -30,7 +30,7 @@
on_clear_callback = CALLBACK(src, PROC_REF(on_cult_rune_removed)), \
effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune) \
)
- AddElement(/datum/element/bane, target_type = /mob/living/simple_animal/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
+ AddElement(/datum/element/bane, target_type = /mob/living/basic/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
/obj/item/gun/ballistic/bow/divine/proc/on_cult_rune_removed(obj/effect/target, mob/living/user)
SIGNAL_HANDLER
diff --git a/code/modules/projectiles/guns/ballistic/launchers.dm b/code/modules/projectiles/guns/ballistic/launchers.dm
index 0b676ae3dfac6a..83c19bf9350129 100644
--- a/code/modules/projectiles/guns/ballistic/launchers.dm
+++ b/code/modules/projectiles/guns/ballistic/launchers.dm
@@ -97,7 +97,7 @@
REMOVE_TRAIT(user, TRAIT_NO_TRANSFORM, REF(src))
process_fire(user, user, TRUE)
if(!QDELETED(user)) //if they weren't gibbed by the explosion, take care of them for good.
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
return MANUAL_SUICIDE
else
sleep(0.5 SECONDS)
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index 220df569ecbd61..f69494e0a18bde 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -99,9 +99,6 @@
actions_types = list(/datum/action/item_action/toggle_firemode)
obj_flags = UNIQUE_RENAME // if you did the sidequest, you get the customization
-/obj/item/gun/ballistic/automatic/pistol/deagle/regal/no_mag
- spawnwithmagazine = FALSE
-
/obj/item/gun/ballistic/automatic/pistol/aps
name = "\improper Stechkin APS machine pistol"
desc = "A modernized reproduction of an old Soviet machine pistol. It fires quickly, but kicks like a mule. Uses 9mm ammo. Has a threaded barrel for suppressors." //SKYRAT EDIT
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 6439b78a9bd986..f87f473ae459bb 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -139,6 +139,11 @@
desc = "A modernized 7 round revolver manufactured by Waffle Co. Uses .357 ammo."
icon_state = "revolversyndie"
+/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/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."
@@ -275,10 +280,15 @@
user.visible_message(span_danger("[user.name]'s soul is captured by \the [src]!"), span_userdanger("You've lost the gamble! Your soul is forfeit!"))
/obj/item/gun/ballistic/revolver/reverse //Fires directly at its user... unless the user is a clown, of course.
- name = "\improper Syndicate Revolver"
clumsy_check = FALSE
icon_state = "revolversyndie"
+/obj/item/gun/ballistic/revolver/reverse/Initialize(mapload)
+ . = ..()
+ var/obj/item/gun/ballistic/revolver/syndicate/syndie_revolver = /obj/item/gun/ballistic/revolver/syndicate
+ name = initial(syndie_revolver.name)
+ desc = initial(syndie_revolver.desc)
+
/obj/item/gun/ballistic/revolver/reverse/can_trigger_gun(mob/living/user, akimbo_usage)
if(akimbo_usage)
return FALSE
diff --git a/code/modules/projectiles/guns/energy/energy_gun.dm b/code/modules/projectiles/guns/energy/energy_gun.dm
index cb0b5862e162f5..69acb79ac2a339 100644
--- a/code/modules/projectiles/guns/energy/energy_gun.dm
+++ b/code/modules/projectiles/guns/energy/energy_gun.dm
@@ -77,7 +77,7 @@
name = "\improper X-01 MultiPhase Energy Gun"
desc = "This is an expensive, modern recreation of an antique laser gun. This gun has several unique firemodes, but lacks the ability to recharge over time."
icon_state = "hoslaser"
- cell_type = /obj/item/stock_parts/cell //SKYRAT EDIT ADDITION - GUNSGALORE
+ cell_type = /obj/item/stock_parts/cell/hos_gun
w_class = WEIGHT_CLASS_NORMAL
force = 10
ammo_type = list(/obj/item/ammo_casing/energy/disabler/hos, /obj/item/ammo_casing/energy/laser/hos, /obj/item/ammo_casing/energy/ion/hos)
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index 0409f2fe037340..94d58bb5e32296 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -37,6 +37,26 @@
cell_type = /obj/item/stock_parts/cell //SKYRAT EDIT ADDITION - GUNSGALORE
ammo_x_offset = 3
+/obj/item/gun/energy/laser/carbine
+ name = "laser carbine"
+ 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)
+
+/obj/item/gun/energy/laser/carbine/practice
+ name = "practice laser carbine"
+ desc = "A modified version of the laser carbine, this one fires even less concentrated energy bolts designed for target practice."
+ ammo_type = list(/obj/item/ammo_casing/energy/lasergun/carbine/practice)
+ 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"
icon_state = "retro"
diff --git a/code/modules/projectiles/guns/special/hand_of_midas.dm b/code/modules/projectiles/guns/special/hand_of_midas.dm
new file mode 100644
index 00000000000000..9907352e3f5059
--- /dev/null
+++ b/code/modules/projectiles/guns/special/hand_of_midas.dm
@@ -0,0 +1,138 @@
+// Hand of Midas
+
+/obj/item/gun/magic/midas_hand
+ name = "The Hand of Midas"
+ desc = "An ancient Egyptian matchlock pistol imbued with the powers of the Greek King Midas. Don't question the cultural or religious implications of this."
+ ammo_type = /obj/item/ammo_casing/magic/midas_round
+ icon_state = "midas_hand"
+ inhand_icon_state = "gun"
+ worn_icon_state = "gun"
+ lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
+ fire_sound = 'sound/weapons/gun/rifle/shot.ogg'
+ pinless = TRUE
+ max_charges = 1
+ can_charge = FALSE
+ item_flags = NEEDS_PERMIT
+ w_class = WEIGHT_CLASS_BULKY // Should fit on a belt.
+ force = 3
+ trigger_guard = TRIGGER_GUARD_NORMAL
+ antimagic_flags = NONE
+ can_hold_up = FALSE
+
+ /// The length of the Midas Blight debuff, dependant on the amount of gold reagent we've sucked up.
+ var/gold_timer = 3 SECONDS
+ /// The range that we can suck gold out of people's bodies
+ var/gold_suck_range = 2
+
+/obj/item/gun/magic/midas_hand/examine(mob/user)
+ . = ..()
+ var/gold_time_converted = gold_time_convert()
+ . += span_notice("Your next shot will inflict [gold_time_converted] second[gold_time_converted == 1 ? "" : "s"] of Midas Blight.")
+ . += span_notice("Right-Click on enemies to drain gold from their bloodstreams to reload [src].")
+ . += span_notice("[src] can be reloaded using gold coins in a pinch.")
+
+/obj/item/gun/magic/midas_hand/shoot_with_empty_chamber(mob/living/user)
+ . = ..()
+ balloon_alert(user, "not enough gold")
+
+// Siphon gold from a victim, recharging our gun & removing their Midas Blight debuff in the process.
+/obj/item/gun/magic/midas_hand/afterattack_secondary(mob/living/victim, mob/living/user, proximity_flag, click_parameters)
+ if(!isliving(victim) || !IN_GIVEN_RANGE(user, victim, gold_suck_range))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ if(victim == user)
+ balloon_alert(user, "can't siphon from self")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ if(!victim.reagents)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ if(!victim.reagents.has_reagent(/datum/reagent/gold, check_subtypes = TRUE))
+ balloon_alert(user, "no gold in bloodstream")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ var/gold_beam = user.Beam(victim, icon_state="drain_gold")
+ if(!do_after(user = user, delay = 1 SECONDS, target = victim, timed_action_flags = (IGNORE_USER_LOC_CHANGE | IGNORE_TARGET_LOC_CHANGE), extra_checks = CALLBACK(src, PROC_REF(check_gold_range), user, victim)))
+ qdel(gold_beam)
+ balloon_alert(user, "link broken")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+ handle_gold_charges(user, victim.reagents.get_reagent_amount(/datum/reagent/gold, include_subtypes = TRUE))
+ victim.reagents.remove_all_type(/datum/reagent/gold, victim.reagents.get_reagent_amount(/datum/reagent/gold, include_subtypes = TRUE))
+ victim.remove_status_effect(/datum/status_effect/midas_blight)
+ qdel(gold_beam)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+// If we botch a shot, we have to start over again by inserting gold coins into the gun. Can only be done if it has no charges or gold.
+/obj/item/gun/magic/midas_hand/attackby(obj/item/I, mob/living/user, params)
+ . = ..()
+ if(charges || gold_timer)
+ balloon_alert(user, "already loaded")
+ return
+ if(istype(I, /obj/item/coin/gold))
+ handle_gold_charges(user, 1.5 SECONDS)
+ qdel(I)
+
+/// Handles recharging & inserting gold amount
+/obj/item/gun/magic/midas_hand/proc/handle_gold_charges(user, gold_amount)
+ gold_timer += gold_amount
+ var/gold_time_converted = gold_time_convert()
+ balloon_alert(user, "[gold_time_converted] second[gold_time_converted == 1 ? "" : "s"]")
+ if(!charges)
+ instant_recharge()
+
+/// Converts our gold_timer to time in seconds, for various ballons/examines
+/obj/item/gun/magic/midas_hand/proc/gold_time_convert()
+ return min(30 SECONDS, round(gold_timer, 0.2)) / 10
+
+/// Checks our range to the person we're sucking gold out of. Double the initial range, so you need to get in close to start.
+/obj/item/gun/magic/midas_hand/proc/check_gold_range(mob/living/user, mob/living/victim)
+ return IN_GIVEN_RANGE(user, victim, gold_suck_range*2)
+
+/obj/item/ammo_casing/magic/midas_round
+ projectile_type = /obj/projectile/magic/midas_round
+
+
+/obj/projectile/magic/midas_round
+ name = "gold pellet"
+ desc = "A typical flintlock ball, save for the fact it's made of cursed Egyptian gold."
+ damage_type = BRUTE
+ damage = 10
+ stamina = 20
+ armour_penetration = 50
+ hitsound = 'sound/effects/coin2.ogg'
+ icon_state = "pellet"
+ color = "#FFD700"
+ /// The gold charge in this pellet
+ var/gold_charge = 0
+
+
+/obj/projectile/magic/midas_round/fire(setAngle)
+ /// Transfer the gold energy to our bullet
+ var/obj/item/gun/magic/midas_hand/my_gun = fired_from
+ gold_charge = my_gun.gold_timer
+ my_gun.gold_timer = 0
+ ..()
+
+// Gives human targets Midas Blight.
+/obj/projectile/magic/midas_round/on_hit(atom/target)
+ . = ..()
+ if(ishuman(target))
+ var/mob/living/carbon/human/my_guy = target
+ if(isskeleton(my_guy)) // No cheap farming
+ return
+ my_guy.apply_status_effect(/datum/status_effect/midas_blight, min(30 SECONDS, round(gold_charge, 0.2))) // 100u gives 10 seconds
+ return
+
+/obj/item/gun/magic/midas_hand/suicide_act(mob/living/user)
+ if(!ishuman(user))
+ return
+
+ var/mob/living/carbon/human/victim = user
+ victim.visible_message(span_suicide("[victim] holds the barrel of [src] to [victim.p_their()] head, lighting the fuse. It looks like [user.p_theyre()] trying to commit suicide!"))
+ if(!do_after(victim, 1.5 SECONDS))
+ return
+ playsound(src, 'sound/weapons/gun/rifle/shot.ogg', 75, TRUE)
+ to_chat(victim, span_danger("You don't even have the time to register the gunshot by the time your body has completely converted into a golden statue."))
+ var/newcolors = list(rgb(206, 164, 50), rgb(146, 146, 139), rgb(28,28,28), rgb(0,0,0))
+ victim.petrify(statue_timer = INFINITY, save_brain = FALSE, colorlist = newcolors)
+ playsound(victim, 'sound/effects/coin2.ogg', 75, TRUE)
+ charges = 0
+ gold_timer = 0
+ return OXYLOSS
diff --git a/code/modules/projectiles/guns/special/medbeam.dm b/code/modules/projectiles/guns/special/medbeam.dm
index d3ee77ef3b3a31..267470f17013c4 100644
--- a/code/modules/projectiles/guns/special/medbeam.dm
+++ b/code/modules/projectiles/guns/special/medbeam.dm
@@ -144,10 +144,13 @@
/obj/item/gun/medbeam/proc/on_beam_tick(mob/living/target)
if(target.health != target.maxHealth)
new /obj/effect/temp_visual/heal(get_turf(target), COLOR_HEALING_CYAN)
- target.adjustBruteLoss(-4)
- target.adjustFireLoss(-4)
- target.adjustToxLoss(-1, forced = TRUE)
- target.adjustOxyLoss(-1, forced = TRUE)
+ var/need_mob_update
+ need_mob_update = target.adjustBruteLoss(-4, updating_health = FALSE, forced = TRUE)
+ need_mob_update += target.adjustFireLoss(-4, updating_health = FALSE, forced = TRUE)
+ need_mob_update += target.adjustToxLoss(-1, updating_health = FALSE, forced = TRUE)
+ need_mob_update += target.adjustOxyLoss(-1, updating_health = FALSE, forced = TRUE)
+ if(need_mob_update)
+ target.updatehealth()
return
/obj/item/gun/medbeam/proc/on_beam_release(mob/living/target)
diff --git a/code/modules/projectiles/guns/special/syringe_gun.dm b/code/modules/projectiles/guns/special/syringe_gun.dm
index 270bb8744cc22d..805378c1ffcc40 100644
--- a/code/modules/projectiles/guns/special/syringe_gun.dm
+++ b/code/modules/projectiles/guns/special/syringe_gun.dm
@@ -206,6 +206,6 @@
/obj/item/gun/syringe/blowgun/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0)
visible_message(span_danger("[user] shoots the blowgun!"))
- user.adjustStaminaLoss(20)
+ user.adjustStaminaLoss(20, updating_stamina = FALSE)
user.adjustOxyLoss(20)
return ..()
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 2f5fa84c4419c5..7eb152d3e4a963 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -1161,13 +1161,17 @@
#undef MUZZLE_EFFECT_PIXEL_INCREMENT
/// Fire a projectile from this atom at another atom
-/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer)
+/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer, list/ignore_targets = list())
if (!isnull(sound))
playsound(src, sound, vol = 100, vary = TRUE)
var/turf/startloc = get_turf(src)
var/obj/projectile/bullet = new projectile_type(startloc)
bullet.starting = startloc
+ var/list/ignore = list()
+ for (var/atom/thing as anything in ignore_targets)
+ ignore[thing] = TRUE
+ bullet.impacted += ignore
bullet.firer = firer || src
bullet.fired_from = src
bullet.yo = target.y - startloc.y
@@ -1175,3 +1179,4 @@
bullet.original = target
bullet.preparePixelProjectile(target, src)
bullet.fire()
+ return bullet
diff --git a/code/modules/projectiles/projectile/beams.dm b/code/modules/projectiles/projectile/beams.dm
index e5c4d520d0e495..a7a5f83329a684 100644
--- a/code/modules/projectiles/projectile/beams.dm
+++ b/code/modules/projectiles/projectile/beams.dm
@@ -28,6 +28,16 @@
damage = 25
bare_wound_bonus = 40
+/obj/projectile/beam/laser/carbine
+ icon_state = "carbine_laser"
+ impact_effect_type = /obj/effect/temp_visual/impact_effect/yellow_laser
+ damage = 10
+
+/obj/projectile/beam/laser/carbine/practice
+ name = "practice laser"
+ impact_effect_type = /obj/effect/temp_visual/impact_effect/yellow_laser
+ damage = 0
+
//overclocked laser, does a bit more damage but has much higher wound power (-0 vs -20)
/obj/projectile/beam/laser/hellfire
name = "hellfire laser"
diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm
index 90a3d723bf5825..5e938c4995390b 100644
--- a/code/modules/projectiles/projectile/bullets.dm
+++ b/code/modules/projectiles/projectile/bullets.dm
@@ -12,7 +12,6 @@
wound_bonus = 0
wound_falloff_tile = -5
embed_falloff_tile = -3
- wound_bonus = 20 //SKYRAT EDIT ADDITION
/obj/projectile/bullet/smite
name = "divine retribution"
diff --git a/code/modules/projectiles/projectile/bullets/pistol.dm b/code/modules/projectiles/projectile/bullets/pistol.dm
index ece260f92fbdec..8fccc510ff8fda 100644
--- a/code/modules/projectiles/projectile/bullets/pistol.dm
+++ b/code/modules/projectiles/projectile/bullets/pistol.dm
@@ -42,3 +42,22 @@
name = "10mm incendiary bullet"
damage = 20
fire_stacks = 3
+
+/obj/projectile/bullet/c10mm/reaper
+ name = "10mm reaper pellet"
+ damage = 50
+ armour_penetration = 40
+ tracer_type = /obj/effect/projectile/tracer/sniper
+ impact_type = /obj/effect/projectile/impact/sniper
+ muzzle_type = /obj/effect/projectile/muzzle/sniper
+ hitscan = TRUE
+ impact_effect_type = null
+ hitscan_light_intensity = 3
+ hitscan_light_range = 0.75
+ hitscan_light_color_override = LIGHT_COLOR_DIM_YELLOW
+ muzzle_flash_intensity = 5
+ muzzle_flash_range = 1
+ muzzle_flash_color_override = LIGHT_COLOR_DIM_YELLOW
+ impact_light_intensity = 5
+ impact_light_range = 1
+ impact_light_color_override = LIGHT_COLOR_DIM_YELLOW
diff --git a/code/modules/projectiles/projectile/special/rocket.dm b/code/modules/projectiles/projectile/special/rocket.dm
index 899f737d8cc83f..08a2c18c2f73d6 100644
--- a/code/modules/projectiles/projectile/special/rocket.dm
+++ b/code/modules/projectiles/projectile/special/rocket.dm
@@ -53,7 +53,7 @@ among other potential differences. This granularity is helpful for things like t
if(random_crit_gib)
var/mob/living/gibbed_dude = target
new /obj/effect/temp_visual/crit(get_turf(gibbed_dude))
- gibbed_dude.gib()
+ gibbed_dude.gib(DROP_ALL_REMAINS)
/// PM9 HEAP rocket - the anti-anything missile you always craved.
/obj/projectile/bullet/rocket/heap
diff --git a/code/modules/reagents/chemistry/equilibrium.dm b/code/modules/reagents/chemistry/equilibrium.dm
index 7d7aff20fae3e1..f46c636c50f59b 100644
--- a/code/modules/reagents/chemistry/equilibrium.dm
+++ b/code/modules/reagents/chemistry/equilibrium.dm
@@ -324,8 +324,8 @@
//keep limited
if(delta_chem_factor > step_target_vol)
delta_chem_factor = step_target_vol
- else if (delta_chem_factor < CHEMICAL_VOLUME_MINIMUM)
- delta_chem_factor = CHEMICAL_VOLUME_MINIMUM
+ else if (delta_chem_factor < CHEMICAL_QUANTISATION_LEVEL)
+ delta_chem_factor = CHEMICAL_QUANTISATION_LEVEL
//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.
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 902ccf35e6ac43..c72bfbb6c3ed79 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -1,136 +1,6 @@
-#define REAGENTS_UI_MODE_LOOKUP 0
-#define REAGENTS_UI_MODE_REAGENT 1
-#define REAGENTS_UI_MODE_RECIPE 2
-
#define REAGENT_TRANSFER_AMOUNT "amount"
#define REAGENT_PURITY "purity"
-/// Initialises all /datum/reagent into a list indexed by reagent id
-/proc/init_chemical_reagent_list()
- var/list/reagent_list = list()
-
- var/paths = subtypesof(/datum/reagent)
-
- for(var/path in paths)
- if(path in GLOB.fake_reagent_blacklist)
- continue
- var/datum/reagent/D = new path()
- D.mass = rand(10, 800) //This is terrible and should be removed ASAP!
- reagent_list[path] = D
-
- return reagent_list
-
-/// Creates an list which is indexed by reagent name . used by plumbing reaction chamber and chemical filter UI
-/proc/init_chemical_name_list()
- var/list/name_list = list()
- for(var/X in GLOB.chemical_reagents_list)
- var/datum/reagent/Reagent = GLOB.chemical_reagents_list[X]
- name_list += Reagent.name
- return sort_list(name_list)
-
-
-/proc/build_chemical_reactions_lists()
- //Chemical Reactions - Initialises all /datum/chemical_reaction into a list
- // It is filtered into multiple lists within a list.
- // For example:
- // chemical_reactions_list_reactant_index[/datum/reagent/toxin/plasma] is a list of all reactions relating to plasma
- //For chemical reaction list product index - indexes reactions based off the product reagent type - see get_recipe_from_reagent_product() in helpers
- //For chemical reactions list lookup list - creates a bit list of info passed to the UI. This is saved to reduce lag from new windows opening, since it's a lot of data.
-
- //Prevent these reactions from appearing in lookup tables (UI code)
- var/list/blacklist = typecacheof(/datum/chemical_reaction/randomized)
-
- if(GLOB.chemical_reactions_list_reactant_index)
- return
-
- //Randomized need to go last since they need to check against conflicts with normal recipes
- var/paths = subtypesof(/datum/chemical_reaction) - typesof(/datum/chemical_reaction/randomized) + subtypesof(/datum/chemical_reaction/randomized)
- GLOB.chemical_reactions_list = list() //typepath to reaction list
- GLOB.chemical_reactions_list_reactant_index = list() //reagents to reaction list
- GLOB.chemical_reactions_results_lookup_list = list() //UI glob
- GLOB.chemical_reactions_list_product_index = list() //product to reaction list
-
- var/list/datum/chemical_reaction/reactions = list()
- for(var/path in paths)
- var/datum/chemical_reaction/reaction = new path()
- reactions += reaction
-
- // Ok so we're gonna do a thingTM here
- // I want to distribute all our reactions such that each reagent id links to as few as possible
- // I get the feeling there's a canonical way of doing this, but I don't know it
- // So instead, we're gonna wing it
- var/list/reagent_to_react_count = list()
- for(var/datum/chemical_reaction/reaction as anything in reactions)
- for(var/reagent_id as anything in reaction.required_reagents)
- reagent_to_react_count[reagent_id] += 1
-
- var/list/reaction_lookup = GLOB.chemical_reactions_list_reactant_index
- // Create filters based on a random reagent id in the required reagents list - this is used to speed up handle_reactions()
- // Basically, we only really need to care about ONE reagent, at least when initially filtering, since any others are ignorable
- // Doing this separately because it relies on the loop above, and this is easier to parse
- for(var/datum/chemical_reaction/reaction as anything in reactions)
- var/preferred_id = null
- for(var/reagent_id as anything in reaction.required_reagents)
- if(!preferred_id)
- preferred_id = reagent_id
- continue
- // If we would have less then they would, take it
- if(length(reaction_lookup[reagent_id]) < length(reaction_lookup[preferred_id]))
- preferred_id = reagent_id
- continue
- // If they potentially have more then us, we take it
- if(reagent_to_react_count[reagent_id] < reagent_to_react_count[preferred_id])
- preferred_id = reagent_id
- continue
- if (preferred_id != null)
- if(!reaction_lookup[preferred_id])
- reaction_lookup[preferred_id] = list()
- reaction_lookup[preferred_id] += reaction
-
- for(var/datum/chemical_reaction/reaction as anything in reactions)
- var/list/product_ids = list()
- var/list/reagents = list()
- var/list/product_names = list()
- var/bitflags = reaction.reaction_tags
-
- if(!reaction.required_reagents || !reaction.required_reagents.len) //Skip impossible reactions
- continue
-
- GLOB.chemical_reactions_list[reaction.type] = reaction
-
- for(var/reagent_path in reaction.required_reagents)
- var/datum/reagent/reagent = find_reagent_object_from_type(reagent_path)
- if(!istype(reagent))
- stack_trace("Invalid reagent found in [reaction] required_reagents: [reagent_path]")
- continue
- reagents += list(list("name" = reagent.name, "id" = reagent.type))
-
- for(var/product in reaction.results)
- var/datum/reagent/reagent = find_reagent_object_from_type(product)
- if(!istype(reagent))
- stack_trace("Invalid reagent found in [reaction] results: [product]")
- continue
- product_names += reagent.name
- product_ids += product
-
- var/product_name
- if(!length(product_names))
- var/list/names = splittext("[reaction.type]", "/")
- product_name = names[names.len]
- else
- product_name = product_names[1]
-
- if(!is_type_in_typecache(reaction.type, blacklist))
- //Master list of ALL reactions that is used in the UI lookup table. This is expensive to make, and we don't want to lag the server by creating it on UI request, so it's cached to send to UIs instantly.
- GLOB.chemical_reactions_results_lookup_list += list(list("name" = product_name, "id" = reaction.type, "bitflags" = bitflags, "reactants" = reagents))
-
- // Create filters based on each reagent id in the required reagents list - this is specifically for finding reactions from product(reagent) ids/typepaths.
- for(var/id in product_ids)
- if(!GLOB.chemical_reactions_list_product_index[id])
- GLOB.chemical_reactions_list_product_index[id] = list()
- GLOB.chemical_reactions_list_product_index[id] += reaction
-
-
///////////////////////////////Main reagents code/////////////////////////////////////////////
/// Holder for a bunch of [/datum/reagent]
@@ -147,8 +17,6 @@
var/chem_temp = 150
///pH of the whole system
var/ph = CHEMICAL_NORMAL_PH
- /// unused
- var/last_tick = 1
/// various flags, see code\__DEFINES\reagents.dm
var/flags
///list of reactions currently on going, this is a lazylist for optimisation
@@ -171,7 +39,7 @@
///If we're syncing with the beaker - so return reactions that are actively happening
var/ui_beaker_sync = FALSE
-/datum/reagents/New(maximum=100, new_flags=0)
+/datum/reagents/New(maximum = 100, new_flags = 0)
maximum_volume = maximum
flags = new_flags
@@ -203,21 +71,36 @@
* * override_base_ph - ingore the present pH of the reagent, and instead use the default (i.e. if buffers/reactions alter it)
* * ignore splitting - Don't call the process that handles reagent spliting in a mob (impure/inverse) - generally leave this false unless you care about REAGENTS_DONOTSPLIT flags (see reagent defines)
*/
-/datum/reagents/proc/add_reagent(reagent, amount, list/data=null, reagtemp = DEFAULT_REAGENT_TEMPERATURE, added_purity = null, added_ph, no_react = FALSE, override_base_ph = FALSE, ignore_splitting = FALSE)
- // Prevents small amount problems, as well as zero and below zero amounts.
- if(amount <= CHEMICAL_QUANTISATION_LEVEL)
+/datum/reagents/proc/add_reagent(
+ datum/reagent/reagent_type,
+ amount,
+ list/data = null,
+ reagtemp = DEFAULT_REAGENT_TEMPERATURE,
+ added_purity = null,
+ added_ph,
+ no_react = FALSE,
+ override_base_ph = FALSE,
+ ignore_splitting = FALSE
+)
+ if(!ispath(reagent_type))
+ stack_trace("invalid reagent passed to add reagent [reagent_type]")
return FALSE
if(!IS_FINITE(amount))
- stack_trace("non finite amount passed to add reagent [amount] [reagent]")
+ stack_trace("non finite amount passed to add reagent [amount] [reagent_type]")
return FALSE
- if(SEND_SIGNAL(src, COMSIG_REAGENTS_PRE_ADD_REAGENT, reagent, amount, reagtemp, data, no_react) & COMPONENT_CANCEL_REAGENT_ADD)
+ if(SEND_SIGNAL(src, COMSIG_REAGENTS_PRE_ADD_REAGENT, reagent_type, amount, reagtemp, data, no_react) & COMPONENT_CANCEL_REAGENT_ADD)
return FALSE
- var/datum/reagent/glob_reagent = GLOB.chemical_reagents_list[reagent]
+ // 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]' which doesn't exist. ([usr])")
+ stack_trace("[my_atom] attempted to add a reagent called '[reagent_type]' which doesn't exist. ([usr])")
return FALSE
if(isnull(added_purity)) //Because purity additions can be 0
added_purity = glob_reagent.creation_purity //Usually 1
@@ -227,8 +110,8 @@
//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 = process_mob_reagent_purity(glob_reagent, amount, added_purity)
- if(!adjusted_vol) //If we're inverse or FALSE cancel addition
+ 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
/* 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),
@@ -240,7 +123,7 @@
update_total()
var/cached_total = total_volume
if(cached_total + amount > maximum_volume)
- amount = (maximum_volume - cached_total) //Doesnt fit in. Make it disappear. shouldn't happen. Will happen.
+ 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
@@ -255,13 +138,13 @@
//add the reagent to the existing if it exists
for(var/datum/reagent/iter_reagent as anything in cached_reagents)
- if(iter_reagent.type == reagent)
+ if(iter_reagent.type == reagent_type)
if(override_base_ph)
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 += round(amount, CHEMICAL_QUANTISATION_LEVEL)
+ iter_reagent.volume = FLOOR(iter_reagent.volume + amount, CHEMICAL_QUANTISATION_LEVEL)
update_total()
iter_reagent.on_merge(data, amount)
@@ -278,7 +161,7 @@
return TRUE
//otherwise make a new one
- var/datum/reagent/new_reagent = new reagent(data)
+ var/datum/reagent/new_reagent = new reagent_type(data)
cached_reagents += new_reagent
new_reagent.holder = src
new_reagent.volume = amount
@@ -306,40 +189,68 @@
handle_reactions()
return TRUE
-/// Like add_reagent but you can enter a list. Format it like this: list(/datum/reagent/toxin = 10, "beer" = 15)
-/datum/reagents/proc/add_reagent_list(list/list_reagents, list/data=null)
+/**
+ * Like add_reagent but you can enter a list.
+ * Arguments
+ *
+ * * [list_reagents][list] - list to add. Format it like this: list(/datum/reagent/toxin = 10, "beer" = 15)
+ * * [data][list] - additional data to add
+ */
+/datum/reagents/proc/add_reagent_list(list/list_reagents, list/data = null)
for(var/r_id in list_reagents)
var/amt = list_reagents[r_id]
add_reagent(r_id, amt, data)
-/// Remove a specific reagent
-/datum/reagents/proc/remove_reagent(reagent, amount, safety = TRUE)//Added a safety check for the trans_id_to
- if(isnull(amount))
- stack_trace("null amount passed to reagent code")
+/**
+ * Removes a specific reagent. can supress reactions if needed
+ * Arguments
+ *
+ * * [reagent_type][datum/reagent] - the type of reagent
+ * * amount - the volume to remove
+ * * safety - if FALSE will initiate reactions upon removing. used for trans_id_to
+ */
+/datum/reagents/proc/remove_reagent(datum/reagent/reagent_type, amount, safety = TRUE)
+ if(!ispath(reagent_type))
+ stack_trace("invalid reagent passed to remove reagent [reagent_type]")
+ return FALSE
+
+ if(!IS_FINITE(amount))
+ stack_trace("non finite amount passed to remove reagent [amount] [reagent_type]")
return FALSE
- if(amount < 0 || !IS_FINITE(amount))
- stack_trace("invalid number passed to remove_reagent [amount]")
+ // Prevents small amount problems, as well as zero and below zero amounts.
+ amount = FLOOR(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)
- //clamp the removal amount to be between current reagent amount
- //and zero, to prevent removing more than the holder has stored
- amount = clamp(amount, 0, cached_reagent.volume)
- cached_reagent.volume -= amount
+ if(cached_reagent.type == reagent_type)
+ cached_reagent.volume = FLOOR(max(cached_reagent.volume - amount, 0), CHEMICAL_QUANTISATION_LEVEL)
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 : cached_reagent, amount)
+ SEND_SIGNAL(src, COMSIG_REAGENTS_REM_REAGENT, QDELING(cached_reagent) ? reagent_type : cached_reagent, amount)
return TRUE
return FALSE
-/// Remove an amount of reagents without caring about what they are
+/**
+ * Removes a reagent at random by the specified amount
+ * Arguments
+ *
+ * * amount- the volume to remove
+ */
/datum/reagents/proc/remove_any(amount = 1)
+ if(!IS_FINITE(amount))
+ stack_trace("non finite amount passed to remove any reagent [amount]")
+ return FALSE
+
+ amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ if(amount <= 0)
+ return FALSE
+
var/list/cached_reagents = reagent_list
var/total_removed = 0
var/current_list_element = 1
@@ -356,10 +267,10 @@
if(current_list_element > cached_reagents.len)
current_list_element = 1
- var/datum/reagent/R = cached_reagents[current_list_element]
- var/remove_amt = min(amount-total_removed,round(amount/rand(2,initial_list_length),round(amount/10,0.01))) //double round to keep it at a somewhat even spread relative to amount without getting funky numbers.
+ var/datum/reagent/target_holder = cached_reagents[current_list_element]
+ var/remove_amt = min(amount - total_removed, round(amount / rand(2, initial_list_length), round(amount / 10, 0.01))) //double round to keep it at a somewhat even spread relative to amount without getting funky numbers.
//min ensures we don't go over amount.
- remove_reagent(R.type, remove_amt)
+ remove_reagent(target_holder.type, remove_amt)
current_list_element++
total_removed += remove_amt
@@ -368,22 +279,62 @@
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.
-/// Removes all reagents from this holder
+/**
+ * Removes all reagents by an amount equal to
+ * [amount specified] / total volume present in this holder
+ * Arguments
+ *
+ * * amount - the volume of each reagent
+ */
+
/datum/reagents/proc/remove_all(amount = 1)
+ if(!total_volume)
+ return FALSE
+
+ if(!IS_FINITE(amount))
+ 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)
+ if(amount <= 0)
+ return FALSE
+
var/list/cached_reagents = reagent_list
- if(total_volume > 0)
- var/part = amount / total_volume
- for(var/datum/reagent/reagent as anything in cached_reagents)
- remove_reagent(reagent.type, reagent.volume * part)
+ var/part = amount / total_volume
+ var/remove_amount
+ var/removed_amount = 0
- //finish_reacting() //A just in case - update total is in here - should be unneeded, make sure to test this
- handle_reactions()
- return amount
+ for(var/datum/reagent/reagent as anything in cached_reagents)
+ remove_amount = FLOOR(reagent.volume * part, CHEMICAL_QUANTISATION_LEVEL)
+ remove_reagent(reagent.type, remove_amount)
+ removed_amount += remove_amount
+
+ handle_reactions()
+ return removed_amount
+
+/**
+ * Removes all reagent of X type
+ * Arguments
+ *
+ * * [reagent_type][datum/reagent] - the reagent typepath we are trying to remove
+ * * amount - the volume of reagent to remove
+ * * strict - If TRUE will also remove childs of this reagent type
+ */
+/datum/reagents/proc/remove_all_type(datum/reagent/reagent_type, amount, strict = 0, safety = 1)
+ if(!ispath(reagent_type))
+ stack_trace("invalid reagent path passed to remove all type [reagent_type]")
+ return FALSE
+
+ if(!IS_FINITE(amount))
+ 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)
+ if(amount <= 0)
+ return FALSE
-/// Removes all reagent of X type. @strict set to 1 determines whether the childs of the type are included.
-/datum/reagents/proc/remove_all_type(reagent_type, amount, strict = 0, safety = 1)
- if(!isnum(amount))
- return 1
var/list/cached_reagents = reagent_list
var/has_removed_reagent = 0
@@ -403,37 +354,58 @@
return has_removed_reagent
-/// Fuck this one reagent
-/datum/reagents/proc/del_reagent(target_reagent_typepath)
+/**
+ * Removes an specific reagent from this holder
+ * Arguments
+ *
+ * * [target_reagent_typepath][datum/reagent] - type typepath of the reagent to remove
+ */
+/datum/reagents/proc/del_reagent(datum/reagent/target_reagent_typepath)
+ if(!ispath(target_reagent_typepath))
+ stack_trace("invalid reagent path passed to del reagent [target_reagent_typepath]")
+ return FALSE
+
+ //setting the volume to 0 will allow update_total() to clear it up for us
var/list/cached_reagents = reagent_list
for(var/datum/reagent/reagent as anything in cached_reagents)
if(reagent.type == target_reagent_typepath)
- if(isliving(my_atom))
- if(reagent.metabolizing)
- reagent.metabolizing = FALSE
- reagent.on_mob_end_metabolize(my_atom)
- reagent.on_mob_delete(my_atom)
-
- reagent_list -= reagent
- LAZYREMOVE(previous_reagent_list, reagent.type)
- qdel(reagent)
+ reagent.volume = 0
update_total()
- SEND_SIGNAL(src, COMSIG_REAGENTS_DEL_REAGENT, reagent)
- return TRUE
+ return TRUE
+
+ return FALSE
+
+/**
+ * Turn one reagent into another, preserving volume, temp, purity, ph
+ * Arguments
+ *
+ * * [source_reagent_typepath][/datum/reagent] - the typepath of the reagent you are trying to convert
+ * * [target_reagent_typepath][/datum/reagent] - the final typepath the source_reagent_typepath will be converted into
+ * * multiplier - the multiplier applied on the source_reagent_typepath volume before converting
+ * * include_source_subtypes- if TRUE will convert all subtypes of source_reagent_typepath into target_reagent_typepath as well
+ */
+/datum/reagents/proc/convert_reagent(
+ datum/reagent/source_reagent_typepath,
+ datum/reagent/target_reagent_typepath,
+ multiplier = 1,
+ include_source_subtypes = FALSE
+)
+ if(!ispath(source_reagent_typepath))
+ stack_trace("invalid reagent path passed to convert reagent [source_reagent_typepath]")
+ return FALSE
-/// Turn one reagent into another, preserving volume, temp, purity, ph
-/datum/reagents/proc/convert_reagent(source_reagent_typepath, target_reagent_typepath, multiplier = 1, include_source_subtypes = FALSE)
var/reagent_amount
var/reagent_purity
var/reagent_ph
if(include_source_subtypes)
reagent_ph = ph
var/weighted_purity
+ var/list/reagent_type_list = typecacheof(source_reagent_typepath)
for(var/datum/reagent/reagent as anything in reagent_list)
- if(reagent.type in typecacheof(source_reagent_typepath))
+ if(reagent.type in reagent_type_list)
weighted_purity += reagent.volume * reagent.purity
reagent_amount += reagent.volume
- remove_reagent(reagent.type, reagent.volume)
+ remove_reagent(reagent.type, reagent.volume * multiplier)
reagent_purity = weighted_purity / reagent_amount
else
var/datum/reagent/source_reagent = get_reagent(source_reagent_typepath)
@@ -443,40 +415,40 @@
remove_reagent(source_reagent_typepath, reagent_amount)
add_reagent(target_reagent_typepath, reagent_amount * multiplier, reagtemp = chem_temp, added_purity = reagent_purity, added_ph = reagent_ph)
-//Converts the creation_purity to purity
-/datum/reagents/proc/uncache_creation_purity(id)
- var/datum/reagent/R = has_reagent(id)
- if(!R)
- return
- R.purity = R.creation_purity
-
-/// Remove every reagent except this one
-/datum/reagents/proc/isolate_reagent(reagent)
- var/list/cached_reagents = reagent_list
- for(var/datum/reagent/cached_reagent as anything in cached_reagents)
- if(cached_reagent.type != reagent)
- del_reagent(cached_reagent.type)
- update_total()
-
/// Removes all reagents
/datum/reagents/proc/clear_reagents()
var/list/cached_reagents = reagent_list
+
+ //setting volume to 0 will allow update_total() to clean it up
for(var/datum/reagent/reagent as anything in cached_reagents)
- del_reagent(reagent.type)
- SEND_SIGNAL(src, COMSIG_REAGENTS_CLEAR_REAGENTS)
+ reagent.volume = 0
+ update_total()
+ SEND_SIGNAL(src, COMSIG_REAGENTS_CLEAR_REAGENTS)
/**
- * Check if this holder contains this reagent.
- * Reagent takes a PATH to a reagent.
- * Amount checks for having a specific amount of that chemical.
- * Needs matabolizing takes into consideration if the chemical is matabolizing when it's checked.
- * Check subtypes controls whether it should it should also include subtypes: ispath(type, reagent) versus type == reagent.
+ * Check if this holder contains this reagent. Reagent takes a PATH to a reagent
+ * Needs matabolizing takes into consideration if the chemical is metabolizing when it's checked.
+ * Arguments
+ *
+ * * [target_reagent][datum/reagent] - the reagent typepath to check for
+ * * amount - checks for having a specific amount of that chemical
+ * * needs_metabolizing - takes into consideration if the chemical is matabolizing when it's checked.
+ * * check_subtypes - controls whether it should it should also include subtypes: ispath(type, reagent) versus type == reagent.
*/
-/datum/reagents/proc/has_reagent(reagent, amount = -1, needs_metabolizing = FALSE, check_subtypes = FALSE)
+/datum/reagents/proc/has_reagent(
+ datum/reagent/target_reagent,
+ amount = -1,
+ needs_metabolizing = FALSE,
+ check_subtypes = FALSE
+)
+ if(!ispath(target_reagent))
+ stack_trace("invalid reagent path passed to has reagent [target_reagent]")
+ return FALSE
+
var/list/cached_reagents = reagent_list
for(var/datum/reagent/holder_reagent as anything in cached_reagents)
- if (check_subtypes ? ispath(holder_reagent.type, reagent) : holder_reagent.type == reagent)
+ if (check_subtypes ? ispath(holder_reagent.type, target_reagent) : holder_reagent.type == target_reagent)
if(!amount)
if(needs_metabolizing && !holder_reagent.metabolizing)
if(check_subtypes)
@@ -484,7 +456,7 @@
return FALSE
return holder_reagent
else
- if(round(holder_reagent.volume, CHEMICAL_QUANTISATION_LEVEL) >= amount)
+ if(holder_reagent.volume >= amount)
if(needs_metabolizing && !holder_reagent.metabolizing)
if(check_subtypes)
continue
@@ -497,7 +469,10 @@
/**
* Check if this holder contains a reagent with a chemical_flags containing this flag
* Reagent takes the bitflag to search for
- * Amount checks for having a specific amount of reagents matching that chemical
+ *
+ * Arguments
+ * * chemical_flag - the flag to check for
+ * * amount - checks for having a specific amount of reagents matching that chemical
*/
/datum/reagents/proc/has_chemical_flag(chemical_flag, amount = 0)
var/found_amount = 0
@@ -521,140 +496,156 @@
* * no_react - passed through to [/datum/reagents/proc/add_reagent]
* * mob/transferred_by - used for logging
* * remove_blacklisted - skips transferring of reagents without REAGENT_CAN_BE_SYNTHESIZED in chemical_flags
- * * methods - passed through to [/datum/reagents/proc/expose_single] and [/datum/reagent/proc/on_transfer]
- * * show_message - passed through to [/datum/reagents/proc/expose_single]
- * * round_robin - if round_robin=TRUE, so transfer 5 from 15 water, 15 sugar and 15 plasma becomes 10, 15, 15 instead of 13.3333, 13.3333 13.3333. Good if you hate floating point errors
+ * * methods - passed through to [/datum/reagents/proc/expose_multiple] and [/datum/reagent/proc/on_transfer]
+ * * show_message - passed through to [/datum/reagents/proc/expose_multiple]
* * ignore_stomach - when using methods INGEST will not use the stomach as the target
*/
-/datum/reagents/proc/trans_to(obj/target, amount = 1, multiplier = 1, preserve_data = TRUE, no_react = FALSE, mob/transferred_by, remove_blacklisted = FALSE, methods = NONE, show_message = TRUE, round_robin = FALSE, ignore_stomach = FALSE)
- var/list/cached_reagents = reagent_list
- if(!target || !total_volume)
- return
- if(amount < 0)
+/datum/reagents/proc/trans_to(
+ obj/target,
+ amount = 1,
+ multiplier = 1,
+ preserve_data = TRUE,
+ no_react = FALSE,
+ mob/transferred_by,
+ remove_blacklisted = FALSE,
+ methods = NONE,
+ show_message = TRUE,
+ ignore_stomach = FALSE
+)
+ if(QDELETED(target) || !total_volume)
return
- var/cached_amount = amount
+ if(!IS_FINITE(amount))
+ stack_trace("non finite amount passed to trans_to [amount] amount of reagents")
+ return FALSE
+
+ var/list/cached_reagents = reagent_list
+
var/atom/target_atom
- var/datum/reagents/R
+ var/datum/reagents/target_holder
if(istype(target, /datum/reagents))
- R = target
- target_atom = R.my_atom
+ target_holder = target
+ target_atom = target_holder.my_atom
else
if(!ignore_stomach && (methods & INGEST) && iscarbon(target))
var/mob/living/carbon/eater = target
var/obj/item/organ/internal/stomach/belly = eater.get_organ_slot(ORGAN_SLOT_STOMACH)
if(!belly)
- eater.expel_ingested(my_atom, amount)
+ var/expel_amount = FLOOR(amount, CHEMICAL_QUANTISATION_LEVEL)
+ if(expel_amount > 0 )
+ eater.expel_ingested(my_atom, expel_amount)
return
- R = belly.reagents
+ target_holder = belly.reagents
target_atom = belly
else if(!target.reagents)
return
else
- R = target.reagents
+ target_holder = target.reagents
target_atom = target
+ 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
+
//Set up new reagents to inherit the old ongoing reactions
if(!no_react)
- transfer_reactions(R)
+ transfer_reactions(target_holder)
- amount = min(min(amount, src.total_volume), R.maximum_volume-R.total_volume)
var/trans_data = null
- var/transfer_log = list()
- var/r_to_send = list() // Validated list of reagents to be exposed
- var/reagents_to_remove = list()
- if(!round_robin)
- var/part = amount / src.total_volume
- for(var/datum/reagent/reagent as anything in cached_reagents)
- if(remove_blacklisted && !(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED))
- continue
- var/transfer_amount = reagent.volume * part
- if(preserve_data)
- trans_data = copy_data(reagent)
- if(reagent.intercept_reagents_transfer(R, cached_amount))//Use input amount instead.
- continue
- if(!R.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.
- continue
- if(methods)
- r_to_send += reagent
+ var/list/transfer_log = list()
+ var/list/r_to_send = list() // Validated list of reagents to be exposed
+ var/list/reagents_to_remove = list()
- reagents_to_remove += reagent
-
- if(isorgan(target_atom))
- R.expose_multiple(r_to_send, target, methods, part, show_message)
- else
- R.expose_multiple(r_to_send, target_atom, methods, part, show_message)
-
- for(var/datum/reagent/reagent as anything in reagents_to_remove)
- var/transfer_amount = reagent.volume * part
- if(methods)
- reagent.on_transfer(target_atom, methods, transfer_amount * multiplier)
- remove_reagent(reagent.type, transfer_amount)
- var/list/reagent_qualities = list(REAGENT_TRANSFER_AMOUNT = transfer_amount, REAGENT_PURITY = reagent.purity)
- transfer_log[reagent.type] = reagent_qualities
+ var/part = amount / total_volume
+ var/transfer_amount
+ var/transfered_amount = 0
- else
- var/to_transfer = amount
- for(var/datum/reagent/reagent as anything in cached_reagents)
- if(!to_transfer)
- break
- if(remove_blacklisted && !(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED))
- continue
- if(preserve_data)
- trans_data = copy_data(reagent)
- var/transfer_amount = amount
- if(amount > reagent.volume)
- transfer_amount = reagent.volume
- if(reagent.intercept_reagents_transfer(R, cached_amount))//Use input amount instead.
- continue
- if(!R.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.
- continue
- to_transfer = max(to_transfer - transfer_amount , 0)
- if(methods)
- if(isorgan(target_atom))
- R.expose_single(reagent, target, methods, transfer_amount, show_message)
- else
- R.expose_single(reagent, target_atom, methods, transfer_amount, show_message)
- reagent.on_transfer(target_atom, methods, transfer_amount * multiplier)
- remove_reagent(reagent.type, transfer_amount)
- var/list/reagent_qualities = list(REAGENT_TRANSFER_AMOUNT = transfer_amount, REAGENT_PURITY = reagent.purity)
- transfer_log[reagent.type] = reagent_qualities
+ //first add reagents to target
+ for(var/datum/reagent/reagent as anything in cached_reagents)
+ if(remove_blacklisted && !(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED))
+ continue
+ if(preserve_data)
+ 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.
+ continue
+ if(methods)
+ r_to_send += reagent
+ reagents_to_remove += list(list("R" = reagent, "T" = transfer_amount))
+ transfered_amount += transfer_amount
+
+ //expose target to reagent changes
+ target_holder.expose_multiple(r_to_send, isorgan(target_atom) ? target : target_atom, methods, part, show_message)
+
+ //remove chemicals that were added above
+ for(var/list/data as anything in reagents_to_remove)
+ var/datum/reagent/reagent = data["R"]
+ transfer_amount = data["T"]
+ if(methods)
+ reagent.on_transfer(target_atom, methods, transfer_amount)
+ remove_reagent(reagent.type, transfer_amount)
+ transfer_log[reagent.type] = list(REAGENT_TRANSFER_AMOUNT = transfer_amount, REAGENT_PURITY = reagent.purity)
if(transferred_by && target_atom)
target_atom.add_hiddenprint(transferred_by) //log prints so admins can figure out who touched it last.
log_combat(transferred_by, target_atom, "transferred reagents ([get_external_reagent_log_string(transfer_log)]) from [my_atom] to")
update_total()
- R.update_total()
+ target_holder.update_total()
if(!no_react)
- R.handle_reactions()
+ target_holder.handle_reactions()
src.handle_reactions()
- return amount
+ return transfered_amount
-/// Transfer a specific reagent id to the target object
-/datum/reagents/proc/trans_id_to(obj/target, reagent, amount=1, preserve_data=1)//Not sure why this proc didn't exist before. It does now! /N
- var/list/cached_reagents = reagent_list
- if (!target)
+/**
+ * Transfer a specific reagent id to the target object
+ * Arguments
+ *
+ * * [target][obj] - the target to transfer reagents to
+ * * [reagent_type][datum/reagent] - the type of reagent to transfer to the target
+ * * amount - volume to transfer
+ * * preserve_data- if TRUE reagent user data will remain preserved
+ */
+/datum/reagents/proc/trans_id_to(
+ obj/target,
+ datum/reagent/reagent_type,
+ amount = 1,
+ preserve_data = 1
+)
+ if (QDELETED(target) || !total_volume)
return
+ if(!IS_FINITE(amount))
+ stack_trace("non finite amount passed to trans_id_to [amount] [reagent_type]")
+ return FALSE
+
+ var/cached_amount = amount
+
+ var/available_volume = get_reagent_amount(reagent_type)
var/datum/reagents/holder
if(istype(target, /datum/reagents))
holder = target
- else if(target.reagents && total_volume > 0 && get_reagent_amount(reagent))
+ else if(target.reagents && available_volume)
holder = target.reagents
else
return
- if(amount < 0)
+
+ // 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)
return
- var/cached_amount = amount
- if(get_reagent_amount(reagent) < amount)
- amount = get_reagent_amount(reagent)
- amount = min(round(amount, CHEMICAL_VOLUME_ROUNDING), holder.maximum_volume - holder.total_volume)
+ var/list/cached_reagents = reagent_list
+
var/trans_data = null
for (var/looping_through_reagents in cached_reagents)
var/datum/reagent/current_reagent = looping_through_reagents
- if(current_reagent.type == reagent)
+ if(current_reagent.type == reagent_type)
if(preserve_data)
trans_data = current_reagent.data
if(current_reagent.intercept_reagents_transfer(holder, cached_amount))//Use input amount instead.
@@ -669,12 +660,29 @@
holder.handle_reactions()
return amount
-/// Copies the reagents to the target object
-/datum/reagents/proc/copy_to(obj/target, amount = 1, multiplier = 1, preserve_data = TRUE, no_react = FALSE)
- var/list/cached_reagents = reagent_list
- if(!target || !total_volume)
+/**
+ * Copies the reagents to the target object
+ * Arguments
+ *
+ * * [target][obj] - the target to transfer reagents to
+ * * multiplier - the multiplier applied on all reagent volumes before transfering
+ * * preserve_data - preserve user data of all reagents after transfering
+ * * no_react - if TRUE will not handle reactions
+ */
+/datum/reagents/proc/copy_to(
+ atom/target,
+ amount = 1,
+ multiplier = 1,
+ preserve_data = TRUE,
+ no_react = FALSE
+)
+ if(QDELETED(target) || !total_volume)
return
+ if(!IS_FINITE(amount))
+ stack_trace("non finite amount passed to copy_to [amount] amount of reagents")
+ return FALSE
+
var/datum/reagents/target_holder
if(istype(target, /datum/reagents))
target_holder = target
@@ -683,17 +691,24 @@
return
target_holder = target.reagents
- if(amount < 0)
+ // 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
- amount = min(min(amount, total_volume), target_holder.maximum_volume - target_holder.total_volume)
+ var/list/cached_reagents = reagent_list
var/part = amount / total_volume
+ var/transfer_amount
+ var/transfered_amount = 0
var/trans_data = null
+
for(var/datum/reagent/reagent as anything in cached_reagents)
- var/copy_amount = reagent.volume * part
+ transfer_amount = FLOOR(reagent.volume * part, CHEMICAL_QUANTISATION_LEVEL)
if(preserve_data)
trans_data = reagent.data
- target_holder.add_reagent(reagent.type, copy_amount * multiplier, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, ignore_splitting = reagent.chemical_flags & REAGENT_DONOTSPLIT)
+ 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))
+ continue
+ transfered_amount += transfer_amount
if(!no_react)
// pass over previous ongoing reactions before handle_reactions is called
@@ -702,10 +717,14 @@
target_holder.update_total()
target_holder.handle_reactions()
- return amount
+ return transfered_amount
-///Multiplies the reagents inside this holder by a specific amount
-/datum/reagents/proc/multiply_reagents(multiplier=1)
+/**
+ * Multiplies the reagents inside this holder by a specific amount
+ * Arguments
+ * * multiplier - the amount to multiply each reagent by
+ */
+/datum/reagents/proc/multiply_reagents(multiplier = 1)
var/list/cached_reagents = reagent_list
if(!total_volume)
return
@@ -784,9 +803,9 @@
// skip metabolizing effects for small units of toxins
if(istype(reagent, /datum/reagent/toxin) && liver && !dead)
var/datum/reagent/toxin/toxin = reagent
- var/amount = round(toxin.volume, CHEMICAL_QUANTISATION_LEVEL)
+ var/amount = toxin.volume
if(belly)
- amount += belly.reagents.get_reagent_amount(toxin.type)
+ amount = FLOOR(amount + belly.reagents.get_reagent_amount(toxin.type), CHEMICAL_QUANTISATION_LEVEL)
if(amount <= liver_tolerance)
owner.reagents.remove_reagent(toxin.type, toxin.metabolization_rate * owner.metabolism_efficiency * seconds_per_tick)
@@ -865,7 +884,13 @@
need_mob_update += reagent.on_mob_dead(owner, seconds_per_tick)
return need_mob_update
-/// Signals that metabolization has stopped, triggering the end of trait-based effects
+/**
+ * Signals that metabolization has stopped, triggering the end of trait-based effects
+ * Arguments
+ *
+ * * [C][mob/living/carbon] - the mob to end metabolization on
+ * * keep_liverless - if true will work without a liver
+ */
/datum/reagents/proc/end_metabolization(mob/living/carbon/C, keep_liverless = TRUE)
var/list/cached_reagents = reagent_list
for(var/datum/reagent/reagent as anything in cached_reagents)
@@ -907,7 +932,14 @@
return FALSE //prevent addition
return added_volume
-///Processes any chems that have the REAGENT_IGNORE_STASIS bitflag ONLY
+/**
+ * Processes any chems that have the REAGENT_IGNORE_STASIS bitflag ONLY
+ * Arguments
+ *
+ * * [owner][mob/living/carbon] - the mob we are doing stasis handlng on
+ * * seconds_per_tick - passed from process
+ * * times_fired - number of times to metabolize this reagent
+ */
/datum/reagents/proc/handle_stasis_chems(mob/living/carbon/owner, seconds_per_tick, times_fired)
var/need_mob_update = FALSE
for(var/datum/reagent/reagent as anything in reagent_list)
@@ -918,19 +950,6 @@
owner.updatehealth()
update_total()
-/**
- * Calls [/datum/reagent/proc/on_move] on every reagent in this holder
- *
- * Arguments:
- * * atom/A - passed to on_move
- * * Running - passed to on_move
- */
-/datum/reagents/proc/conditional_update_move(atom/A, Running = 0)
- var/list/cached_reagents = reagent_list
- for(var/datum/reagent/reagent as anything in cached_reagents)
- reagent.on_move(A, Running)
- update_total()
-
/**
* Calls [/datum/reagent/proc/on_update] on every reagent in this holder
*
@@ -992,16 +1011,16 @@
var/granularity = 1
if(!(reaction.reaction_flags & REACTION_INSTANT))
- granularity = CHEMICAL_VOLUME_MINIMUM
+ granularity = CHEMICAL_QUANTISATION_LEVEL
var/list/cached_required_reagents = reaction.required_reagents
for(var/req_reagent in cached_required_reagents)
- if(!has_reagent(req_reagent, (cached_required_reagents[req_reagent]*granularity)))
+ if(!has_reagent(req_reagent, (cached_required_reagents[req_reagent] * granularity)))
continue reaction_loop
var/list/cached_required_catalysts = reaction.required_catalysts
for(var/_catalyst in cached_required_catalysts)
- if(!has_reagent(_catalyst, (cached_required_catalysts[_catalyst]*granularity)))
+ if(!has_reagent(_catalyst, (cached_required_catalysts[_catalyst] * granularity)))
continue reaction_loop
if(cached_my_atom)
@@ -1114,7 +1133,7 @@
* This ends a single instance of an ongoing reaction
*
* Arguments:
-* * E - the equilibrium that will be ended
+* * [equilibrium][datum/equilibrium] - the equilibrium that will be ended
* Returns:
* * mix_message - the associated mix message of a reaction
*/
@@ -1136,7 +1155,8 @@
//If the reaction pollutes, pollute it here if we have an atom
if(equilibrium.reaction.pollutant_type && my_atom)
var/turf/my_turf = get_turf(my_atom)
- my_turf.pollute_turf(equilibrium.reaction.pollutant_type, equilibrium.reaction.pollutant_amount * equilibrium.reacted_vol)
+ if(my_turf) // reactions can happen in nullspace (like inside of a mob's stomach for instance).
+ my_turf.pollute_turf(equilibrium.reaction.pollutant_type, equilibrium.reaction.pollutant_amount * equilibrium.reacted_vol)
//SKYRAT EDIT END
qdel(equilibrium)
update_total()
@@ -1145,7 +1165,6 @@
/*
* This stops the holder from processing at the end of a series of reactions (i.e. when all the equilibriums are completed)
-*
* Also resets reaction variables to be null/empty/FALSE so that it can restart correctly in the future
*/
/datum/reagents/proc/finish_reacting()
@@ -1153,7 +1172,7 @@
is_reacting = FALSE
//Cap off values
for(var/datum/reagent/reagent as anything in reagent_list)
- reagent.volume = round(reagent.volume, CHEMICAL_VOLUME_ROUNDING)//To prevent runaways.
+ 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))
@@ -1253,7 +1272,7 @@
var/datum/cached_my_atom = my_atom
var/multiplier = INFINITY
for(var/reagent in cached_required_reagents)
- multiplier = min(multiplier, round(get_reagent_amount(reagent) / cached_required_reagents[reagent]))
+ multiplier = FLOOR(min(multiplier, get_reagent_amount(reagent) / cached_required_reagents[reagent]), CHEMICAL_QUANTISATION_LEVEL)
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
return FALSE
@@ -1293,48 +1312,61 @@
//If the reaction pollutes, pollute it here if we have an atom
if(selected_reaction.pollutant_type && my_atom)
var/turf/my_turf = get_turf(my_atom)
- my_turf.pollute_turf(selected_reaction.pollutant_type, selected_reaction.pollutant_amount * multiplier)
+ if(my_turf) // just to be safe here
+ my_turf.pollute_turf(selected_reaction.pollutant_type, selected_reaction.pollutant_amount * multiplier)
//SKYRAT EDIT END
selected_reaction.on_reaction(src, null, multiplier)
-///Possibly remove - see if multiple instant reactions is okay (Though, this "sorts" reactions by temp decending)
-///Presently unused
-/datum/reagents/proc/get_priority_instant_reaction(list/possible_reactions)
- if(!length(possible_reactions))
- return FALSE
- var/datum/chemical_reaction/selected_reaction = possible_reactions[1]
- //select the reaction with the most extreme temperature requirements
- for(var/datum/chemical_reaction/competitor as anything in possible_reactions)
- if(selected_reaction.is_cold_recipe)
- if(competitor.required_temp <= selected_reaction.required_temp)
- selected_reaction = competitor
- else
- if(competitor.required_temp >= selected_reaction.required_temp)
- selected_reaction = competitor
- return selected_reaction
-
/// Updates [/datum/reagents/var/total_volume]
/datum/reagents/proc/update_total()
var/list/cached_reagents = reagent_list
- . = 0 // This is a relatively hot proc.
- var/total_ph = 0 // I know I know, I'm sorry
- for(var/datum/reagent/reagent as anything in cached_reagents)
- if((reagent.volume < 0.05) && !is_reacting)
- del_reagent(reagent.type)
- else if(reagent.volume <= CHEMICAL_VOLUME_MINIMUM)//For clarity
- del_reagent(reagent.type)
- else
- . += reagent.volume
- total_ph += (reagent.ph * reagent.volume)
- total_volume = .
+ var/list/deleted_reagents = list()
+ var/chem_index = 1
+ var/num_reagents = length(cached_reagents)
+ var/total_ph = 0
+ . = 0
+
+ //responsible for removing reagents and computing total ph & volume
+ //all it's code was taken out of del_reagent() initially for efficiency purposes
+ while(chem_index <= num_reagents)
+ var/datum/reagent/reagent = cached_reagents[chem_index]
+ chem_index += 1
- if(!.) // No volume, default to the base
+ //remove very small amounts of reagents
+ if((reagent.volume <= 0.05 && !is_reacting) || reagent.volume <= CHEMICAL_QUANTISATION_LEVEL)
+ //end metabolization
+ if(isliving(my_atom))
+ if(reagent.metabolizing)
+ reagent.metabolizing = FALSE
+ reagent.on_mob_end_metabolize(my_atom)
+ reagent.on_mob_delete(my_atom)
+
+ //removing it and store in a seperate list for processing later
+ cached_reagents -= reagent
+ LAZYREMOVE(previous_reagent_list, reagent.type)
+ deleted_reagents += reagent
+
+ //move pointer back so we don't overflow & decrease length
+ chem_index -= 1
+ num_reagents -= 1
+ continue
+
+ //compute volume & ph like we would normally
+ . += reagent.volume
+ total_ph += (reagent.ph * reagent.volume)
+
+ //assign the final values
+ total_volume = .
+ if(!.)
ph = CHEMICAL_NORMAL_PH
- return .
- //Keep limited // should really be defines
- ph = clamp(total_ph/total_volume, 0, 14)
+ else
+ ph = clamp(total_ph / total_volume, 0, 14)
+ //now send the signals after the volume & ph has been computed
+ for(var/datum/reagent/deleted_reagent as anything in deleted_reagents)
+ SEND_SIGNAL(src, COMSIG_REAGENTS_DEL_REAGENT, deleted_reagent)
+ qdel(deleted_reagent)
/**
* Applies the relevant expose_ proc for every reagent in this holder
@@ -1376,56 +1408,74 @@
return A.expose_reagents(reagents, src, methods, volume_modifier, show_message)
-/// Same as [/datum/reagents/proc/expose] but only for one reagent
-/datum/reagents/proc/expose_single(datum/reagent/R, atom/A, methods = TOUCH, volume_modifier = 1, show_message = TRUE)
- if(isnull(A))
- return null
-
- if(ispath(R))
- R = get_reagent(R)
- if(isnull(R))
- return null
-
- // Yes, we need the parentheses.
- return A.expose_reagents(list((R) = R.volume * volume_modifier), src, methods, volume_modifier, show_message)
-
/// Is this holder full or not
/datum/reagents/proc/holder_full()
return total_volume >= maximum_volume
-/// Get the amount of this reagent
-/datum/reagents/proc/get_reagent_amount(reagent, include_subtypes = FALSE)
+/**
+ * Get the amount of this reagent or the sum of all its subtypes if specified
+ * Arguments
+ * * [reagent][datum/reagent] - the typepath of the reagent to look for
+ * * include_subtypes - if TRUE returns the sum of volumes of all subtypes of the above param reagent
+ */
+/datum/reagents/proc/get_reagent_amount(datum/reagent/reagent, include_subtypes = FALSE)
+ if(!ispath(reagent))
+ stack_trace("invalid path passed to get_reagent_amount [reagent]")
+ return 0
+
var/list/cached_reagents = reagent_list
var/total_amount = 0
for(var/datum/reagent/cached_reagent as anything in cached_reagents)
if((!include_subtypes && cached_reagent.type == reagent) || (include_subtypes && ispath(cached_reagent.type, reagent)))
- total_amount += round(cached_reagent.volume, CHEMICAL_QUANTISATION_LEVEL)
- return total_amount
+ total_amount += cached_reagent.volume
+
+ return FLOOR(total_amount, CHEMICAL_QUANTISATION_LEVEL)
+/**
+ * Gets the sum of volumes of all reagent type paths present in the list
+ * Arguments
+ * * [reagents][list] - list of reagent typepaths
+ */
/datum/reagents/proc/get_multiple_reagent_amounts(list/reagents)
var/list/cached_reagents = reagent_list
var/total_amount = 0
for(var/datum/reagent/cached_reagent as anything in cached_reagents)
if(cached_reagent.type in reagents)
- total_amount += round(cached_reagent.volume, CHEMICAL_QUANTISATION_LEVEL)
+ total_amount += FLOOR(cached_reagent.volume, CHEMICAL_QUANTISATION_LEVEL)
return total_amount
-/// Get the purity of this reagent
-/datum/reagents/proc/get_reagent_purity(reagent)
+/**
+ * Get the purity of this reagent
+ * Arguments
+ * * [reagent][datum/reagent] - the typepath of the specific reagent to get purity of
+ */
+/datum/reagents/proc/get_reagent_purity(datum/reagent/reagent)
+ if(!ispath(reagent))
+ stack_trace("invalid reagent typepath passed to get_reagent_purity [reagent]")
+ return 0
+
var/list/cached_reagents = reagent_list
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
-/// Directly set the purity of all contained reagents to a new value
+/**
+ * Directly set the purity of all contained reagents to a new value
+ * Arguments
+ * * new_purity - the new purity value
+ */
/datum/reagents/proc/set_all_reagents_purity(new_purity = 0)
var/list/cached_reagents = reagent_list
for(var/datum/reagent/cached_reagent as anything in cached_reagents)
cached_reagent.purity = max(0, new_purity)
-/// Get the average purity of all reagents (or all subtypes of provided typepath)
-/datum/reagents/proc/get_average_purity(parent_type = null)
+/**
+ * Get the average purity of all reagents (or all subtypes of provided typepath)
+ * Arguments
+ * * [parent_type][datum/reagent] - the typepath of specific reagents to look for
+ */
+/datum/reagents/proc/get_average_purity(datum/reagent/parent_type = null)
var/total_amount
var/weighted_purity
var/list/cached_reagents = reagent_list
@@ -1436,42 +1486,11 @@
weighted_purity += reagent.volume * reagent.purity
return weighted_purity / total_amount
-/// Get the average nutriment_factor of all consumable reagents
-/datum/reagents/proc/get_average_nutriment_factor()
- var/consumable_volume
- var/weighted_nutriment_factor
- var/list/cached_reagents = reagent_list
- for(var/datum/reagent/reagent as anything in cached_reagents)
- if(istype(reagent, /datum/reagent/consumable))
- var/datum/reagent/consumable/consumable_reagent = reagent
- consumable_volume += consumable_reagent.volume
- weighted_nutriment_factor += consumable_reagent.volume * consumable_reagent.nutriment_factor
- return weighted_nutriment_factor / consumable_volume
-
-/// Get a comma separated string of every reagent name in this holder. UNUSED
-/datum/reagents/proc/get_reagent_names()
- var/list/names = list()
- var/list/cached_reagents = reagent_list
- for(var/datum/reagent/reagent as anything in cached_reagents)
- names += reagent.name
-
- return jointext(names, ",")
-
-/// helper function to preserve data across reactions (needed for xenoarch)
-/datum/reagents/proc/get_data(reagent_id)
- var/list/cached_reagents = reagent_list
- for(var/datum/reagent/reagent as anything in cached_reagents)
- if(reagent.type == reagent_id)
- return reagent.data
-
-/// helper function to preserve data across reactions (needed for xenoarch)
-/datum/reagents/proc/set_data(reagent_id, new_data)
- var/list/cached_reagents = reagent_list
- for(var/datum/reagent/reagent as anything in cached_reagents)
- if(reagent.type == reagent_id)
- reagent.data = new_data
-
-/// Shallow copies (deep copy of viruses) data from the provided reagent into our copy of that reagent
+/**
+ * Shallow copies (deep copy of viruses) data from the provided reagent into our copy of that reagent
+ * Arguments
+ * [current_reagent][datum/reagent] - the reagent(not typepath) to copy data from
+ */
/datum/reagents/proc/copy_data(datum/reagent/current_reagent)
if(!current_reagent || !current_reagent.data)
return null
@@ -1493,8 +1512,12 @@
return trans_data
-/// Get a reference to the reagent if it exists
-/datum/reagents/proc/get_reagent(type)
+/**
+ * Get a reference to the reagent if it exists
+ * Arguments
+ * * [type][datum/reagent] - the typepath of the reagent to look up
+ */
+/datum/reagents/proc/get_reagent(datum/reagent/type)
var/list/cached_reagents = reagent_list
. = locate(type) in cached_reagents
@@ -1560,8 +1583,14 @@
return // no div/0 please
set_temperature(clamp(chem_temp + (delta_energy / heat_capacity), min_temp, max_temp))
-/// Applies heat to this holder
-/datum/reagents/proc/expose_temperature(temperature, coeff=0.02)
+/**
+ * Applies heat to this holder
+ * Arguments
+ *
+ * * temperature - the temperature we to heat/cool by
+ * * coeff - multiplier to be applied on temp diff between param temp and current temp
+ */
+/datum/reagents/proc/expose_temperature(temperature, coeff = 0.02)
if(istype(my_atom,/obj/item/reagent_containers))
var/obj/item/reagent_containers/RCs = my_atom
if(RCs.reagent_flags & NO_REACT) //stasis holders IE cryobeaker
@@ -1601,21 +1630,6 @@
for(var/datum/reagent/reagent as anything in reagent_list)
reagent.ph = clamp(reagent.ph + value, lower_limit, upper_limit)
-/*
-* Adjusts the base pH of all of the listed types
-*
-* - moves it towards acidic
-* + moves it towards basic
-* Arguments:
-* * input_reagents_list - list of reagent objects to adjust
-* * value - How much to adjust the base pH by
-*/
-/datum/reagents/proc/adjust_specific_reagent_list_ph(list/input_reagents_list, value, lower_limit = 0, upper_limit = 14)
- for(var/datum/reagent/reagent as anything in input_reagents_list)
- if(!reagent) //We can call this with missing reagents.
- continue
- reagent.ph = clamp(reagent.ph + value, lower_limit, upper_limit)
-
/*
* Adjusts the base pH of a specific type
*
@@ -1647,16 +1661,11 @@
for(var/reagent_type in external_list)
var/list/qualities = external_list[reagent_type]
- data += "[reagent_type] ([round(qualities[REAGENT_TRANSFER_AMOUNT], 0.1)]u, [qualities[REAGENT_PURITY]] purity)"
+ data += "[reagent_type] ([FLOOR(qualities[REAGENT_TRANSFER_AMOUNT], CHEMICAL_QUANTISATION_LEVEL)]u, [qualities[REAGENT_PURITY]] purity)"
return english_list(data)
-/**
- * Outputs a log-friendly list of reagents based on the internal reagent_list.
- *
- * Arguments:
- * * external_list - Assoc list of (reagent_type) = list(REAGENT_TRANSFER_AMOUNT = amounts, REAGENT_PURITY = purity)
- */
+/// Outputs a log-friendly list of reagents based on the internal reagent_list.
/datum/reagents/proc/get_reagent_log_string()
if(!length(reagent_list))
return "no reagents"
@@ -1664,7 +1673,7 @@
var/list/data = list()
for(var/datum/reagent/reagent as anything in reagent_list)
- data += "[reagent.type] ([round(reagent.volume, 0.1)]u, [reagent.purity] purity)"
+ data += "[reagent.type] ([FLOOR(reagent.volume, CHEMICAL_QUANTISATION_LEVEL)]u, [reagent.purity] purity)"
return english_list(data)
@@ -2015,7 +2024,7 @@
ui_reaction_id = text2path(params["id"])
return TRUE
if("search_reagents")
- var/input_reagent = tgui_input_list(usr, "Select reagent", "Reagent", GLOB.chemical_name_list)
+ var/input_reagent = tgui_input_list(usr, "Select reagent", "Reagent", GLOB.name2reagent)
input_reagent = get_reagent_type_from_product_string(input_reagent) //from string to type
var/datum/reagent/reagent = find_reagent_object_from_type(input_reagent)
if(!reagent)
@@ -2133,13 +2142,5 @@
reagents = new /datum/reagents(max_vol, flags)
reagents.my_atom = src
-/atom/movable/chem_holder
- name = "This atom exists to hold chems. If you can see this, make an issue report"
- desc = "God this is stupid"
-
#undef REAGENT_TRANSFER_AMOUNT
#undef REAGENT_PURITY
-
-#undef REAGENTS_UI_MODE_LOOKUP
-#undef REAGENTS_UI_MODE_REAGENT
-#undef REAGENTS_UI_MODE_RECIPE
diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm
index 0a49fd1beb1374..9d33cccd56c6ce 100644
--- a/code/modules/reagents/chemistry/machinery/chem_master.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_master.dm
@@ -46,9 +46,10 @@ GLOBAL_LIST_INIT(chem_master_containers, list(
/obj/item/reagent_containers/pill/patch/style
)),
// SKYRAT EDIT ADDITION START
- CAT_HYPOS = typecacheof(list(
- /obj/item/reagent_containers/cup/vial
- )),
+ CAT_HYPOS = list(
+ /obj/item/reagent_containers/cup/vial/small,
+ /obj/item/reagent_containers/cup/vial/large,
+ ),
CAT_DARTS = typecacheof(list(
/obj/item/reagent_containers/syringe/smartdart
))
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index 110107d103fc04..f62cf8a86be55c 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -1,21 +1,3 @@
-GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
-
-/proc/build_name2reagent()
- . = list()
- for (var/t in subtypesof(/datum/reagent))
- var/datum/reagent/R = t
- if (length(initial(R.name)))
- .[ckey(initial(R.name))] = t
-
-GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
- /turf/closed/indestructible, //indestructible turfs should be indestructible, metalgen transmutation to plasma allows them to be destroyed
- /turf/open/indestructible
-)))
-
-//Various reagents
-//Toxin & acid reagents
-//Hydroponics stuff
-
/// A single reagent
/datum/reagent
/// datums don't have names by default
@@ -50,8 +32,6 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
var/color = "#000000" // rgb: 0, 0, 0
///how fast the reagent is metabolized by the mob
var/metabolization_rate = REAGENTS_METABOLISM
- /// appears unused
- var/overrides_metab = 0
/// above this overdoses happen
var/overdose_threshold = 0
/// You fucked up and this is now triggering its overdose effects, purge that shit quick.
@@ -62,8 +42,6 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
var/reagent_weight = 1
///is it currently metabolizing
var/metabolizing = FALSE
- /// is it bad for you? Currently only used for borghypo. C2s and Toxins have it TRUE by default.
- var/harmful = FALSE
/// Are we from a material? We might wanna know that for special stuff. Like metalgen. Is replaced with a ref of the material on New()
var/datum/material/material
///A list of causes why this chem should skip being removed, if the length is 0 it will be removed from holder naturally, if this is >0 it will not be removed from the holder.
@@ -121,7 +99,8 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
if(!mass)
mass = rand(10, 800)
-/datum/reagent/Destroy() // This should only be called by the holder, so it's already handled clearing its references
+/// This should only be called by the holder, so it's already handled clearing its references
+/datum/reagent/Destroy()
. = ..()
holder = null
@@ -159,12 +138,27 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
/datum/reagent/proc/burn(datum/reagents/holder)
return
-/// Called from [/datum/reagents/proc/metabolize]
-/datum/reagent/proc/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/**
+ * Ticks on mob Life() for as long as the reagent remains in the mob's reagents.
+ *
+ * Usage: Parent should be called first using . = ..()
+ *
+ * Exceptions: If the holder var needs to be accessed, call the parent afterward that as it can become null if the reagent is fully removed.
+ *
+ * Returns: UPDATE_MOB_HEALTH only if you need to update the health of a mob (this is only needed when damage is dealt to the mob)
+ *
+ * Arguments
+ * * mob/living/carbon/affected_mob - the mob which the reagent currently is inside of
+ * * seconds_per_tick - the time in server seconds between proc calls (when performing normally it will be 2)
+ * * times_fired - the number of times the owner's Life() tick has been called aka The number of times SSmobs has fired
+ *
+ */
+/datum/reagent/proc/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
current_cycle++
if(length(reagent_removal_skip_list))
return
- holder.remove_reagent(type, metabolization_rate * M.metabolism_efficiency * seconds_per_tick) //By default it slowly disappears.
+ if(holder)
+ holder.remove_reagent(type, metabolization_rate * affected_mob.metabolism_efficiency * seconds_per_tick) //By default it slowly disappears.
/// Called in burns.dm *if* the reagent has the REAGENT_AFFECTS_WOUNDS process flag
/datum/reagent/proc/on_burn_wound_processing(datum/wound/burn/flesh/burn_wound)
@@ -174,7 +168,7 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
Used to run functions before a reagent is transferred. Returning TRUE will block the transfer attempt.
Primarily used in reagents/reaction_agents
*/
-/datum/reagent/proc/intercept_reagents_transfer(datum/reagents/target)
+/datum/reagent/proc/intercept_reagents_transfer(datum/reagents/target, amount)
return FALSE
///Called after a reagent is transferred
@@ -182,35 +176,32 @@ Primarily used in reagents/reaction_agents
return
/// Called when this reagent is first added to a mob
-/datum/reagent/proc/on_mob_add(mob/living/L, amount)
+/datum/reagent/proc/on_mob_add(mob/living/affected_mob, amount)
overdose_threshold /= max(normalise_creation_purity(), 1) //Maybe??? Seems like it would help pure chems be even better but, if I normalised this to 1, then everything would take a 25% reduction
return
/// Called when this reagent is removed while inside a mob
-/datum/reagent/proc/on_mob_delete(mob/living/L)
- L.clear_mood_event("[type]_overdose")
+/datum/reagent/proc/on_mob_delete(mob/living/affected_mob)
+ affected_mob.clear_mood_event("[type]_overdose")
return
/// Called when this reagent first starts being metabolized by a liver
-/datum/reagent/proc/on_mob_metabolize(mob/living/L)
+/datum/reagent/proc/on_mob_metabolize(mob/living/affected_mob)
return
/// Called when this reagent stops being metabolized by a liver
-/datum/reagent/proc/on_mob_end_metabolize(mob/living/L)
+/datum/reagent/proc/on_mob_end_metabolize(mob/living/affected_mob)
return
-/// Called when a reagent is inside of a mob when they are dead
-/datum/reagent/proc/on_mob_dead(mob/living/carbon/C, seconds_per_tick)
+/// Called when a reagent is inside of a mob when they are dead. Returning UPDATE_MOB_HEALTH will cause updatehealth() to be called on the holder mob by /datum/reagents/proc/metabolize.
+/datum/reagent/proc/on_mob_dead(mob/living/carbon/affected_mob, seconds_per_tick)
if(!(chemical_flags & REAGENT_DEAD_PROCESS))
return
current_cycle++
if(length(reagent_removal_skip_list))
return
- holder.remove_reagent(type, metabolization_rate * C.metabolism_efficiency * seconds_per_tick)
-
-/// Called by [/datum/reagents/proc/conditional_update_move]
-/datum/reagent/proc/on_move(mob/M)
- return
+ if(holder)
+ holder.remove_reagent(type, metabolization_rate * affected_mob.metabolism_efficiency * seconds_per_tick)
/// Called after add_reagents creates a new reagent.
/datum/reagent/proc/on_new(data)
@@ -225,14 +216,14 @@ Primarily used in reagents/reaction_agents
/datum/reagent/proc/on_update(atom/A)
return
-/// Called if the reagent has passed the overdose threshold and is set to be triggering overdose effects
-/datum/reagent/proc/overdose_process(mob/living/M, seconds_per_tick, times_fired)
+/// Called if the reagent has passed the overdose threshold and is set to be triggering overdose effects. Returning UPDATE_MOB_HEALTH will cause updatehealth() to be called on the holder mob by /datum/reagents/proc/metabolize.
+/datum/reagent/proc/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
return
-/// Called when an overdose starts
-/datum/reagent/proc/overdose_start(mob/living/M)
- to_chat(M, span_userdanger("You feel like you took too much of [name]!"))
- M.add_mood_event("[type]_overdose", /datum/mood_event/overdose, name)
+/// Called when an overdose starts. Returning UPDATE_MOB_HEALTH will cause updatehealth() to be called on the holder mob by /datum/reagents/proc/metabolize.
+/datum/reagent/proc/overdose_start(mob/living/affected_mob)
+ to_chat(affected_mob, span_userdanger("You feel like you took too much of [name]!"))
+ affected_mob.add_mood_event("[type]_overdose", /datum/mood_event/overdose, name)
return
/**
diff --git a/code/modules/reagents/chemistry/reagents/atmos_gas_reagents.dm b/code/modules/reagents/chemistry/reagents/atmos_gas_reagents.dm
index 817e5ed98bfe55..4550edbdfdf410 100644
--- a/code/modules/reagents/chemistry/reagents/atmos_gas_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/atmos_gas_reagents.dm
@@ -12,8 +12,8 @@
breather.add_movespeed_modifier(/datum/movespeed_modifier/reagent/freon)
/datum/reagent/freon/on_mob_end_metabolize(mob/living/breather)
+ . = ..()
breather.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/freon)
- return ..()
/datum/reagent/halon
name = "Halon"
@@ -30,9 +30,9 @@
ADD_TRAIT(breather, TRAIT_RESISTHEAT, type)
/datum/reagent/halon/on_mob_end_metabolize(mob/living/breather)
+ . = ..()
breather.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/halon)
REMOVE_TRAIT(breather, TRAIT_RESISTHEAT, type)
- return ..()
/datum/reagent/healium
name = "Healium"
@@ -44,16 +44,18 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/healium/on_mob_end_metabolize(mob/living/breather)
+ . = ..()
breather.SetSleeping(1 SECONDS)
- return ..()
/datum/reagent/healium/on_mob_life(mob/living/breather, seconds_per_tick, times_fired)
- breather.SetSleeping(30 SECONDS)
- breather.adjustFireLoss(-2 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- breather.adjustToxLoss(-5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- breather.adjustBruteLoss(-2 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
. = ..()
- return TRUE
+ breather.SetSleeping(30 SECONDS)
+ var/need_mob_update
+ need_mob_update = breather.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += breather.adjustToxLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += breather.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/hypernoblium
name = "Hyper-Noblium"
@@ -65,9 +67,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/hypernoblium/on_mob_life(mob/living/carbon/breather, seconds_per_tick, times_fired)
+ . = ..()
if(isplasmaman(breather))
breather.set_timed_status_effect(10 SECONDS * REM * seconds_per_tick, /datum/status_effect/hypernob_protection)
- ..()
/datum/reagent/nitrium_high_metabolization
name = "Nitrosyl plasmide"
@@ -85,14 +87,16 @@
ADD_TRAIT(breather, TRAIT_SLEEPIMMUNE, type)
/datum/reagent/nitrium_high_metabolization/on_mob_end_metabolize(mob/living/breather)
+ . = ..()
REMOVE_TRAIT(breather, TRAIT_SLEEPIMMUNE, type)
- return ..()
/datum/reagent/nitrium_high_metabolization/on_mob_life(mob/living/carbon/breather, seconds_per_tick, times_fired)
- breather.adjustStaminaLoss(-2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- breather.adjustToxLoss(0.1 * current_cycle * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype) // 1 toxin damage per cycle at cycle 10
. = ..()
- return TRUE
+ var/need_mob_update
+ need_mob_update = breather.adjustStaminaLoss(-2 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update += breather.adjustToxLoss(0.1 * (current_cycle-1) * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype) // 1 toxin damage per cycle at cycle 10
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/nitrium_low_metabolization
name = "Nitrium"
@@ -109,8 +113,8 @@
breather.add_movespeed_modifier(/datum/movespeed_modifier/reagent/nitrium)
/datum/reagent/nitrium_low_metabolization/on_mob_end_metabolize(mob/living/breather)
+ . = ..()
breather.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/nitrium)
- return ..()
/datum/reagent/pluoxium
name = "Pluoxium"
@@ -122,18 +126,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/pluoxium/on_mob_life(mob/living/carbon/breather, seconds_per_tick, times_fired)
+ . = ..()
if(!HAS_TRAIT(breather, TRAIT_KNOCKEDOUT))
- return ..()
+ return
. = ..()
for(var/obj/item/organ/organ_being_healed as anything in breather.organs)
if(!organ_being_healed.damage)
continue
- organ_being_healed.apply_organ_damage(-0.5 * REM * seconds_per_tick, required_organ_flag = ORGAN_ORGANIC)
- . = TRUE
-
- return .
+ if(organ_being_healed.apply_organ_damage(-0.5 * REM * seconds_per_tick, required_organ_flag = ORGAN_ORGANIC))
+ return UPDATE_MOB_HEALTH
/datum/reagent/zauker
name = "Zauker"
@@ -147,9 +150,11 @@
affected_respiration_type = ALL
/datum/reagent/zauker/on_mob_life(mob/living/breather, seconds_per_tick, times_fired)
- breather.adjustBruteLoss(6 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- breather.adjustOxyLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- breather.adjustFireLoss(2 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- breather.adjustToxLoss(2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = breather.adjustBruteLoss(6 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += breather.adjustOxyLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += breather.adjustFireLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += breather.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
diff --git a/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
index 693404431faa10..b944fdbee80ce3 100644
--- a/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
@@ -1,6 +1,5 @@
// Category 2 medicines are medicines that have an ill effect regardless of volume/OD to dissuade doping. Mostly used as emergency chemicals OR to convert damage (and heal a bit in the process). The type is used to prompt borgs that the medicine is harmful.
/datum/reagent/medicine/c2
- harmful = TRUE
metabolization_rate = 0.5 * REAGENTS_METABOLISM
inverse_chem = null //Some of these use inverse chems - we're just defining them all to null here to avoid repetition, eventually this will be moved up to parent
creation_purity = REAGENT_STANDARD_PURITY//All sources by default are 0.75 - reactions are primed to resolve to roughly the same with no intervention for these.
@@ -27,21 +26,23 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/helbital/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- . = TRUE
var/death_is_coming = (affected_mob.getToxLoss() + affected_mob.getOxyLoss() + affected_mob.getFireLoss() + affected_mob.getBruteLoss())*normalise_creation_purity()
var/thou_shall_heal = 0
var/good_kind_of_healing = FALSE
+ var/need_mob_update = FALSE
switch(affected_mob.stat)
if(CONSCIOUS) //bad
thou_shall_heal = death_is_coming/50
- affected_mob.adjustOxyLoss(2 * REM * seconds_per_tick, TRUE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustOxyLoss(2 * REM * seconds_per_tick, TRUE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
if(SOFT_CRIT) //meh convert
thou_shall_heal = round(death_is_coming/47,0.1)
- affected_mob.adjustOxyLoss(1 * REM * seconds_per_tick, TRUE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustOxyLoss(1 * REM * seconds_per_tick, TRUE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
else //no convert
thou_shall_heal = round(death_is_coming/45, 0.1)
good_kind_of_healing = TRUE
- affected_mob.adjustBruteLoss(-thou_shall_heal * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustBruteLoss(-thou_shall_heal * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ . = 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?")
@@ -73,21 +74,18 @@
affected_mob.revive(HEAL_ALL)
holder.del_reagent(type)
return
-
- ..()
- return
+ return ..() || .
/datum/reagent/medicine/c2/helbital/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!helbent)
affected_mob.apply_necropolis_curse(CURSE_WASTING | CURSE_BLINDING)
helbent = TRUE
- ..()
- return TRUE
-/datum/reagent/medicine/c2/helbital/on_mob_delete(mob/living/L)
+/datum/reagent/medicine/c2/helbital/on_mob_delete(mob/living/affected_mob)
+ . = ..()
if(helbent)
- L.remove_status_effect(/datum/status_effect/necropolis_curse)
- ..()
+ affected_mob.remove_status_effect(/datum/status_effect/necropolis_curse)
/datum/reagent/medicine/c2/libital //messes with your liber
name = "Libital"
@@ -99,10 +97,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/libital/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- affected_mob.adjustBruteLoss(-3 * REM * normalise_creation_purity() * seconds_per_tick, required_bodytype = affected_bodytype)
- ..()
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.adjustBruteLoss(-3 * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/probital
name = "Probital"
@@ -116,7 +116,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/probital/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustBruteLoss(-2.25 * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(-2.25 * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
var/ooo_youaregettingsleepy = 3.5
switch(round(affected_mob.getStaminaLoss()))
if(10 to 40)
@@ -125,20 +127,22 @@
ooo_youaregettingsleepy = 2.5
if(61 to 200) //you really can only go to 120
ooo_youaregettingsleepy = 2
- affected_mob.adjustStaminaLoss(ooo_youaregettingsleepy * REM * seconds_per_tick)
- ..()
- . = TRUE
+ need_mob_update += affected_mob.adjustStaminaLoss(ooo_youaregettingsleepy * REM * seconds_per_tick, updating_stamina = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/probital/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustStaminaLoss(3 * REM * seconds_per_tick, updating_stamina = FALSE)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(3 * REM * seconds_per_tick, updating_stamina = FALSE)
if(affected_mob.getStaminaLoss() >= 80)
affected_mob.adjust_drowsiness(2 SECONDS * REM * seconds_per_tick)
if(affected_mob.getStaminaLoss() >= 100)
to_chat(affected_mob,span_warning("You feel more tired than you usually do, perhaps if you rest your eyes for a bit..."))
- affected_mob.adjustStaminaLoss(-100, updating_stamina = TRUE) // Don't add the biotype parameter here as it results in infinite sleep and chat spam.
+ need_mob_update += affected_mob.adjustStaminaLoss(-100, updating_stamina = FALSE) // Don't add the biotype parameter here as it results in infinite sleep and chat spam.
affected_mob.Sleeping(10 SECONDS)
- ..()
- . = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/probital/on_transfer(atom/A, methods=INGEST, trans_volume)
if(!(methods & INGEST) || (!iscarbon(A) && !istype(A, /obj/item/organ/internal/stomach)) )
@@ -162,10 +166,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/lenturi/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustFireLoss(-3 * REM * normalise_creation_purity() * seconds_per_tick, required_bodytype = affected_bodytype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_STOMACH, 0.4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- ..()
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustFireLoss(-3 * REM * normalise_creation_purity() * seconds_per_tick, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_STOMACH, 0.4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/aiuri
name = "Aiuri"
@@ -178,10 +184,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/aiuri/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustFireLoss(-2 * REM * normalise_creation_purity() * seconds_per_tick, required_bodytype = affected_bodytype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_EYES, 0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- ..()
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustFireLoss(-2 * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_EYES, 0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/hercuri
name = "Hercuri"
@@ -194,20 +202,27 @@
inverse_chem = /datum/reagent/inverse/hercuri
inverse_chem_val = 0.3
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ process_flags = REAGENT_ORGANIC | REAGENT_SYNTHETIC // SKYRAT EDIT ADDITION - Lets hercuri process in synths
/datum/reagent/medicine/c2/hercuri/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(affected_mob.getFireLoss() > 50)
- affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick * normalise_creation_purity(), FALSE, required_bodytype = affected_bodytype)
- else
- affected_mob.adjustFireLoss(-1.25 * REM * seconds_per_tick * normalise_creation_purity(), FALSE, required_bodytype = affected_bodytype)
+ . = ..()
+ var/need_mob_update
+ // SKYRAT EDIT CHANGE BEGIN -- Adds check for owner_flags; indented the getFireLoss check and everything under it, so synths can get cooled down
+ var/owner_flags = affected_mob.dna.species.reagent_flags
+ if (owner_flags & PROCESS_ORGANIC)
+ if(affected_mob.getFireLoss() > 50)
+ need_mob_update = affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_bodytype = affected_bodytype)
+ else
+ need_mob_update = affected_mob.adjustFireLoss(-1.25 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_bodytype = affected_bodytype)
+ // SKYRAT EDIT CHANGE END
affected_mob.adjust_bodytemperature(rand(-25,-5) * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 50)
if(ishuman(affected_mob))
var/mob/living/carbon/human/humi = affected_mob
humi.adjust_coretemperature(rand(-25,-5) * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 50)
affected_mob.reagents?.chem_temp += (-10 * REM * seconds_per_tick)
affected_mob.adjust_fire_stacks(-1 * REM * seconds_per_tick)
- ..()
- . = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/hercuri/expose_mob(mob/living/carbon/exposed_mob, methods=VAPOR, reac_volume)
. = ..()
@@ -220,11 +235,11 @@
exposed_mob.extinguish_mob()
/datum/reagent/medicine/c2/hercuri/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-10 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 50) //chilly chilly
if(ishuman(affected_mob))
var/mob/living/carbon/human/humi = affected_mob
humi.adjust_coretemperature(-10 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 50)
- ..()
/******OXY******/
@@ -243,20 +258,22 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/convermol/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
- var/oxycalc = 2.5 * REM * current_cycle
+ . = ..()
+ var/oxycalc = 2.5 * REM * (current_cycle-1)
if(!overdosed)
oxycalc = min(oxycalc, affected_mob.getOxyLoss() + 0.5) //if NOT overdosing, we lower our toxdamage to only the damage we actually healed with a minimum of 0.1*current_cycle. IE if we only heal 10 oxygen damage but we COULD have healed 20, we will only take toxdamage for the 10. We would take the toxdamage for the extra 10 if we were overdosing.
- affected_mob.adjustOxyLoss(-oxycalc * seconds_per_tick * normalise_creation_purity(), FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustToxLoss(oxycalc * seconds_per_tick / CONVERMOL_RATIO, FALSE, required_biotype = affected_biotype)
- if(SPT_PROB(current_cycle / 2, seconds_per_tick) && affected_mob.losebreath)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOxyLoss(-oxycalc * seconds_per_tick * normalise_creation_purity(), FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustToxLoss(oxycalc * seconds_per_tick / CONVERMOL_RATIO, updating_health = FALSE, required_biotype = affected_biotype)
+ if(SPT_PROB((current_cycle-1) / 2, seconds_per_tick) && affected_mob.losebreath)
affected_mob.losebreath--
- ..()
- return TRUE
+ need_mob_update = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/convermol/overdose_process(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
metabolization_rate += 2.5 * REAGENTS_METABOLISM
- ..()
- return TRUE
#undef CONVERMOL_RATIO
@@ -273,20 +290,22 @@
COOLDOWN_DECLARE(drowsycd)
/datum/reagent/medicine/c2/tirimol/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOxyLoss(-3 * REM * seconds_per_tick * normalise_creation_purity(), required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustStaminaLoss(2 * REM * seconds_per_tick, required_biotype = affected_biotype)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOxyLoss(-3 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustStaminaLoss(2 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
if(drowsycd && COOLDOWN_FINISHED(src, drowsycd))
affected_mob.adjust_drowsiness(20 SECONDS)
COOLDOWN_START(src, drowsycd, 45 SECONDS)
else if(!drowsycd)
COOLDOWN_START(src, drowsycd, 15 SECONDS)
- ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
-/datum/reagent/medicine/c2/tirimol/on_mob_end_metabolize(mob/living/L)
- if(current_cycle > 20)
- L.Sleeping(10 SECONDS)
- ..()
+/datum/reagent/medicine/c2/tirimol/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
+ if(current_cycle > 21)
+ affected_mob.Sleeping(10 SECONDS)
/******TOXIN******/
/*Suffix: -iver*/
@@ -307,14 +326,16 @@
rads_heal_threshold = rand(rads_heal_threshold - 50, rads_heal_threshold + 50) // Basically this means 50K and below will always give the radiation heal, and upto 150K could. Calculated once.
/datum/reagent/medicine/c2/seiver/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/chemtemp = min(holder.chem_temp, 1000)
chemtemp = chemtemp ? chemtemp : T0C //why do you have null sweaty
var/healypoints = 0 //5 healypoints = 1 heart damage; 5 rads = 1 tox damage healed for the purpose of healypoints
//you're hot
var/toxcalc = min(round(5 + ((chemtemp-1000)/175), 0.1), 5) * REM * seconds_per_tick * normalise_creation_purity() //max 2.5 tox healing per second
+ var/need_mob_update
if(toxcalc > 0)
- affected_mob.adjustToxLoss(-toxcalc, required_biotype = affected_biotype)
+ need_mob_update = affected_mob.adjustToxLoss(-toxcalc, updating_health = FALSE, required_biotype = affected_biotype)
healypoints += toxcalc
//and you're cold
@@ -323,16 +344,16 @@
radcalc *= normalise_creation_purity()
// extra rad healing if you are SUPER cold
if(chemtemp < rads_heal_threshold*0.1)
- affected_mob.adjustToxLoss(-radcalc * 0.9, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustToxLoss(-radcalc * 0.9, updating_health = FALSE, required_biotype = affected_biotype)
else if(chemtemp < rads_heal_threshold)
- affected_mob.adjustToxLoss(-radcalc * 0.75, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustToxLoss(-radcalc * 0.75, updating_health = FALSE, required_biotype = affected_biotype)
healypoints += (radcalc / 5)
//you're yes and... oh no!
healypoints = round(healypoints, 0.1)
affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, healypoints / 5, required_organ_flag = affected_organ_flags)
- ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/multiver //enhanced with MULTIple medicines
name = "Multiver"
@@ -343,6 +364,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/multiver/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/medibonus = 0 //it will always have itself which makes it REALLY start @ 1
for(var/r in affected_mob.reagents.reagent_list)
var/datum/reagent/the_reagent = r
@@ -350,8 +372,9 @@
medibonus += 1
if(creation_purity >= 1) //Perfectly pure multivers gives a bonus of 2!
medibonus += 1
- affected_mob.adjustToxLoss(-0.5 * min(medibonus, 3 * normalise_creation_purity()) * REM * seconds_per_tick, required_biotype = affected_biotype) //not great at healing but if you have nothing else it will work
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags) //kills at 40u
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(-0.5 * min(medibonus, 3 * normalise_creation_purity()) * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype) //not great at healing but if you have nothing else it will work
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags) //kills at 40u
for(var/r2 in affected_mob.reagents.reagent_list)
var/datum/reagent/the_reagent2 = r2
if(the_reagent2 == src)
@@ -360,8 +383,8 @@
if(medibonus >= 3 && istype(the_reagent2, /datum/reagent/medicine)) //3 unique meds (2+multiver) | (1 + pure multiver) will make it not purge medicines
continue
affected_mob.reagents.remove_reagent(the_reagent2.type, amount2purge * REM * seconds_per_tick)
- ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
// Antitoxin binds plants pretty well. So the tox goes significantly down
/datum/reagent/medicine/c2/multiver/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
@@ -385,7 +408,7 @@
return
var/mob/living/carbon/C = A
if(trans_volume >= 0.6) //prevents cheesing with ultralow doses.
- C.adjustToxLoss((-1.5 * min(2, trans_volume) * REM) * normalise_creation_purity(), FALSE, required_biotype = affected_biotype) //This is to promote iv pole use for that chemotherapy feel.
+ C.adjustToxLoss((-1.5 * min(2, trans_volume) * REM) * normalise_creation_purity(), required_biotype = affected_biotype) //This is to promote iv pole use for that chemotherapy feel.
var/obj/item/organ/internal/liver/L = C.organs_slot[ORGAN_SLOT_LIVER]
if(!L || L.organ_flags & ORGAN_FAILING)
return
@@ -395,22 +418,24 @@
..()
/datum/reagent/medicine/c2/syriniver/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.8 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.8 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
for(var/datum/reagent/R in affected_mob.reagents.reagent_list)
if(issyrinormusc(R))
continue
affected_mob.reagents.remove_reagent(R.type, 0.4 * REM * seconds_per_tick)
- ..()
- . = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/syriniver/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 1.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 1.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
affected_mob.adjust_disgust(3 * REM * seconds_per_tick)
affected_mob.reagents.add_reagent(/datum/reagent/medicine/c2/musiver, 0.225 * REM * seconds_per_tick)
- ..()
- . = TRUE
/datum/reagent/medicine/c2/musiver //MUScles
name = "Musiver"
@@ -424,30 +449,32 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/musiver/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.1 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick * normalise_creation_purity(), FALSE, required_biotype = affected_biotype)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.1 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_biotype = affected_biotype)
for(var/datum/reagent/R in affected_mob.reagents.reagent_list)
if(issyrinormusc(R))
continue
affected_mob.reagents.remove_reagent(R.type, 0.2 * REM * seconds_per_tick)
- ..()
- . = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/musiver/overdose_start(mob/living/carbon/affected_mob)
+ . = ..()
trauma = new()
affected_mob.gain_trauma(trauma, TRAUMA_RESILIENCE_ABSOLUTE)
- ..()
-/datum/reagent/medicine/c2/musiver/on_mob_delete(mob/living/carbon/affected_mob)
+/datum/reagent/medicine/c2/musiver/on_mob_delete(mob/living/affected_mob)
+ . = ..()
if(trauma)
QDEL_NULL(trauma)
- return ..()
/datum/reagent/medicine/c2/musiver/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 1.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 1.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
affected_mob.adjust_disgust(3 * REM * seconds_per_tick)
- ..()
- . = TRUE
#undef issyrinormusc
/******COMBOS******/
@@ -469,14 +496,19 @@
show_message = 0
if(!(methods & (PATCH|TOUCH|VAPOR)))
return
- var/harmies = min(carbies.getBruteLoss(), carbies.adjustBruteLoss(-1.25 * reac_volume, required_bodytype = affected_bodytype)*-1)
- var/burnies = min(carbies.getFireLoss(), carbies.adjustFireLoss(-1.25 * reac_volume, required_bodytype = affected_bodytype)*-1)
+ 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)
for(var/i in carbies.all_wounds)
var/datum/wound/iter_wound = i
iter_wound.on_synthflesh(reac_volume)
- carbies.adjustToxLoss((harmies+burnies)*(0.5 + (0.25*(1-creation_purity))), required_biotype = affected_biotype) //0.5 - 0.75
+ 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
+
+ if(need_mob_update)
+ carbies.updatehealth()
if(show_message)
to_chat(carbies, span_danger("You feel your burns and bruises healing! It stings like hell!"))
+
carbies.add_mood_event("painful_medicine", /datum/mood_event/painful_medicine)
if(HAS_TRAIT_FROM(exposed_mob, TRAIT_HUSK, BURN) && carbies.getFireLoss() < UNHUSK_DAMAGE_THRESHOLD && (carbies.reagents.get_reagent_amount(/datum/reagent/medicine/c2/synthflesh) + reac_volume >= SYNTHFLESH_UNHUSK_AMOUNT))
carbies.cure_husk(BURN)
@@ -526,45 +558,51 @@
user.throw_alert("penthrite", /atom/movable/screen/alert/penthrite)
user.add_traits(subject_traits, type)
-/datum/reagent/medicine/c2/penthrite/on_mob_life(mob/living/carbon/human/H, seconds_per_tick, times_fired)
- H.adjustStaminaLoss(-25 * REM) //SKYRAT EDIT ADDITION - COMBAT - makes your heart beat faster, fills you with energy. For miners
- H.adjustOrganLoss(ORGAN_SLOT_STOMACH, 0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- if(H.health <= HEALTH_THRESHOLD_CRIT && H.health > (H.crit_threshold + HEALTH_THRESHOLD_FULLCRIT * (2 * normalise_creation_purity()))) //we cannot save someone below our lowered crit threshold.
+/datum/reagent/medicine/c2/penthrite/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(-12.5 * REM * seconds_per_tick, updating_stamina = FALSE) //SKYRAT EDIT ADDITION - COMBAT - makes your heart beat faster, fills you with energy. For miners
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_STOMACH, 0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(affected_mob.health <= HEALTH_THRESHOLD_CRIT && affected_mob.health > (affected_mob.crit_threshold + HEALTH_THRESHOLD_FULLCRIT * (2 * normalise_creation_purity()))) //we cannot save someone below our lowered crit threshold.
- H.adjustToxLoss(-2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- H.adjustBruteLoss(-2 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- H.adjustFireLoss(-2 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- H.adjustOxyLoss(-6 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-6 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- H.losebreath = 0
+ affected_mob.losebreath = 0
- H.adjustOrganLoss(ORGAN_SLOT_HEART, max(volume/10, 1) * REM * seconds_per_tick, required_organ_flag = affected_organ_flags) // your heart is barely keeping up!
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, max(volume/10, 1) * REM * seconds_per_tick, required_organ_flag = affected_organ_flags) // your heart is barely keeping up!
- H.set_jitter_if_lower(rand(0 SECONDS, 4 SECONDS) * REM * seconds_per_tick)
- H.set_dizzy_if_lower(rand(0 SECONDS, 4 SECONDS) * REM * seconds_per_tick)
+ affected_mob.set_jitter_if_lower(rand(0 SECONDS, 4 SECONDS) * REM * seconds_per_tick)
+ affected_mob.set_dizzy_if_lower(rand(0 SECONDS, 4 SECONDS) * REM * seconds_per_tick)
if(SPT_PROB(18, seconds_per_tick))
- to_chat(H,span_danger("Your body is trying to give up, but your heart is still beating!"))
+ to_chat(affected_mob,span_danger("Your body is trying to give up, but your heart is still beating!"))
- if(H.health <= (H.crit_threshold + HEALTH_THRESHOLD_FULLCRIT*(2*normalise_creation_purity()))) //certain death below this threshold
- REMOVE_TRAIT(H, TRAIT_STABLEHEART, type) //we have to remove the stable heart trait before we give them a heart attack
- to_chat(H,span_danger("You feel something rupturing inside your chest!"))
- H.emote("scream")
- H.set_heartattack(TRUE)
+ if(affected_mob.health <= (affected_mob.crit_threshold + HEALTH_THRESHOLD_FULLCRIT*(2*normalise_creation_purity()))) //certain death below this threshold
+ REMOVE_TRAIT(affected_mob, TRAIT_STABLEHEART, type) //we have to remove the stable heart trait before we give them a heart attack
+ to_chat(affected_mob,span_danger("You feel something rupturing inside your chest!"))
+ affected_mob.emote("scream")
+ affected_mob.set_heartattack(TRUE)
volume = 0
- . = ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
-/datum/reagent/medicine/c2/penthrite/on_mob_end_metabolize(mob/living/user)
- user.clear_alert("penthrite")
- user.remove_traits(subject_traits, type)
+/datum/reagent/medicine/c2/penthrite/on_mob_end_metabolize(mob/living/affected_mob)
. = ..()
+ affected_mob.clear_alert("penthrite")
+ affected_mob.remove_traits(subject_traits, type)
-/datum/reagent/medicine/c2/penthrite/overdose_process(mob/living/carbon/human/H, seconds_per_tick, times_fired)
- REMOVE_TRAIT(H, TRAIT_STABLEHEART, type)
- H.adjustStaminaLoss(10 * REM * seconds_per_tick, required_biotype = affected_biotype)
- H.adjustOrganLoss(ORGAN_SLOT_HEART, 10 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- H.set_heartattack(TRUE)
+/datum/reagent/medicine/c2/penthrite/overdose_process(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ REMOVE_TRAIT(affected_mob, TRAIT_STABLEHEART, type)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(10 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, 10 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.set_heartattack(TRUE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/******NICHE******/
diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
index b81e1600bbfc47..07335fe728feb9 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
@@ -51,9 +51,10 @@
return ..()
/datum/reagent/consumable/ethanol/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.get_drunk_amount() < volume * boozepwr * ALCOHOL_THRESHOLD_MODIFIER || boozepwr < 0)
var/booze_power = boozepwr
- if(HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE)) //we're an accomplished drinker
+ if(HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE)) // we're an accomplished drinker
booze_power *= 0.7
if(HAS_TRAIT(drinker, TRAIT_LIGHT_DRINKER))
booze_power *= 2
@@ -77,8 +78,8 @@
var/obj/item/organ/internal/liver/liver = drinker.get_organ_slot(ORGAN_SLOT_LIVER)
var/heavy_drinker_multiplier = (HAS_TRAIT(drinker, TRAIT_HEAVY_DRINKER) ? 0.5 : 1)
if (istype(liver))
- liver.apply_organ_damage(((max(sqrt(volume) * (boozepwr ** ALCOHOL_EXPONENT) * liver.alcohol_tolerance * heavy_drinker_multiplier * seconds_per_tick, 0))/300)) // SKYRAT EDIT CHANGE - Alcohol Tolerance - Original: (((max(sqrt(volume) * (boozepwr ** ALCOHOL_EXPONENT) * liver.alcohol_tolerance * heavy_drinker_multiplier * seconds_per_tick, 0))/150))
- return ..()
+ if(liver.apply_organ_damage(((max(sqrt(volume) * (boozepwr ** ALCOHOL_EXPONENT) * liver.alcohol_tolerance * heavy_drinker_multiplier * seconds_per_tick, 0))/300))) // SKYRAT EDIT CHANGE - Alcohol Tolerance - Original: if((((max(sqrt(volume) * (boozepwr ** ALCOHOL_EXPONENT) * liver.alcohol_tolerance * heavy_drinker_multiplier * seconds_per_tick, 0))/150)))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/expose_obj(obj/exposed_obj, reac_volume)
if(istype(exposed_obj, /obj/item/paper))
@@ -151,9 +152,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/beer/green/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.color != color)
drinker.add_atom_colour(color, TEMPORARY_COLOUR_PRIORITY)
- return ..()
/datum/reagent/consumable/ethanol/beer/green/on_mob_end_metabolize(mob/living/drinker)
drinker.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, color)
@@ -167,13 +168,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/kahlua/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_dizzy_if_lower(10 SECONDS * REM * seconds_per_tick)
drinker.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
drinker.AdjustSleeping(-40 * REM * seconds_per_tick)
if(!HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE))
drinker.set_jitter_if_lower(10 SECONDS)
- ..()
- . = TRUE
/datum/reagent/consumable/ethanol/whiskey
name = "Whiskey"
@@ -200,9 +200,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/whiskey/candycorn/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(5, seconds_per_tick))
drinker.adjust_hallucinations(4 SECONDS * REM * seconds_per_tick)
- ..()
/datum/reagent/consumable/ethanol/thirteenloko
name = "Thirteen Loko"
@@ -216,20 +216,21 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/thirteenloko/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_drowsiness(-14 SECONDS * REM * seconds_per_tick)
drinker.AdjustSleeping(-40 * REM * seconds_per_tick)
drinker.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, drinker.get_body_temp_normal())
if(!HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE))
drinker.set_jitter_if_lower(10 SECONDS)
- ..()
- return TRUE
/datum/reagent/consumable/ethanol/thirteenloko/overdose_start(mob/living/drinker)
+ . = ..()
to_chat(drinker, span_userdanger("Your entire body violently jitters as you start to feel queasy. You really shouldn't have drank all of that [name]!"))
drinker.set_jitter_if_lower(40 SECONDS)
drinker.Stun(1.5 SECONDS)
/datum/reagent/consumable/ethanol/thirteenloko/overdose_process(mob/living/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(3.5, seconds_per_tick) && iscarbon(drinker))
var/obj/item/held_item = drinker.get_active_held_item()
if(held_item)
@@ -248,15 +249,18 @@
eyes.forceMove(get_turf(drinker))
to_chat(drinker, span_userdanger("You double over in pain as you feel your eyeballs liquify in your head!"))
drinker.emote("scream")
- drinker.adjustBruteLoss(15, required_bodytype = affected_bodytype)
+ if(drinker.adjustBruteLoss(15 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype))
+ . = UPDATE_MOB_HEALTH
else
to_chat(drinker, span_userdanger("You scream in terror as you go blind!"))
- eyes.apply_organ_damage(eyes.maxHealth)
+ if(eyes.apply_organ_damage(eyes.maxHealth))
+ . = UPDATE_MOB_HEALTH
drinker.emote("scream")
if(SPT_PROB(1.5, seconds_per_tick) && iscarbon(drinker))
drinker.visible_message(span_danger("[drinker] starts having a seizure!"), span_userdanger("You have a seizure!"))
- drinker.Unconscious(10 SECONDS)
+ if(drinker.Unconscious(10 SECONDS))
+ . = UPDATE_MOB_HEALTH
drinker.set_jitter_if_lower(700 SECONDS)
if(SPT_PROB(0.5, seconds_per_tick) && iscarbon(drinker))
@@ -285,10 +289,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/bilk/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.getBruteLoss() && SPT_PROB(5, seconds_per_tick))
- drinker.heal_bodypart_damage(brute = 1)
- . = TRUE
- return ..() || .
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/threemileisland
name = "Three Mile Island Iced Tea"
@@ -301,8 +305,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/threemileisland/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_drugginess(100 SECONDS * REM * seconds_per_tick)
- return ..()
/datum/reagent/consumable/ethanol/gin
name = "Gin"
@@ -415,9 +419,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/absinthe/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(5, seconds_per_tick) && !HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE))
- drinker.adjust_hallucinations(8 SECONDS)
- ..()
+ drinker.adjust_hallucinations(8 SECONDS * REM * seconds_per_tick)
/datum/reagent/consumable/ethanol/hooch
name = "Hooch"
@@ -513,13 +517,15 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/cuba_libre/on_mob_life(mob/living/carbon/cubano, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
if(cubano.mind && cubano.mind.has_antag_datum(/datum/antagonist/rev)) //Cuba Libre, the traditional drink of revolutions! Heals revolutionaries.
- cubano.adjustBruteLoss(-1 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- cubano.adjustFireLoss(-1 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- cubano.adjustToxLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- cubano.adjustOxyLoss(-5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- . = TRUE
- return ..() || .
+ need_mob_update = cubano.adjustBruteLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += cubano.adjustFireLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += cubano.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += cubano.adjustOxyLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/whiskey_cola
name = "Whiskey Cola"
@@ -602,17 +608,17 @@
))
/datum/reagent/consumable/ethanol/screwdrivercocktail/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(HAS_TRAIT(liver, TRAIT_ENGINEER_METABOLISM))
ADD_TRAIT(drinker, TRAIT_HALT_RADIATION_EFFECTS, "[type]")
if (HAS_TRAIT(drinker, TRAIT_IRRADIATED))
- drinker.adjustToxLoss(-2 * REM * seconds_per_tick, required_biotype = affected_biotype)
-
- return ..()
+ if(drinker.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/screwdrivercocktail/on_mob_end_metabolize(mob/living/drinker)
+ . = ..()
REMOVE_TRAIT(drinker, TRAIT_HALT_RADIATION_EFFECTS, "[type]")
- return ..()
/datum/reagent/consumable/ethanol/booger
name = "Booger"
@@ -648,6 +654,7 @@
var/tough_text
/datum/reagent/consumable/ethanol/brave_bull/on_mob_metabolize(mob/living/drinker)
+ . = ..()
tough_text = pick("brawny", "tenacious", "tough", "hardy", "sturdy") //Tuff stuff
to_chat(drinker, span_notice("You feel [tough_text]!"))
drinker.maxHealth += 10 //Brave Bull makes you sturdier, and thus capable of withstanding a tiny bit more punishment.
@@ -655,6 +662,7 @@
ADD_TRAIT(drinker, TRAIT_FEARLESS, type)
/datum/reagent/consumable/ethanol/brave_bull/on_mob_end_metabolize(mob/living/drinker)
+ . = ..()
to_chat(drinker, span_notice("You no longer feel [tough_text]."))
drinker.maxHealth -= 10
drinker.health = min(drinker.health - 10, drinker.maxHealth) //This can indeed crit you if you're alive solely based on alchol ingestion
@@ -672,6 +680,7 @@
var/obj/effect/light_holder
/datum/reagent/consumable/ethanol/tequila_sunrise/on_mob_metabolize(mob/living/drinker)
+ . = ..()
to_chat(drinker, span_notice("You feel gentle warmth spread through your body!"))
light_holder = new(drinker)
light_holder.set_light(3, 0.7, "#FFCC00") //Tequila Sunrise makes you radiate dim light, like a sunrise!
@@ -684,6 +693,7 @@
return ..()
/datum/reagent/consumable/ethanol/tequila_sunrise/on_mob_end_metabolize(mob/living/drinker)
+ . = ..()
to_chat(drinker, span_notice("The warmth in your body fades."))
QDEL_NULL(light_holder)
@@ -697,8 +707,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/toxins_special/on_mob_life(mob/living/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_bodytemperature(15 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, drinker.get_body_temp_normal() + 20) //310.15 is the normal bodytemp.
- return ..()
/datum/reagent/consumable/ethanol/beepsky_smash
name = "Beepsky Smash"
@@ -714,6 +724,7 @@
var/datum/brain_trauma/special/beepsky/beepsky_hallucination
/datum/reagent/consumable/ethanol/beepsky_smash/on_mob_metabolize(mob/living/carbon/drinker)
+ . = ..()
if(HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE))
metabolization_rate = 0.8
// if you don't have a liver, or your liver isn't an officer's liver
@@ -721,28 +732,27 @@
if(!liver || !HAS_TRAIT(liver, TRAIT_LAW_ENFORCEMENT_METABOLISM))
beepsky_hallucination = new()
drinker.gain_trauma(beepsky_hallucination, TRAUMA_RESILIENCE_ABSOLUTE)
- ..()
/datum/reagent/consumable/ethanol/beepsky_smash/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_jitter_if_lower(4 SECONDS)
var/obj/item/organ/internal/liver/liver = drinker.get_organ_slot(ORGAN_SLOT_LIVER)
// if you have a liver and that liver is an officer's liver
if(liver && HAS_TRAIT(liver, TRAIT_LAW_ENFORCEMENT_METABOLISM))
- . = TRUE
- drinker.adjustStaminaLoss(-10 * REM * seconds_per_tick, required_biotype = affected_biotype)
+ if(drinker.adjustStaminaLoss(-10 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(10, seconds_per_tick))
drinker.cause_hallucination(get_random_valid_hallucination_subtype(/datum/hallucination/nearby_fake_item), name)
if(SPT_PROB(5, seconds_per_tick))
drinker.cause_hallucination(/datum/hallucination/stray_bullet, name)
- ..()
-
/datum/reagent/consumable/ethanol/beepsky_smash/on_mob_end_metabolize(mob/living/carbon/drinker)
+ . = ..()
if(beepsky_hallucination)
QDEL_NULL(beepsky_hallucination)
- return ..()
/datum/reagent/consumable/ethanol/beepsky_smash/overdose_start(mob/living/carbon/drinker)
+ . = ..()
var/obj/item/organ/internal/liver/liver = drinker.get_organ_slot(ORGAN_SLOT_LIVER)
// if you don't have a liver, or your liver isn't an officer's liver
if(!liver || !HAS_TRAIT(liver, TRAIT_LAW_ENFORCEMENT_METABOLISM))
@@ -768,6 +778,7 @@
var/dorf_mode = FALSE
/datum/reagent/consumable/ethanol/manly_dorf/on_mob_metabolize(mob/living/drinker)
+ . = ..()
if(ishuman(drinker))
var/mob/living/carbon/human/potential_dwarf = drinker
if(HAS_TRAIT(potential_dwarf, TRAIT_DWARF))
@@ -776,10 +787,13 @@
dorf_mode = TRUE
/datum/reagent/consumable/ethanol/manly_dorf/on_mob_life(mob/living/carbon/dwarf, seconds_per_tick, times_fired)
+ . = ..()
if(dorf_mode)
- dwarf.adjustBruteLoss(-2 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
- dwarf.adjustFireLoss(-2 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
- return ..()
+ var/need_mob_update
+ need_mob_update = dwarf.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += dwarf.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/longislandicedtea
name = "Long Island Iced Tea"
@@ -809,6 +823,7 @@
glass_price = DRINK_PRICE_EASY
/datum/reagent/consumable/ethanol/b52/on_mob_metabolize(mob/living/drinker)
+ . = ..()
playsound(drinker, 'sound/effects/explosion_distant.ogg', 100, FALSE)
/datum/reagent/consumable/ethanol/irishcoffee
@@ -859,8 +874,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/manhattan_proj/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_drugginess(1 MINUTES * REM * seconds_per_tick)
- return ..()
/datum/reagent/consumable/ethanol/whiskeysoda
name = "Whiskey Soda"
@@ -881,8 +896,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/antifreeze/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_bodytemperature(20 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, drinker.get_body_temp_normal() + 20) //310.15 is the normal bodytemp.
- return ..()
/datum/reagent/consumable/ethanol/barefoot
name = "Barefoot"
@@ -894,12 +909,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/barefoot/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(ishuman(drinker)) //Barefoot causes the imbiber to quickly regenerate brute trauma if they're not wearing shoes.
var/mob/living/carbon/human/unshoed = drinker
if(!unshoed.shoes)
- unshoed.adjustBruteLoss(-3 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
- return ..() || .
+ if(unshoed.adjustBruteLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/snowwhite
name = "Snow White"
@@ -1028,13 +1043,16 @@
var/static/list/ray_filter = list(type = "rays", size = 40, density = 15, color = SUPERMATTER_SINGULARITY_RAYS_COLOUR, factor = 15)
/datum/reagent/consumable/ethanol/singulo/on_mob_metabolize(mob/living/drinker)
+ . = ..()
ADD_TRAIT(drinker, TRAIT_MADNESS_IMMUNE, type)
/datum/reagent/consumable/ethanol/singulo/on_mob_end_metabolize(mob/living/drinker)
+ . = ..()
REMOVE_TRAIT(drinker, TRAIT_MADNESS_IMMUNE, type)
drinker.remove_filter("singulo_rays")
/datum/reagent/consumable/ethanol/singulo/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(2.5, seconds_per_tick))
// 20u = 1x1, 45u = 2x2, 80u = 3x3
var/volume_to_radius = FLOOR(sqrt(volume/5), 1) - 1
@@ -1050,7 +1068,6 @@
animate(drinker.get_filter("singulo_rays"), offset = 10, time = 1.5 SECONDS, loop = -1)
addtimer(CALLBACK(drinker, TYPE_PROC_REF(/datum, remove_filter), "singulo_rays"), 1.5 SECONDS)
drinker.emote("burp")
- return ..()
/datum/reagent/consumable/ethanol/sbiten
name = "Sbiten"
@@ -1062,8 +1079,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/sbiten/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_bodytemperature(50 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, BODYTEMP_HEAT_DAMAGE_LIMIT) //310.15 is the normal bodytemp.
- return ..()
/datum/reagent/consumable/ethanol/red_mead
name = "Red Mead"
@@ -1093,8 +1110,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/iced_beer/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_bodytemperature(-20 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, T0C) //310.15 is the normal bodytemp.
- return ..()
/datum/reagent/consumable/ethanol/grog
name = "Grog"
@@ -1162,9 +1179,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/changelingsting/on_mob_life(mob/living/carbon/target, seconds_per_tick, times_fired)
+ . = ..()
var/datum/antagonist/changeling/changeling = target.mind?.has_antag_datum(/datum/antagonist/changeling)
changeling?.adjust_chemicals(metabolization_rate * REM * seconds_per_tick)
- return ..()
/datum/reagent/consumable/ethanol/irishcarbomb
name = "Irish Car Bomb"
@@ -1185,9 +1202,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/syndicatebomb/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(2.5, seconds_per_tick))
playsound(get_turf(drinker), 'sound/effects/explosionfar.ogg', 100, TRUE)
- return ..()
/datum/reagent/consumable/ethanol/hiveminderaser
name = "Hivemind Eraser"
@@ -1228,11 +1245,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/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))
- drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- . = TRUE
- return ..() || .
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/silencer
name = "Silencer"
@@ -1245,11 +1262,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/silencer/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(ishuman(drinker) && HAS_MIND_TRAIT(drinker, TRAIT_MIMING))
drinker.set_silence_if_lower(MIMEDRINK_SILENCE_DURATION)
- drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- . = TRUE
- return ..() || .
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/drunkenblumpkin
name = "Drunken Blumpkin"
@@ -1289,9 +1306,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/fetching_fizz/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
for(var/obj/item/stack/ore/O in orange(3, drinker))
step_towards(O, get_turf(drinker))
- return ..()
//Another reference. Heals those in critical condition extremely quickly.
/datum/reagent/consumable/ethanol/hearty_punch
@@ -1305,14 +1322,16 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/hearty_punch/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.health <= 0)
- drinker.adjustBruteLoss(-3 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- drinker.adjustFireLoss(-3 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- drinker.adjustCloneLoss(-5 * REM * seconds_per_tick, 0)
- drinker.adjustOxyLoss(-4 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- drinker.adjustToxLoss(-3 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- return ..() || .
+ var/need_mob_update
+ need_mob_update = drinker.adjustBruteLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustFireLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustCloneLoss(-5 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustOxyLoss(-4 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += drinker.adjustToxLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/bacchus_blessing //An EXTREMELY powerful drink. Smashed in seconds, dead in minutes.
name = "Bacchus' Blessing"
@@ -1333,20 +1352,19 @@
glass_price = DRINK_PRICE_HIGH
/datum/reagent/consumable/ethanol/atomicbomb/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_drugginess(100 SECONDS * REM * seconds_per_tick)
if(!HAS_TRAIT(drinker, TRAIT_ALCOHOL_TOLERANCE))
drinker.adjust_confusion(2 SECONDS * REM * seconds_per_tick)
drinker.set_dizzy_if_lower(20 SECONDS * REM * seconds_per_tick)
drinker.adjust_slurring(6 SECONDS * REM * seconds_per_tick)
switch(current_cycle)
- if(51 to 200)
+ if(52 to 201)
drinker.Sleeping(100 * REM * seconds_per_tick)
- . = TRUE
- if(201 to INFINITY)
+ if(202 to INFINITY)
drinker.AdjustSleeping(40 * REM * seconds_per_tick)
- drinker.adjustToxLoss(2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(drinker.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/gargle_blaster
name = "Pan-Galactic Gargle Blaster"
@@ -1358,20 +1376,19 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/gargle_blaster/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_dizzy(3 SECONDS * REM * seconds_per_tick)
switch(current_cycle)
- if(15 to 45)
+ if(16 to 46)
drinker.adjust_slurring(3 SECONDS * REM * seconds_per_tick)
-
- if(45 to 55)
+ if(46 to 56)
if(SPT_PROB(30, seconds_per_tick))
drinker.adjust_confusion(3 SECONDS * REM * seconds_per_tick)
- if(55 to 200)
+ if(56 to 201)
drinker.set_drugginess(110 SECONDS * REM * seconds_per_tick)
- if(200 to INFINITY)
- drinker.adjustToxLoss(2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(201 to INFINITY)
+ if(drinker.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/neurotoxin
name = "Neurotoxin"
@@ -1387,35 +1404,37 @@
return (pick(TRAIT_PARALYSIS_L_ARM,TRAIT_PARALYSIS_R_ARM,TRAIT_PARALYSIS_R_LEG,TRAIT_PARALYSIS_L_LEG))
/datum/reagent/consumable/ethanol/neurotoxin/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_drugginess(100 SECONDS * REM * seconds_per_tick)
drinker.adjust_dizzy(4 SECONDS * REM * seconds_per_tick)
- drinker.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1 * REM * seconds_per_tick, 150, required_organ_flag = affected_organ_flags)
+ var/need_mob_update
+ need_mob_update = drinker.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1 * REM * seconds_per_tick, 150, required_organ_flag = affected_organ_flags)
if(SPT_PROB(10, seconds_per_tick))
- drinker.adjustStaminaLoss(10, required_biotype = affected_biotype)
+ need_mob_update += drinker.adjustStaminaLoss(10 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
drinker.drop_all_held_items()
to_chat(drinker, span_notice("You cant feel your hands!"))
- if(current_cycle > 5)
+ if(current_cycle > 6)
if(SPT_PROB(10, seconds_per_tick))
var/paralyzed_limb = pick_paralyzed_limb()
ADD_TRAIT(drinker, paralyzed_limb, type)
- drinker.adjustStaminaLoss(10, required_biotype = affected_biotype)
- if(current_cycle > 30)
- drinker.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- if(current_cycle > 50 && SPT_PROB(7.5, seconds_per_tick))
+ need_mob_update += drinker.adjustStaminaLoss(10 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(current_cycle > 31)
+ need_mob_update += drinker.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(current_cycle > 51 && SPT_PROB(7.5, seconds_per_tick))
if(!drinker.undergoing_cardiac_arrest() && drinker.can_heartattack())
drinker.set_heartattack(TRUE)
if(drinker.stat == CONSCIOUS)
drinker.visible_message(span_userdanger("[drinker] clutches at [drinker.p_their()] chest as if [drinker.p_their()] heart stopped!"))
- . = TRUE
- ..()
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/neurotoxin/on_mob_end_metabolize(mob/living/carbon/drinker)
+ . = ..()
REMOVE_TRAIT(drinker, TRAIT_PARALYSIS_L_ARM, type)
REMOVE_TRAIT(drinker, TRAIT_PARALYSIS_R_ARM, type)
REMOVE_TRAIT(drinker, TRAIT_PARALYSIS_R_LEG, type)
REMOVE_TRAIT(drinker, TRAIT_PARALYSIS_L_LEG, type)
drinker.adjustStaminaLoss(10, required_biotype = affected_biotype)
- ..()
/datum/reagent/consumable/ethanol/hippies_delight
name = "Hippie's Delight"
@@ -1429,36 +1448,36 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/hippies_delight/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.set_slurring_if_lower(1 SECONDS * REM * seconds_per_tick)
switch(current_cycle)
- if(1 to 5)
+ if(2 to 6)
drinker.set_dizzy_if_lower(20 SECONDS * REM * seconds_per_tick)
drinker.set_drugginess(1 MINUTES * REM * seconds_per_tick)
if(SPT_PROB(5, seconds_per_tick))
drinker.emote(pick("twitch","giggle"))
- if(5 to 10)
+ if(6 to 11)
drinker.set_jitter_if_lower(40 SECONDS * REM * seconds_per_tick)
drinker.set_dizzy_if_lower(40 SECONDS * REM * seconds_per_tick)
drinker.set_drugginess(1.5 MINUTES * REM * seconds_per_tick)
if(SPT_PROB(10, seconds_per_tick))
drinker.emote(pick("twitch","giggle"))
- if (10 to 200)
+ if (11 to 201)
drinker.set_jitter_if_lower(80 SECONDS * REM * seconds_per_tick)
drinker.set_dizzy_if_lower(80 SECONDS * REM * seconds_per_tick)
drinker.set_drugginess(2 MINUTES * REM * seconds_per_tick)
if(SPT_PROB(16, seconds_per_tick))
drinker.emote(pick("twitch","giggle"))
- if(200 to INFINITY)
+ if(201 to INFINITY)
drinker.set_jitter_if_lower(120 SECONDS * REM * seconds_per_tick)
drinker.set_dizzy_if_lower(120 SECONDS * REM * seconds_per_tick)
drinker.set_drugginess(2.5 MINUTES * REM * seconds_per_tick)
if(SPT_PROB(23, seconds_per_tick))
drinker.emote(pick("twitch","giggle"))
if(SPT_PROB(16, seconds_per_tick))
- drinker.adjustToxLoss(2, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(drinker.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/eggnog
name = "Eggnog"
@@ -1490,9 +1509,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/narsour/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_timed_status_effect(6 SECONDS * REM * seconds_per_tick, /datum/status_effect/speech/slurring/cult, max_duration = 6 SECONDS)
drinker.adjust_stutter_up_to(6 SECONDS * REM * seconds_per_tick, 6 SECONDS)
- return ..()
/datum/reagent/consumable/ethanol/triple_sec
name = "Triple Sec"
@@ -1536,12 +1555,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/quadruple_sec/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
//Securidrink in line with the Screwdriver for engineers or Nothing for mimes
var/obj/item/organ/internal/liver/liver = drinker.get_organ_slot(ORGAN_SLOT_LIVER)
if(liver && HAS_TRAIT(liver, TRAIT_LAW_ENFORCEMENT_METABOLISM))
- drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- . = TRUE
- return ..()
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/quintuple_sec
name = "Quintuple Sec"
@@ -1553,13 +1572,15 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/quintuple_sec/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
//Securidrink in line with the Screwdriver for engineers or Nothing for mimes but STRONG..
var/obj/item/organ/internal/liver/liver = drinker.get_organ_slot(ORGAN_SLOT_LIVER)
if(liver && HAS_TRAIT(liver, TRAIT_LAW_ENFORCEMENT_METABOLISM))
- drinker.heal_bodypart_damage(2 * REM * seconds_per_tick, 2 * REM * seconds_per_tick)
- drinker.adjustStaminaLoss(-2 * REM * seconds_per_tick, required_biotype = affected_biotype)
- . = TRUE
- return ..()
+ var/need_mob_update
+ need_mob_update = drinker.heal_bodypart_damage(2 * REM * seconds_per_tick, 2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustStaminaLoss(-2 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/grasshopper
name = "Grasshopper"
@@ -1592,28 +1613,34 @@
glass_price = DRINK_PRICE_HIGH
/datum/reagent/consumable/ethanol/bastion_bourbon/on_mob_metabolize(mob/living/drinker)
+ . = ..()
var/heal_points = 10
if(drinker.health <= 0)
heal_points = 20 //heal more if we're in softcrit
- for(var/counter in 1 to min(volume, heal_points)) //only heals 1 point of damage per unit on add, for balance reasons
- drinker.adjustBruteLoss(-1, required_bodytype = affected_bodytype)
- drinker.adjustFireLoss(-1, required_bodytype = affected_bodytype)
- drinker.adjustToxLoss(-1, required_biotype = affected_biotype)
- drinker.adjustOxyLoss(-1, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- drinker.adjustStaminaLoss(-1, required_biotype = affected_biotype)
+ var/need_mob_update
+ var/heal_amt = min(volume, heal_points) //only heals 1 point of damage per unit on add, for balance reasons
+ need_mob_update = drinker.adjustBruteLoss(-heal_amt, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustFireLoss(-heal_amt, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustToxLoss(-heal_amt, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += drinker.adjustOxyLoss(-heal_amt, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += drinker.adjustStaminaLoss(-heal_amt, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ drinker.updatehealth()
drinker.visible_message(span_warning("[drinker] shivers with renewed vigor!"), span_notice("One taste of [lowertext(name)] fills you with energy!"))
if(!drinker.stat && heal_points == 20) //brought us out of softcrit
drinker.visible_message(span_danger("[drinker] lurches to [drinker.p_their()] feet!"), span_boldnotice("Up and at 'em, kid."))
/datum/reagent/consumable/ethanol/bastion_bourbon/on_mob_life(mob/living/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.health > 0)
- drinker.adjustBruteLoss(-1 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
- drinker.adjustFireLoss(-1 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
- drinker.adjustToxLoss(-0.5 * REM * seconds_per_tick, required_biotype = affected_biotype)
- drinker.adjustOxyLoss(-3 * REM * seconds_per_tick, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- drinker.adjustStaminaLoss(-5 * REM * seconds_per_tick, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ var/need_mob_update
+ need_mob_update = drinker.adjustBruteLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustFireLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += drinker.adjustToxLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += drinker.adjustOxyLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += drinker.adjustStaminaLoss(-5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/squirt_cider
name = "Squirt Cider"
@@ -1625,9 +1652,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/squirt_cider/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.satiety += 5 * REM * seconds_per_tick //for context, vitamins give 15 satiety per second
- ..()
- . = TRUE
/datum/reagent/consumable/ethanol/fringe_weaver
name = "Fringe Weaver"
@@ -1649,9 +1675,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/sugar_rush/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.satiety -= 10 * REM * seconds_per_tick //junky as hell! a whole glass will keep you from being able to eat junk food
- ..()
- . = TRUE
/datum/reagent/consumable/ethanol/crevice_spike
name = "Crevice Spike"
@@ -1663,6 +1688,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/crevice_spike/on_mob_metabolize(mob/living/drinker) //damage only applies when drink first enters system and won't again until drink metabolizes out
+ . = ..()
drinker.adjustBruteLoss(3 * min(5,volume), required_bodytype = affected_bodytype) //minimum 3 brute damage on ingestion to limit non-drink means of injury - a full 5 unit gulp of the drink trucks you for the full 15
/datum/reagent/consumable/ethanol/sake
@@ -1684,9 +1710,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/peppermint_patty/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.apply_status_effect(/datum/status_effect/throat_soothed)
drinker.adjust_bodytemperature(5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, drinker.get_body_temp_normal())
- ..()
/datum/reagent/consumable/ethanol/alexander
name = "Alexander"
@@ -1699,24 +1725,24 @@
var/obj/item/shield/mighty_shield
/datum/reagent/consumable/ethanol/alexander/on_mob_metabolize(mob/living/drinker)
+ . = ..()
if(ishuman(drinker))
var/mob/living/carbon/human/the_human = drinker
for(var/obj/item/shield/the_shield in the_human.contents)
mighty_shield = the_shield
mighty_shield.block_chance += 10
to_chat(the_human, span_notice("[the_shield] appears polished, although you don't recall polishing it."))
- return TRUE
/datum/reagent/consumable/ethanol/alexander/on_mob_life(mob/living/drinker, seconds_per_tick, times_fired)
- ..()
if(mighty_shield && !(mighty_shield in drinker.contents)) //If you had a shield and lose it, you lose the reagent as well. Otherwise this is just a normal drink.
holder.remove_reagent(type, volume)
+ return ..()
/datum/reagent/consumable/ethanol/alexander/on_mob_end_metabolize(mob/living/drinker)
+ . = ..()
if(mighty_shield)
mighty_shield.block_chance -= 10
to_chat(drinker,span_notice("You notice [mighty_shield] looks worn again. Weird."))
- ..()
/datum/reagent/consumable/ethanol/amaretto_alexander
name = "Amaretto Alexander"
@@ -1748,7 +1774,7 @@
glass_price = DRINK_PRICE_MEDIUM
/datum/reagent/consumable/ethanol/between_the_sheets/on_mob_life(mob/living/drinker, seconds_per_tick, times_fired)
- ..()
+ . = ..()
var/is_between_the_sheets = FALSE
for(var/obj/item/bedsheet/bedsheet in range(drinker.loc, 0))
if(bedsheet.loc != drinker.loc) // bedsheets in your backpack/neck don't count
@@ -1759,15 +1785,18 @@
if(!drinker.IsSleeping() || !is_between_the_sheets)
return
+ var/need_mob_update
if(drinker.getBruteLoss() && drinker.getFireLoss()) //If you are damaged by both types, slightly increased healing but it only heals one. The more the merrier wink wink.
if(prob(50))
- drinker.adjustBruteLoss(-0.25 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
+ need_mob_update = drinker.adjustBruteLoss(-0.25 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
else
- drinker.adjustFireLoss(-0.25 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
+ need_mob_update = drinker.adjustFireLoss(-0.25 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
else if(drinker.getBruteLoss()) //If you have only one, it still heals but not as well.
- drinker.adjustBruteLoss(-0.2 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
+ need_mob_update = drinker.adjustBruteLoss(-0.2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
else if(drinker.getFireLoss())
- drinker.adjustFireLoss(-0.2 * REM * seconds_per_tick, required_bodytype = affected_bodytype)
+ need_mob_update = drinker.adjustFireLoss(-0.2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/kamikaze
name = "Kamikaze"
@@ -1806,11 +1835,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/fernet/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.nutrition <= NUTRITION_LEVEL_STARVING)
- drinker.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ if(drinker.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
drinker.adjust_nutrition(-5 * REM * seconds_per_tick)
drinker.overeatduration = 0
- return ..()
/datum/reagent/consumable/ethanol/fernet_cola
name = "Fernet Cola"
@@ -1822,11 +1852,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/fernet_cola/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.nutrition <= NUTRITION_LEVEL_STARVING)
- drinker.adjustToxLoss(0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ if(drinker.adjustToxLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
drinker.adjust_nutrition(-3 * REM * seconds_per_tick)
drinker.overeatduration = 0
- return ..()
/datum/reagent/consumable/ethanol/fanciulli
name = "Fanciulli"
@@ -1839,15 +1870,14 @@
glass_price = DRINK_PRICE_HIGH
/datum/reagent/consumable/ethanol/fanciulli/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_nutrition(-5 * REM * seconds_per_tick)
drinker.overeatduration = 0
- return ..()
/datum/reagent/consumable/ethanol/fanciulli/on_mob_metabolize(mob/living/drinker)
+ . = ..()
if(drinker.health > 0)
drinker.adjustStaminaLoss(20, required_biotype = affected_biotype)
- . = TRUE
- ..()
/datum/reagent/consumable/ethanol/branca_menta
name = "Branca Menta"
@@ -1860,14 +1890,13 @@
glass_price = DRINK_PRICE_MEDIUM
/datum/reagent/consumable/ethanol/branca_menta/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_bodytemperature(-20 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, T0C)
- return ..()
/datum/reagent/consumable/ethanol/branca_menta/on_mob_metabolize(mob/living/drinker)
+ . = ..()
if(drinker.health > 0)
drinker.adjustStaminaLoss(35, required_biotype = affected_biotype)
- . = TRUE
- ..()
/datum/reagent/consumable/ethanol/blank_paper
name = "Blank Paper"
@@ -1880,11 +1909,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/blank_paper/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(ishuman(drinker) && HAS_MIND_TRAIT(drinker, TRAIT_MIMING))
drinker.set_silence_if_lower(MIMEDRINK_SILENCE_DURATION)
- drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- . = TRUE
- return ..()
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/fruit_wine
name = "Fruit Wine"
@@ -2011,13 +2040,16 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/wizz_fizz/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
//A healing drink similar to Quadruple Sec, Ling Stings, and Screwdrivers for the Wizznerds; the check is consistent with the changeling sting
if(drinker?.mind?.has_antag_datum(/datum/antagonist/wizard))
- drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- drinker.adjustOxyLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- drinker.adjustToxLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- drinker.adjustStaminaLoss(-1 * REM * seconds_per_tick, required_biotype = affected_biotype)
- return ..()
+ var/need_mob_update
+ need_mob_update = drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustOxyLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += drinker.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += drinker.adjustStaminaLoss(-1 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/bug_spray
name = "Bug Spray"
@@ -2027,19 +2059,20 @@
quality = DRINK_GOOD
taste_description = "the pain of ten thousand slain mosquitos"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ affected_biotype = MOB_BUG
/datum/reagent/consumable/ethanol/bug_spray/on_new(data)
. = ..()
AddElement(/datum/element/bugkiller_reagent)
/datum/reagent/consumable/ethanol/bug_spray/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
// Does some damage to bug biotypes
- var/did_damage = drinker.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = MOB_BUG)
- // Random chance of causing a screm if we did some damage
- if(did_damage && SPT_PROB(2, seconds_per_tick))
- drinker.emote("scream")
-
- return ..() || did_damage
+ if(drinker.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
+ // Random chance of causing a screm if we did some damage
+ if(SPT_PROB(2, seconds_per_tick))
+ drinker.emote("scream")
/datum/reagent/consumable/ethanol/applejack
name = "Applejack"
@@ -2068,10 +2101,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/turbo/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(2, seconds_per_tick))
to_chat(drinker, span_notice("[pick("You feel disregard for the rule of law.", "You feel pumped!", "Your head is pounding.", "Your thoughts are racing..")]"))
- drinker.adjustStaminaLoss(-0.25 * drinker.get_drunk_amount() * REM * seconds_per_tick, required_biotype = affected_biotype)
- return ..()
+ if(drinker.adjustStaminaLoss(-0.25 * drinker.get_drunk_amount() * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/old_timer
name = "Old Timer"
@@ -2083,6 +2117,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/old_timer/on_mob_life(mob/living/carbon/human/metabolizer, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(10, seconds_per_tick) && istype(metabolizer))
metabolizer.age += 1
if(metabolizer.age > 70)
@@ -2097,8 +2132,6 @@
metabolizer.visible_message(span_notice("[metabolizer] becomes older than any man should be.. and crumbles into dust!"))
metabolizer.dust(just_ash = FALSE, drop_items = TRUE, force = FALSE)
- return ..()
-
/datum/reagent/consumable/ethanol/rubberneck
name = "Rubberneck"
description = "A quality rubberneck should not contain any gross natural ingredients."
@@ -2135,11 +2168,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/trappist/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.mind?.holy_role)
- drinker.adjustFireLoss(-2.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ if(drinker.adjustFireLoss(-2.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype))
+ . = UPDATE_MOB_HEALTH
drinker.adjust_jitter(-2 SECONDS * REM * seconds_per_tick)
drinker.adjust_stutter(-2 SECONDS * REM * seconds_per_tick)
- return ..()
/datum/reagent/consumable/ethanol/blazaam
name = "Blazaam"
@@ -2150,6 +2184,7 @@
var/stored_teleports = 0
/datum/reagent/consumable/ethanol/blazaam/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.get_drunk_amount() > 40)
if(stored_teleports)
do_teleport(drinker, get_turf(drinker), rand(1,3), channel = TELEPORT_CHANNEL_WORMHOLE)
@@ -2159,7 +2194,6 @@
stored_teleports += rand(2, 6)
if(prob(70))
drinker.vomit(vomit_flags = VOMIT_CATEGORY_DEFAULT, vomit_type = /obj/effect/decal/cleanable/vomit/purple)
- return ..()
/datum/reagent/consumable/ethanol/planet_cracker
name = "Planet Cracker"
@@ -2181,9 +2215,8 @@
// Heats the user up while the reagent is in the body. Occasionally makes you burst into flames.
drinker.adjust_bodytemperature(25 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick)
if (SPT_PROB(2.5, seconds_per_tick))
- drinker.adjust_fire_stacks(1)
+ drinker.adjust_fire_stacks(1 * REM * seconds_per_tick)
drinker.ignite_mob()
- ..()
/datum/reagent/consumable/ethanol/painkiller
name = "Painkiller"
@@ -2211,6 +2244,7 @@
taste_description = "a horrible emulsion of pineapple and olive oil"
/datum/reagent/consumable/ethanol/pina_olivada/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(8, seconds_per_tick))
drinker.manual_emote(pick("coughs up some oil", "swallows the lump in [drinker.p_their()] throat", "gags", "chokes up a bit"))
if(SPT_PROB(3, seconds_per_tick))
@@ -2222,7 +2256,6 @@
"Your throat feels horrible.",
)
to_chat(drinker, span_notice(pick(messages)))
- return ..()
/datum/reagent/consumable/ethanol/pruno // pruno mix is in drink_reagents
name = "Pruno"
@@ -2233,8 +2266,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/pruno/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_disgust(5 * REM * seconds_per_tick)
- ..()
/datum/reagent/consumable/ethanol/ginger_amaretto
name = "Ginger Amaretto"
@@ -2274,9 +2307,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/kortara/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(drinker.getBruteLoss() && SPT_PROB(10, seconds_per_tick))
- drinker.heal_bodypart_damage(1,0)
- . = TRUE
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 0, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/ethanol/sea_breeze
name = "Sea Breeze"
@@ -2288,8 +2322,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/sea_breeze/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.apply_status_effect(/datum/status_effect/throat_soothed)
- ..()
/datum/reagent/consumable/ethanol/white_tiziran
name = "White Tiziran"
@@ -2310,8 +2344,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/drunken_espatier/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.add_mood_event("numb", /datum/mood_event/narcotic_medium, name) //comfortably numb
- ..()
/datum/reagent/consumable/ethanol/drunken_espatier/on_mob_metabolize(mob/living/drinker)
. = ..()
@@ -2332,12 +2366,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/protein_blend/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
drinker.adjust_nutrition(2 * REM * seconds_per_tick)
if(!islizard(drinker))
drinker.adjust_disgust(5 * REM * seconds_per_tick)
else
drinker.adjust_disgust(2 * REM * seconds_per_tick)
- ..()
/datum/reagent/consumable/ethanol/mushi_kombucha
name = "Mushi Kombucha"
@@ -2358,9 +2392,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/triumphal_arch/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(islizard(drinker))
drinker.add_mood_event("triumph", /datum/mood_event/memories_of_home, name)
- ..()
/datum/reagent/consumable/ethanol/the_juice
name = "The Juice"
@@ -2378,9 +2412,9 @@
drinker.gain_trauma(prophet_trauma, TRAUMA_RESILIENCE_ABSOLUTE)
/datum/reagent/consumable/ethanol/the_juice/on_mob_end_metabolize(mob/living/carbon/drinker)
+ . = ..()
if(prophet_trauma)
QDEL_NULL(prophet_trauma)
- return ..()
//a jacked up absinthe that causes hallucinations to the game master controller basically, used in smuggling objectives
/datum/reagent/consumable/ethanol/ritual_wine
@@ -2517,11 +2551,10 @@
var/hal_cap = 24
/datum/reagent/consumable/ethanol/helianthus/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(5, seconds_per_tick))
drinker.adjust_hallucinations_up_to(4 SECONDS * REM * seconds_per_tick, 48 SECONDS)
- ..()
-
/datum/reagent/consumable/ethanol/plumwine
name = "Plum wine"
description = "Plums turned into wine."
@@ -2552,8 +2585,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/gin_garden/on_mob_life(mob/living/carbon/doll, seconds_per_tick, times_fired)
+ . = ..()
doll.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, doll.get_body_temp_normal())
- ..()
/datum/reagent/consumable/ethanol/wine_voltaic
name = "Voltaic Yellow Wine"
@@ -2587,8 +2620,8 @@
ADD_TRAIT(affected_mob, TRAIT_SHOCKIMMUNE, type)
/datum/reagent/consumable/ethanol/telepole/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_SHOCKIMMUNE, type)
- return ..()
/datum/reagent/consumable/ethanol/telepole/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume) //can't be on life because of the way blood works.
. = ..()
@@ -2610,11 +2643,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/pod_tesla/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_traits(list(TRAIT_SHOCKIMMUNE,TRAIT_TESLA_SHOCKIMMUNE,TRAIT_FEARLESS), type)
-
/datum/reagent/consumable/ethanol/pod_tesla/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.remove_traits(list(TRAIT_SHOCKIMMUNE,TRAIT_TESLA_SHOCKIMMUNE,TRAIT_FEARLESS), type)
/datum/reagent/consumable/ethanol/pod_tesla/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume) //can't be on life because of the way blood works.
diff --git a/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm
index 36444d6229b812..8270b42f502d08 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm
@@ -8,10 +8,10 @@
default_container = /obj/item/reagent_containers/cup/glass/bottle/juice/orangejuice
/datum/reagent/consumable/orangejuice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getOxyLoss() && SPT_PROB(16, seconds_per_tick))
- affected_mob.adjustOxyLoss(-1, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- . = TRUE
- ..()
+ if(affected_mob.adjustOxyLoss(-1 * REM * seconds_per_tick, FALSE, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/tomatojuice
name = "Tomato Juice"
@@ -22,10 +22,10 @@
default_container = /obj/item/reagent_containers/cup/glass/bottle/juice/tomatojuice
/datum/reagent/consumable/tomatojuice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getFireLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.heal_bodypart_damage(0, 1)
- . = TRUE
- ..()
+ if(affected_mob.heal_bodypart_damage(brute = 0, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/limejuice
name = "Lime Juice"
@@ -37,10 +37,10 @@
default_container = /obj/item/reagent_containers/cup/glass/bottle/juice/limejuice
/datum/reagent/consumable/limejuice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getToxLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/carrotjuice
name = "Carrot Juice"
@@ -50,17 +50,20 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/carrotjuice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_eye_blur(-2 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_temp_blindness(-2 SECONDS * REM * seconds_per_tick)
+ var/need_mob_update
switch(current_cycle)
if(1 to 20)
//nothing
if(21 to 110)
if(SPT_PROB(100 * (1 - (sqrt(110 - current_cycle) / 10)), seconds_per_tick))
- affected_mob.adjustOrganLoss(ORGAN_SLOT_EYES, -2)
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_EYES, -2 * REM * seconds_per_tick)
if(110 to INFINITY)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_EYES, -2)
- return ..()
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_EYES, -2 * REM * seconds_per_tick)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/berryjuice
name = "Berry Juice"
@@ -84,9 +87,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/poisonberryjuice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ . = ..()
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/watermelonjuice
name = "Watermelon Juice"
@@ -111,11 +114,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/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))
- affected_mob.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- . = TRUE
- ..()
+ 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
/datum/reagent/consumable/nothing
name = "Nothing"
@@ -128,11 +131,11 @@
icon_state = "shotglass"
/datum/reagent/consumable/nothing/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
if(ishuman(drinker) && HAS_MIND_TRAIT(drinker, TRAIT_MIMING))
drinker.set_silence_if_lower(MIMEDRINK_SILENCE_DURATION)
- drinker.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
- . = TRUE
- ..()
+ if(drinker.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/laughter
name = "Laughter"
@@ -143,9 +146,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/laughter/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.emote("laugh")
affected_mob.add_mood_event("chemical_laughter", /datum/mood_event/chemical_laughter)
- ..()
/datum/reagent/consumable/superlaughter
name = "Super Laughter"
@@ -156,11 +159,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/superlaughter/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(16, seconds_per_tick))
affected_mob.visible_message(span_danger("[affected_mob] bursts out into a fit of uncontrollable laughter!"), span_userdanger("You burst out in a fit of uncontrollable laughter!"))
affected_mob.Stun(5)
affected_mob.add_mood_event("chemical_laughter", /datum/mood_event/chemical_superlaughter)
- ..()
/datum/reagent/consumable/potato_juice
name = "Potato Juice"
@@ -179,11 +182,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/pickle/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_CORONER_METABOLISM)))
- affected_mob.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/grapejuice
name = "Grape Juice"
@@ -219,11 +222,11 @@
/datum/reagent/consumable/milk/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
if(affected_mob.getBruteLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.heal_bodypart_damage(1,0)
- . = TRUE
+ if(affected_mob.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 0, updating_health = FALSE))
+ . = UPDATE_MOB_HEALTH
if(holder.has_reagent(/datum/reagent/consumable/capsaicin))
holder.remove_reagent(/datum/reagent/consumable/capsaicin, 1 * seconds_per_tick)
- ..()
+ return ..() || .
/datum/reagent/consumable/soymilk
name = "Soy Milk"
@@ -234,10 +237,10 @@
default_container = /obj/item/reagent_containers/condiment/soymilk
/datum/reagent/consumable/soymilk/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getBruteLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.heal_bodypart_damage(1, 0)
- . = TRUE
- ..()
+ if(affected_mob.heal_bodypart_damage(1, 0))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/cream
name = "Cream"
@@ -265,8 +268,8 @@
/datum/reagent/consumable/coffee/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
- ..()
/datum/reagent/consumable/coffee/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
@@ -276,8 +279,7 @@
affected_mob.adjust_bodytemperature(25 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
if(holder.has_reagent(/datum/reagent/consumable/frostoil))
holder.remove_reagent(/datum/reagent/consumable/frostoil, 5 * REM * seconds_per_tick)
- ..()
- . = TRUE
+ return ..() || .
/datum/reagent/consumable/tea
name = "Tea"
@@ -290,12 +292,16 @@
default_container = /obj/item/reagent_containers/cup/glass/mug/tea
/datum/reagent/consumable/tea/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-4 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-2 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_jitter(-6 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-20 * REM * seconds_per_tick)
if(affected_mob.getToxLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype)
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
+ affected_mob.adjust_bodytemperature(20 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
+
var/to_chatted = FALSE
for(var/datum/wound/iter_wound as anything in affected_mob.all_wounds)
if(SPT_PROB(10, seconds_per_tick))
@@ -303,9 +309,6 @@
if(!to_chatted && helped)
to_chat(affected_mob, span_notice("A calm, relaxed feeling suffuses you. Your wounds feel a little healthier."))
to_chatted = TRUE
- affected_mob.adjust_bodytemperature(20 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
- ..()
- . = TRUE
// Different handling, different name.
// Returns FALSE by default so broken bones and 'loss' wounds don't give a false message
@@ -348,10 +351,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/tea/arnold_palmer/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_notice("[pick("You remember to square your shoulders.","You remember to keep your head down.","You can't decide between squaring your shoulders and keeping your head down.","You remember to relax.","You think about how someday you'll get two strokes off your golf game.")]"))
- ..()
- . = TRUE
/datum/reagent/consumable/icecoffee
name = "Iced Coffee"
@@ -362,13 +364,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/icecoffee/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
- ..()
- . = TRUE
/datum/reagent/consumable/hot_ice_coffee
name = "Hot Ice Coffee"
@@ -379,14 +380,14 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/hot_ice_coffee/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-60 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-7 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- . = TRUE
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/icetea
name = "Iced Tea"
@@ -397,14 +398,14 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/icetea/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-4 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-2 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
if(affected_mob.getToxLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype)
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
- . = TRUE
/datum/reagent/consumable/space_cola
name = "Cola"
@@ -414,9 +415,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/space_cola/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_drowsiness(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/roy_rogers
name = "Roy Rogers"
@@ -441,22 +442,21 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/nuka_cola/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/nuka_cola)
/datum/reagent/consumable/nuka_cola/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/nuka_cola)
- ..()
/datum/reagent/consumable/nuka_cola/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_jitter_if_lower(40 SECONDS * REM * seconds_per_tick)
affected_mob.set_drugginess(1 MINUTES * REM * seconds_per_tick)
affected_mob.adjust_dizzy(3 SECONDS * REM * seconds_per_tick)
affected_mob.remove_status_effect(/datum/status_effect/drowsiness)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
- . = TRUE
/datum/reagent/consumable/rootbeer
name = "root beer"
@@ -471,15 +471,16 @@
var/effect_enabled = FALSE
/datum/reagent/consumable/rootbeer/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_DOUBLE_TAP, type)
if(current_cycle > 10)
to_chat(affected_mob, span_warning("You feel kinda tired as your sugar rush wears off..."))
affected_mob.adjustStaminaLoss(min(80, current_cycle * 3), required_biotype = affected_biotype)
- affected_mob.adjust_drowsiness(current_cycle * 2 SECONDS)
- ..()
+ affected_mob.adjust_drowsiness((current_cycle-1) * 2 SECONDS)
/datum/reagent/consumable/rootbeer/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle >= 3 && !effect_enabled) // takes a few seconds for the bonus to kick in to prevent microdosing
+ . = ..()
+ if(current_cycle > 3 && !effect_enabled) // takes a few seconds for the bonus to kick in to prevent microdosing
to_chat(affected_mob, span_notice("You feel your trigger finger getting itchy..."))
ADD_TRAIT(affected_mob, TRAIT_DOUBLE_TAP, type)
effect_enabled = TRUE
@@ -490,9 +491,6 @@
if(current_cycle > 10)
affected_mob.adjust_dizzy(3 SECONDS * REM * seconds_per_tick)
- ..()
- . = TRUE
-
/datum/reagent/consumable/grey_bull
name = "Grey Bull"
description = "Grey Bull, it gives you gloves!"
@@ -502,7 +500,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/grey_bull/on_mob_metabolize(mob/living/carbon/affected_atom)
- ..()
+ . = ..()
ADD_TRAIT(affected_atom, TRAIT_SHOCKIMMUNE, type)
var/obj/item/organ/internal/liver/liver = affected_atom.get_organ_slot(ORGAN_SLOT_LIVER)
if(HAS_TRAIT(liver, TRAIT_MAINTENANCE_METABOLISM))
@@ -510,16 +508,16 @@
metabolization_rate *= 0.8
/datum/reagent/consumable/grey_bull/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_SHOCKIMMUNE, type)
- ..()
/datum/reagent/consumable/grey_bull/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_jitter_if_lower(40 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_dizzy(2 SECONDS * REM * seconds_per_tick)
affected_mob.remove_status_effect(/datum/status_effect/drowsiness)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/spacemountainwind
name = "SM Wind"
@@ -529,12 +527,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/spacemountainwind/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_drowsiness(-14 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-20 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
- ..()
- . = TRUE
/datum/reagent/consumable/dr_gibb
name = "Dr. Gibb"
@@ -544,9 +541,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/dr_gibb/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_drowsiness(-12 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/space_up
name = "Space-Up"
@@ -556,8 +553,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/space_up/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-8 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/lemon_lime
name = "Lemon Lime"
@@ -567,8 +564,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/lemon_lime/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-8 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/pwr_game
name = "Pwr Game"
@@ -585,10 +582,10 @@
You feel as though a great secret of the universe has been made known to you...")
/datum/reagent/consumable/pwr_game/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-8 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
if(SPT_PROB(5, seconds_per_tick))
affected_mob.mind?.adjust_experience(/datum/skill/gaming, 5)
- ..()
/datum/reagent/consumable/shamblers
name = "Shambler's Juice"
@@ -598,8 +595,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/shamblers/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-8 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/sodawater
name = "Soda Water"
@@ -615,10 +612,10 @@
mytray.adjust_plant_health(round(volume * 0.1))
/datum/reagent/consumable/sodawater/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/tonic
name = "Tonic Water"
@@ -628,12 +625,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/tonic/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
- . = TRUE
/datum/reagent/consumable/wellcheers
name = "Wellcheers"
@@ -643,15 +639,18 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/wellcheers/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_drowsiness(3 SECONDS * REM * seconds_per_tick)
+ var/need_mob_update
switch(affected_mob.mob_mood.sanity_level)
if (SANITY_INSANE to SANITY_CRAZY)
- affected_mob.adjustStaminaLoss(3 * REM * seconds_per_tick, 0)
+ need_mob_update = affected_mob.adjustStaminaLoss(3 * REM * seconds_per_tick, updating_stamina = FALSE)
if (SANITY_UNSTABLE to SANITY_DISTURBED)
affected_mob.add_mood_event("wellcheers", /datum/mood_event/wellcheers)
if (SANITY_NEUTRAL to SANITY_GREAT)
- affected_mob.adjustBruteLoss(-1.5 * REM * seconds_per_tick, 0)
- return ..()
+ need_mob_update = affected_mob.adjustBruteLoss(-1.5 * REM * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/monkey_energy
name = "Monkey Energy"
@@ -662,26 +661,26 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/monkey_energy/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_jitter_if_lower(80 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_dizzy(2 SECONDS * REM * seconds_per_tick)
affected_mob.remove_status_effect(/datum/status_effect/drowsiness)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/monkey_energy/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
if(ismonkey(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)
+ . = ..()
affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/monkey_energy)
- ..()
/datum/reagent/consumable/monkey_energy/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(7.5, seconds_per_tick))
affected_mob.say(pick_list_replacements(BOOMER_FILE, "boomer"), forced = /datum/reagent/consumable/monkey_energy)
- ..()
/datum/reagent/consumable/ice
name = "Ice"
@@ -693,8 +692,9 @@
default_container = /obj/item/reagent_containers/cup/glass/ice
/datum/reagent/consumable/ice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
+ . = ..()
+ if(affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, FALSE, affected_mob.get_body_temp_normal()))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/soy_latte
name = "Soy Latte"
@@ -706,15 +706,17 @@
glass_price = DRINK_PRICE_EASY
/datum/reagent/consumable/soy_latte/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
- affected_mob.SetSleeping(0)
+ var/need_mob_update
+ need_mob_update = affected_mob.SetSleeping(0)
affected_mob.adjust_bodytemperature(5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
if(affected_mob.getBruteLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.heal_bodypart_damage(1,0)
- ..()
- . = TRUE
+ need_mob_update += affected_mob.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 0, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/cafe_latte
name = "Cafe Latte"
@@ -726,15 +728,17 @@
glass_price = DRINK_PRICE_EASY
/datum/reagent/consumable/cafe_latte/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_dizzy(-10 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(-12 SECONDS * REM * seconds_per_tick)
- affected_mob.SetSleeping(0)
+ var/need_mob_update
+ need_mob_update = affected_mob.SetSleeping(0)
affected_mob.adjust_bodytemperature(5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
if(affected_mob.getBruteLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.heal_bodypart_damage(1, 0)
- ..()
- . = TRUE
+ need_mob_update += affected_mob.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 0, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/doctor_delight
name = "The Doctor's Delight"
@@ -745,17 +749,19 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/doctor_delight/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustToxLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustToxLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
if(affected_mob.nutrition && (affected_mob.nutrition - 2 > 0))
var/obj/item/organ/internal/liver/liver = affected_mob.get_organ_slot(ORGAN_SLOT_LIVER)
if(!(HAS_TRAIT(liver, TRAIT_MEDICAL_METABOLISM)))
// Drains the nutrition of the holder. Not medical doctors though, since it's the Doctor's Delight!
affected_mob.adjust_nutrition(-2 * REM * seconds_per_tick)
- ..()
- . = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/cinderella
name = "Cinderella"
@@ -766,8 +772,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/cinderella/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_disgust(-5 * REM * seconds_per_tick)
- return ..()
/datum/reagent/consumable/cherryshake
name = "Cherry Shake"
@@ -886,8 +892,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/grape_soda/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/milk/chocolate_milk
name = "Chocolate Milk"
@@ -908,11 +914,11 @@
/datum/reagent/consumable/hot_coco/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
affected_mob.adjust_bodytemperature(5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
if(affected_mob.getBruteLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.heal_bodypart_damage(1, 0)
- . = TRUE
+ if(affected_mob.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 0, updating_health = FALSE))
+ . = UPDATE_MOB_HEALTH
if(holder.has_reagent(/datum/reagent/consumable/capsaicin))
holder.remove_reagent(/datum/reagent/consumable/capsaicin, 2 * REM * seconds_per_tick)
- ..()
+ return ..() || .
/datum/reagent/consumable/italian_coco
name = "Italian Hot Chocolate"
@@ -924,8 +930,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/italian_coco/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
- return ..()
/datum/reagent/consumable/menthol
name = "Menthol"
@@ -936,8 +942,8 @@
default_container = /obj/item/reagent_containers/cup/glass/bottle/juice/menthol
/datum/reagent/consumable/menthol/on_mob_life(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.apply_status_effect(/datum/status_effect/throat_soothed)
- ..()
/datum/reagent/consumable/grenadine
name = "Grenadine"
@@ -977,8 +983,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/cream_soda/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- ..()
/datum/reagent/consumable/sol_dry
name = "Sol Dry"
@@ -989,8 +995,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/sol_dry/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_disgust(-5 * REM * seconds_per_tick)
- ..()
/datum/reagent/consumable/shirley_temple
name = "Shirley Temple"
@@ -1014,8 +1020,9 @@
var/current_size = RESIZE_DEFAULT_SIZE
/datum/reagent/consumable/red_queen/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(50, seconds_per_tick))
- return ..()
+ return
var/newsize = pick(0.5, 0.75, 1, 1.50, 2)
newsize *= RESIZE_DEFAULT_SIZE
@@ -1023,12 +1030,11 @@
current_size = newsize
if(SPT_PROB(23, seconds_per_tick))
affected_mob.emote("sneeze")
- ..()
/datum/reagent/consumable/red_queen/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.update_transform(RESIZE_DEFAULT_SIZE/current_size)
current_size = RESIZE_DEFAULT_SIZE
- ..()
/datum/reagent/consumable/bungojuice
name = "Bungo Juice"
@@ -1052,10 +1058,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/aloejuice/on_mob_life(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getToxLoss() && SPT_PROB(16, seconds_per_tick))
- affected_mob.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype)
- ..()
- . = TRUE
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/agua_fresca
name = "Agua Fresca"
@@ -1066,10 +1072,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/agua_fresca/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_bodytemperature(-8 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
if(affected_mob.getToxLoss() && SPT_PROB(10, seconds_per_tick))
- affected_mob.adjustToxLoss(-0.5, FALSE, required_biotype = affected_biotype)
- return ..()
+ if(affected_mob.adjustToxLoss(-0.5, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/mushroom_tea
name = "Mushroom Tea"
@@ -1080,10 +1087,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/mushroom_tea/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(islizard(affected_mob))
- affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- ..()
- . = TRUE
+ if(affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type))
+ return UPDATE_MOB_HEALTH
//Moth Stuff
/datum/reagent/consumable/toechtauese_juice
@@ -1166,10 +1173,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/cucumberlemonade/on_mob_life(mob/living/carbon/doll, seconds_per_tick, times_fired)
+ . = ..()
doll.adjust_bodytemperature(-8 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, doll.get_body_temp_normal())
if(doll.getToxLoss() && SPT_PROB(10, seconds_per_tick))
- doll.adjustToxLoss(-0.5, FALSE, required_biotype = affected_biotype)
- return ..()
+ if(doll.adjustToxLoss(-0.5, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/mississippi_queen
name = "Mississippi Queen"
@@ -1179,17 +1187,16 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/mississippi_queen/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
switch(current_cycle)
- if(10 to 20)
+ if(11 to 21)
drinker.adjust_dizzy(4 SECONDS * REM * seconds_per_tick)
- if(20 to 30)
+ if(21 to 31)
if(SPT_PROB(15, seconds_per_tick))
drinker.adjust_confusion(4 SECONDS * REM * seconds_per_tick)
- if(30 to 200)
+ if(31 to 201)
drinker.adjust_hallucinations(60 SECONDS * REM * seconds_per_tick)
- return ..()
-
/datum/reagent/consumable/t_letter
name = "T"
description = "You expected to find this in a soup, but this is fine too."
@@ -1198,14 +1205,15 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/t_letter/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!HAS_MIND_TRAIT(affected_mob, TRAIT_MIMING))
- return ..()
+ return
affected_mob.set_silence_if_lower(MIMEDRINK_SILENCE_DURATION)
affected_mob.adjust_drowsiness(-6 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustSleeping(-40 * REM * seconds_per_tick)
if(affected_mob.getToxLoss() && SPT_PROB(25, seconds_per_tick))
- affected_mob.adjustToxLoss(-2, FALSE, required_biotype = affected_biotype)
- return ..()
+ if(affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/hakka_mate
name = "Hakka-Mate"
diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
index 23d6010f1a94bb..95dcf4995287c5 100644
--- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
@@ -18,22 +18,23 @@
addiction_types = list(/datum/addiction/hallucinogens = 10) //4 per 2 seconds
/datum/reagent/drug/space_drugs/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_drugginess(30 SECONDS * REM * seconds_per_tick)
if(isturf(affected_mob.loc) && !isspaceturf(affected_mob.loc) && !HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && SPT_PROB(5, seconds_per_tick))
step(affected_mob, pick(GLOB.cardinals))
if(SPT_PROB(3.5, seconds_per_tick))
affected_mob.emote(pick("twitch","drool","moan","giggle"))
- ..()
/datum/reagent/drug/space_drugs/overdose_start(mob/living/affected_mob)
+ . = ..()
to_chat(affected_mob, span_userdanger("You start tripping hard!"))
affected_mob.add_mood_event("[type]_overdose", /datum/mood_event/overdose, name)
/datum/reagent/drug/space_drugs/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/hallucination_duration_in_seconds = (affected_mob.get_timed_status_effect_duration(/datum/status_effect/hallucination) / 10)
if(hallucination_duration_in_seconds < volume && SPT_PROB(10, seconds_per_tick))
affected_mob.adjust_hallucinations(10 SECONDS)
- ..()
/datum/reagent/drug/cannabis
name = "Cannabis"
@@ -45,6 +46,7 @@
metabolization_rate = 0.125 * REAGENTS_METABOLISM
/datum/reagent/drug/cannabis/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.apply_status_effect(/datum/status_effect/stoned)
if(SPT_PROB(1, seconds_per_tick))
var/smoke_message = pick("You feel relaxed.","You feel calmed.","Your mouth feels dry.","You could use some water.","Your heart beats quickly.","You feel clumsy.","You crave junk food.","You notice you've been moving more slowly.")
@@ -58,7 +60,6 @@
if(SPT_PROB(4, seconds_per_tick) && affected_mob.buckled && affected_mob.body_position != LYING_DOWN && !affected_mob.IsParalyzed()) //chance to be couchlocked if sitting
to_chat(affected_mob, "It's too comfy to move...")
affected_mob.Paralyze(10 SECONDS)
- return ..()
/datum/reagent/drug/nicotine
name = "Nicotine"
@@ -79,6 +80,7 @@
mytray.adjust_pestlevel(-rand(1, 2))
/datum/reagent/drug/nicotine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(0.5, seconds_per_tick))
var/smoke_message = pick("You feel relaxed.", "You feel calmed.","You feel alert.","You feel rugged.")
to_chat(affected_mob, span_notice("[smoke_message]"))
@@ -89,14 +91,15 @@
affected_mob.AdjustUnconscious(-50 * REM * seconds_per_tick)
affected_mob.AdjustParalyzed(-50 * REM * seconds_per_tick)
affected_mob.AdjustImmobilized(-50 * REM * seconds_per_tick)
- ..()
- . = TRUE
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/nicotine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(0.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOxyLoss(1.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- ..()
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(0.1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOxyLoss(1.1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/krokodil
name = "Krokodil"
@@ -110,28 +113,28 @@
/datum/reagent/drug/krokodil/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/high_message = pick("You feel calm.", "You feel collected.", "You feel like you need to relax.")
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_notice("[high_message]"))
affected_mob.add_mood_event("smacked out", /datum/mood_event/narcotic_heavy, name)
- if(current_cycle == 35 && creation_purity <= 0.6)
+ if(current_cycle == 36 && creation_purity <= 0.6)
if(!istype(affected_mob.dna.species, /datum/species/human/krokodil_addict))
to_chat(affected_mob, span_userdanger("Your skin falls off easily!"))
var/mob/living/carbon/human/affected_human = affected_mob
affected_human.set_facial_hairstyle("Shaved", update = FALSE)
affected_human.set_hairstyle("Bald", update = FALSE)
affected_mob.set_species(/datum/species/human/krokodil_addict)
- affected_mob.adjustBruteLoss(50 * REM, FALSE, required_bodytype = affected_bodytype) // holy shit your skin just FELL THE FUCK OFF
- . = TRUE
- ..()
+ if(affected_mob.adjustBruteLoss(50 * REM, updating_health = FALSE, required_bodytype = affected_bodytype)) // holy shit your skin just FELL THE FUCK OFF
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/krokodil/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- affected_mob.adjustToxLoss(0.25 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- . = TRUE
-
-
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update = affected_mob.adjustToxLoss(0.25 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/methamphetamine
name = "Methamphetamine"
@@ -159,15 +162,16 @@
var/effective_impurity = min(1, (1 - creation_purity)/0.5)
color = BlendRGB(initial(color), "#FAFAFA", effective_impurity)
-/datum/reagent/drug/methamphetamine/on_mob_metabolize(mob/living/L)
- ..()
- L.add_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine)
+/datum/reagent/drug/methamphetamine/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine)
-/datum/reagent/drug/methamphetamine/on_mob_end_metabolize(mob/living/L)
- L.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine)
- ..()
+/datum/reagent/drug/methamphetamine/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
+ affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine)
/datum/reagent/drug/methamphetamine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/high_message = pick("You feel hyper.", "You feel like you need to go faster.", "You feel like you can run the world.")
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_notice("[high_message]"))
@@ -177,15 +181,17 @@
affected_mob.AdjustUnconscious(-40 * REM * seconds_per_tick)
affected_mob.AdjustParalyzed(-40 * REM * seconds_per_tick)
affected_mob.AdjustImmobilized(-40 * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(-2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(-2 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
affected_mob.set_jitter_if_lower(4 SECONDS * REM * seconds_per_tick)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(1, 4) * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, rand(1, 4) * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(2.5, seconds_per_tick))
affected_mob.emote(pick("twitch", "shiver"))
- ..()
- . = TRUE
/datum/reagent/drug/methamphetamine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && !ismovable(affected_mob.loc))
for(var/i in 1 to round(4 * REM * seconds_per_tick, 1))
step(affected_mob, pick(GLOB.cardinals))
@@ -194,10 +200,11 @@
if(SPT_PROB(18, seconds_per_tick))
affected_mob.visible_message(span_danger("[affected_mob]'s hands flip out and flail everywhere!"))
affected_mob.drop_all_held_items()
- ..()
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, (rand(5, 10) / 10) * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- . = TRUE
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, (rand(5, 10) / 10) * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/bath_salts
name = "Bath Salts"
@@ -211,35 +218,38 @@
ph = 8.2
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/drug/bath_salts/on_mob_metabolize(mob/living/L)
- ..()
- L.add_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE), type)
- if(iscarbon(L))
- var/mob/living/carbon/C = L
+/datum/reagent/drug/bath_salts/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ affected_mob.add_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE), type)
+ if(iscarbon(affected_mob))
+ var/mob/living/carbon/carbon_mob = affected_mob
rage = new()
- C.gain_trauma(rage, TRAUMA_RESILIENCE_ABSOLUTE)
+ carbon_mob.gain_trauma(rage, TRAUMA_RESILIENCE_ABSOLUTE)
-/datum/reagent/drug/bath_salts/on_mob_end_metabolize(mob/living/L)
- L.remove_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE), type)
+/datum/reagent/drug/bath_salts/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
+ affected_mob.remove_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE), type)
if(rage)
QDEL_NULL(rage)
- ..()
/datum/reagent/drug/bath_salts/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/high_message = pick("You feel amped up.", "You feel ready.", "You feel like you can push it to the limit.")
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_notice("[high_message]"))
affected_mob.add_mood_event("salted", /datum/mood_event/stimulant_heavy, name)
- affected_mob.adjustStaminaLoss(-5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(-5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
affected_mob.adjust_hallucinations(10 SECONDS * REM * seconds_per_tick)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
if(!HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && !ismovable(affected_mob.loc))
step(affected_mob, pick(GLOB.cardinals))
step(affected_mob, pick(GLOB.cardinals))
- ..()
- . = TRUE
/datum/reagent/drug/bath_salts/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_hallucinations(10 SECONDS * REM * seconds_per_tick)
if(!HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && !ismovable(affected_mob.loc))
for(var/i in 1 to round(8 * REM * seconds_per_tick, 1))
@@ -248,7 +258,6 @@
affected_mob.emote(pick("twitch","drool","moan"))
if(SPT_PROB(28, seconds_per_tick))
affected_mob.drop_all_held_items()
- ..()
/datum/reagent/drug/aranesp
name = "Aranesp"
@@ -259,16 +268,18 @@
addiction_types = list(/datum/addiction/stimulants = 8)
/datum/reagent/drug/aranesp/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/high_message = pick("You feel amped up.", "You feel ready.", "You feel like you can push it to the limit.")
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_notice("[high_message]"))
- affected_mob.adjustStaminaLoss(-18 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(-18 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
if(SPT_PROB(30, seconds_per_tick))
affected_mob.losebreath++
- affected_mob.adjustOxyLoss(1, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- ..()
- . = TRUE
+ need_mob_update += affected_mob.adjustOxyLoss(1, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/happiness
name = "Happiness"
@@ -280,25 +291,26 @@
taste_description = "paint thinner"
addiction_types = list(/datum/addiction/hallucinogens = 18)
-/datum/reagent/drug/happiness/on_mob_metabolize(mob/living/L)
- ..()
- ADD_TRAIT(L, TRAIT_FEARLESS, type)
- L.add_mood_event("happiness_drug", /datum/mood_event/happiness_drug)
+/datum/reagent/drug/happiness/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ ADD_TRAIT(affected_mob, TRAIT_FEARLESS, type)
+ affected_mob.add_mood_event("happiness_drug", /datum/mood_event/happiness_drug)
-/datum/reagent/drug/happiness/on_mob_delete(mob/living/L)
- REMOVE_TRAIT(L, TRAIT_FEARLESS, type)
- L.clear_mood_event("happiness_drug")
- ..()
+/datum/reagent/drug/happiness/on_mob_delete(mob/living/affected_mob)
+ . = ..()
+ REMOVE_TRAIT(affected_mob, TRAIT_FEARLESS, type)
+ affected_mob.clear_mood_event("happiness_drug")
/datum/reagent/drug/happiness/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.remove_status_effect(/datum/status_effect/jitter)
affected_mob.remove_status_effect(/datum/status_effect/confusion)
affected_mob.disgust = 0
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- ..()
- . = TRUE
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/happiness/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(16, seconds_per_tick))
var/reaction = rand(1,3)
switch(reaction)
@@ -311,9 +323,8 @@
if(3)
affected_mob.emote("frown")
affected_mob.add_mood_event("happiness_drug", /datum/mood_event/happiness_drug_bad_od)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- ..()
- . = TRUE
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/pumpup
name = "Pump-Up"
@@ -334,45 +345,54 @@
metabolization_rate *= 0.8
/datum/reagent/drug/pumpup/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_BATON_RESISTANCE, type)
- return ..()
/datum/reagent/drug/pumpup/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(2.5, seconds_per_tick))
to_chat(affected_mob, span_notice("[pick("Go! Go! GO!", "You feel ready...", "You feel invincible...")]"))
if(SPT_PROB(7.5, seconds_per_tick))
affected_mob.losebreath++
- affected_mob.adjustToxLoss(2, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
-
+ affected_mob.adjustToxLoss(2, updating_health = FALSE, required_biotype = affected_biotype)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/pumpup/overdose_start(mob/living/affected_mob)
+ . = ..()
to_chat(affected_mob, span_userdanger("You can't stop shaking, your heart beats faster and faster..."))
/datum/reagent/drug/pumpup/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
+ var/need_mob_update
if(SPT_PROB(2.5, seconds_per_tick))
affected_mob.drop_all_held_items()
if(SPT_PROB(7.5, seconds_per_tick))
affected_mob.emote(pick("twitch","drool"))
if(SPT_PROB(10, seconds_per_tick))
affected_mob.losebreath++
- affected_mob.adjustStaminaLoss(4, FALSE, required_biotype = affected_biotype)
+ affected_mob.adjustStaminaLoss(4, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update = TRUE
if(SPT_PROB(7.5, seconds_per_tick))
- affected_mob.adjustToxLoss(2, FALSE, required_biotype = affected_biotype)
- ..()
+ need_mob_update += affected_mob.adjustToxLoss(2, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/maint
name = "Maintenance Drugs"
chemical_flags = NONE
-/datum/reagent/drug/maint/on_mob_metabolize(mob/living/carbon/L)
- var/obj/item/organ/internal/liver/liver = L.get_organ_slot(ORGAN_SLOT_LIVER)
+/datum/reagent/drug/maint/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ if(!iscarbon(affected_mob))
+ return
+
+ var/mob/living/carbon/carbon_mob = affected_mob
+ var/obj/item/organ/internal/liver/liver = carbon_mob.get_organ_slot(ORGAN_SLOT_LIVER)
if(HAS_TRAIT(liver, TRAIT_MAINTENANCE_METABOLISM))
- L.add_mood_event("maintenance_fun", /datum/mood_event/maintenance_high)
+ carbon_mob.add_mood_event("maintenance_fun", /datum/mood_event/maintenance_high)
metabolization_rate *= 0.8
/datum/reagent/drug/maint/powder
@@ -400,7 +420,8 @@
/datum/reagent/drug/maint/powder/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
. = ..()
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 6 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 6 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/maint/sludge
name = "Maintenance Sludge"
@@ -412,15 +433,14 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
addiction_types = list(/datum/addiction/maintenance_drugs = 8)
-/datum/reagent/drug/maint/sludge/on_mob_metabolize(mob/living/L)
-
+/datum/reagent/drug/maint/sludge/on_mob_metabolize(mob/living/affected_mob)
. = ..()
- ADD_TRAIT(L,TRAIT_HARDLY_WOUNDED,type)
+ ADD_TRAIT(affected_mob,TRAIT_HARDLY_WOUNDED,type)
/datum/reagent/drug/maint/sludge/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- affected_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, required_biotype = affected_biotype)
- return TRUE
+ if(affected_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/maint/sludge/on_mob_end_metabolize(mob/living/affected_mob)
. = ..()
@@ -432,10 +452,13 @@
return
var/mob/living/carbon/carbie = affected_mob
//You will be vomiting so the damage is really for a few ticks before you flush it out of your system
- carbie.adjustToxLoss(1 * REM * seconds_per_tick, required_biotype = affected_biotype)
+ var/need_mob_update
+ need_mob_update = carbie.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
if(SPT_PROB(5, seconds_per_tick))
- carbie.adjustToxLoss(5, required_biotype = affected_biotype)
+ need_mob_update += carbie.adjustToxLoss(5, required_biotype = affected_biotype, updating_health = FALSE)
carbie.vomit(VOMIT_CATEGORY_DEFAULT)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/maint/tar
name = "Maintenance Tar"
@@ -454,13 +477,15 @@
affected_mob.AdjustParalyzed(-10 * REM * seconds_per_tick)
affected_mob.AdjustImmobilized(-10 * REM * seconds_per_tick)
affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 1.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- return TRUE
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/maint/tar/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
. = ..()
-
- affected_mob.adjustToxLoss(5 * REM * seconds_per_tick, required_biotype = affected_biotype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ var/need_update
+ need_update = affected_mob.adjustToxLoss(5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(need_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/mushroomhallucinogen
name = "Mushroom Hallucinogen"
@@ -474,21 +499,21 @@
addiction_types = list(/datum/addiction/hallucinogens = 12)
/datum/reagent/drug/mushroomhallucinogen/on_mob_life(mob/living/carbon/psychonaut, seconds_per_tick, times_fired)
+ . = ..()
psychonaut.set_slurring_if_lower(1 SECONDS * REM * seconds_per_tick)
switch(current_cycle)
- if(1 to 5)
+ if(2 to 6)
if(SPT_PROB(5, seconds_per_tick))
psychonaut.emote(pick("twitch","giggle"))
- if(5 to 10)
+ if(6 to 11)
psychonaut.set_jitter_if_lower(20 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(10, seconds_per_tick))
psychonaut.emote(pick("twitch","giggle"))
- if (10 to INFINITY)
+ if (11 to INFINITY)
psychonaut.set_jitter_if_lower(40 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(16, seconds_per_tick))
psychonaut.emote(pick("twitch","giggle"))
- ..()
/datum/reagent/drug/mushroomhallucinogen/on_mob_metabolize(mob/living/psychonaut)
. = ..()
@@ -601,16 +626,17 @@
/datum/reagent/drug/blastoff/on_mob_life(mob/living/carbon/dancer, seconds_per_tick, times_fired)
. = ..()
- dancer.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(dancer.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
dancer.AdjustKnockdown(-20)
if(SPT_PROB(BLASTOFF_DANCE_MOVE_CHANCE_PER_UNIT * volume, seconds_per_tick))
dancer.emote("flip")
- return TRUE
/datum/reagent/drug/blastoff/overdose_process(mob/living/dancer, seconds_per_tick, times_fired)
. = ..()
- dancer.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(dancer.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(BLASTOFF_DANCE_MOVE_CHANCE_PER_UNIT * volume, seconds_per_tick))
dancer.emote("spin")
@@ -673,8 +699,8 @@
/datum/reagent/drug/saturnx/on_mob_life(mob/living/carbon/invisible_man, seconds_per_tick, times_fired)
. = ..()
- invisible_man.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- return TRUE
+ if(invisible_man.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.3 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/saturnx/on_mob_metabolize(mob/living/invisible_man)
. = ..()
@@ -750,7 +776,8 @@
invisible_man.emote("giggle")
if(SPT_PROB(5, seconds_per_tick))
invisible_man.emote("laugh")
- invisible_man.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(invisible_man.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/drug/saturnx/stable
name = "Stabilized Saturn-X"
@@ -772,27 +799,29 @@
addiction_types = list(/datum/addiction/stimulants = 20)
/datum/reagent/drug/kronkaine/on_mob_metabolize(mob/living/kronkaine_fiend)
- ..()
+ . = ..()
kronkaine_fiend.add_actionspeed_modifier(/datum/actionspeed_modifier/kronkaine)
kronkaine_fiend.sound_environment_override = SOUND_ENVIRONMENT_HANGAR
/datum/reagent/drug/kronkaine/on_mob_end_metabolize(mob/living/kronkaine_fiend)
+ . = ..()
kronkaine_fiend.remove_actionspeed_modifier(/datum/actionspeed_modifier/kronkaine)
kronkaine_fiend.sound_environment_override = NONE
- . = ..()
/datum/reagent/drug/kronkaine/on_transfer(atom/kronkaine_receptacle, methods, trans_volume)
. = ..()
if(!iscarbon(kronkaine_receptacle))
return
var/mob/living/carbon/druggo = kronkaine_receptacle
- druggo.adjustStaminaLoss(-4 * trans_volume, 0)
+ if(druggo.adjustStaminaLoss(-4 * trans_volume, updating_stamina = FALSE))
+ return UPDATE_MOB_HEALTH
//I wish i could give it some kind of bonus when smoked, but we don't have an INHALE method.
/datum/reagent/drug/kronkaine/on_mob_life(mob/living/carbon/kronkaine_fiend, seconds_per_tick, times_fired)
. = ..() || TRUE
kronkaine_fiend.add_mood_event("tweaking", /datum/mood_event/stimulant_medium, name)
- kronkaine_fiend.adjustOrganLoss(ORGAN_SLOT_HEART, 0.4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(kronkaine_fiend.adjustOrganLoss(ORGAN_SLOT_HEART, 0.4 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
kronkaine_fiend.set_jitter_if_lower(20 SECONDS * REM * seconds_per_tick)
kronkaine_fiend.AdjustSleeping(-20 * REM * seconds_per_tick)
kronkaine_fiend.adjust_drowsiness(-10 SECONDS * REM * seconds_per_tick)
@@ -805,7 +834,8 @@
/datum/reagent/drug/kronkaine/overdose_process(mob/living/kronkaine_fiend, seconds_per_tick, times_fired)
. = ..()
- kronkaine_fiend.adjustOrganLoss(ORGAN_SLOT_HEART, 1 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ if(kronkaine_fiend.adjustOrganLoss(ORGAN_SLOT_HEART, 1 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
kronkaine_fiend.set_jitter_if_lower(20 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(10, seconds_per_tick))
to_chat(kronkaine_fiend, span_danger(pick("You feel like your heart is going to explode!", "Your ears are ringing!", "You sweat like a pig!", "You clench your jaw and grind your teeth.", "You feel prickles of pain in your chest.")))
@@ -819,9 +849,10 @@
chemical_flags = NONE
/datum/reagent/drug/kronkaine/gore/overdose_start(mob/living/gored)
+ . = ..()
gored.visible_message(
span_danger("[gored] explodes in a shower of gore!"),
span_userdanger("GORE! GORE! GORE! YOU'RE GORE! TOO MUCH GORE! YOU'RE GORE! GORE! IT'S OVER! GORE! GORE! YOU'RE GORE! TOO MUCH G-"),
)
new /obj/structure/bouncy_castle(gored.loc, gored)
- gored.gib(TRUE, TRUE, TRUE) //no brain, no organs, no bodyparts
+ gored.gib()
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 17867389c3dc19..af75cc46db291c 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -19,15 +19,16 @@
/// affects mood, typically higher for mixed drinks with more complex recipes'
var/quality = 0
-/datum/reagent/consumable/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
current_cycle++
- if(ishuman(M))
- var/mob/living/carbon/human/H = M
- if(!HAS_TRAIT(H, TRAIT_NOHUNGER))
- H.adjust_nutrition(get_nutriment_factor() * REM * seconds_per_tick)
+ if(ishuman(affected_mob))
+ var/mob/living/carbon/human/affected_human = affected_mob
+ if(!HAS_TRAIT(affected_human, TRAIT_NOHUNGER))
+ affected_human.adjust_nutrition(get_nutriment_factor() * REM * seconds_per_tick)
if(length(reagent_removal_skip_list))
return
- holder.remove_reagent(type, metabolization_rate * seconds_per_tick)
+ if(holder)
+ holder.remove_reagent(type, metabolization_rate * seconds_per_tick)
/datum/reagent/consumable/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -73,11 +74,11 @@
/datum/reagent/consumable/nutriment/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
mytray.adjust_plant_health(round(volume * 0.2))
-/datum/reagent/consumable/nutriment/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/nutriment/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(30, seconds_per_tick))
- M.heal_bodypart_damage(brute = brute_heal, burn = burn_heal, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
- . = TRUE
- ..()
+ if(affected_mob.heal_bodypart_damage(brute = brute_heal * REM * seconds_per_tick, burn = burn_heal * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC))
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/nutriment/on_new(list/supplied_data)
. = ..()
@@ -128,10 +129,10 @@
brute_heal = 1
burn_heal = 1
-/datum/reagent/consumable/nutriment/vitamin/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(M.satiety < MAX_SATIETY)
- M.satiety += 30 * REM * seconds_per_tick
+/datum/reagent/consumable/nutriment/vitamin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
+ if(affected_mob.satiety < MAX_SATIETY)
+ affected_mob.satiety += 30 * REM * seconds_per_tick
/// The basic resource of vat growing.
/datum/reagent/consumable/nutriment/protein
@@ -238,15 +239,19 @@
///Amount of satiety that will be drained when the cloth_fibers is fully metabolized
var/delayed_satiety_drain = 2 * CLOTHING_NUTRITION_GAIN
-/datum/reagent/consumable/nutriment/cloth_fibers/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(M.satiety < MAX_SATIETY)
- M.adjust_nutrition(CLOTHING_NUTRITION_GAIN)
+/datum/reagent/consumable/nutriment/cloth_fibers/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(affected_mob.satiety < MAX_SATIETY)
+ affected_mob.adjust_nutrition(CLOTHING_NUTRITION_GAIN)
delayed_satiety_drain += CLOTHING_NUTRITION_GAIN
- return ..()
-/datum/reagent/consumable/nutriment/cloth_fibers/on_mob_delete(mob/living/carbon/M)
- M.adjust_nutrition(-delayed_satiety_drain)
- return ..()
+/datum/reagent/consumable/nutriment/cloth_fibers/on_mob_delete(mob/living/carbon/affected_mob)
+ . = ..()
+ if(!iscarbon(affected_mob))
+ return
+
+ var/mob/living/carbon/carbon_mob = affected_mob
+ carbon_mob.adjust_nutrition(-delayed_satiety_drain)
/datum/reagent/consumable/nutriment/mineral
name = "Mineral Slurry"
@@ -256,14 +261,15 @@
brute_heal = 0
burn_heal = 0
-/datum/reagent/consumable/nutriment/mineral/on_mob_life(mob/living/carbon/eater, delta_time, times_fired)
- current_cycle++
- if (HAS_TRAIT(eater, TRAIT_ROCK_EATER) && !HAS_TRAIT(eater, TRAIT_NOHUNGER) && ishuman(eater))
- var/mob/living/carbon/human/golem_eater = eater
- golem_eater.adjust_nutrition(get_nutriment_factor() * REM * delta_time)
- if(length(reagent_removal_skip_list))
- return
- holder.remove_reagent(type, metabolization_rate * delta_time)
+/datum/reagent/consumable/nutriment/mineral/on_mob_life(mob/living/carbon/eater, seconds_per_tick, times_fired)
+ if(HAS_TRAIT(eater, TRAIT_ROCK_EATER)) // allow mobs who can eat rocks to do so
+ return ..()
+ else // otherwise just let them pass through the system
+ current_cycle++
+ if(length(reagent_removal_skip_list))
+ return
+ if(holder)
+ holder.remove_reagent(type, metabolization_rate * seconds_per_tick)
/datum/reagent/consumable/sugar
name = "Sugar"
@@ -284,15 +290,14 @@
mytray.adjust_weedlevel(rand(1, 2))
mytray.adjust_pestlevel(rand(1, 2))
-/datum/reagent/consumable/sugar/overdose_start(mob/living/M)
- to_chat(M, span_userdanger("You go into hyperglycaemic shock! Lay off the twinkies!"))
- M.AdjustSleeping(20 SECONDS)
- . = TRUE
+/datum/reagent/consumable/sugar/overdose_start(mob/living/affected_mob)
+ . = ..()
+ to_chat(affected_mob, span_userdanger("You go into hyperglycemic shock! Lay off the twinkies!"))
+ affected_mob.AdjustSleeping(20 SECONDS)
-/datum/reagent/consumable/sugar/overdose_process(mob/living/M, seconds_per_tick, times_fired)
- M.adjust_drowsiness_up_to((5 SECONDS * REM * seconds_per_tick), 60 SECONDS)
- ..()
- . = TRUE
+/datum/reagent/consumable/sugar/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.adjust_drowsiness_up_to((5 SECONDS * REM * seconds_per_tick), 60 SECONDS)
/datum/reagent/consumable/virus_food
name = "Virus Food"
@@ -332,29 +337,29 @@
taste_mult = 1.5
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/capsaicin/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/capsaicin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
var/heating = 0
switch(current_cycle)
if(1 to 15)
heating = 5
if(holder.has_reagent(/datum/reagent/cryostylane))
holder.remove_reagent(/datum/reagent/cryostylane, 5 * REM * seconds_per_tick)
- if(isslime(M))
+ if(isslime(affected_mob))
heating = rand(5, 20)
if(15 to 25)
heating = 10
- if(isslime(M))
+ if(isslime(affected_mob))
heating = rand(10, 20)
if(25 to 35)
heating = 15
- if(isslime(M))
+ if(isslime(affected_mob))
heating = rand(15, 20)
if(35 to INFINITY)
heating = 20
- if(isslime(M))
+ if(isslime(affected_mob))
heating = rand(20, 25)
- M.adjust_bodytemperature(heating * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick)
- ..()
+ affected_mob.adjust_bodytemperature(heating * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick)
+ return ..()
/datum/reagent/consumable/frostoil
name = "Frost Oil"
@@ -367,33 +372,33 @@
specific_heat = 40
default_container = /obj/item/reagent_containers/cup/bottle/frostoil
-/datum/reagent/consumable/frostoil/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/frostoil/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
var/cooling = 0
switch(current_cycle)
if(1 to 15)
cooling = -10
if(holder.has_reagent(/datum/reagent/consumable/capsaicin))
holder.remove_reagent(/datum/reagent/consumable/capsaicin, 5 * REM * seconds_per_tick)
- if(isslime(M))
+ if(isslime(affected_mob))
cooling = -rand(5, 20)
if(15 to 25)
cooling = -20
- if(isslime(M))
+ if(isslime(affected_mob))
cooling = -rand(10, 20)
if(25 to 35)
cooling = -30
if(prob(1))
- M.emote("shiver")
- if(isslime(M))
+ affected_mob.emote("shiver")
+ if(isslime(affected_mob))
cooling = -rand(15, 20)
if(35 to INFINITY)
cooling = -40
if(prob(5))
- M.emote("shiver")
- if(isslime(M))
+ affected_mob.emote("shiver")
+ if(isslime(affected_mob))
cooling = -rand(20, 25)
- M.adjust_bodytemperature(cooling * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 50)
- ..()
+ affected_mob.adjust_bodytemperature(cooling * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 50)
+ return ..()
/datum/reagent/consumable/frostoil/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -421,7 +426,6 @@
default_container = /obj/item/reagent_containers/cup/bottle/capsaicin
/datum/reagent/consumable/condensedcapsaicin/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
- . = ..()
if(!ishuman(exposed_mob))
return
@@ -450,12 +454,13 @@
victim.set_dizzy_if_lower(2 SECONDS)
if(prob(5))
victim.vomit(VOMIT_CATEGORY_DEFAULT)
+ return ..()
-/datum/reagent/consumable/condensedcapsaicin/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/condensedcapsaicin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
if(!holder.has_reagent(/datum/reagent/consumable/milk))
if(SPT_PROB(5, seconds_per_tick))
- M.visible_message(span_warning("[M] [pick("dry heaves!","coughs!","splutters!")]"))
- ..()
+ affected_mob.visible_message(span_warning("[affected_mob] [pick("dry heaves!","coughs!","splutters!")]"))
+ return ..()
/datum/reagent/consumable/salt
name = "Table Salt"
@@ -475,6 +480,8 @@
/datum/reagent/consumable/salt/expose_mob(mob/living/exposed_mob, methods, reac_volume)
. = ..()
+ if(!iscarbon(exposed_mob))
+ return
var/mob/living/carbon/carbies = exposed_mob
if(!(methods & (PATCH|TOUCH|VAPOR)))
return
@@ -506,7 +513,6 @@
flesh_healing -= max(VALUE_PER(5, 30) * reac_volume, 0)
to_chat(victim, span_notice("The salt bits seep in and stick to [lowertext(src)], painfully irritating the skin! After a few moments, it feels marginally better."))
-
/datum/reagent/consumable/blackpepper
name = "Black Pepper"
description = "A powder ground from peppercorns. *AAAACHOOO*"
@@ -533,27 +539,27 @@
metabolization_rate = 0.15 * REAGENTS_METABOLISM
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/garlic/on_mob_add(mob/living/L, amount)
+/datum/reagent/consumable/garlic/on_mob_add(mob/living/affected_mob, amount)
+ . = ..()
+ ADD_TRAIT(affected_mob, TRAIT_GARLIC_BREATH, type)
+
+/datum/reagent/consumable/garlic/on_mob_delete(mob/living/affected_mob)
. = ..()
- ADD_TRAIT(L, TRAIT_GARLIC_BREATH, type)
+ REMOVE_TRAIT(affected_mob, TRAIT_GARLIC_BREATH, type)
-/datum/reagent/consumable/garlic/on_mob_delete(mob/living/L)
+/datum/reagent/consumable/garlic/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- REMOVE_TRAIT(L, TRAIT_GARLIC_BREATH, type)
-
-/datum/reagent/consumable/garlic/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(isvampire(M)) //incapacitating but not lethal. Unfortunately, vampires cannot vomit.
- if(SPT_PROB(min(current_cycle/2, 12.5), seconds_per_tick))
- to_chat(M, span_danger("You can't get the scent of garlic out of your nose! You can barely think..."))
- M.Paralyze(10)
- M.set_jitter_if_lower(20 SECONDS)
+ if(isvampire(affected_mob)) //incapacitating but not lethal. Unfortunately, vampires cannot vomit.
+ if(SPT_PROB(min((current_cycle-1)/2, 12.5), seconds_per_tick))
+ to_chat(affected_mob, span_danger("You can't get the scent of garlic out of your nose! You can barely think..."))
+ affected_mob.Paralyze(10)
+ affected_mob.set_jitter_if_lower(20 SECONDS)
else
- var/obj/item/organ/internal/liver/liver = M.get_organ_slot(ORGAN_SLOT_LIVER)
+ var/obj/item/organ/internal/liver/liver = affected_mob.get_organ_slot(ORGAN_SLOT_LIVER)
if(liver && HAS_TRAIT(liver, TRAIT_CULINARY_METABOLISM))
if(SPT_PROB(10, seconds_per_tick)) //stays in the system much longer than sprinkles/banana juice, so heals slower to partially compensate
- M.heal_bodypart_damage(brute = 1, burn = 1)
- . = TRUE
- ..()
+ 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
/datum/reagent/consumable/tearjuice
name = "Tear Juice"
@@ -582,12 +588,12 @@
taste_description = "childhood whimsy"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/sprinkles/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- var/obj/item/organ/internal/liver/liver = M.get_organ_slot(ORGAN_SLOT_LIVER)
+/datum/reagent/consumable/sprinkles/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_LAW_ENFORCEMENT_METABOLISM))
- M.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick, 0)
- . = TRUE
- ..()
+ 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
/datum/reagent/consumable/enzyme
name = "Universal Enzyme"
@@ -623,9 +629,9 @@
taste_description = "your imprisonment"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/hot_ramen/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- M.adjust_bodytemperature(10 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 0, M.get_body_temp_normal())
- ..()
+/datum/reagent/consumable/hot_ramen/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.adjust_bodytemperature(10 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 0, affected_mob.get_body_temp_normal())
/datum/reagent/consumable/hell_ramen
name = "Hell Ramen"
@@ -635,9 +641,9 @@
taste_description = "wet and cheap noodles on fire"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/hell_ramen/on_mob_life(mob/living/carbon/target_mob, seconds_per_tick, times_fired)
- target_mob.adjust_bodytemperature(10 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick)
- ..()
+/datum/reagent/consumable/hell_ramen/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.adjust_bodytemperature(10 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick)
/datum/reagent/consumable/flour
name = "Flour"
@@ -650,6 +656,8 @@
/datum/reagent/consumable/flour/expose_mob(mob/living/exposed_mob, methods, reac_volume)
. = ..()
+ if(!iscarbon(exposed_mob))
+ return
var/mob/living/carbon/carbies = exposed_mob
if(!(methods & (PATCH|TOUCH|VAPOR)))
return
@@ -718,7 +726,7 @@
color = "#FFFFFF" // rgb: 0, 0, 0
taste_description = "chalky wheat with rice"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-
+
/datum/reagent/consumable/vanilla
name = "Vanilla Powder"
description = "A fatty, bitter paste made from vanilla pods."
@@ -754,6 +762,8 @@
// Starch has similar absorbing properties to flour (Stronger here because it's rarer)
/datum/reagent/consumable/corn_starch/expose_mob(mob/living/exposed_mob, methods, reac_volume)
. = ..()
+ if(!iscarbon(exposed_mob))
+ return
var/mob/living/carbon/carbies = exposed_mob
if(!(methods & (PATCH|TOUCH|VAPOR)))
return
@@ -789,9 +799,9 @@
taste_description = "sweet slime"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/corn_syrup/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/corn_syrup/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
holder.add_reagent(/datum/reagent/consumable/sugar, 3 * REM * seconds_per_tick)
- ..()
+ return ..()
/datum/reagent/consumable/honey
name = "Honey"
@@ -812,15 +822,17 @@
mytray.adjust_weedlevel(rand(1, 2))
mytray.adjust_pestlevel(rand(1, 2))
-/datum/reagent/consumable/honey/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/honey/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
holder.add_reagent(/datum/reagent/consumable/sugar, 3 * REM * seconds_per_tick)
+ . = ..()
+ var/need_mob_update
if(SPT_PROB(33, seconds_per_tick))
- M.adjustBruteLoss(-1, FALSE, required_bodytype = affected_bodytype)
- M.adjustFireLoss(-1, FALSE, required_bodytype = affected_bodytype)
- M.adjustOxyLoss(-1, FALSE, required_biotype = affected_biotype)
- M.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ need_mob_update = affected_mob.adjustBruteLoss(-1, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-1, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-1, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustToxLoss(-1, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/honey/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -861,10 +873,10 @@
color = "#664330" // rgb: 102, 67, 48
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/nutriment/stabilized/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(M.nutrition > NUTRITION_LEVEL_FULL - 25)
- M.adjust_nutrition(-3 * REM * get_nutriment_factor() * seconds_per_tick)
- ..()
+/datum/reagent/consumable/nutriment/stabilized/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(affected_mob.nutrition > NUTRITION_LEVEL_FULL - 25)
+ affected_mob.adjust_nutrition(-3 * REM * get_nutriment_factor() * seconds_per_tick)
////Lavaland Flora Reagents////
@@ -877,19 +889,20 @@
ph = 12
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/entpoly/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(current_cycle >= 10)
- M.Unconscious(40 * REM * seconds_per_tick, FALSE)
- . = TRUE
+/datum/reagent/consumable/entpoly/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
+ if(current_cycle > 10)
+ affected_mob.Unconscious(40 * REM * seconds_per_tick, FALSE)
if(SPT_PROB(10, seconds_per_tick))
- M.losebreath += 4
- M.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2*REM, 150, affected_biotype)
- M.adjustToxLoss(3*REM, FALSE, required_biotype = affected_biotype)
- M.adjustStaminaLoss(10*REM, FALSE, required_biotype = affected_biotype)
- M.set_eye_blur_if_lower(10 SECONDS)
- . = TRUE
- ..()
-
+ affected_mob.losebreath += 4
+ affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2*REM, 150, affected_biotype)
+ affected_mob.adjustToxLoss(3*REM, updating_health = FALSE, required_biotype = affected_biotype)
+ affected_mob.adjustStaminaLoss(10*REM, updating_stamina = FALSE, required_biotype = affected_biotype)
+ affected_mob.set_eye_blur_if_lower(10 SECONDS)
+ need_mob_update = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/tinlux
name = "Tinea Luxor"
@@ -925,12 +938,14 @@
ph = 10.4
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/vitfro/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/vitfro/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
if(SPT_PROB(55, seconds_per_tick))
- M.adjustBruteLoss(-1, FALSE, required_bodytype = affected_bodytype)
- M.adjustFireLoss(-1, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
- ..()
+ need_mob_update = affected_mob.adjustBruteLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/consumable/liquidelectricity
name = "Liquid Electricity"
@@ -953,13 +968,13 @@
if(istype(stomach))
stomach.adjust_charge(reac_volume * 30)
-/datum/reagent/consumable/liquidelectricity/enriched/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(isethereal(M))
- M.blood_volume += 1 * seconds_per_tick
+/datum/reagent/consumable/liquidelectricity/enriched/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(isethereal(affected_mob))
+ affected_mob.blood_volume += 1 * seconds_per_tick
else if(SPT_PROB(10, seconds_per_tick)) //lmao at the newbs who eat energy bars
- M.electrocute_act(rand(5,10), "Liquid Electricity in their body", 1, SHOCK_NOGLOVES) //the shock is coming from inside the house
- playsound(M, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- return ..()
+ affected_mob.electrocute_act(rand(5,10), "Liquid Electricity in their body", 1, SHOCK_NOGLOVES) //the shock is coming from inside the house
+ playsound(affected_mob, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
/datum/reagent/consumable/astrotame
name = "Astrotame"
@@ -973,11 +988,10 @@
overdose_threshold = 17
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/astrotame/overdose_process(mob/living/carbon/M, seconds_per_tick, times_fired)
- if(M.disgust < 80)
- M.adjust_disgust(10 * REM * seconds_per_tick)
- ..()
- . = TRUE
+/datum/reagent/consumable/astrotame/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(affected_mob.disgust < 80)
+ affected_mob.adjust_disgust(10 * REM * seconds_per_tick)
/datum/reagent/consumable/secretsauce
name = "Secret Sauce"
@@ -1022,11 +1036,10 @@
overdose_threshold = 15
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/char/overdose_process(mob/living/M, seconds_per_tick, times_fired)
+/datum/reagent/consumable/char/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(13, seconds_per_tick))
- M.say(pick_list_replacements(BOOMER_FILE, "boomer"), forced = /datum/reagent/consumable/char)
- ..()
- return
+ affected_mob.say(pick_list_replacements(BOOMER_FILE, "boomer"), forced = /datum/reagent/consumable/char)
/datum/reagent/consumable/bbqsauce
name = "BBQ Sauce"
@@ -1136,11 +1149,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
default_container = /obj/item/reagent_containers/condiment/peanut_butter
-/datum/reagent/consumable/peanut_butter/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired) //ET loves peanut butter
- if(isabductor(M))
- M.add_mood_event("ET_pieces", /datum/mood_event/et_pieces, name)
- M.set_drugginess(30 SECONDS * REM * seconds_per_tick)
- ..()
+/datum/reagent/consumable/peanut_butter/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) //ET loves peanut butter
+ . = ..()
+ if(isabductor(affected_mob))
+ affected_mob.add_mood_event("ET_pieces", /datum/mood_event/et_pieces, name)
+ affected_mob.set_drugginess(30 SECONDS * REM * seconds_per_tick)
/datum/reagent/consumable/vinegar
name = "Vinegar"
@@ -1198,10 +1211,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/mintextract/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(HAS_TRAIT(affected_mob, TRAIT_FAT))
affected_mob.investigate_log("has been gibbed by consuming [src] while fat.", INVESTIGATE_DEATHS)
affected_mob.inflate_gib()
- return ..()
/datum/reagent/consumable/worcestershire
name = "Worcestershire Sauce"
diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents.dm
index f7eaba3c211bac..1a06ae11cd9606 100644
--- a/code/modules/reagents/chemistry/reagents/impure_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/impure_reagents.dm
@@ -17,10 +17,15 @@
/datum/reagent/impurity/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(isnull(liver)) //Though, lets be safe
- return affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype) //Incase of no liver!
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, liver_damage * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- return TRUE
+ var/need_mob_update
+
+ if(liver)//Though, lets be safe
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, liver_damage * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ else
+ need_mob_update = affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)//Incase of no liver!
+
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
//Basically just so people don't forget to adjust metabolization_rate
/datum/reagent/inverse
@@ -36,7 +41,8 @@
/datum/reagent/inverse/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- return affected_mob.adjustToxLoss(tox_damage * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ if(affected_mob.adjustToxLoss(tox_damage * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
//Failed chems - generally use inverse if you want to use a impure subtype for it
//technically not a impure chem, but it's here because it can only be made with a failed impure reaction
@@ -96,6 +102,7 @@
var/atom/movable/screen/alert/status_effect/freon/cryostylane_alert
/datum/reagent/inverse/cryostylane/on_mob_add(mob/living/carbon/affected_mob, amount)
+ . = ..()
cube = new /obj/structure/ice_stasis(get_turf(affected_mob))
cube.color = COLOR_CYAN
cube.set_anchored(TRUE)
@@ -103,17 +110,21 @@
affected_mob.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_CHEMICAL_EFFECT)
cryostylane_alert = affected_mob.throw_alert("cryostylane_alert", /atom/movable/screen/alert/status_effect/freon/cryostylane)
cryostylane_alert.attached_effect = src //so the alert can reference us, if it needs to
- ..()
/datum/reagent/inverse/cryostylane/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ if(current_cycle >= 60)
+ holder.remove_reagent(type, volume) // remove it all if we're past 60 cycles
+ return ..()
if(!cube || affected_mob.loc != cube)
- affected_mob.reagents.remove_reagent(type, volume) //remove it all if we're past 60s
- if(current_cycle > 60)
metabolization_rate += 0.01
- ..()
+ return ..()
/datum/reagent/inverse/cryostylane/on_mob_delete(mob/living/carbon/affected_mob, amount)
+ . = ..()
QDEL_NULL(cube)
- affected_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_CHEMICAL_EFFECT)
- affected_mob.clear_alert("cryostylane_alert")
- ..()
+ if(!iscarbon(affected_mob))
+ return
+
+ var/mob/living/carbon/carbon_mob = affected_mob
+ carbon_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_CHEMICAL_EFFECT)
+ carbon_mob.clear_alert("cryostylane_alert")
diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm
index db98ed9d622573..23e8537358c489 100644
--- a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm
@@ -35,19 +35,21 @@
affected_respiration_type = ALL
//Random healing of the 4 main groups
-/datum/reagent/impurity/healing/medicine_failure/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
+/datum/reagent/impurity/healing/medicine_failure/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
var/pick = pick("brute", "burn", "tox", "oxy")
switch(pick)
if("brute")
- owner.adjustBruteLoss(-0.5, required_bodytype = affected_bodytype)
+ need_mob_update = affected_mob.adjustBruteLoss(-0.5, updating_health = FALSE, required_bodytype = affected_bodytype)
if("burn")
- owner.adjustFireLoss(-0.5, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-0.5, updating_health = FALSE, required_bodytype = affected_bodytype)
if("tox")
- owner.adjustToxLoss(-0.5, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustToxLoss(-0.5, updating_health = FALSE, required_biotype = affected_biotype)
if("oxy")
- owner.adjustOxyLoss(-0.5, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- ..()
- return TRUE
+ need_mob_update += affected_mob.adjustOxyLoss(-0.5, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
// C2 medications
// Helbital
@@ -64,10 +66,10 @@
var/list/timer_ids
//Warns you about the impenting hands
-/datum/reagent/inverse/helgrasp/on_mob_add(mob/living/L, amount)
- to_chat(L, span_hierophant("You hear laughter as malevolent hands apparate before you, eager to drag you down to hell...! Look out!"))
- playsound(L.loc, 'sound/chemistry/ahaha.ogg', 80, TRUE, -1) //Very obvious tell so people can be ready
+/datum/reagent/inverse/helgrasp/on_mob_add(mob/living/affected_mob, amount)
. = ..()
+ to_chat(affected_mob, span_hierophant("You hear laughter as malevolent hands apparate before you, eager to drag you down to hell...! Look out!"))
+ playsound(affected_mob.loc, 'sound/chemistry/ahaha.ogg', 80, TRUE, -1) //Very obvious tell so people can be ready
//Sends hands after you for your hubris
/*
@@ -81,8 +83,9 @@ Then I attempt to calculate the how many hands to created based off the current
I take the 2s interval period and divide it by the number of hands I want to make (i.e. the current seconds_per_tick) and I keep track of how many hands I'm creating (since I always create one on a tick, then I start at 1 hand). For each hand I then use this time value multiplied by the number of hands. Since we're spawning one now, and it checks to see if hands is less than, but not less than or equal to, seconds_per_tick, no hands will be created on the next expected tick.
Basically, we fill the time between now and 2s from now with hands based off the current lag.
*/
-/datum/reagent/inverse/helgrasp/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
- spawn_hands(owner)
+/datum/reagent/inverse/helgrasp/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ spawn_hands(affected_mob)
lag_remainder += seconds_per_tick - FLOOR(seconds_per_tick, 1)
seconds_per_tick = FLOOR(seconds_per_tick, 1)
if(lag_remainder >= 1)
@@ -91,36 +94,35 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/hands = 1
var/time = 2 / seconds_per_tick
while(hands < seconds_per_tick) //we already made a hand now so start from 1
- LAZYADD(timer_ids, addtimer(CALLBACK(src, PROC_REF(spawn_hands), owner), (time*hands) SECONDS, TIMER_STOPPABLE)) //keep track of all the timers we set up
+ LAZYADD(timer_ids, addtimer(CALLBACK(src, PROC_REF(spawn_hands), affected_mob), (time*hands) SECONDS, TIMER_STOPPABLE)) //keep track of all the timers we set up
hands += time
- return ..()
-/datum/reagent/inverse/helgrasp/proc/spawn_hands(mob/living/carbon/owner)
- if(!owner && iscarbon(holder.my_atom))//Catch timer
- owner = holder.my_atom
+/datum/reagent/inverse/helgrasp/proc/spawn_hands(mob/living/carbon/affected_mob)
+ if(!affected_mob && iscarbon(holder.my_atom))//Catch timer
+ affected_mob = holder.my_atom
//Adapted from the end of the curse - but lasts a short time
- var/grab_dir = turn(owner.dir, pick(-90, 90, 180, 180)) //grab them from a random direction other than the one faced, favoring grabbing from behind
- var/turf/spawn_turf = get_ranged_target_turf(owner, grab_dir, 8)//Larger range so you have more time to dodge
+ var/grab_dir = turn(affected_mob.dir, pick(-90, 90, 180, 180)) //grab them from a random direction other than the one faced, favoring grabbing from behind
+ var/turf/spawn_turf = get_ranged_target_turf(affected_mob, grab_dir, 8)//Larger range so you have more time to dodge
if(!spawn_turf)
return
- new/obj/effect/temp_visual/dir_setting/curse/grasp_portal(spawn_turf, owner.dir)
+ new/obj/effect/temp_visual/dir_setting/curse/grasp_portal(spawn_turf, affected_mob.dir)
playsound(spawn_turf, 'sound/effects/curse2.ogg', 80, TRUE, -1)
var/obj/projectile/curse_hand/hel/hand = new (spawn_turf)
- hand.preparePixelProjectile(owner, spawn_turf)
+ hand.preparePixelProjectile(affected_mob, spawn_turf)
if(QDELETED(hand)) //safety check if above fails - above has a stack trace if it does fail
return
hand.fire()
//At the end, we clear up any loose hanging timers just in case and spawn any remaining lag_remaining hands all at once.
-/datum/reagent/inverse/helgrasp/on_mob_delete(mob/living/owner)
+/datum/reagent/inverse/helgrasp/on_mob_delete(mob/living/affected_mob)
+ . = ..()
var/hands = 0
while(lag_remainder > hands)
- spawn_hands(owner)
+ spawn_hands(affected_mob)
hands++
for(var/id in timer_ids) // So that we can be certain that all timers are deleted at the end.
deltimer(id)
timer_ids.Cut()
- return ..()
/datum/reagent/inverse/helgrasp/heretic
name = "Grasp of the Mansus"
@@ -139,9 +141,9 @@ Basically, we fill the time between now and 2s from now with hands based off the
liver_damage = 0.1
addiction_types = list(/datum/addiction/medicine = 4)
-/datum/reagent/impurity/libitoil/on_mob_add(mob/living/L, amount)
+/datum/reagent/impurity/libitoil/on_mob_add(mob/living/affected_mob, amount)
. = ..()
- var/mob/living/carbon/consumer = L
+ var/mob/living/carbon/consumer = affected_mob
if(!consumer)
return
RegisterSignal(consumer, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_gained_organ))
@@ -163,9 +165,9 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/obj/item/organ/internal/liver/this_liver = organ
this_liver.alcohol_tolerance /= 2
-/datum/reagent/impurity/libitoil/on_mob_delete(mob/living/L)
+/datum/reagent/impurity/libitoil/on_mob_delete(mob/living/affected_mob)
. = ..()
- var/mob/living/carbon/consumer = L
+ var/mob/living/carbon/consumer = affected_mob
UnregisterSignal(consumer, COMSIG_CARBON_LOSE_ORGAN)
UnregisterSignal(consumer, COMSIG_CARBON_GAIN_ORGAN)
var/obj/item/organ/internal/liver/this_liver = consumer.get_organ_slot(ORGAN_SLOT_LIVER)
@@ -187,8 +189,8 @@ Basically, we fill the time between now and 2s from now with hands based off the
liver_damage = 0
/datum/reagent/impurity/probital_failed/overdose_start(mob/living/carbon/M)
+ . = ..()
metabolization_rate = 4 * REAGENTS_METABOLISM
- ..()
/datum/reagent/peptides_failed
name = "Prion Peptides"
@@ -196,11 +198,11 @@ Basically, we fill the time between now and 2s from now with hands based off the
description = "These inhibitory peptides drains nutrition and causes brain damage in the patient!"
ph = 2.1
-/datum/reagent/peptides_failed/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
- owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.25 * seconds_per_tick, 170)
- owner.adjust_nutrition(-5 * REAGENTS_METABOLISM * seconds_per_tick)
- ..()
- return TRUE
+/datum/reagent/peptides_failed/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.25 * seconds_per_tick, 170))
+ . = UPDATE_MOB_HEALTH
+ affected_mob.adjust_nutrition(-5 * REAGENTS_METABOLISM * seconds_per_tick)
//Lenturi
//impure
@@ -210,13 +212,13 @@ Basically, we fill the time between now and 2s from now with hands based off the
addiction_types = list(/datum/addiction/medicine = 8)
liver_damage = 0
-/datum/reagent/impurity/lentslurri/on_mob_metabolize(mob/living/carbon/owner)
- owner.add_movespeed_modifier(/datum/movespeed_modifier/reagent/lenturi)
- return ..()
+/datum/reagent/impurity/lentslurri/on_mob_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
+ affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/lenturi)
-/datum/reagent/impurity/lentslurri/on_mob_end_metabolize(mob/living/carbon/owner)
- owner.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/lenturi)
- return ..()
+/datum/reagent/impurity/lentslurri/on_mob_end_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
+ affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/lenturi)
//failed
/datum/reagent/inverse/ichiyuri
@@ -233,19 +235,19 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/spammer = 0
//Just the removed itching mechanism - omage to it's origins.
-/datum/reagent/inverse/ichiyuri/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
- if(prob(resetting_probability) && !(HAS_TRAIT(owner, TRAIT_RESTRAINED) || owner.incapacitated()))
+/datum/reagent/inverse/ichiyuri/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ if(prob(resetting_probability) && !(HAS_TRAIT(affected_mob, TRAIT_RESTRAINED) || affected_mob.incapacitated()))
. = TRUE
if(spammer < world.time)
- to_chat(owner,span_warning("You can't help but itch yourself."))
+ to_chat(affected_mob,span_warning("You can't help but itch yourself."))
spammer = world.time + (10 SECONDS)
var/scab = rand(1,7)
- owner.adjustBruteLoss(scab*REM)
- owner.bleed(scab)
+ if(affected_mob.adjustBruteLoss(scab*REM, updating_health = FALSE))
+ . = UPDATE_MOB_HEALTH
+ affected_mob.bleed(scab)
resetting_probability = 0
- resetting_probability += (5*(current_cycle/10) * seconds_per_tick) // 10 iterations = >51% to itch
- ..()
- return .
+ resetting_probability += (5*((current_cycle-1)/10) * seconds_per_tick) // 10 iterations = >51% to itch
//Aiuri
//impure
@@ -258,14 +260,14 @@ Basically, we fill the time between now and 2s from now with hands based off the
/// blurriness at the start of taking the med
var/amount_of_blur_applied = 0 SECONDS
-/datum/reagent/impurity/aiuri/on_mob_add(mob/living/owner, amount)
+/datum/reagent/impurity/aiuri/on_mob_add(mob/living/affected_mob, amount)
. = ..()
amount_of_blur_applied = creation_purity * (volume / metabolization_rate) * 2 SECONDS
- owner.adjust_eye_blur(amount_of_blur_applied)
+ affected_mob.adjust_eye_blur(amount_of_blur_applied)
-/datum/reagent/impurity/aiuri/on_mob_delete(mob/living/owner, amount)
+/datum/reagent/impurity/aiuri/on_mob_delete(mob/living/affected_mob, amount)
. = ..()
- owner.adjust_eye_blur(-amount_of_blur_applied)
+ affected_mob.adjust_eye_blur(-amount_of_blur_applied)
//Hercuri
//inverse
@@ -280,14 +282,14 @@ Basically, we fill the time between now and 2s from now with hands based off the
taste_description = "heat! Ouch!"
addiction_types = list(/datum/addiction/medicine = 2.5)
-/datum/reagent/inverse/hercuri/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
+/datum/reagent/inverse/hercuri/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
var/heating = rand(5, 25) * creation_purity * REM * seconds_per_tick
- owner.reagents?.chem_temp += heating
- owner.adjust_bodytemperature(heating * TEMPERATURE_DAMAGE_COEFFICIENT)
- if(!ishuman(owner))
+ affected_mob.reagents?.chem_temp += heating
+ affected_mob.adjust_bodytemperature(heating * TEMPERATURE_DAMAGE_COEFFICIENT)
+ if(!ishuman(affected_mob))
return
- var/mob/living/carbon/human/human = owner
+ var/mob/living/carbon/human/human = affected_mob
human.adjust_coretemperature(heating * TEMPERATURE_DAMAGE_COEFFICIENT)
/datum/reagent/inverse/hercuri/expose_mob(mob/living/carbon/exposed_mob, methods=VAPOR, reac_volume)
@@ -298,13 +300,14 @@ Basically, we fill the time between now and 2s from now with hands based off the
exposed_mob.adjust_bodytemperature(reac_volume * TEMPERATURE_DAMAGE_COEFFICIENT)
exposed_mob.adjust_fire_stacks(reac_volume / 2)
-/datum/reagent/inverse/hercuri/overdose_process(mob/living/carbon/owner, seconds_per_tick, times_fired)
+/datum/reagent/inverse/hercuri/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- owner.adjustOrganLoss(ORGAN_SLOT_LIVER, 2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags) //Makes it so you can't abuse it with pyroxadone very easily (liver dies from 25u unless it's fully upgraded)
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)) //Makes it so you can't abuse it with pyroxadone very easily (liver dies from 25u unless it's fully upgraded)
+ . = UPDATE_MOB_HEALTH
var/heating = 10 * creation_purity * REM * seconds_per_tick * TEMPERATURE_DAMAGE_COEFFICIENT
- owner.adjust_bodytemperature(heating) //hot hot
- if(ishuman(owner))
- var/mob/living/carbon/human/human = owner
+ affected_mob.adjust_bodytemperature(heating) //hot hot
+ if(ishuman(affected_mob))
+ var/mob/living/carbon/human/human = affected_mob
human.adjust_coretemperature(heating)
/datum/reagent/inverse/healing/tirimol
@@ -317,16 +320,17 @@ Basically, we fill the time between now and 2s from now with hands based off the
addiction_types = list(/datum/addiction/medicine = 5)
//Makes patients fall asleep, then boosts the purirty of their medicine reagents if they're asleep
-/datum/reagent/inverse/healing/tirimol/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
+/datum/reagent/inverse/healing/tirimol/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
switch(current_cycle)
- if(1 to 10)//same delay as chloral hydrate
+ if(2 to 11)//same delay as chloral hydrate
if(prob(50))
- owner.emote("yawn")
- if(10 to INFINITY)
- owner.Sleeping(40)
+ affected_mob.emote("yawn")
+ if(11 to INFINITY)
+ affected_mob.Sleeping(40)
. = 1
- if(owner.IsSleeping())
- for(var/datum/reagent/reagent as anything in owner.reagents.reagent_list)
+ if(affected_mob.IsSleeping())
+ for(var/datum/reagent/reagent as anything in affected_mob.reagents.reagent_list)
if(reagent in cached_reagent_list)
continue
if(!istype(reagent, /datum/reagent/medicine))
@@ -334,23 +338,22 @@ Basically, we fill the time between now and 2s from now with hands based off the
reagent.creation_purity *= 1.25
cached_reagent_list += reagent
- else if(!owner.IsSleeping() && length(cached_reagent_list))
+ else if(!affected_mob.IsSleeping() && length(cached_reagent_list))
for(var/datum/reagent/reagent as anything in cached_reagent_list)
if(!reagent)
continue
reagent.creation_purity *= 0.8
cached_reagent_list = list()
- ..()
-/datum/reagent/inverse/healing/tirimol/on_mob_delete(mob/living/owner)
- if(owner.IsSleeping())
- owner.visible_message(span_notice("[icon2html(owner, viewers(DEFAULT_MESSAGE_RANGE, src))] [owner] lets out a hearty snore!"))//small way of letting people know the supersnooze is ended
+/datum/reagent/inverse/healing/tirimol/on_mob_delete(mob/living/affected_mob)
+ . = ..()
+ if(affected_mob.IsSleeping())
+ affected_mob.visible_message(span_notice("[icon2html(affected_mob, viewers(DEFAULT_MESSAGE_RANGE, src))] [affected_mob] lets out a hearty snore!"))//small way of letting people know the supersnooze is ended
for(var/datum/reagent/reagent as anything in cached_reagent_list)
if(!reagent)
continue
reagent.creation_purity *= 0.8
cached_reagent_list = list()
- ..()
//convermol
//inverse
@@ -369,11 +372,11 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/cached_cold_level_2
var/cached_cold_level_3
-/datum/reagent/inverse/healing/convermol/on_mob_add(mob/living/owner, amount)
+/datum/reagent/inverse/healing/convermol/on_mob_add(mob/living/affected_mob, amount)
. = ..()
- RegisterSignal(owner, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_gained_organ))
- RegisterSignal(owner, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_removed_organ))
- var/obj/item/organ/internal/lungs/lungs = owner.get_organ_slot(ORGAN_SLOT_LUNGS)
+ RegisterSignal(affected_mob, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_gained_organ))
+ RegisterSignal(affected_mob, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_removed_organ))
+ var/obj/item/organ/internal/lungs/lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
if(!lungs)
return
apply_lung_levels(lungs)
@@ -416,11 +419,11 @@ Basically, we fill the time between now and 2s from now with hands based off the
lungs.cold_level_2_threshold = cached_cold_level_2
lungs.cold_level_3_threshold = cached_cold_level_3
-/datum/reagent/inverse/healing/convermol/on_mob_delete(mob/living/owner)
+/datum/reagent/inverse/healing/convermol/on_mob_delete(mob/living/affected_mob)
. = ..()
- UnregisterSignal(owner, COMSIG_CARBON_LOSE_ORGAN)
- UnregisterSignal(owner, COMSIG_CARBON_GAIN_ORGAN)
- var/obj/item/organ/internal/lungs/lungs = owner.get_organ_slot(ORGAN_SLOT_LUNGS)
+ UnregisterSignal(affected_mob, COMSIG_CARBON_LOSE_ORGAN)
+ UnregisterSignal(affected_mob, COMSIG_CARBON_GAIN_ORGAN)
+ var/obj/item/organ/internal/lungs/lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
if(!lungs)
return
restore_lung_levels(lungs)
@@ -439,13 +442,13 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/poison_interval = (9 SECONDS)
-/datum/reagent/inverse/technetium/on_mob_life(mob/living/carbon/owner, seconds_per_tick, times_fired)
+/datum/reagent/inverse/technetium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
time_until_next_poison -= seconds_per_tick * (1 SECONDS)
if (time_until_next_poison <= 0)
time_until_next_poison = poison_interval
- owner.adjustToxLoss(creation_purity * 1, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(creation_purity * 1, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
//Kind of a healing effect, Presumably you're using syrinver to purge so this helps that
/datum/reagent/inverse/healing/syriniver
@@ -457,7 +460,7 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/cached_reagent_list = list()
addiction_types = list(/datum/addiction/medicine = 1.75)
-/datum/reagent/inverse/healing/syriniver/on_mob_add(mob/living/affected_mob)
+/datum/reagent/inverse/healing/syriniver/on_mob_add(mob/living/affected_mob, amount)
if(!(iscarbon(affected_mob)))
return ..()
var/mob/living/carbon/affected_carbon = affected_mob
@@ -493,12 +496,13 @@ Basically, we fill the time between now and 2s from now with hands based off the
//Heals toxins if it's the only thing present - kinda the oposite of multiver! Maybe that's why it's inverse!
/datum/reagent/inverse/healing/monover/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ var/need_mob_update
if(length(affected_mob.reagents.reagent_list) > 1)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.5 * seconds_per_tick, required_organ_flag = affected_organ_flags) //Hey! It's everyone's favourite drawback from multiver!
- return ..()
- affected_mob.adjustToxLoss(-2 * REM * creation_purity * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.5 * seconds_per_tick, required_organ_flag = affected_organ_flags) //Hey! It's everyone's favourite drawback from multiver!
+ else
+ need_mob_update = affected_mob.adjustToxLoss(-2 * REM * creation_purity * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
///Can bring a corpse back to life temporarily (if heart is intact)
///Makes wounds bleed more, if it brought someone back, they take additional brute and heart damage
@@ -527,9 +531,10 @@ Basically, we fill the time between now and 2s from now with hands based off the
)
/datum/reagent/inverse/penthrite/on_mob_dead(mob/living/carbon/affected_mob, seconds_per_tick)
+ . = ..()
var/obj/item/organ/internal/heart/heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
if(!heart || heart.organ_flags & ORGAN_FAILING)
- return ..()
+ return
metabolization_rate = 0.2 * REM
affected_mob.add_traits(trait_buffs, type)
affected_mob.set_stat(CONSCIOUS) //This doesn't touch knocked out
@@ -544,18 +549,19 @@ Basically, we fill the time between now and 2s from now with hands based off the
back_from_the_dead = TRUE
affected_mob.emote("gasp")
affected_mob.playsound_local(affected_mob, 'sound/health/fastbeat.ogg', 65)
- ..()
/datum/reagent/inverse/penthrite/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!back_from_the_dead)
- return ..()
+ return
//Following is for those brought back from the dead only
REMOVE_TRAIT(affected_mob, TRAIT_KNOCKEDOUT, CRIT_HEALTH_TRAIT)
REMOVE_TRAIT(affected_mob, TRAIT_KNOCKEDOUT, OXYLOSS_TRAIT)
for(var/datum/wound/iter_wound as anything in affected_mob.all_wounds)
iter_wound.adjust_blood_flow(1-creation_purity)
- affected_mob.adjustBruteLoss(5 * (1-creation_purity) * seconds_per_tick, required_bodytype = affected_bodytype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, (1 + (1-creation_purity)) * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(5 * (1-creation_purity) * seconds_per_tick, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, (1 + (1-creation_purity)) * seconds_per_tick, required_organ_flag = affected_organ_flags)
if(affected_mob.health < HEALTH_THRESHOLD_CRIT)
affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/nooartrium)
if(affected_mob.health < HEALTH_THRESHOLD_FULLCRIT)
@@ -563,19 +569,20 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/obj/item/organ/internal/heart/heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
if(!heart || heart.organ_flags & ORGAN_FAILING)
remove_buffs(affected_mob)
- ..()
- return TRUE
-
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
+
/datum/reagent/inverse/penthrite/on_mob_delete(mob/living/carbon/affected_mob)
+ . = ..()
remove_buffs(affected_mob)
var/obj/item/organ/internal/heart/heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
if(affected_mob.health < -500 || heart.organ_flags & ORGAN_FAILING)//Honestly commendable if you get -500
explosion(affected_mob, light_impact_range = 1, explosion_cause = src)
qdel(heart)
affected_mob.visible_message(span_boldwarning("[affected_mob]'s heart explodes!"))
- return ..()
/datum/reagent/inverse/penthrite/overdose_start(mob/living/carbon/affected_mob)
+ . = ..()
if(!back_from_the_dead)
return ..()
var/obj/item/organ/internal/heart/heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
@@ -650,7 +657,7 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/datum/brain_trauma/temp_trauma
/datum/reagent/inverse/neurine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- .=..()
+ . = ..()
if(temp_trauma)
return
if(!(SPT_PROB(creation_purity*10, seconds_per_tick)))
@@ -658,7 +665,7 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/traumalist = subtypesof(/datum/brain_trauma)
var/list/forbiddentraumas = list(
/datum/brain_trauma/severe/split_personality, // Split personality uses a ghost, I don't want to use a ghost for a temp thing
- /datum/brain_trauma/special/obsessed, // Obsessed sets the owner as an antag - I presume this will lead to problems, so we'll remove it
+ /datum/brain_trauma/special/obsessed, // Obsessed sets the affected_mob as an antag - I presume this will lead to problems, so we'll remove it
/datum/brain_trauma/hypnosis, // Hypnosis, same reason as obsessed, plus a bug makes it remain even after the neurowhine purges and then turn into "nothing" on the med reading upon a second application
/datum/brain_trauma/special/honorbound, // Designed to be chaplain exclusive
)
@@ -672,7 +679,7 @@ Basically, we fill the time between now and 2s from now with hands based off the
return
/datum/reagent/inverse/neurine/on_mob_delete(mob/living/carbon/affected_mob)
- .=..()
+ . = ..()
if(!temp_trauma)
return
if(istype(temp_trauma, /datum/brain_trauma/special/imaginary_friend))//Good friends stay by you, no matter what
@@ -680,7 +687,7 @@ Basically, we fill the time between now and 2s from now with hands based off the
affected_mob.cure_trauma_type(temp_trauma, resilience = TRAUMA_RESILIENCE_MAGIC)
/datum/reagent/inverse/corazargh
- name = "Corazargh" //It's what you yell! Though, if you've a better name feel free. Also an omage to an older chem
+ name = "Corazargh" //It's what you yell! Though, if you've a better name feel free. Also an homage to an older chem
description = "Interferes with the body's natural pacemaker, forcing the patient to manually beat their heart."
color = "#5F5F5F"
self_consuming = TRUE
@@ -689,82 +696,22 @@ Basically, we fill the time between now and 2s from now with hands based off the
metabolization_rate = REM
chemical_flags = REAGENT_DEAD_PROCESS
tox_damage = 0
- ///Weakref to the old heart we're swapping for
- var/datum/weakref/original_heart_ref
- ///Weakref to the new heart that's temp added
- var/datum/weakref/manual_heart_ref
-///Creates a new cursed heart and puts the old inside of it, then replaces the position of the old
+///Give the victim the manual heart beating component.
/datum/reagent/inverse/corazargh/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
if(!iscarbon(affected_mob))
return
var/mob/living/carbon/carbon_mob = affected_mob
- var/obj/item/organ/internal/heart/original_heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
- if(!original_heart)
- return
- original_heart_ref = WEAKREF(original_heart)
-
- var/obj/item/organ/internal/heart/cursed/manual_heart = new(null, src)
- manual_heart_ref = WEAKREF(manual_heart)
- original_heart.Remove(carbon_mob, special = TRUE) //So we don't suddenly die
- original_heart.forceMove(manual_heart)
- original_heart.organ_flags |= ORGAN_FROZEN //Not actually frozen, but we want to pause decay
- manual_heart.Insert(carbon_mob, special = TRUE)
- //these last so instert doesn't call them
- RegisterSignal(carbon_mob, COMSIG_CARBON_GAIN_ORGAN, PROC_REF(on_gained_organ))
- RegisterSignal(carbon_mob, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_removed_organ))
- to_chat(affected_mob, span_userdanger("You feel your heart suddenly stop beating on it's own - you'll have to manually beat it!"))
- ..()
-
-///Intercepts the new heart and creates a new cursed heart - putting the old inside of it
-/datum/reagent/inverse/corazargh/proc/on_gained_organ(mob/affected_mob, obj/item/organ/organ)
- SIGNAL_HANDLER
- if(!istype(organ, /obj/item/organ/internal/heart))
- return
- // DO NOT REACT TO YOUR OWN HEART ADDITION I SWEAR TO CHRIST
- var/obj/item/organ/internal/heart/cursed/manual_heart = manual_heart_ref?.resolve()
- if(organ == manual_heart)
- return
-
- var/mob/living/carbon/affected_carbon = affected_mob
- var/obj/item/organ/internal/heart/original_heart = organ
- original_heart_ref = WEAKREF(original_heart)
- original_heart.Remove(affected_carbon, special = TRUE)
- if(!manual_heart)
- manual_heart = new(null, src)
- manual_heart_ref = WEAKREF(manual_heart)
- original_heart.forceMove(manual_heart)
- original_heart.organ_flags |= ORGAN_FROZEN //Not actually frozen, but we want to pause decay
- manual_heart.Insert(affected_carbon, special = TRUE)
-
-///If we're ejecting out the organ - replace it with the original
-/datum/reagent/inverse/corazargh/proc/on_removed_organ(mob/prev_owner, obj/item/organ/organ)
- SIGNAL_HANDLER
- var/obj/item/organ/internal/heart/cursed/manual_heart = manual_heart_ref?.resolve()
- if(organ != manual_heart)
- return
- var/obj/item/organ/internal/heart/original_heart = original_heart_ref?.resolve()
- if(!original_heart)
+ var/obj/item/organ/internal/heart/affected_heart = carbon_mob.get_organ_slot(ORGAN_SLOT_HEART)
+ if(isnull(affected_heart))
return
+ carbon_mob.AddComponent(/datum/component/manual_heart)
+ return ..()
- original_heart.forceMove(manual_heart.loc)
- original_heart.organ_flags &= ~ORGAN_FROZEN //enable decay again
- QDEL_NULL(manual_heart_ref)
-
-///We're done - remove the curse and restore the old one
+///We're done - remove the curse
/datum/reagent/inverse/corazargh/on_mob_end_metabolize(mob/living/affected_mob)
- //Do these first so Insert doesn't call them
- UnregisterSignal(affected_mob, COMSIG_CARBON_LOSE_ORGAN)
- UnregisterSignal(affected_mob, COMSIG_CARBON_GAIN_ORGAN)
- if(!iscarbon(affected_mob))
- return
- var/mob/living/carbon/affected_carbon = affected_mob
- var/obj/item/organ/internal/heart/original_heart = original_heart_ref?.resolve()
- if(original_heart) //Mostly a just in case
- original_heart.organ_flags &= ~ORGAN_FROZEN //enable decay again
- original_heart.Insert(affected_carbon, special = TRUE)
- QDEL_NULL(manual_heart_ref)
- to_chat(affected_mob, span_userdanger("You feel your heart start beating normally again!"))
+ qdel(affected_mob.GetComponent(/datum/component/manual_heart))
..()
/datum/reagent/inverse/antihol
@@ -778,9 +725,9 @@ Basically, we fill the time between now and 2s from now with hands based off the
tox_damage = 0
/datum/reagent/inverse/antihol/on_mob_life(mob/living/carbon/C, seconds_per_tick, times_fired)
+ . = ..()
for(var/datum/reagent/consumable/ethanol/alcohol in C.reagents.reagent_list)
alcohol.boozepwr += seconds_per_tick
- ..()
/datum/reagent/inverse/oculine
name = "Oculater"
@@ -797,19 +744,19 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/headache = FALSE
/datum/reagent/inverse/oculine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(headache)
return ..()
if(SPT_PROB(100 * creation_purity, seconds_per_tick))
affected_mob.become_blind(IMPURE_OCULINE)
to_chat(affected_mob, span_danger("You suddenly develop a pounding headache as your vision fluxuates."))
headache = TRUE
- ..()
/datum/reagent/inverse/oculine/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.cure_blind(IMPURE_OCULINE)
if(headache)
to_chat(affected_mob, span_notice("Your headache clears up!"))
- ..()
/datum/reagent/impurity/inacusiate
name = "Tinacusiate"
@@ -825,15 +772,15 @@ Basically, we fill the time between now and 2s from now with hands based off the
var/randomSpan
/datum/reagent/impurity/inacusiate/on_mob_metabolize(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
randomSpan = pick(list("clown", "small", "big", "hypnophrase", "alien", "cult", "alert", "danger", "emote", "yell", "brass", "sans", "papyrus", "robot", "his_grace", "phobia"))
RegisterSignal(affected_mob, COMSIG_MOVABLE_HEAR, PROC_REF(owner_hear))
to_chat(affected_mob, span_warning("Your hearing seems to be a bit off!"))
- ..()
/datum/reagent/impurity/inacusiate/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
UnregisterSignal(affected_mob, COMSIG_MOVABLE_HEAR)
to_chat(affected_mob, span_notice("You start hearing things normally again."))
- ..()
/datum/reagent/impurity/inacusiate/proc/owner_hear(mob/living/owner, list/hearing_args)
SIGNAL_HANDLER
diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_toxin_reagents.dm
index 947c83c3166af2..0872fa6658815d 100644
--- a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_toxin_reagents.dm
@@ -18,7 +18,6 @@
owner.adjust_disgust(50)
..()
-
//Formaldehyde - Impure Version
/datum/reagent/impurity/methanol
name = "Methanol"
@@ -29,9 +28,10 @@
liver_damage = 0
/datum/reagent/impurity/methanol/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/obj/item/organ/internal/eyes/eyes = affected_mob.get_organ_slot(ORGAN_SLOT_EYES)
- eyes?.apply_organ_damage(0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- return ..()
+ if(eyes?.apply_organ_damage(0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
//Chloral Hydrate - Impure Version
/datum/reagent/impurity/chloralax
@@ -43,9 +43,9 @@
liver_damage = 0
/datum/reagent/impurity/chloralax/on_mob_life(mob/living/carbon/owner, seconds_per_tick)
- owner.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
-
+ . = ..()
+ if(owner.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
//Mindbreaker Toxin - Impure Version
/datum/reagent/impurity/rosenol
@@ -58,12 +58,12 @@
metabolization_rate = 0.5 * REAGENTS_METABOLISM
/datum/reagent/impurity/rosenol/on_mob_life(mob/living/carbon/owner, seconds_per_tick)
+ . = ..()
var/obj/item/organ/internal/tongue/tongue = owner.get_organ_slot(ORGAN_SLOT_TONGUE)
if(!tongue)
- return ..()
+ return
if(SPT_PROB(4.0, seconds_per_tick))
owner.manual_emote("clicks with [owner.p_their()] tongue.")
owner.say("Noice.", forced = /datum/reagent/impurity/rosenol)
if(SPT_PROB(2.0, seconds_per_tick))
owner.say(pick("Ah! That was a mistake!", "Horrible.", "Watch out everybody, the potato is really hot.", "When I was six I ate a bag of plums.", "And if there is one thing I can't stand it's tomatoes.", "And if there is one thing I love it's tomatoes.", "We had a captain who was so strict, you weren't allowed to breathe in their station.", "The unrobust ones just used to keel over and die, you'd hear them going down behind you."), forced = /datum/reagent/impurity/rosenol)
- ..()
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index 30fae4db9c7df7..a28e47703df4d3 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -13,7 +13,8 @@
current_cycle++
if(length(reagent_removal_skip_list))
return
- holder.remove_reagent(type, metabolization_rate * seconds_per_tick / affected_mob.metabolism_efficiency) //medicine reagents stay longer if you have a better metabolism
+ if(holder)
+ holder.remove_reagent(type, metabolization_rate * seconds_per_tick / affected_mob.metabolism_efficiency) //medicine reagents stay longer if you have a better metabolism
/datum/reagent/medicine/leporazine
name = "Leporazine"
@@ -23,6 +24,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/leporazine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/target_temp = affected_mob.get_body_temp_normal(apply_change = FALSE)
if(affected_mob.bodytemperature > target_temp)
affected_mob.adjust_bodytemperature(-40 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, target_temp)
@@ -34,7 +36,6 @@
affected_human.adjust_coretemperature(-40 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, target_temp)
else if(affected_human.coretemperature < (target_temp + 1))
affected_human.adjust_coretemperature(40 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, 0, target_temp)
- ..()
/datum/reagent/medicine/adminordrazine //An OP chemical for admins
name = "Adminordrazine"
@@ -66,12 +67,11 @@
mytray.visible_message(span_warning("Nothing happens..."))
/datum/reagent/medicine/adminordrazine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.heal_bodypart_damage(5 * REM * seconds_per_tick, 5 * REM * seconds_per_tick, 0, FALSE, affected_bodytype)
- affected_mob.adjustToxLoss(-5 * REM * seconds_per_tick, FALSE, TRUE, affected_biotype)
+ . = ..()
+ affected_mob.heal_bodypart_damage(brute = 5 * REM * seconds_per_tick, burn = 5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ affected_mob.adjustToxLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, forced = TRUE, required_biotype = affected_biotype)
// Heal everything! That we want to. But really don't heal reagents. Otherwise we'll lose ... us.
- affected_mob.fully_heal(full_heal_flags & ~HEAL_ALL_REAGENTS)
- ..()
- return TRUE
+ affected_mob.fully_heal(full_heal_flags & ~HEAL_ALL_REAGENTS) // there is no need to return UPDATE_MOB_HEALTH because this proc calls updatehealth()
/datum/reagent/medicine/adminordrazine/quantum_heal
name = "Quantum Medicine"
@@ -95,11 +95,11 @@
affected_mob.AdjustParalyzed(-20 * REM * seconds_per_tick)
if(holder.has_reagent(/datum/reagent/toxin/mindbreaker))
holder.remove_reagent(/datum/reagent/toxin/mindbreaker, 5 * REM * seconds_per_tick)
+ . = ..()
affected_mob.adjust_hallucinations(-20 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(16, seconds_per_tick))
- affected_mob.adjustToxLoss(1, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(1, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/synaphydramine
name = "Diphen-Synaptizine"
@@ -114,11 +114,11 @@
holder.remove_reagent(/datum/reagent/toxin/mindbreaker, 5 * REM * seconds_per_tick)
if(holder.has_reagent(/datum/reagent/toxin/histamine))
holder.remove_reagent(/datum/reagent/toxin/histamine, 5 * REM * seconds_per_tick)
+ . = ..()
affected_mob.adjust_hallucinations(-20 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(16, seconds_per_tick))
- affected_mob.adjustToxLoss(1, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/sansufentanyl
name = "Sansufentanyl"
@@ -128,16 +128,16 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/sansufentanyl/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_confusion_up_to(3 SECONDS * REM * seconds_per_tick, 5 SECONDS)
affected_mob.adjust_dizzy_up_to(6 SECONDS * REM * seconds_per_tick, 12 SECONDS)
- affected_mob.adjustStaminaLoss(1 * REM * seconds_per_tick)
+ if(affected_mob.adjustStaminaLoss(1 * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(10, seconds_per_tick))
to_chat(affected_mob, "You feel confused and disoriented.")
if(prob(30))
SEND_SOUND(affected_mob, sound('sound/weapons/flash_ring.ogg'))
- ..()
- return TRUE
/datum/reagent/medicine/cryoxadone
name = "Cryoxadone"
@@ -150,22 +150,23 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/cryoxadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
metabolization_rate = REAGENTS_METABOLISM * (0.00001 * (affected_mob.bodytemperature ** 2) + 0.5)
if(affected_mob.bodytemperature >= T0C || !HAS_TRAIT(affected_mob, TRAIT_KNOCKEDOUT))
- ..()
return
var/power = -0.00003 * (affected_mob.bodytemperature ** 2) + 3
- affected_mob.adjustOxyLoss(-3 * power * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustBruteLoss(-power * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-power * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustToxLoss(-power * REM * seconds_per_tick, FALSE, TRUE, affected_biotype) //heals TOXINLOVERs
- affected_mob.adjustCloneLoss(-power * REM * seconds_per_tick, FALSE, affected_biotype)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOxyLoss(-3 * power * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustBruteLoss(-power * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-power * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustToxLoss(-power * REM * seconds_per_tick, updating_health = FALSE, forced = TRUE, required_biotype = affected_biotype) //heals TOXINLOVERs
+ need_mob_update += affected_mob.adjustCloneLoss(-power * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
for(var/i in affected_mob.all_wounds)
var/datum/wound/iter_wound = i
iter_wound.on_xadone(power * REM * seconds_per_tick)
REMOVE_TRAIT(affected_mob, TRAIT_DISFIGURED, TRAIT_GENERIC) //fixes common causes for disfiguration
- ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
// Healing
/datum/reagent/medicine/cryoxadone/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
@@ -181,12 +182,12 @@
metabolization_rate = 1.5 * REAGENTS_METABOLISM
/datum/reagent/medicine/clonexadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.bodytemperature < T0C)
- affected_mob.adjustCloneLoss((0.00006 * (affected_mob.bodytemperature ** 2) - 6) * REM * seconds_per_tick, FALSE)
+ if(affected_mob.adjustCloneLoss((0.00006 * (affected_mob.bodytemperature ** 2) - 6) * REM * seconds_per_tick, updating_health = FALSE))
+ . = UPDATE_MOB_HEALTH
REMOVE_TRAIT(affected_mob, TRAIT_DISFIGURED, TRAIT_GENERIC)
- . = TRUE
metabolization_rate = REAGENTS_METABOLISM * (0.000015 * (affected_mob.bodytemperature ** 2) + 0.75)
- ..()
/datum/reagent/medicine/pyroxadone
name = "Pyroxadone"
@@ -197,6 +198,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/pyroxadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.bodytemperature > BODYTEMP_HEAT_DAMAGE_LIMIT)
var/power = 0
switch(affected_mob.bodytemperature)
@@ -209,17 +211,18 @@
if(affected_mob.on_fire)
power *= 2
- affected_mob.adjustOxyLoss(-2 * power * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustBruteLoss(-power * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-1.5 * power * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustToxLoss(-power * REM * seconds_per_tick, FALSE, TRUE, affected_biotype)
- affected_mob.adjustCloneLoss(-power * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOxyLoss(-2 * power * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustBruteLoss(-power * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-1.5 * power * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustToxLoss(-power * REM * seconds_per_tick, updating_health = FALSE, forced = TRUE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustCloneLoss(-power * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
for(var/i in affected_mob.all_wounds)
var/datum/wound/iter_wound = i
iter_wound.on_xadone(power * REM * seconds_per_tick)
REMOVE_TRAIT(affected_mob, TRAIT_DISFIGURED, TRAIT_GENERIC)
- . = TRUE
- ..()
/datum/reagent/medicine/rezadone
name = "Rezadone"
@@ -232,18 +235,20 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/rezadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.setCloneLoss(0) //Rezadone is almost never used in favor of cryoxadone. Hopefully this will change that. // No such luck so far
- affected_mob.heal_bodypart_damage(1 * REM * seconds_per_tick, 1 * REM * seconds_per_tick)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.setCloneLoss(0) //Rezadone is almost never used in favor of cryoxadone. Hopefully this will change that. // No such luck so far
+ need_mob_update += affected_mob.heal_bodypart_damage(brute = 1 * REM * seconds_per_tick, burn = 1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
REMOVE_TRAIT(affected_mob, TRAIT_DISFIGURED, TRAIT_GENERIC)
- ..()
- . = TRUE
/datum/reagent/medicine/rezadone/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ . = ..()
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
affected_mob.set_dizzy_if_lower(10 SECONDS * REM * seconds_per_tick)
affected_mob.set_jitter_if_lower(10 SECONDS * REM * seconds_per_tick)
- ..()
- . = TRUE
/datum/reagent/medicine/rezadone/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -263,13 +268,13 @@
ph = 8.1
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/medicine/spaceacillin/on_mob_add(mob/living/L)
+/datum/reagent/medicine/spaceacillin/on_mob_add(mob/living/affected_mob)
. = ..()
- ADD_TRAIT(L, TRAIT_VIRUS_RESISTANCE, type)
+ ADD_TRAIT(affected_mob, TRAIT_VIRUS_RESISTANCE, type)
-/datum/reagent/medicine/spaceacillin/on_mob_delete(mob/living/L)
+/datum/reagent/medicine/spaceacillin/on_mob_delete(mob/living/affected_mob)
. = ..()
- REMOVE_TRAIT(L, TRAIT_VIRUS_RESISTANCE, type)
+ REMOVE_TRAIT(affected_mob, TRAIT_VIRUS_RESISTANCE, type)
//Goon Chems. Ported mainly from Goonstation. Easily mixable (or not so easily) and provide a variety of effects.
@@ -284,18 +289,20 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/oxandrolone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
if(affected_mob.getFireLoss() > 25)
- affected_mob.adjustFireLoss(-4 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype) //Twice as effective as AIURI for severe burns
+ need_mob_update = affected_mob.adjustFireLoss(-4 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype) //Twice as effective as AIURI for severe burns
else
- affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype) //But only a quarter as effective for more minor ones
- ..()
- . = TRUE
+ need_mob_update = affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype) //But only a quarter as effective for more minor ones
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/oxandrolone/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getFireLoss()) //It only makes existing burns worse
- affected_mob.adjustFireLoss(4.5 * REM * seconds_per_tick, FALSE, FALSE, BODYTYPE_ORGANIC) // it's going to be healing either 4 or 0.5
- . = TRUE
- ..()
+ if(affected_mob.adjustFireLoss(4.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype)) // it's going to be healing either 4 or 0.5
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/salglu_solution
name = "Saline-Glucose Solution"
@@ -312,6 +319,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/salglu_solution/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
if(last_added)
affected_mob.blood_volume -= last_added
last_added = 0
@@ -321,25 +330,29 @@
last_added = new_blood_level - affected_mob.blood_volume
affected_mob.blood_volume = new_blood_level + (extra_regen * REM * seconds_per_tick)
if(SPT_PROB(18, seconds_per_tick))
- affected_mob.adjustBruteLoss(-0.5, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-0.5, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
- ..()
+ need_mob_update = affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype)
+ need_mob_update += affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/salglu_solution/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
if(SPT_PROB(1.5, seconds_per_tick))
- to_chat(affected_mob, span_warning("You feel salty."))
- holder.add_reagent(/datum/reagent/consumable/salt, 1)
- holder.remove_reagent(/datum/reagent/medicine/salglu_solution, 0.5)
+ if(holder)
+ to_chat(affected_mob, span_warning("You feel salty."))
+ holder.add_reagent(/datum/reagent/consumable/salt, 1)
+ holder.remove_reagent(/datum/reagent/medicine/salglu_solution, 0.5)
else if(SPT_PROB(1.5, seconds_per_tick))
- to_chat(affected_mob, span_warning("You feel sweet."))
- holder.add_reagent(/datum/reagent/consumable/sugar, 1)
- holder.remove_reagent(/datum/reagent/medicine/salglu_solution, 0.5)
+ if(holder)
+ to_chat(affected_mob, span_warning("You feel sweet."))
+ holder.add_reagent(/datum/reagent/consumable/sugar, 1)
+ holder.remove_reagent(/datum/reagent/medicine/salglu_solution, 0.5)
if(SPT_PROB(18, seconds_per_tick))
- affected_mob.adjustBruteLoss(0.5, FALSE, FALSE, BODYTYPE_ORGANIC)
- affected_mob.adjustFireLoss(0.5, FALSE, FALSE, BODYTYPE_ORGANIC)
- . = TRUE
- ..()
+ need_mob_update = affected_mob.adjustBruteLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype)
+ need_mob_update += affected_mob.adjustFireLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/mine_salve
name = "Miner's Salve"
@@ -351,10 +364,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_AFFECTS_WOUNDS
/datum/reagent/medicine/mine_salve/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustBruteLoss(-0.25 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-0.25 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- ..()
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(-0.25 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-0.25 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/mine_salve/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE)
. = ..()
@@ -374,13 +389,13 @@
if(show_message)
to_chat(exposed_carbon, span_danger("You feel your injuries fade away to nothing!") )
-/datum/reagent/medicine/mine_salve/on_mob_metabolize(mob/living/metabolizer)
+/datum/reagent/medicine/mine_salve/on_mob_metabolize(mob/living/affected_mob)
. = ..()
- metabolizer.apply_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type)
+ affected_mob.apply_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type)
-/datum/reagent/medicine/mine_salve/on_mob_end_metabolize(mob/living/metabolizer)
+/datum/reagent/medicine/mine_salve/on_mob_end_metabolize(mob/living/affected_mob)
. = ..()
- metabolizer.remove_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type)
+ affected_mob.remove_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type)
/datum/reagent/medicine/mine_salve/on_burn_wound_processing(datum/wound/burn/flesh/burn_wound)
burn_wound.sanitization += 0.3
@@ -398,20 +413,24 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/omnizine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(-healing * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOxyLoss(-healing * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustBruteLoss(-healing * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-healing * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- ..()
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(-healing * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOxyLoss(-healing * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustBruteLoss(-healing * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-healing * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/omnizine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(1.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOxyLoss(1.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustBruteLoss(1.5 * REM * seconds_per_tick, FALSE, FALSE, BODYTYPE_ORGANIC)
- affected_mob.adjustFireLoss(1.5 * REM * seconds_per_tick, FALSE, FALSE, BODYTYPE_ORGANIC)
- ..()
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(1.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOxyLoss(1.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustBruteLoss(1.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(1.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/omnizine/protozine
name = "Protozine"
@@ -433,21 +452,21 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/calomel/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
for(var/datum/reagent/target_reagent in affected_mob.reagents.reagent_list)
if(istype(target_reagent, /datum/reagent/medicine/calomel))
continue
affected_mob.reagents.remove_reagent(target_reagent.type, 3 * REM * seconds_per_tick)
var/toxin_amount = round(affected_mob.health / 40, 0.1)
- affected_mob.adjustToxLoss(toxin_amount * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ if(affected_mob.adjustToxLoss(toxin_amount * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/calomel/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
for(var/datum/reagent/medicine/calomel/target_reagent in affected_mob.reagents.reagent_list)
affected_mob.reagents.remove_reagent(target_reagent.type, 2 * REM * seconds_per_tick)
- affected_mob.adjustToxLoss(2.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ if(affected_mob.adjustToxLoss(2.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/ammoniated_mercury
name = "Ammoniated Mercury"
@@ -463,22 +482,22 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/ammoniated_mercury/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/toxin_chem_amount = 0
for(var/datum/reagent/toxin/target_reagent in affected_mob.reagents.reagent_list)
toxin_chem_amount += 1
affected_mob.reagents.remove_reagent(target_reagent.type, 5 * REM * seconds_per_tick)
var/toxin_amount = round(affected_mob.getBruteLoss() / 15, 0.1) + round(affected_mob.getFireLoss() / 30, 0.1) - 3
- affected_mob.adjustToxLoss(toxin_amount * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ if(affected_mob.adjustToxLoss(toxin_amount * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
if(toxin_chem_amount == 0)
for(var/datum/reagent/medicine/ammoniated_mercury/target_reagent in affected_mob.reagents.reagent_list)
affected_mob.reagents.remove_reagent(target_reagent.type, 1 * REM * seconds_per_tick)
- ..()
- return TRUE
/datum/reagent/medicine/ammoniated_mercury/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(3 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ . = ..()
+ if(affected_mob.adjustToxLoss(3 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/potass_iodide
name = "Potassium Iodide"
@@ -494,15 +513,14 @@
ADD_TRAIT(affected_mob, TRAIT_HALT_RADIATION_EFFECTS, "[type]")
/datum/reagent/medicine/potass_iodide/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_HALT_RADIATION_EFFECTS, "[type]")
- return ..()
/datum/reagent/medicine/potass_iodide/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if (HAS_TRAIT(affected_mob, TRAIT_IRRADIATED))
- affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, required_biotype = affected_biotype)
-
- ..()
- return TRUE
+ . = ..()
+ if(HAS_TRAIT(affected_mob, TRAIT_IRRADIATED))
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/pen_acid
name = "Pentetic Acid"
@@ -518,16 +536,16 @@
ADD_TRAIT(affected_mob, TRAIT_HALT_RADIATION_EFFECTS, "[type]")
/datum/reagent/medicine/pen_acid/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_HALT_RADIATION_EFFECTS, "[type]")
- return ..()
/datum/reagent/medicine/pen_acid/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ . = ..()
+ if(affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
for(var/datum/reagent/R in affected_mob.reagents.reagent_list)
if(R != src)
affected_mob.reagents.remove_reagent(R.type, 2 * REM * seconds_per_tick)
- ..()
- . = TRUE
/datum/reagent/medicine/sal_acid
name = "Salicylic Acid"
@@ -540,18 +558,20 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/sal_acid/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
if(affected_mob.getBruteLoss() > 25)
- affected_mob.adjustBruteLoss(-4 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ need_mob_update = affected_mob.adjustBruteLoss(-4 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
else
- affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- ..()
- . = TRUE
+ need_mob_update = affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/sal_acid/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.getBruteLoss()) //It only makes existing bruises worse
- affected_mob.adjustBruteLoss(4.5 * REM * seconds_per_tick, FALSE, FALSE, BODYTYPE_ORGANIC) // it's going to be healing either 4 or 0.5
- . = TRUE
- ..()
+ if(affected_mob.adjustBruteLoss(4.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)) // it's going to be healing either 4 or 0.5
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/salbutamol
name = "Salbutamol"
@@ -563,14 +583,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/salbutamol/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOxyLoss(-3 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOxyLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
if(affected_mob.losebreath >= 4)
var/obj/item/organ/internal/lungs/affected_lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
var/our_respiration_type = affected_lungs ? affected_lungs.respiration_type : affected_mob.mob_respiration_type // use lungs' respiration type or mob_respiration_type if no lungs
if(our_respiration_type & affected_respiration_type)
affected_mob.losebreath -= 2 * REM * seconds_per_tick
- ..()
- . = TRUE
+ need_mob_update = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/ephedrine
name = "Ephedrine"
@@ -587,16 +610,17 @@
inverse_chem_val = 0.4
/datum/reagent/medicine/ephedrine/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/ephedrine)
ADD_TRAIT(affected_mob, TRAIT_BATON_RESISTANCE, type)
/datum/reagent/medicine/ephedrine/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/ephedrine)
REMOVE_TRAIT(affected_mob, TRAIT_BATON_RESISTANCE, type)
- ..()
/datum/reagent/medicine/ephedrine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(10 * (1.5-creation_purity), seconds_per_tick) && iscarbon(affected_mob))
var/obj/item/I = affected_mob.get_active_held_item()
if(I && affected_mob.dropItemToGround(I))
@@ -604,11 +628,11 @@
affected_mob.set_jitter_if_lower(20 SECONDS)
affected_mob.AdjustAllImmobility(-20 * REM * seconds_per_tick * normalise_creation_purity())
- affected_mob.adjustStaminaLoss(-1 * REM * seconds_per_tick * normalise_creation_purity(), FALSE)
- ..()
- return TRUE
+ affected_mob.adjustStaminaLoss(-1 * REM * seconds_per_tick * normalise_creation_purity(), updating_stamina = FALSE)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/ephedrine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(1 * (1 + (1-normalise_creation_purity())), seconds_per_tick) && iscarbon(affected_mob))
var/datum/disease/D = new /datum/disease/heart_failure
affected_mob.ForceContractDisease(D)
@@ -619,10 +643,9 @@
to_chat(affected_mob, span_notice("[pick("Your head pounds.", "You feel a tight pain in your chest.", "You find it hard to stay still.", "You feel your heart practically beating out of your chest.")]"))
if(SPT_PROB(18 * (1 + (1-normalise_creation_purity())), seconds_per_tick))
- affected_mob.adjustToxLoss(1, FALSE, required_biotype = affected_biotype)
+ affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
affected_mob.losebreath++
- . = TRUE
- return TRUE
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/diphenhydramine
name = "Diphenhydramine"
@@ -638,7 +661,7 @@
affected_mob.adjust_drowsiness(2 SECONDS)
affected_mob.adjust_jitter(-2 SECONDS * REM * seconds_per_tick)
holder.remove_reagent(/datum/reagent/toxin/histamine, 3 * REM * seconds_per_tick)
- ..()
+ return ..()
/datum/reagent/medicine/morphine
name = "Morphine"
@@ -652,32 +675,31 @@
addiction_types = list(/datum/addiction/opioids = 10)
/datum/reagent/medicine/morphine/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_movespeed_mod_immunities(type, /datum/movespeed_modifier/damage_slowdown)
/datum/reagent/medicine/morphine/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.remove_movespeed_mod_immunities(type, /datum/movespeed_modifier/damage_slowdown)
- ..()
/datum/reagent/medicine/morphine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle >= 5)
+ . = ..()
+ if(current_cycle > 5)
affected_mob.add_mood_event("numb", /datum/mood_event/narcotic_medium, name)
switch(current_cycle)
- if(11)
+ if(12)
to_chat(affected_mob, span_warning("You start to feel tired...") )
- if(12 to 24)
+ if(13 to 25)
affected_mob.adjust_drowsiness(2 SECONDS * REM * seconds_per_tick)
- if(24 to INFINITY)
+ if(25 to INFINITY)
affected_mob.Sleeping(40 * REM * seconds_per_tick)
- . = TRUE
- ..()
/datum/reagent/medicine/morphine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(18, seconds_per_tick))
affected_mob.drop_all_held_items()
affected_mob.set_dizzy_if_lower(4 SECONDS)
affected_mob.set_jitter_if_lower(4 SECONDS)
- ..()
/datum/reagent/medicine/oculine
@@ -729,33 +751,31 @@
restore_eyesight(prev_affected_mob, eyes)
/datum/reagent/medicine/oculine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/normalized_purity = normalise_creation_purity()
affected_mob.adjust_temp_blindness(-4 SECONDS * REM * seconds_per_tick * normalized_purity)
affected_mob.adjust_eye_blur(-4 SECONDS * REM * seconds_per_tick * normalized_purity)
var/obj/item/organ/internal/eyes/eyes = affected_mob.get_organ_slot(ORGAN_SLOT_EYES)
- if(isnull(eyes))
- return ..()
-
- // Healing eye damage will cure nearsightedness and blindness from ... eye damage
- eyes.apply_organ_damage(-2 * REM * seconds_per_tick * normalise_creation_purity(), required_organ_flag = affected_organ_flags)
- // If our eyes are seriously damaged, we have a probability of causing eye blur while healing depending on purity
- if(eyes.damaged && IS_ORGANIC_ORGAN(eyes) && SPT_PROB(16 - min(normalized_purity * 6, 12), seconds_per_tick))
- // While healing, gives some eye blur
- if(affected_mob.is_blind_from(EYE_DAMAGE))
- to_chat(affected_mob, span_warning("Your vision slowly returns..."))
- affected_mob.adjust_eye_blur(20 SECONDS)
- else if(affected_mob.is_nearsighted_from(EYE_DAMAGE))
- to_chat(affected_mob, span_warning("The blackness in your peripheral vision begins to fade."))
- affected_mob.adjust_eye_blur(5 SECONDS)
-
- return ..() || TRUE
+ if(eyes)
+ // Healing eye damage will cure nearsightedness and blindness from ... eye damage
+ if(eyes.apply_organ_damage(-2 * REM * seconds_per_tick * normalise_creation_purity(), required_organ_flag = affected_organ_flags))
+ . = UPDATE_MOB_HEALTH
+ // If our eyes are seriously damaged, we have a probability of causing eye blur while healing depending on purity
+ if(eyes.damaged && IS_ORGANIC_ORGAN(eyes) && SPT_PROB(16 - min(normalized_purity * 6, 12), seconds_per_tick))
+ // While healing, gives some eye blur
+ if(affected_mob.is_blind_from(EYE_DAMAGE))
+ to_chat(affected_mob, span_warning("Your vision slowly returns..."))
+ affected_mob.adjust_eye_blur(20 SECONDS)
+ else if(affected_mob.is_nearsighted_from(EYE_DAMAGE))
+ to_chat(affected_mob, span_warning("The blackness in your peripheral vision begins to fade."))
+ affected_mob.adjust_eye_blur(5 SECONDS)
/datum/reagent/medicine/oculine/on_mob_delete(mob/living/affected_mob)
+ . = ..()
var/obj/item/organ/internal/eyes/eyes = affected_mob.get_organ_slot(ORGAN_SLOT_EYES)
if(!eyes)
return
restore_eyesight(affected_mob, eyes)
- ..()
/datum/reagent/medicine/inacusiate
name = "Inacusiate"
@@ -783,12 +803,12 @@
message = composer.compose_message(affected_mob, message_language, message, null, spans, message_mods)
/datum/reagent/medicine/inacusiate/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/obj/item/organ/internal/ears/ears = affected_mob.get_organ_slot(ORGAN_SLOT_EARS)
if(!ears)
- return ..()
+ return
ears.adjustEarDamage(-4 * REM * seconds_per_tick * normalise_creation_purity(), -4 * REM * seconds_per_tick * normalise_creation_purity())
- ..()
- return TRUE
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/inacusiate/on_mob_delete(mob/living/affected_mob)
. = ..()
@@ -809,16 +829,19 @@
ADD_TRAIT(affected_mob, TRAIT_PREVENT_IMPLANT_AUTO_EXPLOSION, "[type]")
/datum/reagent/medicine/atropine/on_mob_delete(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_PREVENT_IMPLANT_AUTO_EXPLOSION, "[type]")
- return ..()
/datum/reagent/medicine/atropine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.health <= affected_mob.crit_threshold)
- affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustBruteLoss(-2* REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustOxyLoss(-5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- . = TRUE
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustBruteLoss(-2* REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
var/obj/item/organ/internal/lungs/affected_lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
var/our_respiration_type = affected_lungs ? affected_lungs.respiration_type : affected_mob.mob_respiration_type
if(our_respiration_type & affected_respiration_type)
@@ -826,14 +849,13 @@
if(SPT_PROB(10, seconds_per_tick))
affected_mob.set_dizzy_if_lower(10 SECONDS)
affected_mob.set_jitter_if_lower(10 SECONDS)
- ..()
/datum/reagent/medicine/atropine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
+ . = ..()
+ if(affected_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
affected_mob.set_dizzy_if_lower(2 SECONDS * REM * seconds_per_tick)
affected_mob.set_jitter_if_lower(2 SECONDS * REM * seconds_per_tick)
- ..()
/datum/reagent/medicine/epinephrine
name = "Epinephrine"
@@ -845,13 +867,13 @@
ph = 10.2
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/medicine/epinephrine/on_mob_metabolize(mob/living/carbon/affected_mob)
- ..()
+/datum/reagent/medicine/epinephrine/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_NOCRITDAMAGE, type)
-/datum/reagent/medicine/epinephrine/on_mob_end_metabolize(mob/living/carbon/affected_mob)
+/datum/reagent/medicine/epinephrine/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_NOCRITDAMAGE, type)
- ..()
/datum/reagent/medicine/epinephrine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
if(holder.has_reagent(/datum/reagent/toxin/lexorin))
@@ -859,36 +881,45 @@
holder.remove_reagent(/datum/reagent/medicine/epinephrine, 1 * REM * seconds_per_tick)
if(SPT_PROB(10, seconds_per_tick))
holder.add_reagent(/datum/reagent/toxin/histamine, 4)
- ..()
- return FALSE
+ return ..()
+
+ . = ..()
+
+ var/need_mob_update
if(affected_mob.health <= affected_mob.crit_threshold)
- affected_mob.adjustToxLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update = affected_mob.adjustToxLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
if(affected_mob.losebreath >= 4)
var/obj/item/organ/internal/lungs/affected_lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
var/our_respiration_type = affected_lungs ? affected_lungs.respiration_type : affected_mob.mob_respiration_type
if(our_respiration_type & affected_respiration_type)
affected_mob.losebreath -= 2 * REM * seconds_per_tick
+ need_mob_update = TRUE
if(affected_mob.losebreath < 0)
affected_mob.losebreath = 0
- affected_mob.adjustStaminaLoss(-0.5 * REM * seconds_per_tick, 0)
+ need_mob_update = TRUE
+ need_mob_update += affected_mob.adjustStaminaLoss(-0.5 * REM * seconds_per_tick, updating_stamina = FALSE)
if(SPT_PROB(10, seconds_per_tick))
affected_mob.AdjustAllImmobility(-20)
- ..()
- return TRUE
+ need_mob_update = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/epinephrine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(18, REM * seconds_per_tick))
- affected_mob.adjustStaminaLoss(2.5, 0)
- affected_mob.adjustToxLoss(1, FALSE, required_biotype = affected_biotype)
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustStaminaLoss(2.5 * REM * seconds_per_tick, updating_stamina = FALSE)
+ need_mob_update += affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
var/obj/item/organ/internal/lungs/affected_lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
var/our_respiration_type = affected_lungs ? affected_lungs.respiration_type : affected_mob.mob_respiration_type
if(our_respiration_type & affected_respiration_type)
affected_mob.losebreath++
- . = TRUE
- ..()
+ need_mob_update = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/strange_reagent
name = "Strange Reagent"
@@ -897,7 +928,6 @@
color = "#A0E85E"
metabolization_rate = 1.25 * REAGENTS_METABOLISM
taste_description = "magnets"
- harmful = TRUE
ph = 0.5
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/// The amount of damage a single unit of this will heal
@@ -990,11 +1020,13 @@
return ..()
/datum/reagent/medicine/strange_reagent/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/damage_at_random = rand(0, 250)/100 //0 to 2.5
- affected_mob.adjustBruteLoss(damage_at_random * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(damage_at_random * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- ..()
- return TRUE
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(damage_at_random * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(damage_at_random * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/mannitol
name = "Mannitol"
@@ -1009,9 +1041,9 @@
inverse_chem_val = 0.45
/datum/reagent/medicine/mannitol/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -2 * REM * seconds_per_tick * normalise_creation_purity(), required_organ_flag = affected_organ_flags)
- ..()
- return TRUE
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -2 * REM * seconds_per_tick * normalise_creation_purity(), required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
//Having mannitol in you will pause the brain damage from brain tumor (so it heals an even 2 brain damage instead of 1.8)
/datum/reagent/medicine/mannitol/on_mob_metabolize(mob/living/carbon/affected_mob)
@@ -1019,13 +1051,15 @@
ADD_TRAIT(affected_mob, TRAIT_TUMOR_SUPPRESSED, TRAIT_GENERIC)
/datum/reagent/medicine/mannitol/on_mob_end_metabolize(mob/living/carbon/affected_mob)
- REMOVE_TRAIT(affected_mob, TRAIT_TUMOR_SUPPRESSED, TRAIT_GENERIC)
. = ..()
+ REMOVE_TRAIT(affected_mob, TRAIT_TUMOR_SUPPRESSED, TRAIT_GENERIC)
/datum/reagent/medicine/mannitol/overdose_start(mob/living/affected_mob)
+ . = ..()
to_chat(affected_mob, span_notice("You suddenly feel E N L I G H T E N E D!"))
/datum/reagent/medicine/mannitol/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(65, seconds_per_tick))
return
var/list/tips
@@ -1037,7 +1071,6 @@
tips = world.file2list("strings/chemistrytips.txt")
var/message = pick(tips)
send_tip_of_the_round(affected_mob, message)
- return ..()
/datum/reagent/medicine/neurine
name = "Neurine"
@@ -1073,11 +1106,12 @@
holder.remove_reagent(/datum/reagent/consumable/ethanol/neurotoxin, 5 * REM * seconds_per_tick * normalise_creation_purity())
if(SPT_PROB(8 * normalise_creation_purity(), seconds_per_tick))
affected_mob.cure_trauma_type(resilience = TRAUMA_RESILIENCE_BASIC)
- ..()
+ return ..()
/datum/reagent/medicine/neurine/on_mob_dead(mob/living/carbon/affected_mob, seconds_per_tick)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1 * REM * seconds_per_tick * normalise_creation_purity(), required_organ_flag = affected_organ_flags)
- ..()
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1 * REM * seconds_per_tick * normalise_creation_purity(), required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/mutadone
name = "Mutadone"
@@ -1088,11 +1122,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/mutadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.remove_status_effect(/datum/status_effect/jitter)
if(affected_mob.has_dna())
affected_mob.dna.remove_all_mutations(list(MUT_NORMAL, MUT_EXTRA), TRUE)
- if(!QDELETED(affected_mob)) //We were a monkey, now a human
- ..()
/datum/reagent/medicine/antihol
name = "Antihol"
@@ -1114,13 +1147,13 @@
)
/datum/reagent/medicine/antihol/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
for(var/effect in status_effects_to_clear)
affected_mob.remove_status_effect(effect)
affected_mob.reagents.remove_all_type(/datum/reagent/consumable/ethanol, 3 * REM * seconds_per_tick * normalise_creation_purity(), FALSE, TRUE)
- affected_mob.adjustToxLoss(-0.2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ if(affected_mob.adjustToxLoss(-0.2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
affected_mob.adjust_drunk_effect(-10 * REM * seconds_per_tick * normalise_creation_purity())
- ..()
- . = TRUE
/datum/reagent/medicine/antihol/expose_mob(mob/living/carbon/exposed_carbon, methods=TOUCH, reac_volume)
. = ..()
@@ -1141,33 +1174,35 @@
addiction_types = list(/datum/addiction/stimulants = 4) //0.8 per 2 seconds
/datum/reagent/medicine/stimulants/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/stimulants)
ADD_TRAIT(affected_mob, TRAIT_BATON_RESISTANCE, type)
/datum/reagent/medicine/stimulants/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/stimulants)
REMOVE_TRAIT(affected_mob, TRAIT_BATON_RESISTANCE, type)
- ..()
/datum/reagent/medicine/stimulants/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.health < 50 && affected_mob.health > 0)
- affected_mob.adjustOxyLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustBruteLoss(-1 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-1 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
+ var/need_mob_update
+ need_mob_update += affected_mob.adjustOxyLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustBruteLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
affected_mob.AdjustAllImmobility(-60 * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(-5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- . = TRUE
+ affected_mob.adjustStaminaLoss(-5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
/datum/reagent/medicine/stimulants/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(18, seconds_per_tick))
- affected_mob.adjustStaminaLoss(2.5, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustToxLoss(1, FALSE, required_biotype = affected_biotype)
+ affected_mob.adjustStaminaLoss(2.5, updating_stamina = FALSE, required_biotype = affected_biotype)
+ affected_mob.adjustToxLoss(1, updating_health = FALSE, required_biotype = affected_biotype)
affected_mob.losebreath++
- . = TRUE
- ..()
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/insulin
name = "Insulin"
@@ -1179,10 +1214,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/insulin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(affected_mob.AdjustSleeping(-20 * REM * seconds_per_tick))
- . = TRUE
+ affected_mob.AdjustSleeping(-20 * REM * seconds_per_tick)
holder.remove_reagent(/datum/reagent/consumable/sugar, 3 * REM * seconds_per_tick)
- ..()
+ return ..()
//Trek Chems, used primarily by medibots. Only heals a specific damage type, but is very efficient.
@@ -1195,9 +1229,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/medicine/inaprovaline/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.losebreath >= 5)
affected_mob.losebreath -= 5 * REM * seconds_per_tick
- ..()
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/regen_jelly
name = "Regenerative Jelly"
@@ -1219,12 +1254,14 @@
exposed_human.set_haircolor(color, update = TRUE)
/datum/reagent/medicine/regen_jelly/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustBruteLoss(-1.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-1.5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustOxyLoss(-1.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustToxLoss(-1.5 * REM * seconds_per_tick, FALSE, TRUE, affected_biotype) //heals TOXINLOVERs
- ..()
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(-1.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-1.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-1.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustToxLoss(-1.5 * REM * seconds_per_tick, updating_health = FALSE, forced = TRUE, required_biotype = affected_biotype) //heals TOXINLOVERs
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/syndicate_nanites //Used exclusively by Syndicate medical cyborgs
name = "Restorative Nanites"
@@ -1236,21 +1273,22 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/medicine/syndicate_nanites/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustBruteLoss(-5 * REM * seconds_per_tick, FALSE) //A ton of healing - this is a 50 telecrystal investment.
- affected_mob.adjustFireLoss(-5 * REM * seconds_per_tick, FALSE)
- affected_mob.adjustOxyLoss(-15 * REM * seconds_per_tick, FALSE)
- affected_mob.adjustToxLoss(-5 * REM * seconds_per_tick, FALSE)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -15 * REM * seconds_per_tick)
- affected_mob.adjustCloneLoss(-3 * REM * seconds_per_tick, FALSE)
- ..()
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(-5 * REM * seconds_per_tick, updating_health = FALSE) //A ton of healing - this is a 50 telecrystal investment.
+ need_mob_update += affected_mob.adjustFireLoss(-5 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustOxyLoss(-15 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustToxLoss(-5 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -15 * REM * seconds_per_tick)
+ need_mob_update += affected_mob.adjustCloneLoss(-3 * REM * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/syndicate_nanites/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) //wtb flavortext messages that hint that you're vomitting up robots
+ . = ..()
if(SPT_PROB(13, seconds_per_tick))
affected_mob.reagents.remove_reagent(type, metabolization_rate*15) // ~5 units at a rate of 0.4 but i wanted a nice number in code
affected_mob.vomit(vomit_flags = VOMIT_CATEGORY_DEFAULT, vomit_type = /obj/effect/decal/cleanable/vomit/nanites, lost_nutrition = 20) // nanite safety protocols make your body expel them to prevent harmies
- ..()
- . = TRUE
/datum/reagent/medicine/earthsblood //Created by ambrosia gaia plants
name = "Earthsblood"
@@ -1263,48 +1301,53 @@
addiction_types = list(/datum/addiction/hallucinogens = 14)
/datum/reagent/medicine/earthsblood/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle <= 25) //10u has to be processed before u get into THE FUN ZONE
- affected_mob.adjustBruteLoss(-1 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-1 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustToxLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustCloneLoss(-0.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustStaminaLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1 * REM * seconds_per_tick, 150, affected_organ_flags) //This does, after all, come from ambrosia, and the most powerful ambrosia in existence, at that!
+ . = ..()
+ var/need_mob_update
+ if(current_cycle < 25) //10u has to be processed before u get into THE FUN ZONE
+ need_mob_update = affected_mob.adjustBruteLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustToxLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustCloneLoss(-0.1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustStaminaLoss(-0.5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1 * REM * seconds_per_tick, 150, affected_organ_flags) //This does, after all, come from ambrosia, and the most powerful ambrosia in existence, at that!
else
- affected_mob.adjustBruteLoss(-5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype) //slow to start, but very quick healing once it gets going
- affected_mob.adjustFireLoss(-5 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustOxyLoss(-3 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustToxLoss(-3 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustCloneLoss(-1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- affected_mob.adjustStaminaLoss(-3 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ need_mob_update = affected_mob.adjustBruteLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype) //slow to start, but very quick healing once it gets going
+ need_mob_update += affected_mob.adjustFireLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustOxyLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustToxLoss(-3 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustCloneLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustStaminaLoss(-3 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2 * REM * seconds_per_tick, 150, affected_organ_flags)
affected_mob.adjust_jitter_up_to(6 SECONDS * REM * seconds_per_tick, 1 MINUTES)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2 * REM * seconds_per_tick, 150, affected_organ_flags)
if(SPT_PROB(5, seconds_per_tick))
affected_mob.say(return_hippie_line(), forced = /datum/reagent/medicine/earthsblood)
affected_mob.adjust_drugginess_up_to(20 SECONDS * REM * seconds_per_tick, 30 SECONDS * REM * seconds_per_tick)
- ..()
- . = TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/earthsblood/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_PACIFISM, type)
/datum/reagent/medicine/earthsblood/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_PACIFISM, type)
- ..()
/datum/reagent/medicine/earthsblood/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_hallucinations_up_to(10 SECONDS * REM * seconds_per_tick, 120 SECONDS)
- if(current_cycle > 25)
- affected_mob.adjustToxLoss(4 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- if(current_cycle > 100) //podpeople get out reeeeeeeeeeeeeeeeeeeee
- affected_mob.adjustToxLoss(6 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ var/need_mob_update
+ if(current_cycle > 26)
+ need_mob_update = affected_mob.adjustToxLoss(4 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(current_cycle > 101) //podpeople get out reeeeeeeeeeeeeeeeeeeee
+ need_mob_update += affected_mob.adjustToxLoss(6 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
if(iscarbon(affected_mob))
var/mob/living/carbon/hippie = affected_mob
hippie.gain_trauma(/datum/brain_trauma/severe/pacifism)
- ..()
- . = TRUE
+
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/// Returns a hippie-esque string for the person affected by the reagent to say.
/datum/reagent/medicine/earthsblood/proc/return_hippie_line()
@@ -1327,9 +1370,9 @@
metabolization_rate = 0.4 * REAGENTS_METABOLISM
ph = 4.3
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
- harmful = TRUE
/datum/reagent/medicine/haloperidol/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
for(var/datum/reagent/drug/R in affected_mob.reagents.reagent_list)
affected_mob.reagents.remove_reagent(R.type, 5 * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(4 SECONDS * REM * seconds_per_tick)
@@ -1340,11 +1383,12 @@
if (affected_mob.get_timed_status_effect_duration(/datum/status_effect/hallucination) >= 10 SECONDS)
affected_mob.adjust_hallucinations(-10 SECONDS * REM * seconds_per_tick)
+ var/need_mob_update = FALSE
if(SPT_PROB(10, seconds_per_tick))
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1, 50, affected_organ_flags)
- affected_mob.adjustStaminaLoss(2.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1, 50, affected_organ_flags)
+ need_mob_update += affected_mob.adjustStaminaLoss(2.5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
//used for changeling's adrenaline power
/datum/reagent/medicine/changelingadrenaline
@@ -1355,29 +1399,29 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/medicine/changelingadrenaline/on_mob_life(mob/living/carbon/metabolizer, seconds_per_tick, times_fired)
- ..()
+ . = ..()
metabolizer.AdjustAllImmobility(-20 * REM * seconds_per_tick)
- metabolizer.adjustStaminaLoss(-10 * REM * seconds_per_tick, 0)
+ if(metabolizer.adjustStaminaLoss(-10 * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
metabolizer.set_jitter_if_lower(20 SECONDS * REM * seconds_per_tick)
metabolizer.set_dizzy_if_lower(20 SECONDS * REM * seconds_per_tick)
- return TRUE
/datum/reagent/medicine/changelingadrenaline/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_traits(list(TRAIT_SLEEPIMMUNE, TRAIT_BATON_RESISTANCE), type)
affected_mob.add_movespeed_mod_immunities(type, /datum/movespeed_modifier/damage_slowdown)
/datum/reagent/medicine/changelingadrenaline/on_mob_end_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.remove_traits(list(TRAIT_SLEEPIMMUNE, TRAIT_BATON_RESISTANCE), type)
affected_mob.remove_movespeed_mod_immunities(type, /datum/movespeed_modifier/damage_slowdown)
affected_mob.remove_status_effect(/datum/status_effect/dizziness)
affected_mob.remove_status_effect(/datum/status_effect/jitter)
/datum/reagent/medicine/changelingadrenaline/overdose_process(mob/living/metabolizer, seconds_per_tick, times_fired)
- metabolizer.adjustToxLoss(1 * REM * seconds_per_tick, FALSE)
- ..()
- return TRUE
+ . = ..()
+ if(metabolizer.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/changelinghaste
name = "Changeling Haste"
@@ -1387,17 +1431,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/medicine/changelinghaste/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_movespeed_modifier(/datum/movespeed_modifier/reagent/changelinghaste)
/datum/reagent/medicine/changelinghaste/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/changelinghaste)
- ..()
/datum/reagent/medicine/changelinghaste/on_mob_life(mob/living/carbon/metabolizer, seconds_per_tick, times_fired)
- metabolizer.adjustToxLoss(2 * REM * seconds_per_tick, FALSE)
- ..()
- return TRUE
+ . = ..()
+ if(metabolizer.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/higadrite
name = "Higadrite"
@@ -1411,7 +1455,7 @@
ADD_TRAIT(affected_mob, TRAIT_STABLELIVER, type)
/datum/reagent/medicine/higadrite/on_mob_end_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_STABLELIVER, type)
/datum/reagent/medicine/cordiolis_hepatico
@@ -1422,11 +1466,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/medicine/cordiolis_hepatico/on_mob_add(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.add_traits(list(TRAIT_STABLELIVER, TRAIT_STABLEHEART), type)
/datum/reagent/medicine/cordiolis_hepatico/on_mob_end_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
affected_mob.remove_traits(list(TRAIT_STABLELIVER, TRAIT_STABLEHEART), type)
/datum/reagent/medicine/muscle_stimulant
@@ -1455,29 +1499,32 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/modafinil/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_SLEEPIMMUNE, type)
- ..()
/datum/reagent/medicine/modafinil/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_SLEEPIMMUNE, type)
- ..()
/datum/reagent/medicine/modafinil/on_mob_life(mob/living/carbon/metabolizer, seconds_per_tick, times_fired)
+ . = ..()
if(!overdosed) // We do not want any effects on OD
overdose_threshold = overdose_threshold + ((rand(-10, 10) / 10) * REM * seconds_per_tick) // for extra fun
metabolizer.AdjustAllImmobility(-5 * REM * seconds_per_tick)
- metabolizer.adjustStaminaLoss(-0.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ metabolizer.adjustStaminaLoss(-0.5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
metabolizer.set_jitter_if_lower(1 SECONDS * REM * seconds_per_tick)
metabolization_rate = 0.005 * REAGENTS_METABOLISM * rand(5, 20) // randomizes metabolism between 0.02 and 0.08 per second
- . = TRUE
- ..()
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/modafinil/overdose_start(mob/living/affected_mob)
+ . = ..()
to_chat(affected_mob, span_userdanger("You feel awfully out of breath and jittery!"))
metabolization_rate = 0.025 * REAGENTS_METABOLISM // sets metabolism to 0.005 per second on overdose
/datum/reagent/medicine/modafinil/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
overdose_progress++
+ var/need_mob_update
switch(overdose_progress)
if(1 to 40)
affected_mob.adjust_jitter_up_to(2 SECONDS * REM * seconds_per_tick, 20 SECONDS)
@@ -1485,29 +1532,31 @@
affected_mob.set_dizzy_if_lower(10 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(30, seconds_per_tick))
affected_mob.losebreath++
+ need_mob_update = TRUE
if(41 to 80)
- affected_mob.adjustOxyLoss(0.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustStaminaLoss(0.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ need_mob_update = affected_mob.adjustOxyLoss(0.1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustStaminaLoss(0.1 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
affected_mob.adjust_jitter_up_to(2 SECONDS * REM * seconds_per_tick, 40 SECONDS)
affected_mob.adjust_stutter_up_to(2 SECONDS * REM * seconds_per_tick, 40 SECONDS)
affected_mob.set_dizzy_if_lower(20 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(30, seconds_per_tick))
affected_mob.losebreath++
+ need_mob_update = TRUE
if(SPT_PROB(10, seconds_per_tick))
to_chat(affected_mob, span_userdanger("You have a sudden fit!"))
affected_mob.emote("moan")
affected_mob.Paralyze(20) // you should be in a bad spot at this point unless epipen has been used
if(81)
to_chat(affected_mob, span_userdanger("You feel too exhausted to continue!")) // at this point you will eventually die unless you get charcoal
- affected_mob.adjustOxyLoss(0.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustStaminaLoss(0.1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ need_mob_update = affected_mob.adjustOxyLoss(0.1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustStaminaLoss(0.1 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
if(82 to INFINITY)
REMOVE_TRAIT(affected_mob, TRAIT_SLEEPIMMUNE, type)
affected_mob.Sleeping(100 * REM * seconds_per_tick)
- affected_mob.adjustOxyLoss(1.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustStaminaLoss(1.5 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ need_mob_update += affected_mob.adjustOxyLoss(1.5 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustStaminaLoss(1.5 * REM * seconds_per_tick, updating_stamina = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/psicodine
name = "Psicodine"
@@ -1520,28 +1569,27 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/psicodine/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_FEARLESS, type)
/datum/reagent/medicine/psicodine/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_FEARLESS, type)
- ..()
/datum/reagent/medicine/psicodine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_jitter(-12 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_dizzy(-12 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_confusion(-6 SECONDS * REM * seconds_per_tick)
affected_mob.disgust = max(affected_mob.disgust - (6 * REM * seconds_per_tick), 0)
if(affected_mob.mob_mood != null && affected_mob.mob_mood.sanity <= SANITY_NEUTRAL) // only take effect if in negative sanity and then...
affected_mob.mob_mood.set_sanity(min(affected_mob.mob_mood.sanity + (5 * REM * seconds_per_tick), SANITY_NEUTRAL)) // set minimum to prevent unwanted spiking over neutral
- ..()
- . = TRUE
/datum/reagent/medicine/psicodine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_hallucinations_up_to(10 SECONDS * REM * seconds_per_tick, 120 SECONDS)
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- . = TRUE
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/metafactor
name = "Mitogen Metabolism Factor"
@@ -1555,12 +1603,13 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/metafactor/overdose_start(mob/living/carbon/affected_mob)
+ . = ..()
metabolization_rate = 2 * REAGENTS_METABOLISM
/datum/reagent/medicine/metafactor/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(13, seconds_per_tick))
affected_mob.vomit(VOMIT_CATEGORY_DEFAULT)
- ..()
/datum/reagent/medicine/silibinin
name = "Silibinin"
@@ -1571,9 +1620,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/silibinin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, -2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)//Add a chance to cure liver trauma once implemented.
- ..()
- . = TRUE
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, -2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)) // Add a chance to cure liver trauma once implemented.
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/polypyr //This is intended to be an ingredient in advanced chems.
name = "Polypyrylium Oligomers"
@@ -1587,9 +1635,11 @@
/datum/reagent/medicine/polypyr/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) //I wanted a collection of small positive effects, this is as hard to obtain as coniine after all.
. = ..()
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, -0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- affected_mob.adjustBruteLoss(-0.35 * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- return TRUE
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, -0.25 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.adjustBruteLoss(-0.35 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/polypyr/expose_mob(mob/living/carbon/human/exposed_human, methods=TOUCH, reac_volume)
. = ..()
@@ -1600,9 +1650,9 @@
exposed_human.update_body_parts()
/datum/reagent/medicine/polypyr/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- ..()
- . = TRUE
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_LUNGS, 0.5 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags))
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/granibitaluri
name = "Granibitaluri" //achieve "GRANular" amounts of C2
@@ -1615,16 +1665,19 @@
/datum/reagent/medicine/granibitaluri/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
var/healamount = max(0.5 - round(0.01 * (affected_mob.getBruteLoss() + affected_mob.getFireLoss()), 0.1), 0) //base of 0.5 healing per cycle and loses 0.1 healing for every 10 combined brute/burn damage you have
- affected_mob.adjustBruteLoss(-healamount * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- affected_mob.adjustFireLoss(-healamount * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- ..()
- . = TRUE
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(-healamount * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustFireLoss(-healamount * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/granibitaluri/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- . = TRUE
- affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
- affected_mob.adjustToxLoss(0.2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype) //Only really deadly if you eat over 100u
- ..()
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.2 * REM * seconds_per_tick, required_organ_flag = affected_organ_flags)
+ need_mob_update += affected_mob.adjustToxLoss(0.2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype) //Only really deadly if you eat over 100u
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
// helps bleeding wounds clot faster
/datum/reagent/medicine/coagulant
@@ -1643,15 +1696,15 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/coagulant/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_COAGULATING, /datum/reagent/medicine/coagulant)
if(ishuman(affected_mob))
var/mob/living/carbon/human/blood_boy = affected_mob
blood_boy.physiology?.bleed_mod *= passive_bleed_modifier
- return ..()
-
/datum/reagent/medicine/coagulant/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_COAGULATING, /datum/reagent/medicine/coagulant)
if(was_working)
@@ -1660,8 +1713,6 @@
var/mob/living/carbon/human/blood_boy = affected_mob
blood_boy.physiology?.bleed_mod /= passive_bleed_modifier
- return ..()
-
/datum/reagent/medicine/coagulant/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
if(!affected_mob.blood_volume || !affected_mob.all_wounds)
@@ -1690,19 +1741,21 @@
if(SPT_PROB(7.5, seconds_per_tick))
affected_mob.losebreath += rand(2, 4)
- affected_mob.adjustOxyLoss(rand(1, 3), required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ affected_mob.adjustOxyLoss(rand(1, 3), updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
if(prob(30))
to_chat(affected_mob, span_danger("You can feel your blood clotting up in your veins!"))
else if(prob(10))
to_chat(affected_mob, span_userdanger("You feel like your blood has stopped moving!"))
- affected_mob.adjustOxyLoss(rand(3, 4), required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ affected_mob.adjustOxyLoss(rand(3, 4) * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
if(prob(50))
var/obj/item/organ/internal/lungs/our_lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS)
- our_lungs.apply_organ_damage(1)
+ our_lungs.apply_organ_damage(1 * REM * seconds_per_tick)
else
var/obj/item/organ/internal/heart/our_heart = affected_mob.get_organ_slot(ORGAN_SLOT_HEART)
- our_heart.apply_organ_damage(1)
+ our_heart.apply_organ_damage(1 * REM * seconds_per_tick)
+
+ return UPDATE_MOB_HEALTH
// i googled "natural coagulant" and a couple of results came up for banana peels, so after precisely 30 more seconds of research, i now dub grinding banana peels good for your blood
/datum/reagent/medicine/coagulant/banana_peel
@@ -1745,11 +1798,11 @@
ph = 10.6
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/medicine/ondansetron/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired)
+/datum/reagent/medicine/ondansetron/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
if(SPT_PROB(8, seconds_per_tick))
- M.adjust_drowsiness(2 SECONDS * REM * seconds_per_tick)
- if(SPT_PROB(15, seconds_per_tick) && !M.getStaminaLoss())
- M.adjustStaminaLoss(10)
- . = TRUE
- M.adjust_disgust(-10 * REM * seconds_per_tick)
+ affected_mob.adjust_drowsiness(2 SECONDS * REM * seconds_per_tick)
+ if(SPT_PROB(15, seconds_per_tick) && !affected_mob.getStaminaLoss())
+ if(affected_mob.adjustStaminaLoss(10 * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
+ affected_mob.adjust_disgust(-10 * REM * seconds_per_tick)
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index dddf966be72c07..be5df37ffdb483 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -321,6 +321,8 @@
/datum/reagent/water/salt/expose_mob(mob/living/exposed_mob, methods, reac_volume)
. = ..()
+ if(!iscarbon(exposed_mob))
+ return
var/mob/living/carbon/carbies = exposed_mob
if(!(methods & (PATCH|TOUCH|VAPOR)))
return
@@ -369,7 +371,7 @@
mytray.myseed?.adjust_instability(round(volume * 0.15))
/datum/reagent/water/holywater/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_HOLY, type)
/datum/reagent/water/holywater/on_mob_add(mob/living/affected_mob, amount)
@@ -378,8 +380,8 @@
data["misc"] = 0
/datum/reagent/water/holywater/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_HOLY, type)
- ..()
/datum/reagent/water/holywater/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -387,8 +389,7 @@
to_chat(exposed_mob, span_userdanger("A vile holiness begins to spread its shining tendrils through your mind, purging the Geometer of Blood's influence!"))
/datum/reagent/water/holywater/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(affected_mob.blood_volume)
- affected_mob.blood_volume += 0.1 * REM * seconds_per_tick // water is good for you!
+ . = ..()
if(!data)
data = list("misc" = 0)
@@ -415,9 +416,11 @@
affected_mob.Unconscious(100)
affected_mob.remove_status_effect(/datum/status_effect/jitter)
affected_mob.remove_status_effect(/datum/status_effect/speech/stutter)
- holder.remove_reagent(type, volume) // maybe this is a little too perfect and a max() cap on the statuses would be better??
+ if(holder)
+ holder.remove_reagent(type, volume) // maybe this is a little too perfect and a max() cap on the statuses would be better??
return
- holder.remove_reagent(type, 1 * REAGENTS_METABOLISM * seconds_per_tick) //fixed consumption to prevent balancing going out of whack
+ if(holder)
+ holder.remove_reagent(type, 1 * REAGENTS_METABOLISM * seconds_per_tick) //fixed consumption to prevent balancing going out of whack
/datum/reagent/water/holywater/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -483,25 +486,47 @@
ph = 6.5
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+/datum/reagent/fuel/unholywater/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
+ if(IS_CULTIST(affected_mob))
+ ADD_TRAIT(affected_mob, TRAIT_COAGULATING, type)
+
/datum/reagent/fuel/unholywater/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update = FALSE
if(IS_CULTIST(affected_mob))
affected_mob.adjust_drowsiness(-10 SECONDS * REM * seconds_per_tick)
affected_mob.AdjustAllImmobility(-40 * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(-10 * REM * seconds_per_tick, 0)
- affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, 0)
- affected_mob.adjustOxyLoss(-2 * REM * seconds_per_tick, 0)
- affected_mob.adjustBruteLoss(-2 * REM * seconds_per_tick, 0)
- affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick, 0)
+ need_mob_update += affected_mob.adjustStaminaLoss(-10 * REM * seconds_per_tick, updating_stamina = FALSE)
+ need_mob_update += affected_mob.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustOxyLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update = TRUE
if(ishuman(affected_mob) && affected_mob.blood_volume < BLOOD_VOLUME_NORMAL)
affected_mob.blood_volume += 3 * REM * seconds_per_tick
+
+ var/datum/wound/bloodiest_wound
+
+ for(var/datum/wound/iter_wound as anything in affected_mob.all_wounds)
+ if(iter_wound.blood_flow && iter_wound.blood_flow > bloodiest_wound?.blood_flow)
+ bloodiest_wound = iter_wound
+
+ if(bloodiest_wound)
+ bloodiest_wound.adjust_blood_flow(-2 * REM * seconds_per_tick)
+
else // Will deal about 90 damage when 50 units are thrown
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * REM * seconds_per_tick, 150)
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, 0)
- affected_mob.adjustFireLoss(1 * REM * seconds_per_tick, 0)
- affected_mob.adjustOxyLoss(1 * REM * seconds_per_tick, 0)
- affected_mob.adjustBruteLoss(1 * REM * seconds_per_tick, 0)
- ..()
- return TRUE
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * REM * seconds_per_tick, 150)
+ need_mob_update += affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustFireLoss(1 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustOxyLoss(1 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustBruteLoss(1 * REM * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
+
+/datum/reagent/fuel/unholywater/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
+ REMOVE_TRAIT(affected_mob, TRAIT_COAGULATING, type) //We don't cult check here because potentially our imbiber may no longer be a cultist for whatever reason! It doesn't purge holy water, after all!
/datum/reagent/hellwater //if someone has this in their system they've really pissed off an eldrich god
name = "Hell Water"
@@ -511,13 +536,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/hellwater/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_fire_stacks(min(affected_mob.fire_stacks + (1.5 * seconds_per_tick), 5))
affected_mob.ignite_mob() //Only problem with igniting people is currently the commonly available fire suits make you immune to being on fire
- affected_mob.adjustToxLoss(0.5*seconds_per_tick, 0)
- affected_mob.adjustFireLoss(0.5*seconds_per_tick, 0) //Hence the other damages... ain't I a bastard?
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustToxLoss(0.5*seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustFireLoss(0.5*seconds_per_tick, updating_health = FALSE) //Hence the other damages... ain't I a bastard?
affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2.5*seconds_per_tick, 150)
- holder.remove_reagent(type, 0.5*seconds_per_tick)
- return TRUE
+ if(holder)
+ holder.remove_reagent(type, 0.5*seconds_per_tick)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/medicine/omnizine/godblood
name = "Godblood"
@@ -629,8 +658,8 @@
if((methods & INGEST) && show_message)
to_chat(exposed_mob, span_notice("That tasted horrible."))
-
/datum/reagent/spraytan/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
metabolization_rate = 1 * REAGENTS_METABOLISM
if(ishuman(affected_mob))
@@ -656,13 +685,18 @@
affected_mob.visible_message("[affected_mob] flexes [affected_mob.p_their()] arms.")
if(SPT_PROB(5, seconds_per_tick))
affected_mob.say(pick("Shit was SO cash.", "You are everything bad in the world.", "What sports do you play, other than 'jack off to naked drawn Japanese people?'", "Don???t be a stranger. Just hit me with your best shot.", "My name is John and I hate every single one of you."), forced = /datum/reagent/spraytan)
- ..()
- return
#define MUT_MSG_IMMEDIATE 1
#define MUT_MSG_EXTENDED 2
#define MUT_MSG_ABOUT2TURN 3
+/// the current_cycle threshold / iterations needed before one can transform
+#define CYCLES_TO_TURN 20
+/// the cycle at which 'immediate' mutation text begins displaying
+#define CYCLES_MSG_IMMEDIATE 6
+/// the cycle at which 'extended' mutation text begins displaying
+#define CYCLES_MSG_EXTENDED 16
+
/datum/reagent/mutationtoxin
name = "Stable Mutation Toxin"
description = "A humanizing toxin."
@@ -675,21 +709,20 @@
"Your limbs begin to take on a different shape." = MUT_MSG_EXTENDED,
"Your appendages begin morphing." = MUT_MSG_EXTENDED,
"You feel as though you're about to change at any moment!" = MUT_MSG_ABOUT2TURN)
- var/cycles_to_turn = 20 //the current_cycle threshold / iterations needed before one can transform
/datum/reagent/mutationtoxin/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
- . = TRUE
+ . = ..()
if(!istype(affected_mob))
return
- if(!(affected_mob.dna?.species) || !(affected_mob.mob_biotypes & MOB_ORGANIC))
+ if(!(affected_mob.dna?.species) || !(affected_mob.mob_biotypes & affected_biotype))
return
if(SPT_PROB(5, seconds_per_tick))
var/list/pick_ur_fav = list()
var/filter = NONE
- if(current_cycle <= (cycles_to_turn*0.3))
+ if(current_cycle <= CYCLES_MSG_IMMEDIATE)
filter = MUT_MSG_IMMEDIATE
- else if(current_cycle <= (cycles_to_turn*0.8))
+ else if(current_cycle <= CYCLES_MSG_EXTENDED)
filter = MUT_MSG_EXTENDED
else
filter = MUT_MSG_ABOUT2TURN
@@ -699,14 +732,15 @@
pick_ur_fav += i
to_chat(affected_mob, span_warning("[pick(pick_ur_fav)]"))
- if(current_cycle >= cycles_to_turn)
+ if(current_cycle >= CYCLES_TO_TURN)
var/datum/species/species_type = race
//affected_mob.set_species(species_type) //ORIGINAL
affected_mob.set_species(species_type, TRUE, FALSE, null, null, null, null, TRUE) //SKYRAT EDIT CHANGE - CUSTOMIZATION
holder.del_reagent(type)
to_chat(affected_mob, span_warning("You've become \a [lowertext(initial(species_type.name))]!"))
return
- ..()
+
+ return ..()
/datum/reagent/mutationtoxin/classic //The one from plasma on green slimes
name = "Mutation Toxin"
@@ -769,14 +803,14 @@
//affected_mob.set_species(species_type) //ORIGINAL
affected_mob.set_species(species_type, TRUE, FALSE, null, null, null, null, TRUE, TRUE) //SKYRAT EDIT CHANGE - CUSTOMIZATION
holder.del_reagent(type)
- return TRUE
- if(current_cycle >= cycles_to_turn) //overwrite since we want subtypes of jelly
+ return UPDATE_MOB_HEALTH
+ if(current_cycle >= CYCLES_TO_TURN) //overwrite since we want subtypes of jelly
var/datum/species/species_type = pick(subtypesof(race))
//affected_mob.set_species(species_type) //ORIGINAL
affected_mob.set_species(species_type, TRUE, FALSE, null, null, null, null, TRUE, TRUE) //SKYRAT EDIT CHANGE - CUSTOMIZATION
holder.del_reagent(type)
to_chat(affected_mob, span_warning("You've become \a [initial(species_type.name)]!"))
- return TRUE
+ return UPDATE_MOB_HEALTH
return ..()
/datum/reagent/mutationtoxin/golem
@@ -849,6 +883,10 @@
#undef MUT_MSG_EXTENDED
#undef MUT_MSG_ABOUT2TURN
+#undef CYCLES_TO_TURN
+#undef CYCLES_MSG_IMMEDIATE
+#undef CYCLES_MSG_EXTENDED
+
/datum/reagent/mulligan
name = "Mulligan Toxin"
description = "This toxin will rapidly change the DNA of humanoid beings. Commonly used by Syndicate spies and assassins in need of an emergency ID change."
@@ -858,7 +896,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/mulligan/on_mob_life(mob/living/carbon/human/affected_mob, seconds_per_tick, times_fired)
- ..()
+ . = ..()
if (!istype(affected_mob))
return
to_chat(affected_mob, span_warning("You grit your teeth in pain as your body rapidly mutates!"))
@@ -900,10 +938,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/serotrotium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(ishuman(affected_mob))
if(SPT_PROB(3.5, seconds_per_tick))
affected_mob.emote(pick("twitch","drool","moan","gasp"))
- ..()
/datum/reagent/oxygen
name = "Oxygen"
@@ -978,13 +1016,13 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/mercury/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!HAS_TRAIT(src, TRAIT_IMMOBILIZED) && !isspaceturf(affected_mob.loc))
step(affected_mob, pick(GLOB.cardinals))
if(SPT_PROB(3.5, seconds_per_tick))
affected_mob.emote(pick("twitch","drool","moan"))
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5*seconds_per_tick)
- ..()
- return TRUE
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5*seconds_per_tick))
+ return UPDATE_MOB_HEALTH
/datum/reagent/sulfur
name = "Sulfur"
@@ -1052,9 +1090,9 @@
mytray.adjust_weedlevel(-rand(1, 4))
/datum/reagent/fluorine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(0.5*REM*seconds_per_tick, 0)
- . = TRUE
- ..()
+ . = ..()
+ if(affected_mob.adjustToxLoss(0.5*REM*seconds_per_tick, updating_health = FALSE))
+ . = TRUE
/datum/reagent/sodium
name = "Sodium"
@@ -1090,11 +1128,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/lithium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!HAS_TRAIT(affected_mob, TRAIT_IMMOBILIZED) && !isspaceturf(affected_mob.loc) && isturf(affected_mob.loc))
step(affected_mob, pick(GLOB.cardinals))
if(SPT_PROB(2.5, seconds_per_tick))
affected_mob.emote(pick("twitch","drool","moan"))
- ..()
/datum/reagent/glycerol
name = "Glycerol"
@@ -1134,9 +1172,9 @@
ph = 6
/datum/reagent/iron/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.blood_volume < BLOOD_VOLUME_NORMAL)
affected_mob.blood_volume += 0.25 * seconds_per_tick
- ..()
/datum/reagent/gold
name = "Gold"
@@ -1170,9 +1208,9 @@
var/tox_damage = 0.5
/datum/reagent/uranium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustToxLoss(tox_damage * seconds_per_tick * REM)
- ..()
- return TRUE
+ . = ..()
+ if(affected_mob.adjustToxLoss(tox_damage * seconds_per_tick * REM, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/uranium/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -1216,12 +1254,12 @@
do_teleport(exposed_mob, get_turf(exposed_mob), (reac_volume / 5), asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE) //4 tiles per crystal
/datum/reagent/bluespace/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(current_cycle > 10 && SPT_PROB(7.5, seconds_per_tick))
to_chat(affected_mob, span_warning("You feel unstable..."))
affected_mob.set_jitter_if_lower(2 SECONDS)
current_cycle = 1
addtimer(CALLBACK(affected_mob, TYPE_PROC_REF(/mob/living, bluespace_shuffle)), 30)
- ..()
/mob/living/proc/bluespace_shuffle()
do_teleport(src, get_turf(src), 5, asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE)
@@ -1268,9 +1306,9 @@
exposed_mob.adjust_fire_stacks(reac_volume / 10)
/datum/reagent/fuel/on_mob_life(mob/living/carbon/victim, seconds_per_tick, times_fired)
- victim.adjustToxLoss(0.5 * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- return TRUE
+ . = ..()
+ if(victim.adjustToxLoss(0.5 * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/fuel/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -1337,11 +1375,13 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/space_cleaner/ez_clean/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustBruteLoss(1.665*seconds_per_tick)
- affected_mob.adjustFireLoss(1.665*seconds_per_tick)
- affected_mob.adjustToxLoss(1.665*seconds_per_tick)
- ..()
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustBruteLoss(1.665*seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustFireLoss(1.665*seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustToxLoss(1.665*seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/space_cleaner/ez_clean/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -1359,6 +1399,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/cryptobiolin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.set_dizzy_if_lower(2 SECONDS)
// Cryptobiolin adjusts the mob's confusion down to 20 seconds if it's higher,
@@ -1370,8 +1411,6 @@
else if(confusion_left > 20 SECONDS)
affected_mob.set_confusion(20 SECONDS)
- ..()
-
/datum/reagent/impedrezene
name = "Impedrezene"
description = "Impedrezene is a narcotic that impedes one's ability by slowing down the higher brain cell functions."
@@ -1382,6 +1421,7 @@
addiction_types = list(/datum/addiction/opioids = 10)
/datum/reagent/impedrezene/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_jitter(-5 SECONDS * seconds_per_tick)
. = FALSE
if(SPT_PROB(55, seconds_per_tick))
@@ -1391,7 +1431,6 @@
affected_mob.adjust_drowsiness(6 SECONDS)
if(SPT_PROB(5, seconds_per_tick))
affected_mob.emote("drool")
- ..()
/datum/reagent/cyborg_mutation_nanomachines
name = "Nanomachines"
@@ -1542,13 +1581,13 @@
exposed_mob.adjust_drowsiness(drowsiness_to_apply)
/datum/reagent/nitrous_oxide/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
if(!HAS_TRAIT(affected_mob, TRAIT_COAGULATING)) //IF the mob does not have a coagulant in them, we add the blood mess trait to make the bleed quicker
ADD_TRAIT(affected_mob, TRAIT_BLOODY_MESS, type)
- return ..()
/datum/reagent/nitrous_oxide/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_BLOODY_MESS, type)
- return ..()
/datum/reagent/nitrous_oxide/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
affected_mob.adjust_drowsiness(4 SECONDS * REM * seconds_per_tick)
@@ -1706,10 +1745,10 @@
ph = 3
/datum/reagent/plantnutriment/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(tox_prob, seconds_per_tick))
- affected_mob.adjustToxLoss(1, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(1, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/plantnutriment/eznutriment
name = "E-Z Nutrient"
@@ -2183,15 +2222,17 @@
/datum/reagent/barbers_aid/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message=TRUE, touch_protection=FALSE)
. = ..()
- if(!(methods & (TOUCH|VAPOR)) || !ishuman(exposed_mob) || HAS_TRAIT(exposed_mob, TRAIT_BALD) || HAS_TRAIT(exposed_mob, TRAIT_SHAVED))
+ if(!(methods & (TOUCH|VAPOR)) || !ishuman(exposed_mob) || (HAS_TRAIT(exposed_mob, TRAIT_BALD) && HAS_TRAIT(exposed_mob, TRAIT_SHAVED)))
return
var/mob/living/carbon/human/exposed_human = exposed_mob
- var/datum/sprite_accessory/hair/picked_hair = pick(GLOB.hairstyles_list)
- var/datum/sprite_accessory/facial_hair/picked_beard = pick(GLOB.facial_hairstyles_list)
- to_chat(exposed_human, span_notice("Hair starts sprouting from your scalp."))
- exposed_human.set_facial_hairstyle(picked_beard, update = FALSE)
- exposed_human.set_hairstyle(picked_hair, update = TRUE)
+ if(!HAS_TRAIT(exposed_human, TRAIT_SHAVED))
+ var/datum/sprite_accessory/facial_hair/picked_beard = pick(GLOB.facial_hairstyles_list)
+ exposed_human.set_facial_hairstyle(picked_beard, update = FALSE)
+ if(!HAS_TRAIT(exposed_human, TRAIT_BALD))
+ var/datum/sprite_accessory/hair/picked_hair = pick(GLOB.hairstyles_list)
+ exposed_human.set_hairstyle(picked_hair, update = TRUE)
+ to_chat(exposed_human, span_notice("Hair starts sprouting from your [HAS_TRAIT(exposed_human, TRAIT_BALD) ? "face" : "scalp"]."))
/datum/reagent/concentrated_barbers_aid
name = "Concentrated Barber's Aid"
@@ -2204,17 +2245,19 @@
/datum/reagent/concentrated_barbers_aid/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message=TRUE, touch_protection=FALSE)
. = ..()
- if(!(methods & (TOUCH|VAPOR)) || !ishuman(exposed_mob) || HAS_TRAIT(exposed_mob, TRAIT_BALD) || HAS_TRAIT(exposed_mob, TRAIT_SHAVED))
+ if(!(methods & (TOUCH|VAPOR)) || !ishuman(exposed_mob) || (HAS_TRAIT(exposed_mob, TRAIT_BALD) && HAS_TRAIT(exposed_mob, TRAIT_SHAVED)))
return
var/mob/living/carbon/human/exposed_human = exposed_mob
- to_chat(exposed_human, span_notice("Your hair starts growing at an incredible speed!"))
- exposed_human.set_facial_hairstyle("Beard (Very Long)", update = FALSE)
- exposed_human.set_hairstyle("Very Long Hair", update = TRUE)
+ if(!HAS_TRAIT(exposed_human, TRAIT_SHAVED))
+ exposed_human.set_facial_hairstyle("Beard (Very Long)", update = FALSE)
+ if(!HAS_TRAIT(exposed_human, TRAIT_BALD))
+ exposed_human.set_hairstyle("Very Long Hair", update = TRUE)
+ to_chat(exposed_human, span_notice("Your[HAS_TRAIT(exposed_human, TRAIT_BALD) ? " facial" : ""] hair starts growing at an incredible speed!"))
/datum/reagent/concentrated_barbers_aid/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- if(current_cycle > 20 / creation_purity)
+ if(current_cycle > 21 / creation_purity)
if(!ishuman(affected_mob))
return
var/mob/living/carbon/human/human_mob = affected_mob
@@ -2362,9 +2405,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/royal_bee_jelly/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(1, seconds_per_tick))
affected_mob.say(pick("Bzzz...","BZZ BZZ","Bzzzzzzzzzzz..."), forced = "royal bee jelly")
- ..()
//Misc reagents
@@ -2396,8 +2439,8 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/magillitis/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- ..()
- if((ishuman(affected_mob)) && current_cycle >= 10)
+ . = ..()
+ if((ishuman(affected_mob)) && current_cycle > 10)
affected_mob.gorillize()
/datum/reagent/growthserum
@@ -2409,6 +2452,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/growthserum/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/newsize = current_size
switch(volume)
if(0 to 19)
@@ -2424,12 +2468,11 @@
affected_mob.update_transform(newsize/current_size)
current_size = newsize
- ..()
/datum/reagent/growthserum/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.update_transform(RESIZE_DEFAULT_SIZE/current_size)
current_size = RESIZE_DEFAULT_SIZE
- ..()
/datum/reagent/plastic_polymers
name = "Plastic Polymers"
@@ -2491,12 +2534,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/pax/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_PACIFISM, type)
/datum/reagent/pax/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_PACIFISM, type)
- ..()
/datum/reagent/bz_metabolites
name = "BZ Metabolites"
@@ -2507,19 +2550,19 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/bz_metabolites/on_mob_metabolize(mob/living/ling)
- ..()
+ . = ..()
ADD_TRAIT(ling, CHANGELING_HIVEMIND_MUTE, type)
/datum/reagent/bz_metabolites/on_mob_end_metabolize(mob/living/ling)
- ..()
+ . = ..()
REMOVE_TRAIT(ling, CHANGELING_HIVEMIND_MUTE, type)
/datum/reagent/bz_metabolites/on_mob_life(mob/living/carbon/target, seconds_per_tick, times_fired)
+ . = ..()
if(target.mind)
var/datum/antagonist/changeling/changeling = target.mind.has_antag_datum(/datum/antagonist/changeling)
if(changeling)
- changeling.adjust_chemicals(-4 * REM * seconds_per_tick) //SKYRAT EDIT - BZ-BUFF-VS-LING - ORIGINAL: changeling.adjust_chemicals(-2 * REM * delta_time)
- return ..()
+ changeling.adjust_chemicals(-4 * REM * seconds_per_tick) //SKYRAT EDIT - BZ-BUFF-VS-LING - ORIGINAL: changeling.adjust_chemicals(-2 * REM * seconds_per_tick)
/datum/reagent/pax/peaceborg
name = "Synthpax"
@@ -2550,14 +2593,14 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/peaceborg/tire/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
var/healthcomp = (100 - affected_mob.health) //DOES NOT ACCOUNT FOR ADMINBUS THINGS THAT MAKE YOU HAVE MORE THAN 200/210 HEALTH, OR SOMETHING OTHER THAN A HUMAN PROCESSING THIS.
. = FALSE
if(affected_mob.getStaminaLoss() < (45 - healthcomp)) //At 50 health you would have 200 - 150 health meaning 50 compensation. 60 - 50 = 10, so would only do 10-19 stamina.)
- affected_mob.adjustStaminaLoss(10 * REM * seconds_per_tick)
- . = TRUE
+ if(affected_mob.adjustStaminaLoss(10 * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(16, seconds_per_tick))
to_chat(affected_mob, "You should sit down and take a rest...")
- ..()
/datum/reagent/gondola_mutation_toxin
name = "Tranquility"
@@ -2565,11 +2608,12 @@
color = "#9A6750" //RGB: 154, 103, 80
taste_description = "inner peace"
penetrates_skin = NONE
+ var/datum/disease/transformation/gondola_disease = /datum/disease/transformation/gondola
/datum/reagent/gondola_mutation_toxin/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
. = ..()
if((methods & (PATCH|INGEST|INJECT)) || ((methods & VAPOR) && prob(min(reac_volume,100)*(1 - touch_protection))))
- exposed_mob.ForceContractDisease(new /datum/disease/transformation/gondola(), FALSE, TRUE)
+ exposed_mob.ForceContractDisease(new gondola_disease, FALSE, TRUE)
/datum/reagent/spider_extract
@@ -2595,13 +2639,14 @@
desc = "It smells like a carcass, and doesn't look much better."
/datum/reagent/yuck/on_mob_add(mob/living/affected_mob)
- . = ..()
if(HAS_TRAIT(affected_mob, TRAIT_NOHUNGER)) //they can't puke
holder.del_reagent(type)
+ return ..()
#define YUCK_PUKE_CYCLES 3 // every X cycle is a puke
#define YUCK_PUKES_TO_STUN 3 // hit this amount of pukes in a row to start stunning
/datum/reagent/yuck/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!yuck_cycle)
if(SPT_PROB(4, seconds_per_tick))
var/dread = pick("Something is moving in your stomach...", \
@@ -2613,19 +2658,19 @@
var/yuck_cycles = current_cycle - yuck_cycle
if(yuck_cycles % YUCK_PUKE_CYCLES == 0)
if(yuck_cycles >= YUCK_PUKE_CYCLES * YUCK_PUKES_TO_STUN)
- holder.remove_reagent(type, 5)
+ if(holder)
+ holder.remove_reagent(type, 5)
var/passable_flags = (MOB_VOMIT_MESSAGE | MOB_VOMIT_HARM)
if(yuck_cycles >= (YUCK_PUKE_CYCLES * YUCK_PUKES_TO_STUN))
passable_flags |= MOB_VOMIT_STUN
affected_mob.vomit(vomit_flags = passable_flags, lost_nutrition = rand(14, 26))
- if(holder)
- return ..()
+
#undef YUCK_PUKE_CYCLES
#undef YUCK_PUKES_TO_STUN
/datum/reagent/yuck/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
yuck_cycle = 0 // reset vomiting
- return ..()
/datum/reagent/yuck/on_transfer(atom/A, methods=TOUCH, trans_volume)
if((methods & INGEST) || !iscarbon(A))
@@ -2724,10 +2769,11 @@
addtimer(CALLBACK(exposed_obj, PROC_REF(_RemoveElement), list(/datum/element/forced_gravity, 0)), volume * time_multiplier, TIMER_UNIQUE|TIMER_OVERRIDE)
/datum/reagent/gravitum/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.AddElement(/datum/element/forced_gravity, 0) //0 is the gravity, and in this case weightless
- return ..()
/datum/reagent/gravitum/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.RemoveElement(/datum/element/forced_gravity, 0)
/datum/reagent/cellulose
@@ -2751,6 +2797,7 @@
var/significant = FALSE
/datum/reagent/determination/on_mob_end_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
if(significant)
var/stam_crash = 0
for(var/thing in affected_mob.all_wounds)
@@ -2758,9 +2805,9 @@
stam_crash += (W.severity + 1) * 3 // spike of 3 stam damage per wound severity (moderate = 6, severe = 9, critical = 12) when the determination wears off if it was a combat rush
affected_mob.adjustStaminaLoss(stam_crash)
affected_mob.remove_status_effect(/datum/status_effect/determined)
- ..()
/datum/reagent/determination/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(!significant && volume >= WOUND_DETERMINATION_SEVERE)
significant = TRUE
affected_mob.apply_status_effect(/datum/status_effect/determined) // in addition to the slight healing, limping cooldowns are divided by 4 during the combat high
@@ -2772,8 +2819,8 @@
var/obj/item/bodypart/wounded_part = W.limb
if(wounded_part)
wounded_part.heal_damage(0.25 * REM * seconds_per_tick, 0.25 * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(-0.25 * REM * seconds_per_tick) // the more wounds, the more stamina regen
- ..()
+ if(affected_mob.adjustStaminaLoss(-0.25 * REM * seconds_per_tick, updating_stamina = FALSE)) // the more wounds, the more stamina regen
+ return UPDATE_MOB_HEALTH
// unholy water, but for heretics.
// why couldn't they have both just used the same reagent?
@@ -2791,24 +2838,26 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/eldritch/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update = FALSE
if(IS_HERETIC(drinker))
drinker.adjust_drowsiness(-10 * REM * seconds_per_tick)
drinker.AdjustAllImmobility(-40 * REM * seconds_per_tick)
- drinker.adjustStaminaLoss(-10 * REM * seconds_per_tick, FALSE)
- drinker.adjustToxLoss(-2 * REM * seconds_per_tick, FALSE, forced = TRUE)
- drinker.adjustOxyLoss(-2 * REM * seconds_per_tick, FALSE)
- drinker.adjustBruteLoss(-2 * REM * seconds_per_tick, FALSE)
- drinker.adjustFireLoss(-2 * REM * seconds_per_tick, FALSE)
+ need_mob_update += drinker.adjustStaminaLoss(-10 * REM * seconds_per_tick, updating_stamina = FALSE)
+ need_mob_update += drinker.adjustToxLoss(-2 * REM * seconds_per_tick, updating_health = FALSE, forced = TRUE)
+ need_mob_update += drinker.adjustOxyLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustBruteLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustFireLoss(-2 * REM * seconds_per_tick, updating_health = FALSE)
if(drinker.blood_volume < BLOOD_VOLUME_NORMAL)
drinker.blood_volume += 3 * REM * seconds_per_tick
else
- drinker.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * REM * seconds_per_tick, 150)
- drinker.adjustToxLoss(2 * REM * seconds_per_tick, FALSE)
- drinker.adjustFireLoss(2 * REM * seconds_per_tick, FALSE)
- drinker.adjustOxyLoss(2 * REM * seconds_per_tick, FALSE)
- drinker.adjustBruteLoss(2 * REM * seconds_per_tick, FALSE)
- ..()
- return TRUE
+ need_mob_update = drinker.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * REM * seconds_per_tick, 150)
+ need_mob_update += drinker.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustFireLoss(2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustOxyLoss(2 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += drinker.adjustBruteLoss(2 * REM * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/universal_indicator
name = "Universal Indicator"
@@ -2854,10 +2903,11 @@
desc = "Bottoms up...?"
/datum/reagent/ants/on_mob_life(mob/living/carbon/victim, seconds_per_tick)
+ . = ..()
victim.adjustBruteLoss(max(0.1, round((ant_damage * 0.025),0.1))) //Scales with time. Roughly 32 brute with 100u.
ant_damage++
if(ant_damage < 5) // Makes ant food a little more appetizing, since you won't be screaming as much.
- return ..()
+ return
if(SPT_PROB(5, seconds_per_tick))
if(SPT_PROB(5, seconds_per_tick)) //Super rare statement
victim.say("AUGH NO NOT THE ANTS! NOT THE ANTS! AAAAUUGH THEY'RE IN MY EYES! MY EYES! AUUGH!!", forced = /datum/reagent/ants)
@@ -2866,14 +2916,12 @@
if(SPT_PROB(15, seconds_per_tick))
victim.emote("scream")
if(SPT_PROB(2, seconds_per_tick)) // Stuns, but purges ants.
- victim.vomit(rand(5,10), FALSE, TRUE, 1, TRUE, FALSE, purge_ratio = 1)
- ..()
- return TRUE
+ victim.vomit(VOMIT_CATEGORY_DEFAULT, lost_nutrition = rand(5,10), purge_ratio = 1)
/datum/reagent/ants/on_mob_end_metabolize(mob/living/living_anthill)
+ . = ..()
ant_damage = 0
to_chat(living_anthill, "You feel like the last of the ants are out of your system.")
- return ..()
/datum/reagent/ants/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -2918,9 +2966,9 @@
metabolization_rate = 0.4 * REAGENTS_METABOLISM
/datum/reagent/lead/on_mob_life(mob/living/carbon/victim)
- victim.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5)
- ..()
- return TRUE
+ . = ..()
+ if(victim.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5))
+ return UPDATE_MOB_HEALTH
//The main feedstock for kronkaine production, also a shitty stamina healer.
/datum/reagent/kronkus_extract
@@ -2932,10 +2980,12 @@
addiction_types = list(/datum/addiction/stimulants = 5)
/datum/reagent/kronkus_extract/on_mob_life(mob/living/carbon/kronkus_enjoyer)
- ..()
- kronkus_enjoyer.adjustOrganLoss(ORGAN_SLOT_HEART, 0.1)
- kronkus_enjoyer.adjustStaminaLoss(-2, FALSE)
- return TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = kronkus_enjoyer.adjustOrganLoss(ORGAN_SLOT_HEART, 0.1)
+ need_mob_update += kronkus_enjoyer.adjustStaminaLoss(-2, updating_stamina = FALSE)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/brimdust
name = "Brimdust"
@@ -2947,7 +2997,8 @@
/datum/reagent/brimdust/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- return affected_mob.adjustFireLoss((ispodperson(affected_mob) ? -1 : 1) * seconds_per_tick)
+ if(affected_mob.adjustFireLoss((ispodperson(affected_mob) ? -1 : 1 * seconds_per_tick), updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/brimdust/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
mytray.adjust_weedlevel(-1)
@@ -2975,14 +3026,15 @@
. = ..()
metabolizer.add_mood_event(name, /datum/mood_event/love_reagent)
-/datum/reagent/love/on_mob_delete(mob/living/deleted_from)
+/datum/reagent/love/on_mob_delete(mob/living/affected_mob)
. = ..()
// When we exit the system we'll leave the moodlet based on the amount we had
var/duration_of_moodlet = current_cycle * 20 SECONDS
- deleted_from.clear_mood_event(name)
- deleted_from.add_mood_event(name, /datum/mood_event/love_reagent, duration_of_moodlet)
+ affected_mob.clear_mood_event(name)
+ affected_mob.add_mood_event(name, /datum/mood_event/love_reagent, duration_of_moodlet)
/datum/reagent/love/overdose_process(mob/living/metabolizer, seconds_per_tick, times_fired)
+ . = ..()
var/mob/living/carbon/carbon_metabolizer = metabolizer
if(!istype(carbon_metabolizer) || !carbon_metabolizer.can_heartattack() || carbon_metabolizer.undergoing_cardiac_arrest())
metabolizer.reagents.del_reagent(type)
@@ -3016,6 +3068,8 @@
affected_mob.add_mood_event("hauntium_spirits", /datum/mood_event/hauntium_spirits, name) //8 minutes of mood debuff
/datum/reagent/hauntium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+
if(affected_mob.mob_biotypes & MOB_UNDEAD || HAS_MIND_TRAIT(affected_mob, TRAIT_MORBID)) //if morbid or undead,acts like an addiction-less drug
affected_mob.remove_status_effect(/datum/status_effect/jitter)
affected_mob.AdjustStun(-50 * REM * seconds_per_tick)
@@ -3023,10 +3077,13 @@
affected_mob.AdjustUnconscious(-50 * REM * seconds_per_tick)
affected_mob.AdjustParalyzed(-50 * REM * seconds_per_tick)
affected_mob.AdjustImmobilized(-50 * REM * seconds_per_tick)
- ..()
else
- affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, REM * seconds_per_tick) //1 heart damage per tick
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, REM * seconds_per_tick)) //1 heart damage per tick
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(10, seconds_per_tick))
affected_mob.emote(pick("twitch","choke","shiver","gag"))
- ..()
- return TRUE
+
+// The same as gold just with a slower metabolism rate, to make using the Hand of Midas easier.
+/datum/reagent/gold/cursed
+ name = "Cursed Gold"
+ metabolization_rate = 0.2 * REAGENTS_METABOLISM
diff --git a/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm b/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
index 6f99273ad4e937..2db3682ef21850 100644
--- a/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
@@ -13,9 +13,9 @@
exposed_turf.AddComponent(/datum/component/thermite, reac_volume)
/datum/reagent/thermite/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustFireLoss(1 * REM * seconds_per_tick, 0)
- ..()
- return TRUE
+ . = ..()
+ if(affected_mob.adjustFireLoss(1 * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/nitroglycerin
name = "Nitroglycerin"
@@ -47,10 +47,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/clf3/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_fire_stacks(2 * REM * seconds_per_tick)
- affected_mob.adjustFireLoss(0.3 * max(affected_mob.fire_stacks, 1) * REM * seconds_per_tick, 0)
- ..()
- return TRUE
+ if(affected_mob.adjustFireLoss(0.3 * max(affected_mob.fire_stacks, 1) * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/clf3/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -177,10 +177,10 @@
exposed_mob.ignite_mob()
/datum/reagent/phlogiston/on_mob_life(mob/living/carbon/metabolizer, seconds_per_tick, times_fired)
+ . = ..()
metabolizer.adjust_fire_stacks(1 * REM * seconds_per_tick)
- metabolizer.adjustFireLoss(0.3 * max(metabolizer.fire_stacks, 0.15) * REM * seconds_per_tick, 0)
- ..()
- return TRUE
+ if(metabolizer.adjustFireLoss(0.3 * max(metabolizer.fire_stacks, 0.15) * REM * seconds_per_tick, updating_health = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/napalm
name = "Napalm"
@@ -201,9 +201,8 @@
mytray.adjust_weedlevel(-rand(5,9)) //At least give them a small reward if they bother.
/datum/reagent/napalm/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_fire_stacks(1 * REM * seconds_per_tick)
- ..()
- return TRUE
/datum/reagent/napalm/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -249,6 +248,7 @@
metabolization_rate = 0.05 * REM //slower consumption when dead
/datum/reagent/cryostylane/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
metabolization_rate = 0.25 * REM//faster consumption when alive
if(affected_mob.reagents.has_reagent(/datum/reagent/oxygen))
affected_mob.reagents.remove_reagent(/datum/reagent/oxygen, 0.5 * REM * seconds_per_tick)
@@ -256,7 +256,6 @@
if(ishuman(affected_mob))
var/mob/living/carbon/human/humi = affected_mob
humi.adjust_coretemperature(-15 * REM * seconds_per_tick)
- ..()
/datum/reagent/cryostylane/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -284,9 +283,9 @@
holder.remove_reagent(/datum/reagent/oxygen, 0.5 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(15 * REM * seconds_per_tick)
if(ishuman(affected_mob))
- var/mob/living/carbon/human/humi = affected_mob
- humi.adjust_coretemperature(15 * REM * seconds_per_tick)
- ..()
+ var/mob/living/carbon/human/affected_human = affected_mob
+ affected_human.adjust_coretemperature(15 * REM * seconds_per_tick)
+ return ..()
/datum/reagent/pyrosium/burn(datum/reagents/holder)
if(holder.has_reagent(/datum/reagent/oxygen))
@@ -306,12 +305,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/teslium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
shock_timer++
if(shock_timer >= rand(5, 30)) //Random shocks are wildly unpredictable
shock_timer = 0
affected_mob.electrocute_act(rand(5, 20), "Teslium in their body", 1, SHOCK_NOGLOVES) //SHOCK_NOGLOVES because it's caused from INSIDE of you
playsound(affected_mob, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- ..()
/datum/reagent/teslium/on_mob_metabolize(mob/living/carbon/human/affected_mob)
. = ..()
@@ -334,15 +333,16 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/teslium/energized_jelly/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(isjellyperson(affected_mob))
shock_timer = 0 //immune to shocks
affected_mob.AdjustAllImmobility(-40 *REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(-2 * REM * seconds_per_tick, 0)
+ if(affected_mob.adjustStaminaLoss(-2 * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
if(is_species(affected_mob, /datum/species/jelly/luminescent))
var/mob/living/carbon/human/affected_human = affected_mob
var/datum/species/jelly/luminescent/slime_species = affected_human.dna.species
slime_species.extract_cooldown = max(slime_species.extract_cooldown - (2 SECONDS * REM * seconds_per_tick), 0)
- ..()
/datum/reagent/firefighting_foam
name = "Firefighting Foam"
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index d655698646ca28..390c3d7bfd4ddf 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -7,7 +7,6 @@
color = "#CF3600" // rgb: 207, 54, 0
taste_description = "bitterness"
taste_mult = 1.2
- harmful = TRUE
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
///The amount of toxin damage this will cause when metabolized (also used to calculate liver damage)
var/toxpwr = 1.5
@@ -23,10 +22,10 @@
mytray.adjust_toxic(round(volume * 2))
/datum/reagent/toxin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(toxpwr && affected_mob.health > health_required)
- affected_mob.adjustToxLoss(toxpwr * REM * normalise_creation_purity() * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(toxpwr * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/amatoxin
name = "Amatoxin"
@@ -64,8 +63,9 @@
exposed_mob.domutcheck()
/datum/reagent/toxin/mutagen/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- . = affected_mob.adjustToxLoss(0.5 * seconds_per_tick * REM, required_biotype = affected_biotype)
- return ..() || .
+ . = ..()
+ if(affected_mob.adjustToxLoss(0.5 * seconds_per_tick * REM, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/mutagen/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
mytray.mutation_roll(user)
@@ -100,14 +100,16 @@
/datum/reagent/toxin/plasma/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
if(holder.has_reagent(/datum/reagent/medicine/epinephrine))
holder.remove_reagent(/datum/reagent/medicine/epinephrine, 2 * REM * seconds_per_tick)
+ . = ..()
affected_mob.adjustPlasma(20 * REM * seconds_per_tick)
- return ..()
/datum/reagent/toxin/plasma/on_mob_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
if(HAS_TRAIT(affected_mob, TRAIT_PLASMA_LOVER_METABOLISM)) // sometimes mobs can temporarily metabolize plasma (e.g. plasma fixation disease symptom)
toxpwr = 0
/datum/reagent/toxin/plasma/on_mob_end_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
toxpwr = initial(toxpwr)
/// Handles plasma boiling.
@@ -154,18 +156,20 @@
/datum/reagent/toxin/hot_ice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
if(holder.has_reagent(/datum/reagent/medicine/epinephrine))
holder.remove_reagent(/datum/reagent/medicine/epinephrine, 2 * REM * seconds_per_tick)
+ . = ..()
affected_mob.adjustPlasma(20 * REM * seconds_per_tick)
affected_mob.adjust_bodytemperature(-7 * TEMPERATURE_DAMAGE_COEFFICIENT * REM * seconds_per_tick, affected_mob.get_body_temp_normal())
if(ishuman(affected_mob))
var/mob/living/carbon/human/humi = affected_mob
humi.adjust_coretemperature(-7 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick, affected_mob.get_body_temp_normal())
- return ..()
/datum/reagent/toxin/hot_ice/on_mob_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
if(HAS_TRAIT(affected_mob, TRAIT_PLASMA_LOVER_METABOLISM))
toxpwr = 0
/datum/reagent/toxin/hot_ice/on_mob_end_metabolize(mob/living/carbon/affected_mob)
+ . = ..()
toxpwr = initial(toxpwr)
/datum/reagent/toxin/lexorin
@@ -180,22 +184,20 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/lexorin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- . = TRUE
-
- if(HAS_TRAIT(affected_mob, TRAIT_NOBREATH))
- . = FALSE
-
- if(.)
+ . = ..()
+ if(!HAS_TRAIT(affected_mob, TRAIT_NOBREATH))
affected_mob.adjustOxyLoss(5 * REM * normalise_creation_purity() * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
affected_mob.losebreath += 2 * REM * normalise_creation_purity() * seconds_per_tick
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(10, seconds_per_tick))
affected_mob.emote("gasp")
- ..()
/datum/reagent/toxin/lexorin/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
RegisterSignal(affected_mob, COMSIG_CARBON_ATTEMPT_BREATHE, PROC_REF(block_breath))
/datum/reagent/toxin/lexorin/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
UnregisterSignal(affected_mob, COMSIG_CARBON_ATTEMPT_BREATHE, PROC_REF(block_breath))
/datum/reagent/toxin/lexorin/proc/block_breath(mob/living/source)
@@ -213,14 +215,14 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/slimejelly/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(5, seconds_per_tick))
to_chat(affected_mob, span_danger("Your insides are burning!"))
- affected_mob.adjustToxLoss(rand(20, 60), FALSE, required_biotype = affected_biotype)
- . = TRUE
+ if(affected_mob.adjustToxLoss(rand(20, 60), updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
else if(SPT_PROB(23, seconds_per_tick))
- affected_mob.heal_bodypart_damage(5)
- . = TRUE
- ..()
+ if(affected_mob.heal_bodypart_damage(5))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/carpotoxin
name = "Carpotoxin"
@@ -252,9 +254,9 @@
if((data?["method"] & INGEST) && holder_mob.stat != DEAD)
holder_mob.fakedeath(type)
-/datum/reagent/toxin/zombiepowder/on_mob_end_metabolize(mob/living/holder_mob)
- holder_mob.cure_fakedeath(type)
- return ..()
+/datum/reagent/toxin/zombiepowder/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
+ affected_mob.cure_fakedeath(type)
/datum/reagent/toxin/zombiepowder/on_transfer(atom/target_atom, methods, trans_volume)
. = ..()
@@ -265,21 +267,22 @@
zombiepowder.data["method"] |= INGEST
/datum/reagent/toxin/zombiepowder/on_mob_life(mob/living/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(HAS_TRAIT(affected_mob, TRAIT_FAKEDEATH) && HAS_TRAIT(affected_mob, TRAIT_DEATHCOMA))
- ..()
- return TRUE
+ return
+ var/need_mob_update
switch(current_cycle)
- if(1 to 5)
+ if(2 to 6)
affected_mob.adjust_confusion(1 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_drowsiness(2 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_slurring(6 SECONDS * REM * seconds_per_tick)
- if(5 to 8)
- affected_mob.adjustStaminaLoss(40 * REM * seconds_per_tick, 0)
- if(9 to INFINITY)
+ if(6 to 9)
+ need_mob_update = affected_mob.adjustStaminaLoss(40 * REM * seconds_per_tick, updating_stamina = FALSE)
+ if(10 to INFINITY)
if(affected_mob.stat != DEAD)
affected_mob.fakedeath(type)
- ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/ghoulpowder
name = "Ghoul Powder"
@@ -294,17 +297,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/ghoulpowder/on_mob_metabolize(mob/living/affected_mob)
- ..()
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_FAKEDEATH, type)
/datum/reagent/toxin/ghoulpowder/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_FAKEDEATH, type)
- ..()
/datum/reagent/toxin/ghoulpowder/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOxyLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- ..()
- . = TRUE
+ . = ..()
+ if(affected_mob.adjustOxyLoss(1 * REM * seconds_per_tick, FALSE, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/mindbreaker
name = "Mindbreaker Toxin"
@@ -319,25 +322,23 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
addiction_types = list(/datum/addiction/hallucinogens = 18) //7.2 per 2 seconds
-
-/datum/reagent/toxin/mindbreaker/on_mob_metabolize(mob/living/metabolizer)
+/datum/reagent/toxin/mindbreaker/on_mob_metabolize(mob/living/affected_mob)
. = ..()
- ADD_TRAIT(metabolizer, TRAIT_RDS_SUPPRESSED, type)
+ ADD_TRAIT(affected_mob, TRAIT_RDS_SUPPRESSED, type)
-/datum/reagent/toxin/mindbreaker/on_mob_end_metabolize(mob/living/metabolizer)
+/datum/reagent/toxin/mindbreaker/on_mob_end_metabolize(mob/living/affected_mob)
. = ..()
- REMOVE_TRAIT(metabolizer, TRAIT_RDS_SUPPRESSED, type)
+ REMOVE_TRAIT(affected_mob, TRAIT_RDS_SUPPRESSED, type)
-/datum/reagent/toxin/mindbreaker/on_mob_life(mob/living/carbon/metabolizer, seconds_per_tick, times_fired)
+/datum/reagent/toxin/mindbreaker/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
// mindbreaker toxin assuages hallucinations in those plagued with it, mentally
- if(metabolizer.has_trauma_type(/datum/brain_trauma/mild/hallucinations))
- metabolizer.remove_status_effect(/datum/status_effect/hallucination)
+ if(affected_mob.has_trauma_type(/datum/brain_trauma/mild/hallucinations))
+ affected_mob.remove_status_effect(/datum/status_effect/hallucination)
// otherwise it creates hallucinations. truly a miracle medicine.
else
- metabolizer.adjust_hallucinations(10 SECONDS * REM * seconds_per_tick)
-
- return ..()
+ affected_mob.adjust_hallucinations(10 SECONDS * REM * seconds_per_tick)
/datum/reagent/toxin/plantbgone
name = "Plant-B-Gone"
@@ -374,8 +375,8 @@
var/damage = min(round(0.4 * reac_volume, 0.1), 10)
if(exposed_mob.mob_biotypes & MOB_PLANT)
// spray bottle emits 5u so it's dealing ~15 dmg per spray
- exposed_mob.adjustToxLoss(damage * 20, required_biotype = affected_biotype)
- return
+ if(exposed_mob.adjustToxLoss(damage * 20, required_biotype = affected_biotype))
+ return
if(!(methods & VAPOR) || !iscarbon(exposed_mob))
return
@@ -409,8 +410,9 @@
AddElement(/datum/element/bugkiller_reagent)
/datum/reagent/toxin/pestkiller/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- . = affected_mob.adjustToxLoss(2 * toxpwr * REM * seconds_per_tick, updating_health = FALSE, required_biotype = MOB_BUG)
- return ..() || .
+ . = ..()
+ if(affected_mob.adjustToxLoss(2 * toxpwr * REM * seconds_per_tick, updating_health = FALSE, required_biotype = MOB_BUG))
+ return UPDATE_MOB_HEALTH
//Pest Spray
/datum/reagent/toxin/pestkiller/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
@@ -438,10 +440,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/spore/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.damageoverlaytemp = 60
affected_mob.update_damage_hud()
affected_mob.set_eye_blur_if_lower(6 SECONDS * REM * seconds_per_tick)
- return ..()
/datum/reagent/toxin/spore_burning
name = "Burning Spore Toxin"
@@ -453,9 +455,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/spore_burning/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_fire_stacks(2 * REM * seconds_per_tick)
affected_mob.ignite_mob()
- return ..()
/datum/reagent/toxin/chloralhydrate
name = "Chloral Hydrate"
@@ -472,18 +474,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/chloralhydrate/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
switch(current_cycle)
- if(1 to 10)
+ if(2 to 11)
affected_mob.adjust_confusion(2 SECONDS * REM * normalise_creation_purity() * seconds_per_tick)
affected_mob.adjust_drowsiness(4 SECONDS * REM * normalise_creation_purity() * seconds_per_tick)
- if(10 to 50)
+ if(11 to 51)
affected_mob.Sleeping(40 * REM * normalise_creation_purity() * seconds_per_tick)
- . = TRUE
- if(51 to INFINITY)
+ if(52 to INFINITY)
affected_mob.Sleeping(40 * REM * normalise_creation_purity() * seconds_per_tick)
- affected_mob.adjustToxLoss(1 * (current_cycle - 50) * REM * normalise_creation_purity() * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- ..()
+ if(affected_mob.adjustToxLoss(1 * (current_cycle - 51) * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/fakebeer //disguised as normal beer for use by emagged brobots
name = "B33r"
@@ -507,14 +508,14 @@
icon_state = initial(copy_from.icon_state)
/datum/reagent/toxin/fakebeer/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
switch(current_cycle)
- if(1 to 50)
+ if(2 to 51)
affected_mob.Sleeping(40 * REM * seconds_per_tick)
- if(51 to INFINITY)
+ if(52 to INFINITY)
affected_mob.Sleeping(40 * REM * seconds_per_tick)
- affected_mob.adjustToxLoss(1 * (current_cycle - 50) * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
- return ..() || .
+ if(affected_mob.adjustToxLoss(1 * (current_cycle - 50) * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/coffeepowder
name = "Coffee Grounds"
@@ -558,9 +559,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/mutetoxin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
// Gain approximately 12 seconds * creation purity seconds of silence every metabolism tick.
affected_mob.set_silence_if_lower(6 SECONDS * REM * normalise_creation_purity() * seconds_per_tick)
- return ..()
/datum/reagent/toxin/staminatoxin
name = "Tirizene"
@@ -572,10 +573,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/staminatoxin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustStaminaLoss(data * REM * seconds_per_tick, 0)
+ . = ..()
+ if(affected_mob.adjustStaminaLoss(data * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
data = max(data - 1, 3)
- ..()
- . = TRUE
/datum/reagent/toxin/polonium
name = "Polonium"
@@ -587,12 +588,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/polonium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if (!HAS_TRAIT(affected_mob, TRAIT_IRRADIATED) && SSradiation.can_irradiate_basic(affected_mob))
affected_mob.AddComponent(/datum/component/irradiated)
else
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, required_biotype = affected_biotype)
- . = TRUE
- return ..() || .
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/histamine
name = "Histamine"
@@ -606,6 +607,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/histamine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(30, seconds_per_tick))
switch(pick(1, 2, 3, 4))
if(1)
@@ -618,16 +620,17 @@
if(4)
if(prob(75))
to_chat(affected_mob, span_danger("You scratch at an itch."))
- affected_mob.adjustBruteLoss(2*REM, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
- ..()
+ if(affected_mob.adjustBruteLoss(2* REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/histamine/overdose_process(mob/living/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOxyLoss(2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- affected_mob.adjustBruteLoss(2 * REM * seconds_per_tick, FALSE, FALSE, BODYTYPE_ORGANIC)
- affected_mob.adjustToxLoss(2 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- ..()
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOxyLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update += affected_mob.adjustBruteLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update += affected_mob.adjustToxLoss(2 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/formaldehyde
name = "Formaldehyde"
@@ -646,14 +649,13 @@
/datum/reagent/toxin/formaldehyde/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_CORONER_METABOLISM)) //mmmm, the forbidden pickle juice
- affected_mob.adjustToxLoss(-1, FALSE, required_biotype = affected_biotype) //it counteracts its own toxin damage.
- . = TRUE
- return ..()
+ if(affected_mob.adjustToxLoss(-1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)) //it counteracts its own toxin damage.
+ return UPDATE_MOB_HEALTH
+ return
else if(SPT_PROB(2.5, seconds_per_tick))
holder.add_reagent(/datum/reagent/toxin/histamine, pick(5,15))
holder.remove_reagent(/datum/reagent/toxin/formaldehyde, 1.2)
- else
- return ..()
+ return ..()
/datum/reagent/toxin/venom
name = "Venom"
@@ -670,20 +672,23 @@
var/newsize = 1.1 * RESIZE_DEFAULT_SIZE
affected_mob.update_transform(newsize/current_size)
current_size = newsize
-
toxpwr = 0.1 * volume
- affected_mob.adjustBruteLoss((0.3 * volume) * REM * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
+
+ if(affected_mob.adjustBruteLoss((0.3 * volume) * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype))
+ . = UPDATE_MOB_HEALTH
+
+ // chance to either decay into histamine or go the normal route of toxin metabolization
if(SPT_PROB(8, seconds_per_tick))
+ current_cycle++
holder.add_reagent(/datum/reagent/toxin/histamine, pick(5, 10))
holder.remove_reagent(/datum/reagent/toxin/venom, 1.1)
else
- ..()
+ return ..() || .
/datum/reagent/toxin/venom/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
affected_mob.update_transform(RESIZE_DEFAULT_SIZE/current_size)
current_size = RESIZE_DEFAULT_SIZE
- ..()
/datum/reagent/toxin/fentanyl
name = "Fentanyl"
@@ -699,15 +704,17 @@
addiction_types = list(/datum/addiction/opioids = 25)
/datum/reagent/toxin/fentanyl/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * REM * normalise_creation_purity() * seconds_per_tick, 150)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * REM * normalise_creation_purity() * seconds_per_tick, 150)
if(affected_mob.toxloss <= 60)
- affected_mob.adjustToxLoss(1 * REM * normalise_creation_purity() * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- if(current_cycle >= 4)
+ need_mob_update += affected_mob.adjustToxLoss(1 * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
+ if(current_cycle > 4)
affected_mob.add_mood_event("smacked out", /datum/mood_event/narcotic_heavy, name)
- if(current_cycle >= 18)
+ if(current_cycle > 18)
affected_mob.Sleeping(40 * REM * normalise_creation_purity() * seconds_per_tick)
- ..()
- return TRUE
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/cyanide
name = "Cyanide"
@@ -722,13 +729,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/cyanide/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update = FALSE
if(SPT_PROB(2.5, seconds_per_tick))
affected_mob.losebreath += 1
+ need_mob_update = TRUE
if(SPT_PROB(4, seconds_per_tick))
to_chat(affected_mob, span_danger("You feel horrendously weak!"))
affected_mob.Stun(40)
- affected_mob.adjustToxLoss(2*REM * normalise_creation_purity(), FALSE, required_biotype = affected_biotype)
- return ..()
+ need_mob_update += affected_mob.adjustToxLoss(2*REM * normalise_creation_purity(), updating_health = FALSE, required_biotype = affected_biotype)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/bad_food
name = "Bad Food"
@@ -755,23 +766,26 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/itching_powder/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ var/need_mob_update = FALSE
if(SPT_PROB(8, seconds_per_tick))
to_chat(affected_mob, span_danger("You scratch at your head."))
- affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
+ need_mob_update += affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype)
if(SPT_PROB(8, seconds_per_tick))
to_chat(affected_mob, span_danger("You scratch at your leg."))
- affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
+ need_mob_update += affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype)
if(SPT_PROB(8, seconds_per_tick))
to_chat(affected_mob, span_danger("You scratch at your arm."))
- affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
+ need_mob_update += affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype)
+
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
+
if(SPT_PROB(1.5, seconds_per_tick))
holder.add_reagent(/datum/reagent/toxin/histamine,rand(1,3))
holder.remove_reagent(/datum/reagent/toxin/itching_powder,1.2)
return
- ..()
+ else
+ return ..() || .
/datum/reagent/toxin/initropidril
name = "Initropidril"
@@ -784,16 +798,17 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/initropidril/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(13, seconds_per_tick))
var/picked_option = rand(1,3)
+ var/need_mob_update
switch(picked_option)
if(1)
affected_mob.Paralyze(60)
- . = TRUE
if(2)
affected_mob.losebreath += 10
- affected_mob.adjustOxyLoss(rand(5,25), FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- . = TRUE
+ affected_mob.adjustOxyLoss(rand(5,25), updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ need_mob_update = TRUE
if(3)
if(!affected_mob.undergoing_cardiac_arrest() && affected_mob.can_heartattack())
affected_mob.set_heartattack(TRUE)
@@ -801,9 +816,9 @@
affected_mob.visible_message(span_userdanger("[affected_mob] clutches at [affected_mob.p_their()] chest as if [affected_mob.p_their()] heart stopped!"))
else
affected_mob.losebreath += 10
- affected_mob.adjustOxyLoss(rand(5,25), FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- . = TRUE
- return ..() || .
+ need_mob_update = affected_mob.adjustOxyLoss(rand(5,25), updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/pancuronium
name = "Pancuronium"
@@ -817,12 +832,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/pancuronium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle >= 10)
+ . = ..()
+ if(current_cycle > 10)
affected_mob.Stun(40 * REM * seconds_per_tick)
- . = TRUE
if(SPT_PROB(10, seconds_per_tick))
affected_mob.losebreath += 4
- ..()
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/sodium_thiopental
name = "Sodium Thiopental"
@@ -843,11 +858,11 @@
REMOVE_TRAIT(affected_mob, TRAIT_ANTICONVULSANT, name)
/datum/reagent/toxin/sodium_thiopental/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle >= 10)
+ . = ..()
+ if(current_cycle > 10)
affected_mob.Sleeping(40 * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(10 * REM * seconds_per_tick, 0)
- ..()
- return TRUE
+ if(affected_mob.adjustStaminaLoss(10 * REM * seconds_per_tick, updating_stamina = FALSE))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/sulfonal
name = "Sulfonal"
@@ -863,9 +878,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/sulfonal/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle >= 22)
+ . = ..()
+ if(current_cycle > 22)
affected_mob.Sleeping(40 * REM * normalise_creation_purity() * seconds_per_tick)
- return ..()
/datum/reagent/toxin/amanitin
name = "Amanitin"
@@ -879,13 +894,13 @@
var/delayed_toxin_damage = 0
/datum/reagent/toxin/amanitin/on_mob_life(mob/living/affected_mob, seconds_per_tick, times_fired)
- delayed_toxin_damage += (seconds_per_tick * 3)
. = ..()
+ delayed_toxin_damage += (seconds_per_tick * 3)
/datum/reagent/toxin/amanitin/on_mob_delete(mob/living/affected_mob)
+ . = ..()
affected_mob.log_message("has taken [delayed_toxin_damage] toxin damage from amanitin toxin", LOG_ATTACK)
affected_mob.adjustToxLoss(delayed_toxin_damage, required_biotype = affected_biotype)
- . = ..()
/datum/reagent/toxin/lipolicide
name = "Lipolicide"
@@ -903,12 +918,12 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/lipolicide/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.nutrition <= NUTRITION_LEVEL_STARVING)
- affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
- . = TRUE
+ if(affected_mob.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
affected_mob.adjust_nutrition(-3 * REM * normalise_creation_purity() * seconds_per_tick) // making the chef more valuable, one meme trap at a time
affected_mob.overeatduration = 0
- return ..() || .
/datum/reagent/toxin/coniine
name = "Coniine"
@@ -920,9 +935,10 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/coniine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.losebreath < 5)
affected_mob.losebreath = min(affected_mob.losebreath + 5 * REM * seconds_per_tick, 5)
- return ..()
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/spewium
name = "Spewium"
@@ -936,8 +952,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/spewium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- .=..()
- if(current_cycle >= 11 && SPT_PROB(min(30, current_cycle), seconds_per_tick))
+ . = ..()
+ if(current_cycle > 11 && SPT_PROB(min(31, current_cycle), seconds_per_tick))
+ affected_mob.vomit(10, prob(10), prob(50), rand(0,4), TRUE)
var/constructed_flags = (MOB_VOMIT_MESSAGE | MOB_VOMIT_HARM)
if(prob(10))
constructed_flags |= MOB_VOMIT_BLOOD
@@ -950,7 +967,7 @@
/datum/reagent/toxin/spewium/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
- if(current_cycle >= 33 && SPT_PROB(7.5, seconds_per_tick))
+ if(current_cycle > 33 && SPT_PROB(7.5, seconds_per_tick))
affected_mob.spew_organ()
affected_mob.vomit(VOMIT_CATEGORY_BLOOD, lost_nutrition = 0, distance = 4)
to_chat(affected_mob, span_userdanger("You feel something lumpy come up as you vomit."))
@@ -965,11 +982,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/curare/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(current_cycle >= 11)
+ . = ..()
+ if(current_cycle > 11)
affected_mob.Paralyze(60 * REM * seconds_per_tick)
- affected_mob.adjustOxyLoss(0.5*REM*seconds_per_tick, FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
- . = TRUE
- ..()
+ if(affected_mob.adjustOxyLoss(0.5*REM*seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/heparin //Based on a real-life anticoagulant. I'm not a doctor, so this won't be realistic.
name = "Heparin"
@@ -990,12 +1007,12 @@
return ..()
/datum/reagent/toxin/heparin/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_BLOODY_MESS, /datum/reagent/toxin/heparin)
- return ..()
/datum/reagent/toxin/heparin/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_BLOODY_MESS, /datum/reagent/toxin/heparin)
- return ..()
/datum/reagent/toxin/rotatium //Rotatium. Fucks up your rotation and is hilarious
name = "Rotatium"
@@ -1012,6 +1029,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/rotatium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(affected_mob.hud_used)
if(current_cycle >= 20 && (current_cycle % 20) == 0)
var/atom/movable/plane_master_controller/pm_controller = affected_mob.hud_used.plane_master_controllers[PLANE_MASTERS_GAME]
@@ -1020,14 +1038,13 @@
for(var/atom/movable/screen/plane_master/plane as anything in pm_controller.get_planes())
animate(plane, transform = matrix(rotation, MATRIX_ROTATE), time = 5, easing = QUAD_EASING, loop = -1)
animate(transform = matrix(-rotation, MATRIX_ROTATE), time = 5, easing = QUAD_EASING)
- return ..()
/datum/reagent/toxin/rotatium/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
if(affected_mob?.hud_used)
var/atom/movable/plane_master_controller/pm_controller = affected_mob.hud_used.plane_master_controllers[PLANE_MASTERS_GAME]
for(var/atom/movable/screen/plane_master/plane as anything in pm_controller.get_planes())
animate(plane, transform = matrix(), time = 5, easing = QUAD_EASING)
- ..()
/datum/reagent/toxin/anacea
name = "Anacea"
@@ -1045,13 +1062,12 @@
var/remove_amt = 5
if(holder.has_reagent(/datum/reagent/medicine/calomel) || holder.has_reagent(/datum/reagent/medicine/pen_acid))
remove_amt = 0.5
+ . = ..()
for(var/datum/reagent/medicine/R in affected_mob.reagents.reagent_list)
affected_mob.reagents.remove_reagent(R.type, remove_amt * REM * normalise_creation_purity() * seconds_per_tick)
- return ..()
//ACID
-
/datum/reagent/toxin/acid
name = "Sulfuric Acid"
description = "A strong mineral acid with the molecular formula H2SO4."
@@ -1115,9 +1131,9 @@
mytray.adjust_weedlevel(-rand(1,4))
/datum/reagent/toxin/acid/fluacid/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustFireLoss((current_cycle/15) * REM * normalise_creation_purity() * seconds_per_tick, FALSE, required_bodytype = affected_bodytype)
- . = TRUE
- ..()
+ . = ..()
+ if(affected_mob.adjustFireLoss(((current_cycle-1)/15) * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype))
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/acid/nitracid
name = "Nitric Acid"
@@ -1131,9 +1147,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/acid/nitracid/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustFireLoss((volume/10) * REM * normalise_creation_purity() * seconds_per_tick, FALSE, required_bodytype = affected_bodytype) //here you go nervar
- . = TRUE
- ..()
+ . = ..()
+ if(affected_mob.adjustFireLoss((volume/10) * REM * normalise_creation_purity() * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)) //here you go nervar
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/delayed
name = "Toxin Microcapsules"
@@ -1143,17 +1159,18 @@
var/actual_metaboliztion_rate = REAGENTS_METABOLISM
toxpwr = 0
var/actual_toxpwr = 5
- var/delay = 30
+ var/delay = 31
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/delayed/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(current_cycle > delay)
- holder.remove_reagent(type, actual_metaboliztion_rate * affected_mob.metabolism_efficiency * seconds_per_tick)
- affected_mob.adjustToxLoss(actual_toxpwr * REM * seconds_per_tick, FALSE, required_biotype = affected_biotype)
+ if(holder)
+ holder.remove_reagent(type, actual_metaboliztion_rate * affected_mob.metabolism_efficiency * seconds_per_tick)
+ if(affected_mob.adjustToxLoss(actual_toxpwr * REM * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype))
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(5, seconds_per_tick))
affected_mob.Paralyze(20)
- . = TRUE
- ..()
/datum/reagent/toxin/mimesbane
name = "Mime's Bane"
@@ -1168,9 +1185,11 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/mimesbane/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
ADD_TRAIT(affected_mob, TRAIT_EMOTEMUTE, type)
/datum/reagent/toxin/mimesbane/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
REMOVE_TRAIT(affected_mob, TRAIT_EMOTEMUTE, type)
/datum/reagent/toxin/bonehurtingjuice //oof ouch
@@ -1187,11 +1206,13 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/bonehurtingjuice/on_mob_add(mob/living/carbon/affected_mob)
+ . = ..()
affected_mob.say("oof ouch my bones", forced = /datum/reagent/toxin/bonehurtingjuice)
- return ..()
/datum/reagent/toxin/bonehurtingjuice/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustStaminaLoss(7.5 * REM * seconds_per_tick, 0)
+ . = ..()
+ if(affected_mob.adjustStaminaLoss(7.5 * REM * seconds_per_tick, updating_stamina = FALSE))
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(10, seconds_per_tick))
switch(rand(1, 3))
if(1)
@@ -1200,9 +1221,9 @@
affected_mob.manual_emote(pick("oofs silently.", "looks like [affected_mob.p_their()] bones hurt.", "grimaces, as though [affected_mob.p_their()] bones hurt."))
if(3)
to_chat(affected_mob, span_warning("Your bones hurt!"))
- return ..() || TRUE
/datum/reagent/toxin/bonehurtingjuice/overdose_process(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
if(SPT_PROB(2, seconds_per_tick) && iscarbon(affected_mob)) //big oof
var/selected_part = pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG) //God help you if the same limb gets picked twice quickly.
var/obj/item/bodypart/BP = affected_mob.get_bodypart(selected_part)
@@ -1210,11 +1231,11 @@
playsound(affected_mob, get_sfx(SFX_DESECRATION), 50, TRUE, -1)
affected_mob.visible_message(span_warning("[affected_mob]'s bones hurt too much!!"), span_danger("Your bones hurt too much!!"))
affected_mob.say("OOF!!", forced = /datum/reagent/toxin/bonehurtingjuice)
- BP.receive_damage(20, 0, 200, wound_bonus = rand(30, 130))
+ if(BP.receive_damage(brute = 20 * REM * seconds_per_tick, burn = 0, blocked = 200, updating_health = FALSE, wound_bonus = rand(30, 130)))
+ . = UPDATE_MOB_HEALTH
else //SUCH A LUST FOR REVENGE!!!
to_chat(affected_mob, span_warning("A phantom limb hurts!"))
affected_mob.say("Why are we still here, just to suffer?", forced = /datum/reagent/toxin/bonehurtingjuice)
- return ..()
/datum/reagent/toxin/bungotoxin
name = "Bungotoxin"
@@ -1227,7 +1248,9 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/bungotoxin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, 3 * REM * seconds_per_tick)
+ . = ..()
+ if(affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, 3 * REM * seconds_per_tick))
+ . = UPDATE_MOB_HEALTH
// If our mob's currently dizzy from anything else, we will also gain confusion
var/mob_dizziness = affected_mob.get_timed_status_effect_duration(/datum/status_effect/confusion)
@@ -1235,11 +1258,9 @@
// Gain confusion equal to about half the duration of our current dizziness
affected_mob.set_confusion(mob_dizziness / 2)
- if(current_cycle >= 12 && SPT_PROB(4, seconds_per_tick))
+ if(current_cycle >= 13 && SPT_PROB(4, seconds_per_tick))
var/tox_message = pick("You feel your heart spasm in your chest.", "You feel faint.","You feel you need to catch your breath.","You feel a prickle of pain in your chest.")
to_chat(affected_mob, span_notice("[tox_message]"))
- . = TRUE
- ..()
/datum/reagent/toxin/leadacetate
name = "Lead Acetate"
@@ -1252,13 +1273,16 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/toxin/leadacetate/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_EARS, 1 * REM * seconds_per_tick)
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1 * REM * seconds_per_tick)
- . = TRUE
+ . = ..()
+ var/need_mob_update
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_EARS, 1 * REM * seconds_per_tick)
+ need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1 * REM * seconds_per_tick)
+ if(need_mob_update)
+ . = UPDATE_MOB_HEALTH
if(SPT_PROB(0.5, seconds_per_tick))
to_chat(affected_mob, span_notice("Ah, what was that? You thought you heard something..."))
affected_mob.adjust_confusion(5 SECONDS)
- return ..() || .
+
/datum/reagent/toxin/hunterspider
name = "Spider Toxin"
description = "A toxic chemical produced by spiders to weaken prey."
@@ -1273,8 +1297,8 @@
liver_damage_multiplier = 0
/datum/reagent/toxin/viperspider/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
affected_mob.adjust_hallucinations(10 SECONDS * REM * seconds_per_tick)
- return ..()
/datum/reagent/toxin/tetrodotoxin
name = "Tetrodotoxin"
@@ -1294,10 +1318,12 @@
)
/datum/reagent/toxin/tetrodotoxin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
//be ready for a cocktail of symptoms, including:
//numbness, nausea, vomit, breath loss, weakness, paralysis and nerve damage/impairment and eventually a heart attack if enough time passes.
+ var/need_mob_update
switch(current_cycle)
- if(6 to 12)
+ if(7 to 13)
if(SPT_PROB(20, seconds_per_tick))
affected_mob.set_jitter_if_lower(rand(2 SECONDS, 3 SECONDS) * REM * seconds_per_tick)
if(SPT_PROB(5, seconds_per_tick))
@@ -1306,51 +1332,57 @@
to_chat(affected_mob, span_warning("your [tongue.name] feels numb..."))
affected_mob.set_slurring_if_lower(5 SECONDS * REM * seconds_per_tick)
affected_mob.adjust_disgust(3.5 * REM * seconds_per_tick)
- if(12 to 20)
+ if(13 to 21)
silent_toxin = FALSE
toxpwr = 0.5
- affected_mob.adjustStaminaLoss(2.5 * REM * seconds_per_tick, 0)
+ need_mob_update = affected_mob.adjustStaminaLoss(2.5 * REM * seconds_per_tick, updating_stamina = FALSE)
if(SPT_PROB(20, seconds_per_tick))
affected_mob.losebreath += 1 * REM * seconds_per_tick
+ need_mob_update = TRUE
if(SPT_PROB(40, seconds_per_tick))
affected_mob.set_jitter_if_lower(rand(2 SECONDS, 3 SECONDS) * REM * seconds_per_tick)
affected_mob.adjust_disgust(3 * REM * seconds_per_tick)
affected_mob.set_slurring_if_lower(1 SECONDS * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(2 * REM * seconds_per_tick, 0)
+ affected_mob.adjustStaminaLoss(2 * REM * seconds_per_tick, updating_stamina = FALSE)
if(SPT_PROB(4, seconds_per_tick))
paralyze_limb(affected_mob)
+ need_mob_update = TRUE
if(SPT_PROB(10, seconds_per_tick))
affected_mob.adjust_confusion(rand(6 SECONDS, 8 SECONDS))
- if(20 to 28)
+ if(21 to 29)
toxpwr = 1
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5)
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5)
if(SPT_PROB(40, seconds_per_tick))
affected_mob.losebreath += 2 * REM * seconds_per_tick
+ need_mob_update = TRUE
affected_mob.adjust_disgust(3 * REM * seconds_per_tick)
affected_mob.set_slurring_if_lower(3 SECONDS * REM * seconds_per_tick)
if(SPT_PROB(5, seconds_per_tick))
to_chat(affected_mob, span_danger("you feel horribly weak."))
- affected_mob.adjustStaminaLoss(5 * REM * seconds_per_tick, 0)
+ need_mob_update += affected_mob.adjustStaminaLoss(5 * REM * seconds_per_tick, updating_stamina = FALSE)
if(SPT_PROB(8, seconds_per_tick))
paralyze_limb(affected_mob)
+ need_mob_update = TRUE
if(SPT_PROB(10, seconds_per_tick))
affected_mob.adjust_confusion(rand(6 SECONDS, 8 SECONDS))
- if(28 to INFINITY)
+ if(29 to INFINITY)
toxpwr = 1.5
- affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1, BRAIN_DAMAGE_DEATH)
+ need_mob_update = affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1, BRAIN_DAMAGE_DEATH)
affected_mob.set_silence_if_lower(3 SECONDS * REM * seconds_per_tick)
- affected_mob.adjustStaminaLoss(5 * REM * seconds_per_tick, 0)
+ need_mob_update += affected_mob.adjustStaminaLoss(5 * REM * seconds_per_tick, updating_stamina = FALSE)
affected_mob.adjust_disgust(2 * REM * seconds_per_tick)
if(SPT_PROB(15, seconds_per_tick))
paralyze_limb(affected_mob)
+ need_mob_update = TRUE
if(SPT_PROB(10, seconds_per_tick))
affected_mob.adjust_confusion(rand(6 SECONDS, 8 SECONDS))
- if(current_cycle >= 38 && !length(traits_not_applied) && SPT_PROB(5, seconds_per_tick) && !affected_mob.undergoing_cardiac_arrest())
+ if(current_cycle > 38 && !length(traits_not_applied) && SPT_PROB(5, seconds_per_tick) && !affected_mob.undergoing_cardiac_arrest())
affected_mob.set_heartattack(TRUE)
to_chat(affected_mob, span_danger("you feel a burning pain spread throughout your chest, oh no..."))
- return ..()
+ if(need_mob_update)
+ return UPDATE_MOB_HEALTH
/datum/reagent/toxin/tetrodotoxin/proc/paralyze_limb(mob/living/affected_mob)
if(!length(traits_not_applied))
@@ -1360,9 +1392,11 @@
traits_not_applied -= added_trait
/datum/reagent/toxin/tetrodotoxin/on_mob_metabolize(mob/living/affected_mob)
+ . = ..()
RegisterSignal(affected_mob, COMSIG_CARBON_ATTEMPT_BREATHE, PROC_REF(block_breath))
/datum/reagent/toxin/tetrodotoxin/on_mob_end_metabolize(mob/living/affected_mob)
+ . = ..()
UnregisterSignal(affected_mob, COMSIG_CARBON_ATTEMPT_BREATHE, PROC_REF(block_breath))
// the initial() proc doesn't work for lists.
var/list/initial_list = list(
@@ -1376,5 +1410,5 @@
/datum/reagent/toxin/tetrodotoxin/proc/block_breath(mob/living/source)
SIGNAL_HANDLER
- if(current_cycle >= 28)
+ if(current_cycle > 28)
return COMSIG_CARBON_BLOCK_BREATH
diff --git a/code/modules/reagents/chemistry/reagents/unique/eigenstasium.dm b/code/modules/reagents/chemistry/reagents/unique/eigenstasium.dm
index 70db34460601f1..73dcf8aa60b482 100644
--- a/code/modules/reagents/chemistry/reagents/unique/eigenstasium.dm
+++ b/code/modules/reagents/chemistry/reagents/unique/eigenstasium.dm
@@ -77,21 +77,21 @@
return ..()
/datum/reagent/eigenstate/on_mob_life(mob/living/carbon/living_mob)
+ . = ..()
if(prob(20))
do_sparks(5,FALSE,living_mob)
- return ..()
-
/datum/reagent/eigenstate/on_mob_delete(mob/living/living_mob) //returns back to original location
+ . = ..()
do_sparks(5,FALSE,living_mob)
to_chat(living_mob, span_userdanger("You feel strangely whole again."))
if(!living_mob.reagents.has_reagent(/datum/reagent/stabilizing_agent))
do_teleport(living_mob, location_return, 0, asoundin = 'sound/effects/phasein.ogg') //Teleports home
do_sparks(5,FALSE,living_mob)
qdel(eigenstate)
- return ..()
/datum/reagent/eigenstate/overdose_start(mob/living/living_mob) //Overdose, makes you teleport randomly
+ . = ..()
to_chat(living_mob, span_userdanger("You feel like your perspective is being ripped apart as you begin flitting in and out of reality!"))
living_mob.set_jitter_if_lower(40 SECONDS)
metabolization_rate += 0.5 //So you're not stuck forever teleporting.
@@ -101,10 +101,10 @@
return ..()
/datum/reagent/eigenstate/overdose_process(mob/living/living_mob) //Overdose, makes you teleport randomly
+ . = ..()
do_sparks(5, FALSE, living_mob)
do_teleport(living_mob, get_turf(living_mob), 10, asoundin = 'sound/effects/phasein.ogg')
do_sparks(5, FALSE, living_mob)
- return ..()
//FOR ADDICTION-LIKE EFFECTS, SEE datum/status_effect/eigenstasium
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index b632bc7b6c86e9..7afd69182619b8 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -584,7 +584,7 @@
var/location = get_turf(M)
if(iscarbon(M))
if(ismonkey(M))
- M.gib()
+ M.gib(DROP_ALL_REMAINS)
else
M.vomit(VOMIT_CATEGORY_BLOOD)
new /mob/living/carbon/human/species/monkey(location, TRUE)
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index 9083de70902e76..d49976ac10eed2 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -125,16 +125,16 @@
///special size for anti cult effect
var/effective_size = round(created_volume/48)
playsound(T, 'sound/effects/pray.ogg', 80, FALSE, effective_size)
- for(var/mob/living/simple_animal/revenant/R in get_hearers_in_view(7,T))
+ for(var/mob/living/basic/revenant/ghostie in get_hearers_in_view(7,T))
var/deity
if(GLOB.deity)
deity = GLOB.deity
else
deity = "Christ"
- to_chat(R, span_userdanger("The power of [deity] compels you!"))
- R.stun(20)
- R.reveal(100)
- R.adjustHealth(50)
+ to_chat(ghostie, span_userdanger("The power of [deity] compels you!"))
+ ghostie.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, 2 SECONDS)
+ ghostie.apply_status_effect(/datum/status_effect/revenant/revealed, 10 SECONDS)
+ ghostie.adjust_health(50)
for(var/mob/living/carbon/C in get_hearers_in_view(effective_size,T))
if(IS_CULTIST(C))
to_chat(C, span_userdanger("The divine explosion sears you!"))
diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm
index 49ef6980142aa0..5234018bbd9fa7 100644
--- a/code/modules/reagents/reagent_containers.dm
+++ b/code/modules/reagents/reagent_containers.dm
@@ -144,7 +144,7 @@
span_danger("You splash the contents of [src] onto [target][punctuation]"),
ignored_mobs = target,
)
-
+ SEND_SIGNAL(target, COMSIG_ATOM_SPLASHED)
if (ismob(target))
var/mob/target_mob = target
target_mob.show_message(
diff --git a/code/modules/reagents/reagent_containers/cups/_cup.dm b/code/modules/reagents/reagent_containers/cups/_cup.dm
index b5930955a3143e..5caec9de68168c 100644
--- a/code/modules/reagents/reagent_containers/cups/_cup.dm
+++ b/code/modules/reagents/reagent_containers/cups/_cup.dm
@@ -125,7 +125,7 @@
return
var/trans = reagents.trans_to(target, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You transfer [trans] unit\s of the solution to [target]."))
+ to_chat(user, span_notice("You transfer [round(trans, 0.01)] unit\s of the solution to [target]."))
else if(target.is_drainable()) //A dispenser. Transfer FROM it TO us.
if(!target.reagents.total_volume)
@@ -137,7 +137,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 [trans] unit\s of the contents of [target]."))
+ to_chat(user, span_notice("You fill [src] with [round(trans, 0.01)] unit\s of the contents of [target]."))
target.update_appearance()
@@ -158,7 +158,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 [trans] unit\s of the contents of [target]."))
+ to_chat(user, span_notice("You fill [src] with [round(trans, 0.01)] unit\s of the contents of [target]."))
target.update_appearance()
return SECONDARY_ATTACK_CONTINUE_CHAIN
diff --git a/code/modules/reagents/reagent_containers/dropper.dm b/code/modules/reagents/reagent_containers/dropper.dm
index cf01c2cd4c2a0e..ac8d0af0d1c6b9 100644
--- a/code/modules/reagents/reagent_containers/dropper.dm
+++ b/code/modules/reagents/reagent_containers/dropper.dm
@@ -20,7 +20,7 @@
return
if(reagents.total_volume > 0)
- if(target.reagents.total_volume >= target.reagents.maximum_volume)
+ if(target.reagents.holder_full())
to_chat(user, span_notice("[target] is full."))
return
@@ -29,7 +29,7 @@
return
var/trans = 0
- var/fraction = min(amount_per_transfer_from_this/reagents.total_volume, 1)
+ var/fraction = min(amount_per_transfer_from_this / reagents.total_volume, 1)
if(ismob(target))
if(ishuman(target))
@@ -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 [trans] unit\s of the solution."))
+ to_chat(user, span_notice("You transfer [round(trans, 0.01)] 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 [trans] unit\s of the solution."))
+ to_chat(user, span_notice("You transfer [round(trans, 0.01)] 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 [trans] unit\s of the solution."))
+ to_chat(user, span_notice("You fill [src] with [round(trans, 0.01)] unit\s of the solution."))
update_appearance()
target.update_appearance()
diff --git a/code/modules/religion/burdened/psyker.dm b/code/modules/religion/burdened/psyker.dm
index 4499030642d93b..d4c752751b8bc2 100644
--- a/code/modules/religion/burdened/psyker.dm
+++ b/code/modules/religion/burdened/psyker.dm
@@ -183,7 +183,7 @@
on_clear_callback = CALLBACK(src, PROC_REF(on_cult_rune_removed)), \
effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune), \
)
- AddElement(/datum/element/bane, target_type = /mob/living/simple_animal/revenant, damage_multiplier = 0, added_damage = 25)
+ AddElement(/datum/element/bane, target_type = /mob/living/basic/revenant, damage_multiplier = 0, added_damage = 25)
name = pick(possible_names)
desc = possible_names[name]
diff --git a/code/modules/religion/honorbound/honorbound_rites.dm b/code/modules/religion/honorbound/honorbound_rites.dm
index 6ba557d5a30e2c..c9c9e71135404a 100644
--- a/code/modules/religion/honorbound/honorbound_rites.dm
+++ b/code/modules/religion/honorbound/honorbound_rites.dm
@@ -59,7 +59,7 @@
if(joining_now.mind.has_antag_datum(/datum/antagonist/cult))//what the fuck?!
to_chat(user, span_warning("[GLOB.deity] has seen a true, dark evil in [joining_now]'s heart, and they have been smitten!"))
playsound(get_turf(religious_tool), 'sound/effects/pray.ogg', 50, TRUE)
- joining_now.gib(TRUE)
+ joining_now.gib(DROP_ORGANS|DROP_BODYPARTS)
return FALSE
var/datum/brain_trauma/special/honorbound/honor = user.has_trauma_type(/datum/brain_trauma/special/honorbound)
if(joining_now in honor.guilty)
diff --git a/code/modules/religion/religion_sects.dm b/code/modules/religion/religion_sects.dm
index 9075a656ae9d79..ebd90388fda64c 100644
--- a/code/modules/religion/religion_sects.dm
+++ b/code/modules/religion/religion_sects.dm
@@ -115,7 +115,7 @@
if(hurt_limbs.len)
for(var/X in hurt_limbs)
var/obj/item/bodypart/affecting = X
- if(affecting.heal_damage(heal_amt, heal_amt, BODYTYPE_ORGANIC))
+ if(affecting.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC))
blessed.update_damage_overlays()
blessed.visible_message(span_notice("[chap] heals [blessed] with the power of [GLOB.deity]!"))
to_chat(blessed, span_boldnotice("May the power of [GLOB.deity] compel you to be healed!"))
@@ -272,7 +272,7 @@
var/list/hurt_limbs = blessed.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC)
if(hurt_limbs.len)
for(var/obj/item/bodypart/affecting as anything in hurt_limbs)
- if(affecting.heal_damage(heal_amt, heal_amt, BODYTYPE_ORGANIC))
+ if(affecting.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC))
blessed.update_damage_overlays()
blessed.visible_message(span_notice("[chap] barters a heal for [blessed] from [GLOB.deity]!"))
to_chat(blessed, span_boldnotice("May the power of [GLOB.deity] compel you to be healed! Thank you for choosing [GLOB.deity]!"))
diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm
index 56220956182b57..febb25add5301e 100644
--- a/code/modules/research/anomaly/anomaly_core.dm
+++ b/code/modules/research/anomaly/anomaly_core.dm
@@ -23,7 +23,7 @@
/obj/item/assembly/signaler/anomaly/manual_suicide(mob/living/carbon/user)
user.visible_message(span_suicide("[user]'s [src] is reacting to the radio signal, warping [user.p_their()] body!"))
user.set_suicide(TRUE)
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
/obj/item/assembly/signaler/anomaly/attack_self()
return
diff --git a/code/modules/research/bepis.dm b/code/modules/research/bepis.dm
deleted file mode 100644
index d0640ed5d1c499..00000000000000
--- a/code/modules/research/bepis.dm
+++ /dev/null
@@ -1,296 +0,0 @@
-//This system is designed to act as an in-between for cargo and science, and the first major money sink in the game outside of just buying things from cargo (As of 10/9/19, anyway).
-
-//economics defined values, subject to change should anything be too high or low in practice.
-
-#define MACHINE_OPERATION 100000
-#define MACHINE_OVERLOAD 500000
-#define MAJOR_THRESHOLD (6*CARGO_CRATE_VALUE)
-#define MINOR_THRESHOLD (4*CARGO_CRATE_VALUE)
-#define STANDARD_DEVIATION (2*CARGO_CRATE_VALUE)
-#define PART_CASH_OFFSET_AMOUNT (0.5*CARGO_CRATE_VALUE)
-
-/obj/machinery/rnd/bepis
- name = "\improper B.E.P.I.S. Chamber"
- desc = "A high fidelity testing device which unlocks the secrets of the known universe using the two most powerful substances available to man: excessive amounts of electricity and capital."
- icon = 'icons/obj/machines/bepis.dmi'
- icon_state = "chamber"
- base_icon_state = "chamber"
- density = TRUE
- layer = ABOVE_MOB_LAYER
- plane = GAME_PLANE_UPPER
- circuit = /obj/item/circuitboard/machine/bepis
-
- ///How much cash the UI and machine are depositing at a time.
- var/banking_amount = 100
- ///How much stored player cash exists within the machine.
- var/banked_cash = 0
- ///Payer's bank account.
- var/datum/bank_account/account
- ///Name on the payer's bank account.
- var/account_name
- ///When the BEPIS fails to hand out any reward, the ERROR cause will be a randomly picked string displayed on the UI.
- var/error_cause = null
-
- //Vars related to probability and chance of success for testing, using gaussian normal distribution.
- ///How much cash you will need to obtain a Major Tech Disk reward.
- var/major_threshold = MAJOR_THRESHOLD
- ///How much cash you will need to obtain a minor invention reward.
- var/minor_threshold = MINOR_THRESHOLD
- ///The standard deviation of the BEPIS's gaussian normal distribution.
- var/std = STANDARD_DEVIATION
-
- //Stock part variables
- ///Multiplier that lowers how much the BEPIS' power costs are. Maximum of 1, upgraded to a minimum of 0.7. See RefreshParts.
- var/power_saver = 1
- ///Variability on the money you actively spend on the BEPIS, with higher inaccuracy making the most change, good and bad to spent cash.
- var/inaccuracy_percentage = 1.5
- ///How much "cash" is added to your inserted cash efforts for free. Based on manipulator stock part level.
- var/positive_cash_offset = 0
- ///How much "cost" is removed from both the minor and major threshold costs. Based on laser stock part level.
- var/negative_cash_offset = 0
- ///List of objects that constitute your minor rewards. All rewards are unique or rare outside of the BEPIS.
- var/minor_rewards = list(
- //To add a new minor reward, add it here.
- /obj/item/stack/circuit_stack/full,
- /obj/item/pen/survival,
- /obj/item/flashlight/flashdark,//SKYRAT EDIT
- /obj/item/circuitboard/machine/sleeper/party,
- /obj/item/toy/sprayoncan,
- )
-
-/obj/machinery/rnd/bepis/attackby(obj/item/O, mob/user, params)
- if(!is_operational)
- to_chat(user, span_notice("[src] can't accept money when it's not functioning."))
- return
- if(istype(O, /obj/item/holochip) || istype(O, /obj/item/stack/spacecash))
- var/deposit_value = O.get_item_credit_value()
- banked_cash += deposit_value
- qdel(O)
- say("Deposited [deposit_value] credits into storage.")
- update_appearance()
- return
- if(isidcard(O))
- var/obj/item/card/id/Card = O
- if(Card.registered_account)
- account = Card.registered_account
- account_name = Card.registered_name
- say("New account detected. Console Updated.")
- else
- say("No account detected on card. Aborting.")
- return
- return ..()
-
-/obj/machinery/rnd/bepis/screwdriver_act(mob/living/user, obj/item/tool)
- return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)
-
-/obj/machinery/rnd/bepis/screwdriver_act_secondary(mob/living/user, obj/item/tool)
- return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)
-
-/obj/machinery/rnd/bepis/RefreshParts()
- . = ..()
- var/C = 0
- var/M = 0
- var/L = 0
- var/S = 0
- for(var/datum/stock_part/capacitor/capacitor in component_parts)
- C += ((capacitor.tier - 1) * 0.1)
- power_saver = 1 - C
- for(var/datum/stock_part/servo/servo in component_parts)
- M += ((servo.tier - 1) * PART_CASH_OFFSET_AMOUNT)
- positive_cash_offset = M
- for(var/datum/stock_part/micro_laser/Laser in component_parts)
- L += ((Laser.tier - 1) * PART_CASH_OFFSET_AMOUNT)
- negative_cash_offset = L
- for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
- S += ((scanning_module.tier - 1) * 0.25)
- inaccuracy_percentage = (1.5 - S)
-
-/obj/machinery/rnd/bepis/update_icon_state()
- if(panel_open == TRUE)
- icon_state = "[base_icon_state]_open"
- return ..()
- if((use_power == ACTIVE_POWER_USE) && (banked_cash > 0) && (is_operational))
- icon_state = "[base_icon_state]_active_loaded"
- return ..()
- if (((use_power == IDLE_POWER_USE) && (banked_cash > 0)) || (banked_cash > 0) && (!is_operational))
- icon_state = "[base_icon_state]_loaded"
- return ..()
- if(use_power == ACTIVE_POWER_USE && is_operational)
- icon_state = "[base_icon_state]_active"
- return ..()
- if(((use_power == IDLE_POWER_USE) && (banked_cash == 0)) || (!is_operational))
- icon_state = base_icon_state
- return ..()
- return ..()
-
-/obj/machinery/rnd/bepis/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "Bepis", name)
- ui.open()
- RefreshParts()
- if(isliving(user))
- var/mob/living/customer = user
- account = customer.get_bank_account()
-
-/obj/machinery/rnd/bepis/ui_data(mob/user)
- var/list/data = list()
- var/powered = FALSE
- var/zvalue = ((banking_amount + banked_cash) - (major_threshold - positive_cash_offset - negative_cash_offset))/(std)
- var/std_success = 0
- var/prob_success = 0
- //Admittedly this is messy, but not nearly as messy as the alternative, which is jury-rigging an entire Z-table into the code, or making an adaptive z-table.
- var/z = abs(zvalue)
- if(z > 0 && z <= 0.5)
- std_success = 19.1
- else if(z > 0.5 && z <= 1.0)
- std_success = 34.1
- else if(z > 1.0 && z <= 1.5)
- std_success = 43.3
- else if(z > 1.5 && z <= 2.0)
- std_success = 47.7
- else if(z > 2.0 && z <= 2.5)
- std_success = 49.4
- else
- std_success = 50
- if(zvalue > 0)
- prob_success = 50 + std_success
- else if(zvalue == 0)
- prob_success = 50
- else
- prob_success = 50 - std_success
-
- if(use_power == ACTIVE_POWER_USE)
- powered = TRUE
- data["account_owner"] = account_name
- data["amount"] = banking_amount
- data["stored_cash"] = account?.account_balance
- data["mean_value"] = (major_threshold - positive_cash_offset - negative_cash_offset)
- data["error_name"] = error_cause
- data["power_saver"] = power_saver
- data["accuracy_percentage"] = inaccuracy_percentage * 100
- data["positive_cash_offset"] = positive_cash_offset
- data["negative_cash_offset"] = negative_cash_offset
- data["manual_power"] = powered ? FALSE : TRUE
- data["silicon_check"] = issilicon(user)
- data["success_estimate"] = prob_success
- return data
-
-/obj/machinery/rnd/bepis/ui_act(action,params)
- . = ..()
- if(.)
- return
- switch(action)
- if("begin_experiment")
- if(use_power == IDLE_POWER_USE)
- return
- depositcash()
- if(banked_cash == 0)
- say("Please select funds to deposit to begin testing.")
- return
- calcsuccess()
- use_power(MACHINE_OPERATION * power_saver) //This thing should eat your APC battery if you're not careful.
- update_use_power(IDLE_POWER_USE) //Machine shuts off after use to prevent spam and look better visually.
- update_appearance()
- if("amount")
- var/input = text2num(params["amount"])
- if(input)
- banking_amount = input
- if("toggle_power")
- if(use_power == ACTIVE_POWER_USE)
- update_use_power(IDLE_POWER_USE)
- else
- update_use_power(ACTIVE_POWER_USE)
- update_appearance()
- if("account_reset")
- if(use_power == IDLE_POWER_USE)
- return
- account_name = ""
- account = null
- say("Account settings reset.")
- . = TRUE
-
-/**
- * Proc that handles the user's account to deposit credits for the BEPIS.
- * Handles success and fail cases for transferring credits, then logs the transaction and uses small amounts of power.
- **/
-/obj/machinery/rnd/bepis/proc/depositcash()
- var/deposit_value = 0
- deposit_value = banking_amount
- if(deposit_value == 0)
- update_appearance()
- say("Attempting to deposit 0 credits. Aborting.")
- return
- deposit_value = clamp(round(deposit_value, 1), 1, 10000)
- if(!account)
- say("Cannot find user account. Please swipe a valid ID.")
- return
- if(!account.has_money(deposit_value))
- say("You do not possess enough credits.")
- return
- account.adjust_money(-deposit_value, "Vending: B.E.P.I.S. Chamber") //The money vanishes, not paid to any accounts.
- SSblackbox.record_feedback("amount", "BEPIS_credits_spent", deposit_value)
- log_econ("[deposit_value] credits were inserted into [src] by [account.account_holder]")
- banked_cash += deposit_value
- use_power(1000 * power_saver)
- return
-
-/**
- * Proc used to determine the experiment math and results all in one.
- * Uses banked_cash and stock part levels to determine minor, major, and real gauss values for the BEPIS to hold.
- * If by the end real is larger than major, You get a tech disk. If all the disks are earned or you at least beat minor, you get a minor reward.
- **/
-
-/obj/machinery/rnd/bepis/proc/calcsuccess()
- var/turf/dropturf = null
- var/gauss_major = 0
- var/gauss_minor = 0
- var/gauss_real = 0
-
- var/turf/my_turf = get_turf(src)
- var/list/turfs = TURF_NEIGHBORS(my_turf) //NO MORE DISCS IN WINDOWS
- while(length(turfs))
- var/turf/T = pick_n_take(turfs)
- if(T.is_blocked_turf(TRUE))
- continue
- else
- dropturf = T
- break
-
- if (!dropturf)
- dropturf = drop_location()
- gauss_major = (gaussian(major_threshold, std) - negative_cash_offset) //This is the randomized profit value that this experiment has to surpass to unlock a tech.
- gauss_minor = (gaussian(minor_threshold, std) - negative_cash_offset) //And this is the threshold to instead get a minor prize.
- gauss_real = (gaussian(banked_cash, std*inaccuracy_percentage) + positive_cash_offset) //this is the randomized profit value that your experiment expects to give.
- say("Real: [gauss_real]. Minor: [gauss_minor]. Major: [gauss_major].")
- flick("chamber_flash",src)
- update_appearance()
- banked_cash = 0
- if((gauss_real >= gauss_major)) //Major Success.
- if(SSresearch.techweb_nodes_experimental.len > 0)
- say("Experiment concluded with major success. New technology node discovered on technology disc.")
- new /obj/item/disk/design_disk/bepis/remove_tech(dropturf,1)
- return
- say("Expended all available experimental technology nodes. Resorting to minor rewards.")
- if(gauss_real >= gauss_minor) //Minor Success.
- var/reward = pick(minor_rewards)
- new reward(dropturf)
- say("Experiment concluded with partial success. Dispensing compiled research efforts.")
- return
- if(gauss_real <= -1) //Critical Failure
- say("ERROR: CRITICAL MACHIME MALFUNCTI- ON. CURRENCY IS NOT CRASH. CANNOT COMPUTE COMMAND: 'make bucks'") //not a typo, for once.
- new /mob/living/basic/deer(dropturf, 1)
- use_power(MACHINE_OVERLOAD * power_saver) //To prevent gambling at low cost and also prevent spamming for infinite deer.
- return
- //Minor Failure
- error_cause = pick("attempted to sell grey products to American dominated market.","attempted to sell gray products to British dominated market.","placed wild assumption that PDAs would go out of style.","simulated product #76 damaged brand reputation mortally.","simulated business model resembled 'pyramid scheme' by 98.7%.","product accidently granted override access to all station doors.")
- say("Experiment concluded with zero product viability. Cause of error: [error_cause]")
- return
-
-
-#undef MACHINE_OPERATION
-#undef MACHINE_OVERLOAD
-#undef MAJOR_THRESHOLD
-#undef MINOR_THRESHOLD
-#undef STANDARD_DEVIATION
-#undef PART_CASH_OFFSET_AMOUNT
diff --git a/code/modules/research/designs/autolathe/service_designs.dm b/code/modules/research/designs/autolathe/service_designs.dm
index ea65fe3ef380e2..687e85d64361bb 100644
--- a/code/modules/research/designs/autolathe/service_designs.dm
+++ b/code/modules/research/designs/autolathe/service_designs.dm
@@ -142,6 +142,18 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+/datum/design/tongs
+ name = "Tongs"
+ id = "tongs"
+ build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2)
+ build_path = /obj/item/kitchen/tongs
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_KITCHEN,
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
/datum/design/tray
name = "Serving Tray"
id = "servingtray"
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 1c44e8bc4fb268..3b0bf62c267816 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -358,16 +358,6 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
-/datum/design/board/bepis
- name = "B.E.P.I.S. Board"
- desc = "The circuit board for a B.E.P.I.S."
- id = "bepis"
- build_path = /obj/item/circuitboard/machine/bepis
- category = list(
- RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_RESEARCH
- )
- departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_CARGO
-
/datum/design/board/protolathe
name = "Protolathe Board"
desc = "The circuit board for a protolathe."
@@ -542,6 +532,16 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+/datum/design/board/microwave_engineering
+ name = "Wireless Microwave Board"
+ desc = "The circuit board for a cell-powered microwave."
+ id = "microwave_engineering"
+ build_path = /obj/item/circuitboard/machine/microwave/engineering
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_KITCHEN
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE | DEPARTMENT_BITFLAG_ENGINEERING
+
/datum/design/board/gibber
name = "Gibber Board"
desc = "The circuit board for a gibber."
@@ -1106,3 +1106,13 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ROBOTICS
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/board/fishing_portal_generator
+ name = "Fishing Portal Generator Board"
+ desc = "The circuit board for the fishing portal generator"
+ id = "fishing_portal_generator"
+ build_path = /obj/item/circuitboard/machine/fishing_portal_generator
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_SERVICE
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm
index 521504da351d0b..8a06607ec59a84 100644
--- a/code/modules/research/techweb/_techweb.dm
+++ b/code/modules/research/techweb/_techweb.dm
@@ -283,7 +283,7 @@
var/datum/experiment/experiment = completed_experiment
if (experiment == experiment_type)
return FALSE
- available_experiments += new experiment_type()
+ available_experiments += new experiment_type(src)
/**
* Adds a list of experiments to this techweb by their types, ensures that no duplicates are added.
@@ -310,13 +310,21 @@
var/refund = skipped_experiment_types[completed_experiment.type] || 0
if(refund > 0)
add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = refund))
- result_text += ", refunding [refund] points."
+ result_text += ", refunding [refund] points"
// Nothing more to gain here, but we keep it in the list to prevent double dipping
skipped_experiment_types[completed_experiment.type] = -1
- else
- result_text += "!"
-
- log_research("[completed_experiment.name] ([completed_experiment.type]) has been completed on techweb [id]/[organization][refund ? ", refunding [refund] points" : ""].")
+ var/points_rewarded
+ if(completed_experiment.points_reward)
+ add_point_list(completed_experiment.points_reward)
+ points_rewarded = ",[refund > 0 ? " and" : ""] rewarding "
+ var/list/english_list_keys = list()
+ for(var/points_type in completed_experiment.points_reward)
+ english_list_keys += "[completed_experiment.points_reward[points_type]] [points_type]"
+ points_rewarded += "[english_list(english_list_keys)] points"
+ result_text += points_rewarded
+ result_text += "!"
+
+ log_research("[completed_experiment.name] ([completed_experiment.type]) has been completed on techweb [id]/[organization][refund ? ", refunding [refund] points" : ""][points_rewarded].")
return result_text
/datum/techweb/proc/printout_points()
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index 2f01252548a21c..ae50ea7f65f9e6 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -16,7 +16,7 @@
var/description = "Why are you seeing this?"
/// Whether it starts off hidden
var/hidden = FALSE
- /// If the tech can be randomly generated by the BEPIS as a reward. MEant to be fully given in tech disks, not researched
+ /// If the tech can be randomly generated by BEPIS tech as a reward. Meant to be fully given in tech disks, not researched
var/experimental = FALSE
/// Whether it's available without any research
var/starting_node = FALSE
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 3f64efe851eae8..441e6f0038b5e7 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -14,7 +14,6 @@
"basic_matter_bin",
"basic_micro_laser",
"basic_scanning",
- "bepis",
"blast",
"bounced_radio",
"bowl",
@@ -50,6 +49,7 @@
"extinguisher",
"fax",
"fishing_rod",
+ "fishing_portal_generator",
"flashlight",
"fluid_ducts",
"foam_dart",
@@ -115,6 +115,7 @@
"titaniumglass",
"toner_large",
"toner",
+ "tongs",
"toy_armblade",
"toy_balloon",
"toygun",
@@ -475,6 +476,7 @@
"gibber",
"griddle",
"microwave",
+ "microwave_engineering",
"monkey_recycler",
"oven",
"processor",
@@ -1491,6 +1493,19 @@
required_experiments = list(/datum/experiment/scanning/random/plants/wild)
discount_experiments = list(/datum/experiment/scanning/random/plants/traits = 3000)
+/datum/techweb_node/fishing
+ id = "fishing"
+ display_name = "Fishing Technology"
+ description = "Cutting edge fishing advancements."
+ prereq_ids = list("base")
+ design_ids = list(
+ "fishing_rod_tech",
+ "stabilized_hook",
+ "fish_analyzer",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2000)
+ required_experiments = list(/datum/experiment/scanning/fish)
+
/datum/techweb_node/exp_tools
id = "exp_tools"
display_name = "Experimental Tools"
@@ -2355,20 +2370,6 @@
hidden = TRUE
experimental = TRUE
-/datum/techweb_node/fishing
- id = "fishing"
- display_name = "Fishing Technology"
- description = "Cutting edge fishing advancements."
- prereq_ids = list("base")
- design_ids = list(
- "fishing_rod_tech",
- "stabilized_hook",
- "fish_analyzer",
- )
- research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
- hidden = TRUE
- experimental = TRUE
-
/datum/techweb_node/advanced_plastic_surgery
id = "plastic_surgery"
display_name = "Advanced Plastic Surgery"
diff --git a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
index 5c6ff3c811a6f1..6d4d6a7b27200b 100644
--- a/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_status_effects.dm
@@ -173,10 +173,13 @@
alert_type = /atom/movable/screen/alert/status_effect/clone_decay
/datum/status_effect/slime_clone_decay/tick(seconds_between_ticks)
- owner.adjustToxLoss(1, 0)
- owner.adjustOxyLoss(1, 0)
- owner.adjustBruteLoss(1, 0)
- owner.adjustFireLoss(1, 0)
+ var/need_mob_update
+ need_mob_update = owner.adjustToxLoss(1, updating_health = FALSE)
+ need_mob_update += owner.adjustOxyLoss(1, updating_health = FALSE)
+ need_mob_update += owner.adjustBruteLoss(1, updating_health = FALSE)
+ need_mob_update += owner.adjustFireLoss(1, updating_health = FALSE)
+ if(need_mob_update)
+ owner.updatehealth()
owner.color = "#007BA7"
/atom/movable/screen/alert/status_effect/bloodchill
@@ -505,20 +508,24 @@
/datum/status_effect/stabilized/purple/tick(seconds_between_ticks)
healed_last_tick = FALSE
+ var/need_mob_update = FALSE
if(owner.getBruteLoss() > 0)
- owner.adjustBruteLoss(-0.2)
+ need_mob_update += owner.adjustBruteLoss(-0.2, updating_health = FALSE)
healed_last_tick = TRUE
if(owner.getFireLoss() > 0)
- owner.adjustFireLoss(-0.2)
+ need_mob_update += owner.adjustFireLoss(-0.2, updating_health = FALSE)
healed_last_tick = TRUE
if(owner.getToxLoss() > 0)
// Forced, so slimepeople are healed as well.
- owner.adjustToxLoss(-0.2, forced = TRUE)
+ need_mob_update += owner.adjustToxLoss(-0.2, updating_health = FALSE, forced = TRUE)
healed_last_tick = TRUE
+ if(need_mob_update)
+ owner.updatehealth()
+
// Technically, "healed this tick" by now.
if(healed_last_tick)
new /obj/effect/temp_visual/heal(get_turf(owner), "#FF0000")
diff --git a/code/modules/research/xenobiology/crossbreeding/consuming.dm b/code/modules/research/xenobiology/crossbreeding/consuming.dm
index 007bacf8bb785f..ec90edd6550bf3 100644
--- a/code/modules/research/xenobiology/crossbreeding/consuming.dm
+++ b/code/modules/research/xenobiology/crossbreeding/consuming.dm
@@ -120,12 +120,15 @@ Consuming extracts:
taste = "fruit jam and cough medicine"
/obj/item/slime_cookie/purple/do_effect(mob/living/M, mob/user)
- M.adjustBruteLoss(-5)
- M.adjustFireLoss(-5)
- M.adjustToxLoss(-5, forced=1) //To heal slimepeople.
- M.adjustOxyLoss(-5)
- M.adjustCloneLoss(-5)
- M.adjustOrganLoss(ORGAN_SLOT_BRAIN, -5)
+ var/need_mob_update = FALSE
+ need_mob_update += M.adjustBruteLoss(-5, updating_health = FALSE)
+ need_mob_update += M.adjustFireLoss(-5, updating_health = FALSE)
+ need_mob_update += M.adjustToxLoss(-5, updating_health = FALSE, forced = TRUE) //To heal slimepeople.
+ need_mob_update += M.adjustOxyLoss(-5, updating_health = FALSE)
+ need_mob_update += M.adjustCloneLoss(-5, updating_health = FALSE)
+ need_mob_update += M.adjustOrganLoss(ORGAN_SLOT_BRAIN, -5)
+ if(need_mob_update)
+ M.updatehealth()
/obj/item/slimecross/consuming/blue
colour = SLIME_TYPE_BLUE
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 ece96fd41e04c1..0152b343c45dfd 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
@@ -263,7 +263,7 @@
/datum/reagent/medicine/psicodine = -2) //Blob zombies likely wouldn't appreciate psicodine so why this is here
virus_suspectibility = 0
- resulting_atoms = list(/mob/living/simple_animal/hostile/blob/blobspore/independent = 2) //These are useless so we might as well spawn 2.
+ resulting_atoms = list(/mob/living/basic/blob_minion/spore = 2) //These are useless so we might as well spawn 2.
/datum/micro_organism/cell_line/blobbernaut
desc = "Blobular myocytes"
@@ -282,7 +282,7 @@
suppressive_reagents = list(/datum/reagent/consumable/tinlux = -6)
virus_suspectibility = 0
- resulting_atoms = list(/mob/living/simple_animal/hostile/blob/blobbernaut/independent = 1)
+ resulting_atoms = list(/mob/living/basic/blob_minion/blobbernaut = 1)
/datum/micro_organism/cell_line/gelatinous_cube
desc = "Cubic ooze particles"
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index f81f328c0f3593..b3945fe60c437e 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -460,7 +460,7 @@
to_chat(user, span_userdanger("You explode!"))
explosion(user, devastation_range = 1, heavy_impact_range = 3, light_impact_range = 6, explosion_cause = src)
user.investigate_log("has been gibbed by an oil slime extract explosion.", INVESTIGATE_DEATHS)
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
return
to_chat(user, span_notice("You stop feeding [src], and the feeling passes."))
diff --git a/code/modules/security_levels/keycard_authentication.dm b/code/modules/security_levels/keycard_authentication.dm
index 21664265b6812d..33bbdd76f29c1b 100644
--- a/code/modules/security_levels/keycard_authentication.dm
+++ b/code/modules/security_levels/keycard_authentication.dm
@@ -59,11 +59,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/keycard_auth, 26)
/obj/machinery/keycard_auth/ui_status(mob/user)
if(isdrone(user))
return UI_CLOSE
- if(!isanimal(user))
+ if(!isanimal_or_basicmob(user))
return ..()
- var/mob/living/simple_animal/A = user
- if(!A.dextrous)
- to_chat(user, span_warning("You are too primitive to use this device!"))
+ if(!HAS_TRAIT(user, TRAIT_CAN_HOLD_ITEMS))
+ balloon_alert(user, "no hands!")
return UI_CLOSE
return ..()
diff --git a/code/modules/shuttle/on_move.dm b/code/modules/shuttle/on_move.dm
index ca49d5792725fb..7c8b26b5d16bfc 100644
--- a/code/modules/shuttle/on_move.dm
+++ b/code/modules/shuttle/on_move.dm
@@ -35,7 +35,7 @@ All ShuttleMove procs go here
SSblackbox.record_feedback("tally", "shuttle_gib", 1, M.type)
log_shuttle("[key_name(M)] was shuttle gibbed by [shuttle].")
M.investigate_log("has been gibbed by [shuttle].", INVESTIGATE_DEATHS)
- M.gib()
+ M.gib(DROP_ALL_REMAINS)
else //non-living mobs shouldn't be affected by shuttles, which is why this is an else
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 4ae1c241d06f37..e11e6bf9156f01 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -239,6 +239,9 @@
for(var/turf/T in return_turfs())
T.turf_flags |= NO_RUINS
+ if(SSshuttle.initialized)
+ INVOKE_ASYNC(SSshuttle, TYPE_PROC_REF(/datum/controller/subsystem/shuttle, setup_shuttles), list(src))
+
#ifdef DOCKING_PORT_HIGHLIGHT
highlight("#f00")
#endif
diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm
index 44c13e39c76c2c..cdabfdc4926407 100644
--- a/code/modules/shuttle/supply.dm
+++ b/code/modules/shuttle/supply.dm
@@ -322,5 +322,17 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
new /obj/structure/closet/crate/mail/economy(pick(empty_turfs))
+/// Takes a supply pack, returns the amount we currently have on order (or OVER_ORDER_LIMIT if we are over the hardcap on orders of this type)
+/obj/docking_port/mobile/supply/proc/get_order_count(datum/supply_pack/ordering)
+ var/similar_count = 0
+ for(var/datum/supply_order/order as anything in (SSshuttle.shopping_list | SSshuttle.request_list))
+ if(order.pack == ordering)
+ similar_count += 1
+
+ if(similar_count >= CARGO_MAX_ORDER)
+ return OVER_ORDER_LIMIT
+
+ return similar_count
+
#undef GOODY_FREE_SHIPPING_MAX
#undef CRATE_TAX
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 966f618376d781..5bc39b389b315d 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -140,7 +140,7 @@
// Where the cast chain starts
/datum/action/cooldown/spell/PreActivate(atom/target)
- if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src) & COMPONENT_BLOCK_ABILITY_START)
+ if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src, target) & COMPONENT_BLOCK_ABILITY_START)
return FALSE
if(target == owner)
target = get_caster_from_target(target)
@@ -180,11 +180,6 @@
to_chat(owner, span_warning("Some form of antimagic is preventing you from casting [src]!"))
return FALSE
- if(!(spell_requirements & SPELL_CASTABLE_WHILE_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED))
- if(feedback)
- to_chat(owner, span_warning("[src] cannot be cast unless you are completely manifested in the material plane!"))
- return FALSE
-
if(!try_invoke(owner, feedback = feedback))
return FALSE
diff --git a/code/modules/spells/spell_types/conjure/simian.dm b/code/modules/spells/spell_types/conjure/simian.dm
index 556a78e50127c4..aa9aabc681009d 100644
--- a/code/modules/spells/spell_types/conjure/simian.dm
+++ b/code/modules/spells/spell_types/conjure/simian.dm
@@ -14,14 +14,18 @@
invocation_type = INVOCATION_SHOUT
summon_radius = 2
- summon_type = list(/mob/living/carbon/human/species/monkey/angry, /mob/living/carbon/human/species/monkey/angry, /mob/living/simple_animal/hostile/gorilla/lesser)
+ summon_type = list(
+ /mob/living/basic/gorilla/lesser,
+ /mob/living/carbon/human/species/monkey/angry,
+ /mob/living/carbon/human/species/monkey/angry, // Listed twice so it's twice as likely, this class doesn't use pick weight
+ )
summon_amount = 4
/datum/action/cooldown/spell/conjure/simian/level_spell(bypass_cap)
. = ..()
summon_amount++ // MORE, MOOOOORE
if(spell_level == spell_max_level) // We reward the faithful.
- summon_type = list(/mob/living/carbon/human/species/monkey/angry, /mob/living/simple_animal/hostile/gorilla)
+ summon_type = list(/mob/living/carbon/human/species/monkey/angry, /mob/living/basic/gorilla)
spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC // Max level lets you cast it naked, for monkey larp.
to_chat(owner, span_notice("Your simian power has reached maximum capacity! You can now cast this spell naked, and you will create adult Gorillas with each cast."))
diff --git a/code/modules/spells/spell_types/jaunt/_jaunt.dm b/code/modules/spells/spell_types/jaunt/_jaunt.dm
index 4a94f03c0410aa..207a7ed8b5be48 100644
--- a/code/modules/spells/spell_types/jaunt/_jaunt.dm
+++ b/code/modules/spells/spell_types/jaunt/_jaunt.dm
@@ -62,7 +62,7 @@
var/obj/effect/dummy/phased_mob/jaunt = new jaunt_type(loc_override || get_turf(jaunter), jaunter)
RegisterSignal(jaunt, COMSIG_MOB_EJECTED_FROM_JAUNT, PROC_REF(on_jaunt_exited))
- spell_requirements |= SPELL_CASTABLE_WHILE_PHASED
+ check_flags &= ~AB_CHECK_PHASED
jaunter.add_traits(list(TRAIT_MAGICALLY_PHASED, TRAIT_RUNECHAT_HIDDEN, TRAIT_WEATHER_IMMUNE), REF(src))
// Don't do the feedback until we have runechat hidden.
// Otherwise the text will follow the jaunt holder, which reveals where our caster is travelling.
@@ -106,7 +106,7 @@
*/
/datum/action/cooldown/spell/jaunt/proc/on_jaunt_exited(obj/effect/dummy/phased_mob/jaunt, mob/living/unjaunter)
SHOULD_CALL_PARENT(TRUE)
- spell_requirements &= ~SPELL_CASTABLE_WHILE_PHASED
+ check_flags |= AB_CHECK_PHASED
unjaunter.remove_traits(list(TRAIT_MAGICALLY_PHASED, TRAIT_RUNECHAT_HIDDEN, TRAIT_WEATHER_IMMUNE), REF(src))
// This needs to happen at the end, after all the traits and stuff is handled
SEND_SIGNAL(unjaunter, COMSIG_MOB_AFTER_EXIT_JAUNT, src)
diff --git a/code/modules/spells/spell_types/self/basic_heal.dm b/code/modules/spells/spell_types/self/basic_heal.dm
index 135b80942062da..f68403ddeeb3f2 100644
--- a/code/modules/spells/spell_types/self/basic_heal.dm
+++ b/code/modules/spells/spell_types/self/basic_heal.dm
@@ -26,5 +26,8 @@
span_warning("A wreath of gentle light passes over [cast_on]!"),
span_notice("You wreath yourself in healing light!"),
)
- cast_on.adjustBruteLoss(-brute_to_heal, FALSE)
- cast_on.adjustFireLoss(-burn_to_heal)
+ var/need_mob_update = FALSE
+ need_mob_update += cast_on.adjustBruteLoss(-brute_to_heal, updating_health = FALSE)
+ need_mob_update += cast_on.adjustFireLoss(-burn_to_heal, updating_health = FALSE)
+ if(need_mob_update)
+ cast_on.updatehealth()
diff --git a/code/modules/spells/spell_types/self/splattercasting_spell.dm b/code/modules/spells/spell_types/self/splattercasting_spell.dm
index 1af0cacd2aa741..184a2afab7ca23 100644
--- a/code/modules/spells/spell_types/self/splattercasting_spell.dm
+++ b/code/modules/spells/spell_types/self/splattercasting_spell.dm
@@ -29,6 +29,7 @@
merely a vessel for the arcane flow. Soon, all that is left is not pain, but hunger."))
cast_on.set_species(/datum/species/vampire)
+ cast_on.blood_volume = BLOOD_VOLUME_NORMAL ///for predictable blood total amounts when the spell is first cast.
cast_on.AddComponent(/datum/component/splattercasting)
diff --git a/code/modules/spells/spell_types/shapeshift/_shape_status.dm b/code/modules/spells/spell_types/shapeshift/_shape_status.dm
index 10d42760c91b9d..94e1d549af2680 100644
--- a/code/modules/spells/spell_types/shapeshift/_shape_status.dm
+++ b/code/modules/spells/spell_types/shapeshift/_shape_status.dm
@@ -115,7 +115,7 @@
// Our caster inside was gibbed, mirror the gib to our mob
if(gibbed)
- owner.gib()
+ owner.gib(DROP_ALL_REMAINS)
// Otherwise our caster died, just make our mob die
else
diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
index 8acd6ca92475e4..5aecd863bce43c 100644
--- a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
+++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
@@ -132,7 +132,7 @@
// 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)
- cast_on.gib(TRUE, TRUE, TRUE)
+ cast_on.gib()
/// Callback for the radial that allows the user to choose their species.
/datum/action/cooldown/spell/shapeshift/proc/check_menu(mob/living/caster)
@@ -159,6 +159,12 @@
spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB)
ADD_TRAIT(new_shape, TRAIT_DONT_WRITE_MEMORY, SHAPESHIFT_TRAIT) // If you shapeshift into a pet subtype we don't want to update Poly's deathcount or something when you die
+ // Make sure that if you shapechanged into a bot, the AI can't just turn you off.
+ var/mob/living/simple_animal/bot/polymorph_bot = new_shape
+ if (istype(polymorph_bot))
+ polymorph_bot.bot_cover_flags |= BOT_COVER_EMAGGED
+ polymorph_bot.bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED
+
return new_shape
/// Actually does the un-shapeshift, from the caster. (Caster is a shapeshifted mob.)
diff --git a/code/modules/spells/spell_types/touch/smite.dm b/code/modules/spells/spell_types/touch/smite.dm
index bcd234979c6620..f02ea8247dd227 100644
--- a/code/modules/spells/spell_types/touch/smite.dm
+++ b/code/modules/spells/spell_types/touch/smite.dm
@@ -48,7 +48,7 @@
return TRUE
victim.investigate_log("has been gibbed by the smite spell.", INVESTIGATE_DEATHS)
- victim.gib()
+ victim.gib(DROP_ALL_REMAINS)
return TRUE
/obj/item/melee/touch_attack/smite
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 4b9c0114024456..f033c081eadb92 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -152,7 +152,7 @@
/// How much generic bleedstacks we have on this bodypart
var/generic_bleedstacks
/// If we have a gauze wrapping currently applied (not including splints)
- var/obj/item/stack/current_gauze
+ var/obj/item/stack/medical/gauze/current_gauze
/// If something is currently grasping this bodypart and trying to staunch bleeding (see [/obj/item/hand_item/self_grasp])
var/obj/item/hand_item/self_grasp/grasped_by
@@ -215,7 +215,6 @@
wound_resistance = reset_fantasy_variable("wound_resistance", wound_resistance)
return ..()
-
/obj/item/bodypart/Initialize(mapload)
. = ..()
if(can_be_disabled)
@@ -349,7 +348,6 @@
var/stuck_word = embedded_thing.isEmbedHarmless() ? "stuck" : "embedded"
check_list += "\t There is \a [embedded_thing] [stuck_word] in your [name]!"
-
/obj/item/bodypart/blob_act()
receive_damage(max_damage, wound_bonus = CANT_WOUND)
@@ -408,10 +406,10 @@
var/atom/drop_loc = drop_location()
if(IS_ORGANIC_LIMB(src))
playsound(drop_loc, 'sound/misc/splort.ogg', 50, TRUE, -1)
- seep_gauze(9999) // destroy any existing gauze if any exists
- for(var/obj/item/organ/bodypart_organ in get_organs())
+ QDEL_NULL(current_gauze)
+ for(var/obj/item/organ/bodypart_organ as anything in get_organs())
bodypart_organ.transfer_to_limb(src, owner)
- for(var/obj/item/organ/external/external in external_organs)
+ for(var/obj/item/organ/external/external as anything in external_organs)
external.remove_from_limb()
external.forceMove(drop_loc)
for(var/obj/item/item_in_bodypart in src)
@@ -456,16 +454,20 @@
* attack_direction - The direction the bodypart is attacked from, used to send blood flying in the opposite direction.
* damage_source - The source of damage, typically a weapon.
*/
-/obj/item/bodypart/proc/receive_damage(brute = 0, burn = 0, blocked = 0, updating_health = TRUE, required_bodytype = null, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, damage_source)
+/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
if((!brute && !burn) || hit_percent <= 0)
return FALSE
- if(owner && (owner.status_flags & GODMODE))
- return FALSE //godmode
- if(required_bodytype && !(bodytype & required_bodytype))
- return FALSE
+ if (!forced)
+ if(!isnull(owner))
+ if (owner.status_flags & GODMODE)
+ return FALSE
+ if (SEND_SIGNAL(owner, COMSIG_CARBON_LIMB_DAMAGED, src, brute, burn) & COMPONENT_PREVENT_LIMB_DAMAGE)
+ return FALSE
+ if(required_bodytype && !(bodytype & required_bodytype))
+ return FALSE
var/dmg_multi = CONFIG_GET(number/damage_multiplier) * hit_percent
brute = round(max(brute * dmg_multi * brute_modifier, 0), DAMAGE_PRECISION)
@@ -649,10 +651,10 @@
//Heals brute and burn damage for the organ. Returns 1 if the damage-icon states changed at all.
//Damage cannot go below zero.
//Cannot remove negative damage (i.e. apply damage)
-/obj/item/bodypart/proc/heal_damage(brute, burn, required_bodytype, updating_health = TRUE)
+/obj/item/bodypart/proc/heal_damage(brute, burn, updating_health = TRUE, forced = FALSE, required_bodytype)
SHOULD_CALL_PARENT(TRUE)
- if(required_bodytype && !(bodytype & required_bodytype)) //So we can only heal certain kinds of limbs, ie robotic vs organic.
+ if(!forced && required_bodytype && !(bodytype & required_bodytype)) //So we can only heal certain kinds of limbs, ie robotic vs organic.
return
if(brute)
@@ -674,7 +676,6 @@
cremation_progress = min(0, cremation_progress - ((brute_dam + burn_dam)*(100/max_damage)))
return update_bodypart_damage_state()
-
///Sets the damage of a bodypart when it is created.
/obj/item/bodypart/proc/set_initial_damage(brute_damage, burn_damage)
set_brute_dam(brute_damage)
@@ -689,7 +690,6 @@
. = brute_dam
brute_dam = new_value
-
///Proc to hook behavior associated to the change of the burn_dam variable's value.
/obj/item/bodypart/proc/set_burn_dam(new_value)
PROTECTED_PROC(TRUE)
@@ -701,8 +701,7 @@
//Returns total damage.
/obj/item/bodypart/proc/get_damage()
- var/total = brute_dam + burn_dam
- return total
+ return brute_dam + burn_dam
//Checks disabled status thresholds
/obj/item/bodypart/proc/update_disabled()
@@ -745,7 +744,6 @@
last_maxed = FALSE
set_disabled(FALSE)
-
///Proc to change the value of the `disabled` variable and react to the event of its change.
/obj/item/bodypart/proc/set_disabled(new_disabled)
SHOULD_CALL_PARENT(TRUE)
@@ -761,7 +759,6 @@
owner.update_health_hud() //update the healthdoll
owner.update_body()
-
///Proc to change the value of the `owner` variable and react to the event of its change.
/obj/item/bodypart/proc/set_owner(new_owner)
SHOULD_CALL_PARENT(TRUE)
@@ -855,7 +852,6 @@
))
set_disabled(FALSE)
-
///Called when TRAIT_PARALYSIS is added to the limb.
/obj/item/bodypart/proc/on_paralysis_trait_gain(obj/item/bodypart/source)
PROTECTED_PROC(TRUE)
@@ -864,7 +860,6 @@
if(can_be_disabled)
set_disabled(TRUE)
-
///Called when TRAIT_PARALYSIS is removed from the limb.
/obj/item/bodypart/proc/on_paralysis_trait_loss(obj/item/bodypart/source)
PROTECTED_PROC(TRUE)
@@ -873,7 +868,6 @@
if(can_be_disabled)
update_disabled()
-
///Called when TRAIT_NOLIMBDISABLE is added to the owner.
/obj/item/bodypart/proc/on_owner_nolimbdisable_trait_gain(mob/living/carbon/source)
PROTECTED_PROC(TRUE)
@@ -881,7 +875,6 @@
set_can_be_disabled(FALSE)
-
///Called when TRAIT_NOLIMBDISABLE is removed from the owner.
/obj/item/bodypart/proc/on_owner_nolimbdisable_trait_loss(mob/living/carbon/source)
PROTECTED_PROC(TRUE)
@@ -1308,17 +1301,18 @@
* Arguments:
* * gauze- Just the gauze stack we're taking a sheet from to apply here
*/
-/obj/item/bodypart/proc/apply_gauze(obj/item/stack/gauze)
- if(!istype(gauze) || !gauze.absorption_capacity)
+/obj/item/bodypart/proc/apply_gauze(obj/item/stack/medical/gauze/new_gauze)
+ if(!istype(new_gauze) || !new_gauze.absorption_capacity)
return
var/newly_gauzed = FALSE
if(!current_gauze)
newly_gauzed = TRUE
QDEL_NULL(current_gauze)
- current_gauze = new gauze.type(src, 1)
- gauze.use(1)
+ current_gauze = new new_gauze.type(src, 1)
+ new_gauze.use(1)
+ current_gauze.gauzed_bodypart = src
if(newly_gauzed)
- SEND_SIGNAL(src, COMSIG_BODYPART_GAUZED, gauze)
+ SEND_SIGNAL(src, COMSIG_BODYPART_GAUZED, current_gauze, new_gauze)
/**
* seep_gauze() is for when a gauze wrapping absorbs blood or pus from wounds, lowering its absorption capacity.
@@ -1335,7 +1329,6 @@
if(current_gauze.absorption_capacity <= 0)
owner.visible_message(span_danger("\The [current_gauze.name] on [owner]'s [name] falls away in rags."), span_warning("\The [current_gauze.name] on your [name] falls away in rags."), vision_distance=COMBAT_MESSAGE_RANGE)
QDEL_NULL(current_gauze)
- SEND_SIGNAL(src, COMSIG_BODYPART_GAUZE_DESTROYED)
///Loops through all of the bodypart's external organs and update's their color.
/obj/item/bodypart/proc/recolor_external_organs()
@@ -1383,9 +1376,10 @@
else
update_icon_dropped()
+// Note: Does NOT return EMP protection value from parent call or pass it on to subtypes
/obj/item/bodypart/emp_act(severity)
- . = ..()
- if(. & EMP_PROTECT_WIRES || !IS_ROBOTIC_LIMB(src))
+ var/protection = ..()
+ if((protection & EMP_PROTECT_WIRES) || !IS_ROBOTIC_LIMB(src))
return FALSE
// with defines at the time of writing, this is 2 brute and 1.5 burn
@@ -1402,16 +1396,14 @@
burn_damage *= 1.3 // SKYRAT EDIT : Balance - Lowers total damage from ~104 Burn to ~24
receive_damage(brute_damage, burn_damage)
- do_sparks(number = 1, cardinal_only = FALSE, source = owner)
- var/damage_percent_to_max = (get_damage() / max_damage)
- if (time_needed && (damage_percent_to_max >= robotic_emp_paralyze_damage_percent_threshold))
- owner.visible_message(span_danger("[owner]'s [src] seems to malfunction!"))
+ do_sparks(number = 1, cardinal_only = FALSE, source = owner || src)
+
+ if(can_be_disabled && (get_damage() / max_damage) >= robotic_emp_paralyze_damage_percent_threshold)
ADD_TRAIT(src, TRAIT_PARALYSIS, EMP_TRAIT)
- addtimer(CALLBACK(src, PROC_REF(un_paralyze)), time_needed)
- return TRUE
+ addtimer(TRAIT_CALLBACK_REMOVE(src, TRAIT_PARALYSIS, EMP_TRAIT), time_needed)
+ owner?.visible_message(span_danger("[owner]'s [plaintext_zone] seems to malfunction!"))
-/obj/item/bodypart/proc/un_paralyze()
- REMOVE_TRAITS_IN(src, EMP_TRAIT)
+ return TRUE
/// Returns the generic description of our BIO_EXTERNAL feature(s), prioritizing certain ones over others. Returns error on failure.
/obj/item/bodypart/proc/get_external_description()
diff --git a/code/modules/surgery/bodyparts/robot_bodyparts.dm b/code/modules/surgery/bodyparts/robot_bodyparts.dm
index 37b6cef9897507..99591daaa4b2b2 100644
--- a/code/modules/surgery/bodyparts/robot_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/robot_bodyparts.dm
@@ -111,15 +111,16 @@
/obj/item/bodypart/leg/left/robot/emp_act(severity)
. = ..()
- if(!.)
+ if(!. || isnull(owner))
return
+
var/knockdown_time = AUGGED_LEG_EMP_KNOCKDOWN_TIME
if (severity == EMP_HEAVY)
knockdown_time *= 2
owner.Knockdown(knockdown_time)
if(owner.incapacitated(IGNORE_RESTRAINTS|IGNORE_GRAB)) // So the message isn't duplicated. If they were stunned beforehand by something else, then the message not showing makes more sense anyways.
return
- to_chat(owner, span_danger("As your [src] unexpectedly malfunctions, it causes you to fall to the ground!"))
+ to_chat(owner, span_danger("As your [plaintext_zone] unexpectedly malfunctions, it causes you to fall to the ground!"))
/obj/item/bodypart/leg/right/robot
name = "cyborg right leg"
@@ -156,15 +157,16 @@
/obj/item/bodypart/leg/right/robot/emp_act(severity)
. = ..()
- if(!.)
+ if(!. || isnull(owner))
return
+
var/knockdown_time = AUGGED_LEG_EMP_KNOCKDOWN_TIME
if (severity == EMP_HEAVY)
knockdown_time *= 2
owner.Knockdown(knockdown_time)
if(owner.incapacitated(IGNORE_RESTRAINTS|IGNORE_GRAB)) // So the message isn't duplicated. If they were stunned beforehand by something else, then the message not showing makes more sense anyways.
return
- to_chat(owner, span_danger("As your [src] unexpectedly malfunctions, it causes you to fall to the ground!"))
+ to_chat(owner, span_danger("As your [plaintext_zone] unexpectedly malfunctions, it causes you to fall to the ground!"))
/obj/item/bodypart/chest/robot
name = "cyborg torso"
@@ -203,7 +205,7 @@
/obj/item/bodypart/chest/robot/emp_act(severity)
. = ..()
- if(!.)
+ if(!. || isnull(owner))
return
var/stun_time = 0
@@ -219,7 +221,7 @@
var/damage_percent_to_max = (get_damage() / max_damage)
if (stun_time && (damage_percent_to_max >= robotic_emp_paralyze_damage_percent_threshold))
- to_chat(owner, span_danger("Your [src]'s logic boards temporarily become unresponsive!"))
+ to_chat(owner, span_danger("Your [plaintext_zone]'s logic boards temporarily become unresponsive!"))
owner.Stun(stun_time)
owner.Shake(pixelshiftx = shift_x, pixelshifty = shift_y, duration = shake_duration)
@@ -338,9 +340,10 @@
/obj/item/bodypart/head/robot/emp_act(severity)
. = ..()
- if(!.)
+ if(!. || isnull(owner))
return
- to_chat(owner, span_danger("Your [src]'s optical transponders glitch out and malfunction!"))
+
+ to_chat(owner, span_danger("Your [plaintext_zone]'s optical transponders glitch out and malfunction!"))
var/glitch_duration = AUGGED_HEAD_EMP_GLITCH_DURATION
if (severity == EMP_HEAVY)
diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm
index 632e4c8b511ebf..5b78cb30796e75 100644
--- a/code/modules/surgery/organs/_organ.dm
+++ b/code/modules/surgery/organs/_organ.dm
@@ -6,6 +6,8 @@
throwforce = 0
/// The mob that owns this organ.
var/mob/living/carbon/owner = null
+ /// The cached info about the blood this organ belongs to
+ var/list/blood_dna_info = list("Synthetic DNA" = "O+") // not every organ spawns inside a person
/// The body zone this organ is supposed to inhabit.
var/zone = BODY_ZONE_CHEST
/**
@@ -14,7 +16,7 @@
*/
var/slot
/// Random flags that describe this organ
- var/organ_flags = ORGAN_ORGANIC | ORGAN_EDIBLE
+ var/organ_flags = ORGAN_ORGANIC | ORGAN_EDIBLE | ORGAN_VIRGIN
/// Maximum damage the organ can take, ever.
var/maxHealth = STANDARD_ORGAN_THRESHOLD
/**
@@ -75,6 +77,9 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
volume = reagent_vol,\
after_eat = CALLBACK(src, PROC_REF(OnEatFrom)))
+ if(!IS_ROBOTIC_ORGAN(src))
+ add_blood_DNA(blood_dna_info)
+
/*
* Insert the organ into the select mob.
*
@@ -100,6 +105,14 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
receiver.organs_slot[slot] = src
owner = receiver
+ if(!IS_ROBOTIC_ORGAN(src) && (organ_flags & ORGAN_VIRGIN))
+ blood_dna_info = receiver.get_blood_dna_list()
+ // need to remove the synethic blood DNA that is initialized
+ // wash also adds the blood dna again
+ wash(CLEAN_TYPE_BLOOD)
+ organ_flags &= ~ORGAN_VIRGIN
+
+
// Apply unique side-effects. Return value does not matter.
on_insert(receiver, special)
@@ -168,6 +181,9 @@ 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))
+ AddElement(/datum/element/decal/blood)
+
var/list/diseases = organ_owner.get_static_viruses()
if(!LAZYLEN(diseases))
return
@@ -228,6 +244,14 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
/obj/item/organ/proc/on_surgical_removal(mob/living/user, mob/living/carbon/old_owner, target_zone, obj/item/tool)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_ORGAN_SURGICALLY_REMOVED, user, old_owner, target_zone, tool)
+ RemoveElement(/datum/element/decal/blood)
+
+/obj/item/organ/wash(clean_types)
+ . = ..()
+
+ // always add the original dna to the organ after it's washed
+ if(!IS_ROBOTIC_ORGAN(src) && (clean_types & CLEAN_TYPE_BLOOD))
+ add_blood_DNA(blood_dna_info)
/obj/item/organ/process(seconds_per_tick, times_fired)
return
@@ -267,17 +291,18 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
/obj/item/organ/item_action_slot_check(slot,mob/user)
return //so we don't grant the organ's action to mobs who pick up the organ.
-///Adjusts an organ's damage by the amount "damage_amount", up to a maximum amount, which is by default max damage
+///Adjusts an organ's damage by the amount "damage_amount", up to a maximum amount, which is by default max damage. Returns the net change in organ damage.
/obj/item/organ/proc/apply_organ_damage(damage_amount, maximum = maxHealth, required_organ_flag = NONE) //use for damaging effects
if(!damage_amount) //Micro-optimization.
- return
+ return FALSE
maximum = clamp(maximum, 0, maxHealth) // the logical max is, our max
if(maximum < damage)
- return
+ return FALSE
if(required_organ_flag && !(organ_flags & required_organ_flag))
- return
+ return FALSE
damage = clamp(damage + damage_amount, 0, maximum)
- var/mess = check_damage_thresholds(owner)
+ . = (prev_damage - damage) // return net damage
+ var/message = check_damage_thresholds(owner)
prev_damage = damage
if(damage >= maxHealth)
@@ -285,8 +310,8 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
else
organ_flags &= ~ORGAN_FAILING
- if(mess && owner && owner.stat <= SOFT_CRIT)
- to_chat(owner, mess)
+ if(message && owner && owner.stat <= SOFT_CRIT)
+ to_chat(owner, message)
///SETS an organ's damage to the amount "damage_amount", and in doing so clears or sets the failing flag, good for when you have an effect that should fix an organ if broken
/obj/item/organ/proc/set_organ_damage(damage_amount, required_organ_flag = NONE) //use mostly for admin heals
diff --git a/code/modules/surgery/organs/internal/appendix/_appendix.dm b/code/modules/surgery/organs/internal/appendix/_appendix.dm
index a52479c10a7321..bb02c8b9ef9e9c 100644
--- a/code/modules/surgery/organs/internal/appendix/_appendix.dm
+++ b/code/modules/surgery/organs/internal/appendix/_appendix.dm
@@ -34,7 +34,7 @@
if(organ_flags & ORGAN_FAILING)
// forced to ensure people don't use it to gain tox as slime person
- owner.adjustToxLoss(2 * seconds_per_tick, updating_health = TRUE, forced = TRUE)
+ owner.adjustToxLoss(2 * seconds_per_tick, forced = TRUE)
else if(inflamation_stage)
inflamation(seconds_per_tick)
else if(SPT_PROB(APPENDICITIS_PROB, seconds_per_tick))
@@ -62,7 +62,7 @@
to_chat(organ_owner, span_warning("You feel a stabbing pain in your abdomen!"))
organ_owner.adjustOrganLoss(ORGAN_SLOT_APPENDIX, 5)
organ_owner.Stun(rand(40, 60))
- organ_owner.adjustToxLoss(1, updating_health = TRUE, forced = TRUE)
+ organ_owner.adjustToxLoss(1, forced = TRUE)
if(3)
if(SPT_PROB(0.5, seconds_per_tick))
organ_owner.vomit(VOMIT_CATEGORY_DEFAULT, lost_nutrition = 95)
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm b/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm
index 60322a7f8d6a98..1ea3a1bf9c4abc 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm
@@ -52,43 +52,84 @@
slot = ORGAN_SLOT_HEART_AID
var/revive_cost = 0
var/reviving = FALSE
+ /// revival/defibrillation possibility flag that gathered from owner's .can_defib() proc
+ var/can_defib_owner
COOLDOWN_DECLARE(reviver_cooldown)
+/obj/item/organ/internal/cyberimp/chest/reviver/on_death(seconds_per_tick, times_fired)
+ if(isnull(owner)) // owner can be null, on_death() gets called by /obj/item/organ/internal/process() for decay
+ return
+ try_heal() // Allows implant to work even on dead people
/obj/item/organ/internal/cyberimp/chest/reviver/on_life(seconds_per_tick, times_fired)
+ try_heal()
+
+/obj/item/organ/internal/cyberimp/chest/reviver/proc/try_heal()
if(reviving)
- switch(owner.stat)
- if(UNCONSCIOUS, HARD_CRIT, SOFT_CRIT)
- addtimer(CALLBACK(src, PROC_REF(heal)), 3 SECONDS)
- else
- COOLDOWN_START(src, reviver_cooldown, revive_cost)
- reviving = FALSE
- to_chat(owner, span_notice("Your reviver implant shuts down and starts recharging. It will be ready again in [DisplayTimeText(revive_cost)]."))
+ if(owner.stat == CONSCIOUS)
+ COOLDOWN_START(src, reviver_cooldown, revive_cost)
+ reviving = FALSE
+ to_chat(owner, span_notice("Your reviver implant shuts down and starts recharging. It will be ready again in [DisplayTimeText(revive_cost)]."))
+ else
+ addtimer(CALLBACK(src, PROC_REF(heal)), 3 SECONDS)
return
if(!COOLDOWN_FINISHED(src, reviver_cooldown) || HAS_TRAIT(owner, TRAIT_SUICIDED))
return
- switch(owner.stat)
- if(UNCONSCIOUS, HARD_CRIT)
- revive_cost = 0
- reviving = TRUE
- to_chat(owner, span_notice("You feel a faint buzzing as your reviver implant starts patching your wounds..."))
+ if(owner.stat != CONSCIOUS)
+ revive_cost = 0
+ reviving = TRUE
+ to_chat(owner, span_notice("You feel a faint buzzing as your reviver implant starts patching your wounds..."))
/obj/item/organ/internal/cyberimp/chest/reviver/proc/heal()
+ if(can_defib_owner == DEFIB_POSSIBLE)
+ revive_dead()
+ can_defib_owner = null
+ revive_cost += 10 MINUTES // Additional 10 minutes cooldown after revival.
+ // this check goes after revive_dead() to delay revival a bit
+ if(owner.stat == DEAD)
+ can_defib_owner = owner.can_defib()
+ if(can_defib_owner == DEFIB_POSSIBLE)
+ owner.notify_ghost_cloning("You are being revived by [src]!")
+ owner.grab_ghost()
+ /// boolean that stands for if PHYSICAL damage being patched
+ var/body_damage_patched = FALSE
+ var/need_mob_update = FALSE
if(owner.getOxyLoss())
- owner.adjustOxyLoss(-5)
+ need_mob_update += owner.adjustOxyLoss(-5, updating_health = FALSE)
revive_cost += 5
if(owner.getBruteLoss())
- owner.adjustBruteLoss(-2)
+ need_mob_update += owner.adjustBruteLoss(-2, updating_health = FALSE)
revive_cost += 40
+ body_damage_patched = TRUE
if(owner.getFireLoss())
- owner.adjustFireLoss(-2)
+ need_mob_update += owner.adjustFireLoss(-2, updating_health = FALSE)
revive_cost += 40
+ body_damage_patched = TRUE
if(owner.getToxLoss())
- owner.adjustToxLoss(-1)
+ need_mob_update += owner.adjustToxLoss(-1, updating_health = FALSE)
revive_cost += 40
+ if(need_mob_update)
+ owner.updatehealth()
+
+ if(body_damage_patched && prob(35)) // healing is called every few seconds, not every tick
+ owner.visible_message(span_warning("[owner]'s body twitches a bit."), span_notice("You feel like something is patching your injured body."))
+
+
+/obj/item/organ/internal/cyberimp/chest/reviver/proc/revive_dead()
+ owner.grab_ghost()
+
+ owner.visible_message(span_warning("[owner]'s body convulses a bit."))
+ playsound(owner, SFX_BODYFALL, 50, TRUE)
+ playsound(owner, 'sound/machines/defib_zap.ogg', 75, TRUE, -1)
+ owner.revive()
+ owner.emote("gasp")
+ owner.set_jitter_if_lower(200 SECONDS)
+ SEND_SIGNAL(owner, COMSIG_LIVING_MINOR_SHOCK)
+ log_game("[owner] been revived by [src]")
+
/obj/item/organ/internal/cyberimp/chest/reviver/emp_act(severity)
. = ..()
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index aca812a6186f58..05f532ae444f5c 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -187,7 +187,7 @@
/obj/item/organ/internal/eyes/apply_organ_damage(damage_amount, maximum = maxHealth, required_organ_flag)
. = ..()
if(!owner)
- return
+ return FALSE
apply_damaged_eye_effects()
/// Applies effects to our owner based on how damaged our eyes are
diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm
index e553f8f130739e..fb97ca7eda00b7 100644
--- a/code/modules/surgery/organs/internal/heart/_heart.dm
+++ b/code/modules/surgery/organs/internal/heart/_heart.dm
@@ -6,7 +6,7 @@
visual = FALSE
zone = BODY_ZONE_CHEST
slot = ORGAN_SLOT_HEART
-
+ item_flags = NO_BLOOD_ON_ITEM
healing_factor = STANDARD_ORGAN_HEALING
decay_factor = 2.5 * STANDARD_ORGAN_DECAY //designed to fail around 6 minutes after death
@@ -104,20 +104,12 @@
icon_state = "cursedheart-off"
base_icon_state = "cursedheart"
decay_factor = 0
- actions_types = list(/datum/action/item_action/organ_action/cursed_heart)
- var/last_pump = 0
- var/add_colour = TRUE //So we're not constantly recreating colour datums
- /// How long between needed pumps; you can pump one second early
var/pump_delay = 3 SECONDS
- /// How much blood volume you lose every missed pump, this is a flat amount not a percentage!
- var/blood_loss = (BLOOD_VOLUME_NORMAL / 5) // 20% of normal volume, missing five pumps is instant death
-
- //How much to heal per pump, negative numbers would HURT the player
+ var/blood_loss = BLOOD_VOLUME_NORMAL * 0.2
var/heal_brute = 0
var/heal_burn = 0
var/heal_oxy = 0
-
/obj/item/organ/internal/heart/cursed/attack(mob/living/carbon/human/accursed, mob/living/carbon/human/user, obj/target)
if(accursed == user && istype(accursed))
playsound(user,'sound/effects/singlebeat.ogg',40,TRUE)
@@ -126,75 +118,13 @@
else
return ..()
-/// Worker proc that checks logic for if a pump can happen, and applies effects/notifications from doing so
-/obj/item/organ/internal/heart/cursed/proc/on_pump(mob/owner)
- var/next_pump = last_pump + pump_delay - (1 SECONDS) // pump a second early
- if(world.time < next_pump)
- to_chat(owner, span_userdanger("Too soon!"))
- return
-
- last_pump = world.time
- playsound(owner,'sound/effects/singlebeat.ogg', 40, TRUE)
- to_chat(owner, span_notice("Your heart beats."))
-
- if(!ishuman(owner))
- return
- var/mob/living/carbon/human/accursed = owner
-
- if(HAS_TRAIT(accursed, TRAIT_NOBLOOD) || !accursed.dna)
- return
- accursed.blood_volume = min(accursed.blood_volume + (blood_loss * 0.5), BLOOD_VOLUME_MAXIMUM)
- accursed.remove_client_colour(/datum/client_colour/cursed_heart_blood)
- add_colour = TRUE
- accursed.adjustBruteLoss(-heal_brute)
- accursed.adjustFireLoss(-heal_burn)
- accursed.adjustOxyLoss(-heal_oxy)
-
-/obj/item/organ/internal/heart/cursed/on_life(seconds_per_tick, times_fired)
- if(!owner.client || !ishuman(owner)) // Let's be fair, if you're not here to pump, you're not here to suffer.
- last_pump = world.time
- return
-
- if(world.time <= (last_pump + pump_delay))
- return
-
- var/mob/living/carbon/human/accursed = owner
- if(HAS_TRAIT(accursed, TRAIT_NOBLOOD) || !accursed.dna)
- return
-
- accursed.blood_volume = max(accursed.blood_volume - blood_loss, 0)
- to_chat(accursed, span_userdanger("You have to keep pumping your blood!"))
- if(add_colour)
- accursed.add_client_colour(/datum/client_colour/cursed_heart_blood) //bloody screen so real
- add_colour = FALSE
-
/obj/item/organ/internal/heart/cursed/on_insert(mob/living/carbon/accursed)
. = ..()
- last_pump = world.time // give them time to react
- to_chat(accursed, span_userdanger("Your heart has been replaced with a cursed one, you have to pump this one manually otherwise you'll die!"))
+ accursed.AddComponent(/datum/component/manual_heart, pump_delay = pump_delay, blood_loss = blood_loss, heal_brute = heal_brute, heal_burn = heal_burn, heal_oxy = heal_oxy)
/obj/item/organ/internal/heart/cursed/Remove(mob/living/carbon/accursed, special = FALSE)
. = ..()
- accursed.remove_client_colour(/datum/client_colour/cursed_heart_blood)
-
-/datum/action/item_action/organ_action/cursed_heart
- name = "Pump your blood"
- check_flags = NONE
-
-//You are now brea- pumping blood manually
-/datum/action/item_action/organ_action/cursed_heart/Trigger(trigger_flags)
- . = ..()
- if(!.)
- return
-
- var/obj/item/organ/internal/heart/cursed/cursed_heart = target
- if(!istype(cursed_heart))
- CRASH("Cursed heart pump action created on non-cursed heart!")
- cursed_heart.on_pump(owner)
-
-/datum/client_colour/cursed_heart_blood
- priority = 100 //it's an indicator you're dying, so it's very high priority
- colour = "#FF0000"
+ qdel(accursed.GetComponent(/datum/component/manual_heart))
/obj/item/organ/internal/heart/cybernetic
name = "basic cybernetic heart"
diff --git a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
index f29ee2e731df91..bb0b30ddd62869 100644
--- a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
+++ b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
@@ -108,7 +108,7 @@
if(!COOLDOWN_FINISHED(src, crystalize_cooldown) || ethereal.stat != DEAD)
return //Should probably not happen, but lets be safe.
- if(ismob(location) || isitem(location) || HAS_TRAIT_FROM(src, TRAIT_HUSK, CHANGELING_DRAIN)) //Stops crystallization if they are eaten by a dragon, turned into a legion, consumed by his grace, etc.
+ if(ismob(location) || isitem(location) || iseffect(location) || HAS_TRAIT_FROM(src, TRAIT_HUSK, CHANGELING_DRAIN)) //Stops crystallization if they are eaten by a dragon, turned into a legion, consumed by his grace, etc.
to_chat(ethereal, span_warning("You were unable to finish your crystallization, for obvious reasons."))
stop_crystalization_process(ethereal, FALSE)
return
diff --git a/code/modules/surgery/organs/internal/liver/liver_skeleton.dm b/code/modules/surgery/organs/internal/liver/liver_skeleton.dm
index f9b41741b4a34d..b57afd245f587a 100644
--- a/code/modules/surgery/organs/internal/liver/liver_skeleton.dm
+++ b/code/modules/surgery/organs/internal/liver/liver_skeleton.dm
@@ -19,8 +19,8 @@
if((. & COMSIG_MOB_STOP_REAGENT_CHECK) || (organ_flags & ORGAN_FAILING))
return
if(istype(chem, /datum/reagent/toxin/bonehurtingjuice))
- organ_owner.adjustStaminaLoss(7.5 * REM * seconds_per_tick, 0)
- organ_owner.adjustBruteLoss(0.5 * REM * seconds_per_tick, 0)
+ organ_owner.adjustStaminaLoss(7.5 * REM * seconds_per_tick, updating_stamina = FALSE)
+ organ_owner.adjustBruteLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE)
if(SPT_PROB(10, seconds_per_tick))
switch(rand(1, 3))
if(1)
diff --git a/code/modules/surgery/organs/internal/tongue/_tongue.dm b/code/modules/surgery/organs/internal/tongue/_tongue.dm
index e34ceb8bd617e5..0dc9a6cfb267de 100644
--- a/code/modules/surgery/organs/internal/tongue/_tongue.dm
+++ b/code/modules/surgery/organs/internal/tongue/_tongue.dm
@@ -148,7 +148,7 @@
/obj/item/organ/internal/tongue/apply_organ_damage(damage_amount, maximum = maxHealth, required_organ_flag)
. = ..()
if(!owner)
- return
+ return FALSE
apply_tongue_effects()
/// Applies effects to our owner based on how damaged our tongue is
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 20ca34d52596b3..dd3a7ec9eff452 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -100,6 +100,7 @@
#include "baseturfs.dm"
#include "bespoke_id.dm"
#include "binary_insert.dm"
+#include "bitrunning.dm"
#include "blindness.dm"
#include "bloody_footprints.dm"
#include "breath.dm"
@@ -175,6 +176,8 @@
#include "metabolizing.dm"
#include "mindbound_actions.dm"
#include "missing_icons.dm"
+#include "mob_chains.dm"
+#include "mob_damage.dm"
#include "mob_faction.dm"
#include "mob_spawn.dm"
#include "modify_fantasy_variable.dm"
diff --git a/code/modules/unit_tests/bitrunning.dm b/code/modules/unit_tests/bitrunning.dm
new file mode 100644
index 00000000000000..568eeeed8c1334
--- /dev/null
+++ b/code/modules/unit_tests/bitrunning.dm
@@ -0,0 +1,15 @@
+/// Ensures settings on vdoms are correct
+/datum/unit_test/bitrunner_vdom_settings
+
+/datum/unit_test/bitrunner_vdom_settings/Run()
+ var/obj/structure/closet/crate/secure/bitrunning/decrypted/cache = allocate(/obj/structure/closet/crate/secure/bitrunning/decrypted)
+
+ for(var/path in subtypesof(/datum/lazy_template/virtual_domain))
+ var/datum/lazy_template/virtual_domain/vdom = new path
+ TEST_ASSERT_NOTNULL(vdom.key, "[path] should have a key")
+ TEST_ASSERT_NOTNULL(vdom.map_name, "[path] should have a map name")
+
+ if(!length(vdom.extra_loot))
+ continue
+
+ TEST_ASSERT_EQUAL(cache.spawn_loot(vdom.extra_loot), TRUE, "[path] didn't spawn loot. Extra loot should be an associative list")
diff --git a/code/modules/unit_tests/fish_unit_tests.dm b/code/modules/unit_tests/fish_unit_tests.dm
index 1ef15f8d0f5126..d0d39227f43b73 100644
--- a/code/modules/unit_tests/fish_unit_tests.dm
+++ b/code/modules/unit_tests/fish_unit_tests.dm
@@ -28,10 +28,10 @@
var/obj/structure/aquarium/traits/aquarium = allocate(/obj/structure/aquarium/traits)
TEST_ASSERT(!aquarium.sterile.try_to_reproduce(), "The test aquarium's sterile fish managed to reproduce when it shouldn't have")
var/obj/item/fish/crossbreeder_jr = aquarium.crossbreeder.try_to_reproduce()
- TEST_ASSERT(crossbreeder_jr, "The test aquarium's crossbreeder fish didn't manage to reproduce when it should have.")
+ TEST_ASSERT(crossbreeder_jr, "The test aquarium's crossbreeder fish didn't manage to reproduce when it should have")
TEST_ASSERT_EQUAL(crossbreeder_jr.type, aquarium.cloner.type, "The test aquarium's crossbreeder fish mated with the wrong type of fish")
var/obj/item/fish/cloner_jr = aquarium.cloner.try_to_reproduce()
- TEST_ASSERT(cloner_jr, "The test aquarium's cloner fish didn't manage to reproduce when it should have.")
+ TEST_ASSERT(cloner_jr, "The test aquarium's cloner fish didn't manage to reproduce when it should have")
TEST_ASSERT_NOTEQUAL(cloner_jr.type, aquarium.sterile.type, "The test aquarium's cloner fish mated with the sterile fish")
///Checks that fish evolutions work correctly.
@@ -41,11 +41,24 @@
var/obj/structure/aquarium/evolution/aquarium = allocate(/obj/structure/aquarium/evolution)
var/obj/item/fish/evolve_jr = aquarium.evolve.try_to_reproduce()
TEST_ASSERT(evolve_jr, "The test aquarium's evolution fish didn't manage to reproduce when it should have")
- TEST_ASSERT_NOTEQUAL(evolve_jr.type, /obj/item/fish/goldfish, "The test aquarium's evolution fish managed to pass the conditions of an impossible evolution.")
+ TEST_ASSERT_NOTEQUAL(evolve_jr.type, /obj/item/fish/goldfish, "The test aquarium's evolution fish managed to pass the conditions of an impossible evolution")
TEST_ASSERT_EQUAL(evolve_jr.type, /obj/item/fish/clownfish, "The test aquarium's evolution fish's offspring isn't of the expected type")
TEST_ASSERT(!(/datum/fish_trait/dummy in evolve_jr.fish_traits), "The test aquarium's evolution fish's offspring still has the old trait that ought to be removed by the evolution datum")
TEST_ASSERT(/datum/fish_trait/dummy/two in evolve_jr.fish_traits, "The test aquarium's evolution fish's offspring doesn't have the evolution trait")
+/datum/unit_test/fish_scanning
+
+/datum/unit_test/fish_scanning/Run()
+ var/scannable_fishes = 0
+ for(var/obj/item/fish/fish_prototype as anything in subtypesof(/obj/item/fish))
+ if(initial(fish_prototype.experisci_scannable))
+ scannable_fishes++
+ for(var/datum/experiment/scanning/fish/fish_scan as anything in typesof(/datum/experiment/scanning/fish))
+ fish_scan = new fish_scan
+ var/scan_key = fish_scan.required_atoms[1]
+ if(fish_scan.required_atoms[scan_key] > scannable_fishes)
+ TEST_FAIL("[fish_scan.type] has requirements higher than the number of scannable fish types in the game: [scannable_fishes]")
+
///dummy fish item used for the tests, as well with related subtypes and datums.
/obj/item/fish/testdummy
grind_results = list()
diff --git a/code/modules/unit_tests/heretic_rituals.dm b/code/modules/unit_tests/heretic_rituals.dm
index 7298a16327490a..4ac5bce8d3d3a1 100644
--- a/code/modules/unit_tests/heretic_rituals.dm
+++ b/code/modules/unit_tests/heretic_rituals.dm
@@ -63,6 +63,8 @@
var/list/created_atoms = list()
for(var/ritual_item_path in knowledge.required_atoms)
var/amount_to_create = knowledge.required_atoms[ritual_item_path]
+ if(islist(ritual_item_path))
+ ritual_item_path = pick(ritual_item_path)
for(var/i in 1 to amount_to_create)
created_atoms += new ritual_item_path(get_turf(our_heretic))
diff --git a/code/modules/unit_tests/mob_chains.dm b/code/modules/unit_tests/mob_chains.dm
new file mode 100644
index 00000000000000..2562019958e46d
--- /dev/null
+++ b/code/modules/unit_tests/mob_chains.dm
@@ -0,0 +1,31 @@
+/// Checks if mobs who are linked together with the mob chain component react as expected
+/datum/unit_test/mob_chains
+
+/datum/unit_test/mob_chains/Run()
+ var/mob/living/centipede_head = allocate(/mob/living/basic/pet/dog)
+ var/list/segments = list(centipede_head)
+ centipede_head.AddComponent(/datum/component/mob_chain)
+ var/mob/living/centipede_tail = centipede_head
+ for (var/i in 1 to 2)
+ var/mob/living/new_segment = allocate(/mob/living/basic/pet/dog)
+ new_segment.AddComponent(/datum/component/mob_chain, front = centipede_tail)
+ segments += new_segment
+ centipede_tail = new_segment
+
+ var/test_damage = 15
+ centipede_head.apply_damage(test_damage, BRUTE)
+ TEST_ASSERT_EQUAL(centipede_head.bruteloss, 0, "Centipede head took damage which should have been passed to its tail.")
+ TEST_ASSERT_EQUAL(centipede_tail.bruteloss, test_damage, "Centipede tail did not take damage which should have originated from its head.")
+
+ var/expected_damage = 5
+ for (var/mob/living/segment as anything in segments)
+ segment.combat_mode = TRUE
+ segment.melee_damage_lower = expected_damage
+ segment.melee_damage_upper = expected_damage
+
+ var/mob/living/victim = allocate(/mob/living/basic/pet/dog)
+ centipede_head.ClickOn(victim)
+ TEST_ASSERT_EQUAL(victim.bruteloss, expected_damage * 3, "Centipede failed to do damage with all of its segments.")
+
+ centipede_head.death()
+ TEST_ASSERT_EQUAL(centipede_tail.stat, DEAD, "Centipede tail failed to die with head.")
diff --git a/code/modules/unit_tests/mob_damage.dm b/code/modules/unit_tests/mob_damage.dm
new file mode 100644
index 00000000000000..50046141a88b7f
--- /dev/null
+++ b/code/modules/unit_tests/mob_damage.dm
@@ -0,0 +1,622 @@
+/// Tests to make sure mob damage procs are working correctly
+/datum/unit_test/mob_damage
+ priority = TEST_LONGER
+
+/datum/unit_test/mob_damage/Destroy()
+ SSmobs.ignite()
+ return ..()
+
+/datum/unit_test/mob_damage/Run()
+ SSmobs.pause()
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
+ dummy.maxHealth = 200 // tank mode
+
+ /* The sanity tests: here we make sure that:
+ 1) That damage procs are returning the expected values. They should be returning the actual amount of damage taken/healed.
+ (Negative values mean damage was taken, positive mean healing)
+ 2) Verifying that the damage has been accurately applied to the mob afterwards. */
+
+ test_sanity_simple(dummy)
+ test_sanity_complex(dummy)
+
+ // Testing if biotypes are working as intended
+ test_biotypes(dummy)
+
+ // Testing whether or not TRAIT_NOBREATH is working as intended
+ test_nobreath(dummy)
+
+ // Testing whether or not TRAIT_TOXINLOVER and TRAIT_TOXIMMUNE are working as intended
+ test_toxintraits(dummy)
+
+ // Testing whether or not TRAIT_NOCLONELOSS is working as intended
+ test_nocloneloss(dummy)
+
+ // Testing the proc ordered_healing()
+ test_ordered_healing(dummy)
+
+ // testing with godmode enabled
+ test_godmode(dummy)
+
+/**
+ * Test whether the adjust damage procs return the correct values and that the mob's health is the expected value afterwards.
+ *
+ * By default this calls apply_damage(amount) followed by verify_damage(amount_after) and returns TRUE if both succeeded.
+ * amount_after defaults to the mob's current stamina loss but can be overridden as needed.
+ *
+ * Arguments:
+ * * testing_mob - the mob to apply the damage to
+ * * amount - the amount of damage to apply to the mob
+ * * expected - what the expected return value of the damage proc is
+ * * amount_after - in case you want to specify what the damage amount on the mob should be afterwards
+ * * included_types - Bitflag of damage types to apply
+ * * biotypes - the biotypes of damage to apply
+ * * bodytypes - the bodytypes of damage to apply
+ * * forced - whether or not this is forced damage
+ */
+/datum/unit_test/mob_damage/proc/test_apply_damage(mob/living/testing_mob, amount, expected = -amount, amount_after, included_types, biotypes, bodytypes, forced)
+ if(isnull(amount_after))
+ amount_after = testing_mob.getStaminaLoss() - expected // stamina loss applies to both carbon and basic mobs the same way, so that's why we're using it here
+ if(!apply_damage(testing_mob, amount, expected, included_types, biotypes, bodytypes, forced))
+ return FALSE
+ if(!verify_damage(testing_mob, amount_after, included_types))
+ return FALSE
+ return TRUE
+
+/**
+ * Test whether the set damage procs return the correct values and that the mob's health is the expected value afterwards.
+ *
+ * By default this calls set_damage(amount) followed by verify_damage(amount_after) and returns TRUE if both succeeded.
+ * amount_after defaults to the mob's current stamina loss but can be overridden as needed.
+ *
+ * Arguments:
+ * * testing_mob - the mob to apply the damage to
+ * * amount - the amount of damage to apply to the mob
+ * * expected - what the expected return value of the damage proc is
+ * * amount_after - in case you want to specify what the damage amount on the mob should be afterwards
+ * * included_types - Bitflag of damage types to apply
+ * * biotypes - the biotypes of damage to apply
+ * * bodytypes - the bodytypes of damage to apply
+ * * forced - whether or not this is forced damage
+ */
+/datum/unit_test/mob_damage/proc/test_set_damage(mob/living/testing_mob, amount, expected, amount_after, included_types, biotypes, bodytypes, forced)
+ if(isnull(amount_after))
+ amount_after = testing_mob.getStaminaLoss() - expected
+ if(!set_damage(testing_mob, amount, expected, included_types, biotypes, bodytypes, forced))
+ return FALSE
+ if(!verify_damage(testing_mob, amount_after, included_types))
+ return FALSE
+ return TRUE
+
+/**
+ * Check that the mob has a specific amount of damage
+ *
+ * By default this checks that the mob has of every type of damage.
+ * Arguments:
+ * * testing_mob - the mob to check the damage of
+ * * amount - the amount of damage to verify that the mob has
+ * * included_types - Bitflag of damage types to check.
+ */
+/datum/unit_test/mob_damage/proc/verify_damage(mob/living/testing_mob, amount, included_types = ALL)
+ if(included_types & TOXLOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getToxLoss(), amount, \
+ "[testing_mob] should have [amount] toxin damage, instead they have [testing_mob.getToxLoss()]!")
+ if(included_types & CLONELOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getCloneLoss(), amount, \
+ "[testing_mob] should have [amount] clone damage, instead they have [testing_mob.getCloneLoss()]!")
+ if(included_types & BRUTELOSS)
+ TEST_ASSERT_EQUAL(round(testing_mob.getBruteLoss(), 1), amount, \
+ "[testing_mob] should have [amount] brute damage, instead they have [testing_mob.getBruteLoss()]!")
+ if(included_types & FIRELOSS)
+ TEST_ASSERT_EQUAL(round(testing_mob.getFireLoss(), 1), amount, \
+ "[testing_mob] should have [amount] burn damage, instead they have [testing_mob.getFireLoss()]!")
+ if(included_types & OXYLOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getOxyLoss(), amount, \
+ "[testing_mob] should have [amount] oxy damage, instead they have [testing_mob.getOxyLoss()]!")
+ if(included_types & STAMINALOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getStaminaLoss(), amount, \
+ "[testing_mob] should have [amount] stamina damage, instead they have [testing_mob.getStaminaLoss()]!")
+ return TRUE
+
+/**
+ * Apply a specific amount of damage to the mob using adjustBruteLoss(), adjustToxLoss(), etc.
+ *
+ * By default this applies damage of every type to the mob, and checks that the damage procs return the value
+ * Arguments:
+ * * testing_mob - the mob to apply the damage to
+ * * amount - the amount of damage to apply to the mob
+ * * expected - what the expected return value of the damage proc is
+ * * included_types - Bitflag of damage types to apply
+ * * biotypes - the biotypes of damage to apply
+ * * bodytypes - the bodytypes of damage to apply
+ * * forced - whether or not this is forced damage
+ */
+/datum/unit_test/mob_damage/proc/apply_damage(mob/living/testing_mob, amount, expected = -amount, included_types = ALL, biotypes = ALL, bodytypes = ALL, forced = FALSE)
+ var/damage_returned
+ if(included_types & TOXLOSS)
+ damage_returned = testing_mob.adjustToxLoss(amount, updating_health = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "adjustToxLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & CLONELOSS)
+ damage_returned = testing_mob.adjustCloneLoss(amount, updating_health = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "adjustCloneLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & BRUTELOSS)
+ damage_returned = round(testing_mob.adjustBruteLoss(amount, updating_health = FALSE, forced = forced, required_bodytype = bodytypes), 1)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "adjustBruteLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & FIRELOSS)
+ damage_returned = round(testing_mob.adjustFireLoss(amount, updating_health = FALSE, forced = forced, required_bodytype = bodytypes), 1)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "adjustFireLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & OXYLOSS)
+ damage_returned = testing_mob.adjustOxyLoss(amount, updating_health = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "adjustOxyLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & STAMINALOSS)
+ damage_returned = testing_mob.adjustStaminaLoss(amount, updating_stamina = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "adjustStaminaLoss() should have returned [expected], but returned [damage_returned] instead!")
+ return TRUE
+
+/**
+ * Set a specific amount of damage for the mob using setBruteLoss(), setToxLoss(), etc.
+ *
+ * By default this sets every type of damage to for the mob, and checks that the damage procs return the value
+ * Arguments:
+ * * testing_mob - the mob to apply the damage to
+ * * amount - the amount of damage to apply to the mob
+ * * expected - what the expected return value of the damage proc is
+ * * included_types - Bitflag of damage types to apply
+ * * biotypes - the biotypes of damage to apply
+ * * bodytypes - the bodytypes of damage to apply
+ * * forced - whether or not this is forced damage
+ */
+/datum/unit_test/mob_damage/proc/set_damage(mob/living/testing_mob, amount, expected = -amount, included_types = ALL, biotypes = ALL, bodytypes = ALL, forced = FALSE)
+ var/damage_returned
+ if(included_types & TOXLOSS)
+ damage_returned = testing_mob.setToxLoss(amount, updating_health = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "setToxLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & CLONELOSS)
+ damage_returned = testing_mob.setCloneLoss(amount, updating_health = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "setCloneLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & BRUTELOSS)
+ damage_returned = round(testing_mob.setBruteLoss(amount, updating_health = FALSE, forced = forced), 1)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "setBruteLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & FIRELOSS)
+ damage_returned = round(testing_mob.setFireLoss(amount, updating_health = FALSE, forced = forced), 1)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "setFireLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & OXYLOSS)
+ damage_returned = testing_mob.setOxyLoss(amount, updating_health = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "setOxyLoss() should have returned [expected], but returned [damage_returned] instead!")
+ if(included_types & STAMINALOSS)
+ damage_returned = testing_mob.setStaminaLoss(amount, updating_stamina = FALSE, forced = forced, required_biotype = biotypes)
+ TEST_ASSERT_EQUAL(damage_returned, expected, \
+ "setStaminaLoss() should have returned [expected], but returned [damage_returned] instead!")
+ return TRUE
+
+/// Sanity tests damage and healing using adjustToxLoss, adjustBruteLoss, etc
+/datum/unit_test/mob_damage/proc/test_sanity_simple(mob/living/carbon/human/consistent/dummy)
+ // Apply 5 damage and then heal it
+ if(!test_apply_damage(dummy, amount = 5))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! damage was not applied correctly")
+
+ if(!test_apply_damage(dummy, amount = -5))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! healing was not applied correctly")
+
+ // Apply 15 damage and heal 3
+ if(!test_apply_damage(dummy, amount = 15))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! damage was not applied correctly")
+
+ if(!test_apply_damage(dummy, amount = -3))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! underhealing was not applied correctly")
+
+ // Now overheal by 666. It should heal for 12.
+
+ if(!test_apply_damage(dummy, amount = -666, expected = 12))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! overhealing was not applied correctly")
+
+ // Now test the damage setter procs
+
+ // set all types of damage to 5
+ if(!test_set_damage(dummy, amount = 5, expected = -5))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! failed to set damage to 5")
+ // now try healing 5
+ if(!test_set_damage(dummy, amount = 0, expected = 5))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! failed to set damage to 0")
+
+/// Sanity tests damage and healing using the more complex procs like take_overall_damage(), heal_overall_damage(), etc
+/datum/unit_test/mob_damage/proc/test_sanity_complex(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+
+ var/damage_returned
+ // take 5 brute, 2 burn
+ damage_returned = round(dummy.take_bodypart_damage(5, 2, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, -7, \
+ "take_bodypart_damage() should have returned -7, but returned [damage_returned] instead!")
+
+ TEST_ASSERT_EQUAL(round(dummy.getBruteLoss(), 1), 5, \
+ "Dummy should have 5 brute damage, instead they have [dummy.getBruteLoss()]!")
+ TEST_ASSERT_EQUAL(round(dummy.getFireLoss(), 1), 2, \
+ "Dummy should have 2 burn damage, instead they have [dummy.getFireLoss()]!")
+
+ // heal 4 brute, 1 burn
+ damage_returned = round(dummy.heal_bodypart_damage(4, 1, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 5, \
+ "heal_bodypart_damage() should have returned 5, but returned [damage_returned] instead!")
+
+ if(!verify_damage(dummy, 1, included_types = BRUTELOSS|FIRELOSS))
+ TEST_FAIL("heal_bodypart_damage did not apply its healing correctly on the mob!")
+
+ // heal 1 brute, 1 burn
+ damage_returned = round(dummy.heal_overall_damage(1, 1, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 2, \
+ "heal_overall_damage() should have returned 2, but returned [damage_returned] instead!")
+
+ if(!verify_damage(dummy, 0, included_types = BRUTELOSS|FIRELOSS))
+ TEST_FAIL("heal_overall_damage did not apply its healing correctly on the mob!")
+
+ // take 50 brute, 50 burn
+ damage_returned = round(dummy.take_overall_damage(50, 50, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, -100, \
+ "take_overall_damage() should have returned -100, but returned [damage_returned] instead!")
+
+ if(!verify_damage(dummy, 50, included_types = BRUTELOSS|FIRELOSS))
+ TEST_FAIL("take_overall_damage did not apply its damage correctly on the mob!")
+
+ // testing negative damage amount args with the overall damage procs - the sign should be ignored for these procs
+
+ damage_returned = round(dummy.take_bodypart_damage(-5, -5, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, -10, \
+ "take_bodypart_damage() should have returned -10, but returned [damage_returned] instead!")
+
+ damage_returned = round(dummy.heal_bodypart_damage(-5, -5, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 10, \
+ "heal_bodypart_damage() should have returned 10, but returned [damage_returned] instead!")
+
+ damage_returned = round(dummy.take_overall_damage(-5, -5, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, -10, \
+ "take_overall_damage() should have returned -10, but returned [damage_returned] instead!")
+
+ damage_returned = round(dummy.heal_overall_damage(-5, -5, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 10, \
+ "heal_overall_damage() should have returned 10, but returned [damage_returned] instead!")
+
+ if(!verify_damage(dummy, 50, included_types = BRUTELOSS|FIRELOSS))
+ TEST_FAIL("heal_overall_damage did not apply its healingcorrectly on the mob!")
+
+ // testing overhealing
+
+ damage_returned = round(dummy.heal_overall_damage(75, 99, updating_health = FALSE), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 100, \
+ "heal_overall_damage() should have returned 100, but returned [damage_returned] instead!")
+
+ if(!verify_damage(dummy, 0, included_types = BRUTELOSS|FIRELOSS))
+ TEST_FAIL("heal_overall_damage did not apply its healing correctly on the mob!")
+
+/// Tests damage procs with godmode on
+/datum/unit_test/mob_damage/proc/test_godmode(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+ // flip godmode bit to 1
+ dummy.status_flags ^= GODMODE
+
+ // Apply 9 damage and then heal it
+ if(!test_apply_damage(dummy, amount = 9, expected = 0))
+ TEST_FAIL("ABOVE FAILURE: failed test_godmode! mob took damage despite having godmode enabled.")
+
+ if(!test_apply_damage(dummy, amount = -9, expected = 0))
+ TEST_FAIL("ABOVE FAILURE: failed test_godmode! mob healed when they should've been at full health.")
+
+ // Apply 11 damage and then heal it, this time with forced enabled. The damage should go through regardless of godmode.
+ if(!test_apply_damage(dummy, amount = 11, forced = TRUE))
+ TEST_FAIL("ABOVE FAILURE: failed test_godmode! godmode did not respect forced = TRUE")
+
+ if(!test_apply_damage(dummy, amount = -11, forced = TRUE))
+ TEST_FAIL("ABOVE FAILURE: failed test_godmode! godmode did not respect forced = TRUE")
+
+ // flip godmode bit back to 0
+ dummy.status_flags ^= GODMODE
+
+/// Testing biotypes
+/datum/unit_test/mob_damage/proc/test_biotypes(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+ // Testing biotypes using a plasmaman, who is MOB_MINERAL and MOB_HUMANOID
+ dummy.set_species(/datum/species/plasmaman)
+
+ // argumentless default: should default to required_biotype = ALL. The damage should be applied in that case.
+ if(!test_apply_damage(dummy, 1, included_types = TOXLOSS|CLONELOSS|STAMINALOSS))
+ TEST_FAIL("ABOVE FAILURE: plasmaman did not take damage with biotypes = ALL")
+
+ // If we specify MOB_ORGANIC, the damage should not get applied because plasmamen lack that biotype.
+ if(!test_apply_damage(dummy, 1, expected = 0, included_types = TOXLOSS|CLONELOSS|STAMINALOSS, biotypes = MOB_ORGANIC))
+ TEST_FAIL("ABOVE FAILURE: plasmaman took damage with biotypes = MOB_ORGANIC")
+
+ // Now if we specify MOB_MINERAL the damage should get applied.
+ if(!test_apply_damage(dummy, 1, included_types = TOXLOSS|CLONELOSS|STAMINALOSS, biotypes = MOB_MINERAL))
+ TEST_FAIL("ABOVE FAILURE: plasmaman did not take damage with biotypes = MOB_MINERAL")
+
+ // Transform back to human
+ dummy.set_species(/datum/species/human)
+
+ // We have 2 damage presently.
+ // Try to heal it; let's specify MOB_MINERAL, which should no longer work because we have changed back to a human.
+ if(!test_apply_damage(dummy, -2, expected = 0, included_types = TOXLOSS|CLONELOSS|STAMINALOSS, biotypes = MOB_MINERAL))
+ TEST_FAIL("ABOVE FAILURE: human took damage with biotypes = MOB_MINERAL")
+
+ // Force heal some of the damage. When forced = TRUE the damage/healing gets applied no matter what.
+ if(!test_apply_damage(dummy, -1, included_types = TOXLOSS|CLONELOSS|STAMINALOSS, biotypes = MOB_MINERAL, forced = TRUE))
+ TEST_FAIL("ABOVE FAILURE: human did not get healed when biotypes = MOB_MINERAL and forced = TRUE")
+
+ // Now heal the rest of it with the correct biotype. Make sure that this works. We should have 0 damage afterwards.
+ if(!test_apply_damage(dummy, -1, included_types = TOXLOSS|CLONELOSS|STAMINALOSS, biotypes = MOB_ORGANIC))
+ TEST_FAIL("ABOVE FAILURE: human did not get healed with biotypes = MOB_ORGANIC")
+
+/// Testing oxyloss with the TRAIT_NOBREATH
+/datum/unit_test/mob_damage/proc/test_nobreath(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+
+ // TRAIT_NOBREATH is supposed to prevent oxyloss damage (but not healing). Let's make sure that's the case.
+ ADD_TRAIT(dummy, TRAIT_NOBREATH, TRAIT_SOURCE_UNIT_TESTS)
+ // force some oxyloss here
+ dummy.setOxyLoss(2, updating_health = FALSE, forced = TRUE)
+
+ // Try to take more oxyloss damage with TRAIT_NOBREATH. It should not work.
+ if(!test_apply_damage(dummy, 2, expected = 0, amount_after = dummy.getOxyLoss(), included_types = OXYLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_nobreath! mob took oxyloss damage while having TRAIT_NOBREATH")
+
+ // Make sure we are still be able to heal the oxyloss. This should work.
+ if(!test_apply_damage(dummy, -2, amount_after = dummy.getOxyLoss()-2, included_types = OXYLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_nobreath! mob could not heal oxyloss damage while having TRAIT_NOBREATH")
+
+ REMOVE_TRAIT(dummy, TRAIT_NOBREATH, TRAIT_SOURCE_UNIT_TESTS)
+
+/// Testing toxloss with TRAIT_TOXINLOVER and TRAIT_TOXIMMUNE
+/datum/unit_test/mob_damage/proc/test_toxintraits(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+
+ // TRAIT_TOXINLOVER is supposed to invert toxin damage and healing. Things that would normally cause toxloss now heal it, and vice versa.
+ ADD_TRAIT(dummy, TRAIT_TOXINLOVER, TRAIT_SOURCE_UNIT_TESTS)
+ // force some toxloss here
+ dummy.setToxLoss(2, updating_health = FALSE, forced = TRUE)
+
+ // Try to take more toxloss damage with TRAIT_TOXINLOVER. It should heal instead.
+ if(!test_apply_damage(dummy, 2, expected = 2, amount_after = dummy.getToxLoss()-2, included_types = TOXLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_toxintraits! mob did not heal from toxin damage with TRAIT_TOXINLOVER")
+
+ // If we try to heal the toxloss we should take damage instead
+ if(!test_apply_damage(dummy, -2, expected = -2, amount_after = dummy.getToxLoss()+2, included_types = TOXLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_toxintraits! mob did not take damage from toxin healing with TRAIT_TOXINLOVER")
+
+ // TOXIMMUNE trait should prevent the damage you get from being healed by toxins medicines while having TRAIT_TOXINLOVER
+ ADD_TRAIT(dummy, TRAIT_TOXIMMUNE, TRAIT_SOURCE_UNIT_TESTS)
+
+ // need to force apply some toxin damage since the TOXIMUNNE trait sets toxloss to 0 upon being added
+ dummy.setToxLoss(2, updating_health = FALSE, forced = TRUE)
+
+ // try to 'heal' again - this time it should just do nothing because we should be immune to any sort of toxin damage - including from inverted healing
+ if(!test_apply_damage(dummy, -2, expected = 0, amount_after = dummy.getToxLoss(), included_types = TOXLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_toxintraits! mob should not have taken any damage or healing with TRAIT_TOXINLOVER + TRAIT_TOXIMMUNE")
+
+ // ok, let's try taking 'damage'. The inverted damage should still heal mobs with the TOXIMMUNE trait.
+ if(!test_apply_damage(dummy, 2, expected = 2, amount_after = dummy.getToxLoss()-2, included_types = TOXLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_toxintraits! mob did not heal from taking toxin damage with TRAIT_TOXINLOVER + TRAIT_TOXIMMUNE")
+
+ REMOVE_TRAIT(dummy, TRAIT_TOXINLOVER, TRAIT_SOURCE_UNIT_TESTS)
+ REMOVE_TRAIT(dummy, TRAIT_TOXIMMUNE, TRAIT_SOURCE_UNIT_TESTS)
+
+/// Testing cloneloss with TRAIT_NOCLONELOSS
+/datum/unit_test/mob_damage/proc/test_nocloneloss(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+
+ // TRAIT_TRAIT_NOCLONELOSS is supposed to prevent cloneloss damage and healing. Let's make sure that's the case.
+ ADD_TRAIT(dummy, TRAIT_NOCLONELOSS, TRAIT_SOURCE_UNIT_TESTS)
+ // force some cloneloss here
+ dummy.setCloneLoss(2, updating_health = FALSE, forced = TRUE)
+
+ // Try to take more cloneloss damage with TRAIT_NOCLONELOSS. It should not work.
+ if(!test_apply_damage(dummy, 2, expected = 0, amount_after = dummy.getCloneLoss(), included_types = CLONELOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_nocloneloss! mob took cloneloss damage with TRAIT_NOCLONELOSS")
+
+ // Healing the cloneloss should not work either, unless we force it
+ if(!test_apply_damage(dummy, -2, expected = 0, amount_after = dummy.getCloneLoss(), included_types = CLONELOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_nocloneloss! mob healed cloneloss damage with TRAIT_NOCLONELOSS")
+ // so let's force it
+ if(!test_apply_damage(dummy, -2, expected = 2, amount_after = dummy.getCloneLoss()-2, included_types = CLONELOSS, forced = TRUE))
+ TEST_FAIL("ABOVE FAILURE: failed test_nocloneloss! mob could not heal cloneloss damage with forced = TRUE and TRAIT_NOCLONELOSS")
+
+ REMOVE_TRAIT(dummy, TRAIT_NOCLONELOSS, TRAIT_SOURCE_UNIT_TESTS)
+
+/// Testing heal_ordered_damage()
+/datum/unit_test/mob_damage/proc/test_ordered_healing(mob/living/carbon/human/consistent/dummy)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ dummy.fully_heal(HEAL_DAMAGE)
+ var/damage_returned
+
+ // We apply 20 brute, 20 burn, and 20 toxin damage. 60 damage total
+ apply_damage(dummy, 20, included_types = TOXLOSS|BRUTELOSS|FIRELOSS)
+
+ // Heal 30 damage of that, starting from brute
+ damage_returned = round(dummy.heal_ordered_damage(30, list(BRUTE, BURN, TOX)), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 30, \
+ "heal_ordered_damage() should have returned 30, but returned [damage_returned] instead!")
+
+ // Should have 10 burn damage and 20 toxins damage remaining, let's check
+ TEST_ASSERT_EQUAL(dummy.getBruteLoss(), 0, \
+ "[src] should have 0 brute damage, but has [dummy.getBruteLoss()] instead!")
+ TEST_ASSERT_EQUAL(dummy.getFireLoss(), 10, \
+ "[src] should have 10 burn damage, but has [dummy.getFireLoss()] instead!")
+ TEST_ASSERT_EQUAL(dummy.getToxLoss(), 20, \
+ "[src] should have 20 toxin damage, but has [dummy.getToxLoss()] instead!")
+
+ // Now heal the remaining 30, overhealing by 5.
+ damage_returned = round(dummy.heal_ordered_damage(35, list(BRUTE, BURN, TOX)), 1)
+ TEST_ASSERT_EQUAL(damage_returned, 30, \
+ "heal_ordered_damage() should have returned 30, but returned [damage_returned] instead!")
+
+ // Should have no damage remaining
+ TEST_ASSERT_EQUAL(dummy.getBruteLoss(), 0, \
+ "[src] should have 0 brute damage, but has [dummy.getBruteLoss()] instead!")
+ TEST_ASSERT_EQUAL(dummy.getFireLoss(), 0, \
+ "[src] should have 0 burn damage, but has [dummy.getFireLoss()] instead!")
+ TEST_ASSERT_EQUAL(dummy.getToxLoss(), 0, \
+ "[src] should have 0 toxin damage, but has [dummy.getToxLoss()] instead!")
+
+/// Tests that mob damage procs are working as intended for basic mobs
+/datum/unit_test/mob_damage/basic
+
+/datum/unit_test/mob_damage/basic/Run()
+ SSmobs.pause()
+ var/mob/living/basic/mouse/gray/gusgus = allocate(/mob/living/basic/mouse/gray)
+ // give gusgus a damage_coeff of 1 for this test
+ gusgus.damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
+ // tank mouse
+ gusgus.maxHealth = 200
+
+ test_sanity_simple(gusgus)
+ test_sanity_complex(gusgus)
+
+/**
+ * Check that the mob has a specific amount of damage. Note: basic mobs have all incoming damage types besides stam converted into brute damage.
+ *
+ * By default this checks that the mob has of every type of damage.
+ * Arguments:
+ * * testing_mob - the mob to check the damage of
+ * * amount - the amount of damage to verify that the mob has
+ * * expected - the expected return value of the damage procs, if it differs from the default of (amount * 5)
+ * * included_types - Bitflag of damage types to check.
+ */
+/datum/unit_test/mob_damage/basic/verify_damage(mob/living/testing_mob, amount, expected, included_types = ALL)
+ if(included_types & TOXLOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getToxLoss(), 0, \
+ "[testing_mob] should have [0] toxin damage, instead they have [testing_mob.getToxLoss()]!")
+ if(included_types & CLONELOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getCloneLoss(), 0, \
+ "[testing_mob] should have [0] clone damage, instead they have [testing_mob.getCloneLoss()]!")
+ if(included_types & BRUTELOSS)
+ TEST_ASSERT_EQUAL(round(testing_mob.getBruteLoss(), 1), expected || amount * 5, \
+ "[testing_mob] should have [expected || amount * 5] brute damage, instead they have [testing_mob.getBruteLoss()]!")
+ if(included_types & FIRELOSS)
+ TEST_ASSERT_EQUAL(round(testing_mob.getFireLoss(), 1), 0, \
+ "[testing_mob] should have [0] burn damage, instead they have [testing_mob.getFireLoss()]!")
+ if(included_types & OXYLOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getOxyLoss(), 0, \
+ "[testing_mob] should have [0] oxy damage, instead they have [testing_mob.getOxyLoss()]!")
+ if(included_types & STAMINALOSS)
+ TEST_ASSERT_EQUAL(testing_mob.getStaminaLoss(), amount, \
+ "[testing_mob] should have [amount] stamina damage, instead they have [testing_mob.getStaminaLoss()]!")
+ return TRUE
+
+/datum/unit_test/mob_damage/basic/test_sanity_simple(mob/living/basic/mouse/gray/gusgus)
+ // check to see if basic mob damage works
+
+ // Simple damage and healing
+ // Take 1 damage, heal for 1
+ if(!test_apply_damage(gusgus, amount = 1))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! damage was not applied correctly")
+
+ if(!test_apply_damage(gusgus, amount = -1))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! healing was not applied correctly")
+
+ // Give 2 damage of every time (translates to 10 brute, 2 staminaloss)
+ if(!test_apply_damage(gusgus, amount = 2))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! damage was not applied correctly")
+
+ // underhealing: heal 1 damage of every type (translates to 5 brute, 1 staminaloss)
+ if(!test_apply_damage(gusgus, amount = -1))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! healing was not applied correctly")
+
+ // overhealing
+
+ // heal 11 points of toxloss (should take care of all 5 brute damage remaining)
+ if(!apply_damage(gusgus, -11, expected = 5, included_types = TOXLOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! toxloss was not applied correctly")
+ // heal the remaining point of staminaloss
+ if(!apply_damage(gusgus, -11, expected = 1, included_types = STAMINALOSS))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! failed to heal staminaloss correctly")
+ // heal 35 points of each type, we should already be at full health so nothing should happen
+ if(!test_apply_damage(gusgus, amount = -35, expected = 0))
+ TEST_FAIL("ABOVE FAILURE: failed test_sanity_simple! overhealing was not applied correctly")
+
+/datum/unit_test/mob_damage/basic/test_sanity_complex(mob/living/basic/mouse/gray/gusgus)
+ // Heal up, so that errors from the previous tests we won't cause this one to fail
+ gusgus.fully_heal(HEAL_DAMAGE)
+ var/damage_returned
+ // overall damage procs
+
+ // take 5 brute, 2 burn
+ damage_returned = gusgus.take_bodypart_damage(5, 2, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, -7, \
+ "take_bodypart_damage() should have returned -7, but returned [damage_returned] instead!")
+
+ TEST_ASSERT_EQUAL(gusgus.bruteloss, 7, \
+ "Mouse should have 7 brute damage, instead they have [gusgus.bruteloss]!")
+ TEST_ASSERT_EQUAL(gusgus.fireloss, 0, \
+ "Mouse should have 0 burn damage, instead they have [gusgus.fireloss]!")
+
+ // heal 4 brute, 1 burn
+ damage_returned = gusgus.heal_bodypart_damage(4, 1, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, 5, \
+ "heal_bodypart_damage() should have returned 5, but returned [damage_returned] instead!")
+
+ TEST_ASSERT_EQUAL(gusgus.bruteloss, 2, \
+ "Mouse should have 2 brute damage, instead they have [gusgus.bruteloss]!")
+ TEST_ASSERT_EQUAL(gusgus.fireloss, 0, \
+ "Mouse should have 0 burn damage, instead they have [gusgus.fireloss]!")
+
+ // heal 1 brute, 1 burn
+ damage_returned = gusgus.heal_overall_damage(1, 1, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, 2, \
+ "heal_overall_damage() should have returned 2, but returned [damage_returned] instead!")
+
+ TEST_ASSERT_EQUAL(gusgus.bruteloss, 0, \
+ "Mouse should have 0 brute damage, instead they have [gusgus.bruteloss]!")
+ TEST_ASSERT_EQUAL(gusgus.fireloss, 0, \
+ "Mouse should have 0 burn damage, instead they have [gusgus.fireloss]!")
+
+ // take 50 brute, 50 burn
+ damage_returned = gusgus.take_overall_damage(3, 3, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, -6, \
+ "take_overall_damage() should have returned -6, but returned [damage_returned] instead!")
+
+ if(!verify_damage(gusgus, 1, expected = 6, included_types = BRUTELOSS))
+ TEST_FAIL("take_overall_damage did not apply its damage correctly on the mouse!")
+
+ // testing negative args with the overall damage procs
+
+ damage_returned = gusgus.take_bodypart_damage(-1, -1, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, -2, \
+ "take_bodypart_damage() should have returned -2, but returned [damage_returned] instead!")
+
+ damage_returned = gusgus.heal_bodypart_damage(-1, -1, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, 2, \
+ "heal_bodypart_damage() should have returned 2, but returned [damage_returned] instead!")
+
+ damage_returned = gusgus.take_overall_damage(-1, -1, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, -2, \
+ "take_overall_damage() should have returned -2, but returned [damage_returned] instead!")
+
+ damage_returned = gusgus.heal_overall_damage(-1, -1, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, 2, \
+ "heal_overall_damage() should have returned 2, but returned [damage_returned] instead!")
+
+ if(!verify_damage(gusgus, 1, expected = 6, included_types = BRUTELOSS))
+ TEST_FAIL("heal_overall_damage did not apply its healing correctly on the mouse!")
+
+ // testing overhealing
+
+ damage_returned = gusgus.heal_overall_damage(75, 99, updating_health = FALSE)
+ TEST_ASSERT_EQUAL(damage_returned, 6, \
+ "heal_overall_damage() should have returned 6, but returned [damage_returned] instead!")
+
+ if(!verify_damage(gusgus, 0, included_types = BRUTELOSS))
+ TEST_FAIL("heal_overall_damage did not apply its healing correctly on the mouse!")
diff --git a/code/modules/unit_tests/organs.dm b/code/modules/unit_tests/organs.dm
index 4c99bfaa339fd2..4ba51e0870c00a 100644
--- a/code/modules/unit_tests/organs.dm
+++ b/code/modules/unit_tests/organs.dm
@@ -96,19 +96,22 @@
var/slot_to_use = test_organ.slot
// Tests [mob/living/proc/adjustOrganLoss]
- dummy.adjustOrganLoss(slot_to_use, test_organ.maxHealth * 10)
+ TEST_ASSERT_EQUAL(dummy.adjustOrganLoss(slot_to_use, test_organ.maxHealth * 10), -test_organ.maxHealth, \
+ "Mob level \"apply organ damage\" returned the wrong value for [slot_to_use] organ with default arguments.")
TEST_ASSERT_EQUAL(dummy.get_organ_loss(slot_to_use), test_organ.maxHealth, \
"Mob level \"apply organ damage\" can exceed the [slot_to_use] organ's damage cap with default arguments.")
dummy.fully_heal(HEAL_ORGANS)
// Tests [mob/living/proc/set_organ_damage]
- dummy.setOrganLoss(slot_to_use, test_organ.maxHealth * 10)
+ TEST_ASSERT_EQUAL(dummy.setOrganLoss(slot_to_use, test_organ.maxHealth * 10), -test_organ.maxHealth, \
+ "Mob level \"set organ damage\" returned the wrong value for [slot_to_use] organ with default arguments.")
TEST_ASSERT_EQUAL(dummy.get_organ_loss(slot_to_use), test_organ.maxHealth, \
"Mob level \"set organ damage\" can exceed the [slot_to_use] organ's damage cap with default arguments.")
dummy.fully_heal(HEAL_ORGANS)
// Tests [mob/living/proc/adjustOrganLoss] with a large max supplied
- dummy.adjustOrganLoss(slot_to_use, test_organ.maxHealth * 10, INFINITY)
+ TEST_ASSERT_EQUAL(dummy.adjustOrganLoss(slot_to_use, test_organ.maxHealth * 10, INFINITY), -test_organ.maxHealth, \
+ "Mob level \"apply organ damage\" returned the wrong value for [slot_to_use] organ with a large maximum supplied.")
TEST_ASSERT_EQUAL(dummy.get_organ_loss(slot_to_use), test_organ.maxHealth, \
"Mob level \"apply organ damage\" can exceed the [slot_to_use] organ's damage cap with a large maximum supplied.")
dummy.fully_heal(HEAL_ORGANS)
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_changelingmidround.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_changelingmidround.png
index ac412207c236df..d002f7466675bd 100644
Binary files a/code/modules/unit_tests/screenshots/screenshot_antag_icons_changelingmidround.png and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_changelingmidround.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_cyberpolice.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_cyberpolice.png
new file mode 100644
index 00000000000000..180be6064f8545
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_cyberpolice.png differ
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 97631c0b78a8c2..335e353b1e7beb 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -64,23 +64,15 @@
/mob/living/simple_animal/hostile/asteroid/gutlunch/grublunch,
/mob/living/simple_animal/hostile/asteroid/gutlunch/gubbuck,
/mob/living/simple_animal/hostile/asteroid/gutlunch/guthen,
- /mob/living/simple_animal/hostile/asteroid/ice_demon,
/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/blob,
- /mob/living/simple_animal/hostile/blob/blobbernaut,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
- /mob/living/simple_animal/hostile/blob/blobspore,
- /mob/living/simple_animal/hostile/blob/blobspore/independent,
- /mob/living/simple_animal/hostile/blob/blobspore/weak,
/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/harvester,
/mob/living/simple_animal/hostile/construct/juggernaut,
/mob/living/simple_animal/hostile/construct/juggernaut/angelic,
/mob/living/simple_animal/hostile/construct/juggernaut/hostile,
@@ -94,9 +86,6 @@
/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/gorilla,
- /mob/living/simple_animal/hostile/gorilla/lesser,
- /mob/living/simple_animal/hostile/gorilla/cargo_domestic,
/mob/living/simple_animal/hostile/guardian,
/mob/living/simple_animal/hostile/guardian/assassin,
/mob/living/simple_animal/hostile/guardian/charger,
@@ -109,40 +98,38 @@
/mob/living/simple_animal/hostile/guardian/ranged,
/mob/living/simple_animal/hostile/guardian/standard,
/mob/living/simple_animal/hostile/guardian/support,
- /mob/living/simple_animal/hostile/heretic_summon,
- /mob/living/simple_animal/hostile/heretic_summon/armsy,
- /mob/living/simple_animal/hostile/heretic_summon/armsy/prime,
- /mob/living/simple_animal/hostile/heretic_summon/ash_spirit,
- /mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror,
- /mob/living/simple_animal/hostile/heretic_summon/raw_prophet,
- /mob/living/simple_animal/hostile/heretic_summon/rust_spirit,
- /mob/living/simple_animal/hostile/heretic_summon/stalker,
/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/jungle/mook,
/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,
@@ -164,7 +151,6 @@
/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/goat,
/mob/living/simple_animal/hostile/retaliate/goose,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace,
@@ -177,12 +163,9 @@
/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/smspider,
- /mob/living/simple_animal/hostile/smspider/overcharged,
/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/venus_human_trap,
/mob/living/simple_animal/hostile/wizard,
/mob/living/simple_animal/hostile/zombie,
/mob/living/simple_animal/parrot,
@@ -201,15 +184,12 @@
/mob/living/simple_animal/pet/cat/space,
/mob/living/simple_animal/pet/gondola,
/mob/living/simple_animal/pet/gondola/gondolapod,
- /mob/living/simple_animal/revenant,
+ /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,
/mob/living/simple_animal/slime/transformed_slime,
- /mob/living/simple_animal/sloth,
- /mob/living/simple_animal/sloth/citrus,
- /mob/living/simple_animal/sloth/paperwork,
/mob/living/simple_animal/soulscythe,
//MODULAR SKYRAT ENTRIES
diff --git a/code/modules/uplink/uplink_items/ammunition.dm b/code/modules/uplink/uplink_items/ammunition.dm
index 292f87ffe13b32..e88727812528de 100644
--- a/code/modules/uplink/uplink_items/ammunition.dm
+++ b/code/modules/uplink/uplink_items/ammunition.dm
@@ -18,7 +18,6 @@
/datum/uplink_item/ammo/pistol
name = "9mm Handgun Magazine"
desc = "An additional 8-round 9mm magazine, compatible with the Makarov pistol."
- progression_minimum = 10 MINUTES
item = /obj/item/ammo_box/magazine/m9mm
cost = 1
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
@@ -28,7 +27,6 @@
name = "9mm Armour Piercing Magazine"
desc = "An additional 8-round 9mm magazine, compatible with the Makarov pistol. \
These rounds are less effective at injuring the target but penetrate protective gear."
- progression_minimum = 30 MINUTES
item = /obj/item/ammo_box/magazine/m9mm/ap
cost = 2
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
@@ -37,7 +35,6 @@
name = "9mm Hollow Point Magazine"
desc = "An additional 8-round 9mm magazine, compatible with the Makarov pistol. \
These rounds are more damaging but ineffective against armour."
- progression_minimum = 30 MINUTES
item = /obj/item/ammo_box/magazine/m9mm/hp
cost = 3
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
@@ -46,7 +43,6 @@
name = "9mm Incendiary Magazine"
desc = "An additional 8-round 9mm magazine, compatible with the Makarov pistol. \
Loaded with incendiary rounds which inflict little damage, but ignite the target."
- progression_minimum = 30 MINUTES
item = /obj/item/ammo_box/magazine/m9mm/fire
cost = 2
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
@@ -55,7 +51,6 @@
name = ".357 Speed Loader"
desc = "A speed loader that contains seven additional .357 Magnum rounds; usable with the Syndicate revolver. \
For when you really need a lot of things dead."
- progression_minimum = 30 MINUTES
item = /obj/item/ammo_box/a357
cost = 4
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) //nukies get their own version
diff --git a/code/modules/uplink/uplink_items/badass.dm b/code/modules/uplink/uplink_items/badass.dm
index 93087be9dd00e9..67676edcd7da9f 100644
--- a/code/modules/uplink/uplink_items/badass.dm
+++ b/code/modules/uplink/uplink_items/badass.dm
@@ -48,7 +48,6 @@
manufactured to pack a little bit more of a punch if your client needs some convincing."
item = /obj/item/storage/secure/briefcase/syndie
cost = 3
- progression_minimum = 5 MINUTES
restricted = TRUE
illegal_tech = FALSE
diff --git a/code/modules/uplink/uplink_items/bundle.dm b/code/modules/uplink/uplink_items/bundle.dm
index 912c28e2bcb2b8..a930784fe1462c 100644
--- a/code/modules/uplink/uplink_items/bundle.dm
+++ b/code/modules/uplink/uplink_items/bundle.dm
@@ -58,7 +58,6 @@
These items are collectively worth more than 25 telecrystals, but you do not know which specialization \
you will receive. May contain discontinued and/or exotic items. \
The Syndicate will only provide one Syndi-Kit per agent."
- progression_minimum = 30 MINUTES
item = /obj/item/storage/box/syndicate/bundle_a
cost = 20
stock_key = UPLINK_SHARED_STOCK_KITS
@@ -70,7 +69,6 @@
In Syndi-kit Special, you will receive items used by famous syndicate agents of the past. \
Collectively worth more than 25 telecrystals, the syndicate loves a good throwback. \
The Syndicate will only provide one Syndi-Kit per agent."
- progression_minimum = 30 MINUTES
item = /obj/item/storage/box/syndicate/bundle_b
cost = 20
stock_key = UPLINK_SHARED_STOCK_KITS
@@ -149,7 +147,6 @@
The Syndicate will only provide one surplus item per agent."
cost = 20
item = /obj/structure/closet/crate/syndicrate
- progression_minimum = 30 MINUTES
stock_key = UPLINK_SHARED_STOCK_SURPLUS
crate_tc_value = 80
crate_type = /obj/structure/closet/crate/syndicrate
@@ -173,6 +170,5 @@
The Syndicate will only provide one surplus item per agent."
cost = 20
item = /obj/item/syndicrate_key
- progression_minimum = 30 MINUTES
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
stock_key = UPLINK_SHARED_STOCK_SURPLUS
diff --git a/code/modules/uplink/uplink_items/dangerous.dm b/code/modules/uplink/uplink_items/dangerous.dm
index 0cdcf3a7bb7f6f..f1788c6e1dec32 100644
--- a/code/modules/uplink/uplink_items/dangerous.dm
+++ b/code/modules/uplink/uplink_items/dangerous.dm
@@ -19,7 +19,6 @@
name = "Makarov Pistol"
desc = "A small, easily concealable handgun that uses 9mm auto rounds in 8-round magazines and is compatible \
with suppressors."
- progression_minimum = 10 MINUTES
item = /obj/item/gun/ballistic/automatic/pistol
cost = 7
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
@@ -28,7 +27,6 @@
name = "Box of Throwing Weapons"
desc = "A box of shurikens and reinforced bolas from ancient Earth martial arts. They are highly effective \
throwing weapons. The bolas can knock a target down and the shurikens will embed into limbs."
- progression_minimum = 10 MINUTES
item = /obj/item/storage/box/syndie_kit/throwing_weapons
cost = 3
illegal_tech = FALSE
@@ -95,7 +93,6 @@
name = "Syndicate Revolver"
desc = "Waffle Co.'s modernized Syndicate revolver. Fires 7 brutal rounds of .357 Magnum."
item = /obj/item/gun/ballistic/revolver/syndicate
- progression_minimum = 30 MINUTES
cost = 13
surplus = 50
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) //nukies get their own version
diff --git a/code/modules/uplink/uplink_items/explosive.dm b/code/modules/uplink/uplink_items/explosive.dm
index 32a5ad49d57abd..59a443256739a9 100644
--- a/code/modules/uplink/uplink_items/explosive.dm
+++ b/code/modules/uplink/uplink_items/explosive.dm
@@ -16,7 +16,6 @@
desc = "C-4 is plastic explosive of the common variety Composition C. You can use it to breach walls, sabotage equipment, or connect \
an assembly to it in order to alter the way it detonates. It can be attached to almost all objects and has a modifiable timer with a \
minimum setting of 10 seconds."
- progression_minimum = 5 MINUTES
item = /obj/item/grenade/c4
cost = 1
@@ -24,7 +23,6 @@
name = "Bag of C-4 explosives"
desc = "Because sometimes quantity is quality. Contains 10 C-4 plastic explosives."
item = /obj/item/storage/backpack/duffelbag/syndie/c4
- progression_minimum = 10 MINUTES
cost = 8 //20% discount!
cant_discount = TRUE
@@ -43,7 +41,6 @@
desc = "When inserted into a tablet, this cartridge gives you four opportunities to \
detonate tablets of crewmembers who have their message feature enabled. \
The concussive effect from the explosion will knock the recipient out for a short period, and deafen them for longer."
- progression_minimum = 20 MINUTES
item = /obj/item/computer_disk/virus/detomatix
cost = 6
restricted = TRUE
@@ -64,7 +61,7 @@
name = "Pizza Bomb"
desc = "A pizza box with a bomb cunningly attached to the lid. The timer needs to be set by opening the box; afterwards, \
opening the box again will trigger the detonation after the timer has elapsed. Comes with free pizza, for you or your target!"
- progression_minimum = 30 MINUTES
+ progression_minimum = 15 MINUTES
item = /obj/item/pizzabox/bomb
cost = 6
surplus = 8
@@ -82,7 +79,6 @@
/datum/uplink_item/explosives/syndicate_bomb/emp
name = "Syndicate EMP Bomb"
desc = "A variation of the syndicate bomb designed to produce a large EMP effect."
- progression_minimum = 30 MINUTES
item = /obj/item/sbeacondrop/emp
cost = 7
diff --git a/code/modules/uplink/uplink_items/implant.dm b/code/modules/uplink/uplink_items/implant.dm
index 34fc9eedb0ff1d..87c9fd6c96c07f 100644
--- a/code/modules/uplink/uplink_items/implant.dm
+++ b/code/modules/uplink/uplink_items/implant.dm
@@ -9,11 +9,14 @@
/datum/uplink_item/implants/freedom
name = "Freedom Implant"
- desc = "An implant injected into the body and later activated at the user's will. It will attempt to free the \
- user from common restraints such as handcuffs."
+ desc = "Can be activated to release common restraints such as handcuffs, legcuffs, and even bolas tethered around the legs."
item = /obj/item/storage/box/syndie_kit/imp_freedom
cost = 5
+/datum/uplink_item/implants/freedom/New()
+ . = ..()
+ desc += " Implant has enough energy for [FREEDOM_IMPLANT_CHARGES] uses before it becomes inert and harmlessly self-destructs."
+
/datum/uplink_item/implants/radio
name = "Internal Syndicate Radio Implant"
desc = "An implant injected into the body, allowing the use of an internal Syndicate radio. \
diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm
index e585b07bb5fb6d..dca197774716f1 100644
--- a/code/modules/uplink/uplink_items/job.dm
+++ b/code/modules/uplink/uplink_items/job.dm
@@ -150,7 +150,6 @@
name = "Magillitis Serum Autoinjector"
desc = "A single-use autoinjector which contains an experimental serum that causes rapid muscular growth in Hominidae. \
Side-affects may include hypertrichosis, violent outbursts, and an unending affinity for bananas."
- progression_minimum = 10 MINUTES
item = /obj/item/reagent_containers/hypospray/medipen/magillitis
cost = 15
restricted_roles = list(JOB_GENETICIST, JOB_RESEARCH_DIRECTOR)
@@ -159,7 +158,6 @@
name = "Box of Gorilla Cubes"
desc = "A box with three Waffle Co. brand gorilla cubes. Eat big to get big. \
Caution: Product may rehydrate when exposed to water."
- progression_minimum = 15 MINUTES
item = /obj/item/storage/box/gorillacubes
cost = 6
restricted_roles = list(JOB_GENETICIST, JOB_RESEARCH_DIRECTOR)
@@ -209,7 +207,8 @@
name = "Kinetic Accelerator Pressure Mod"
desc = "A modification kit which allows Kinetic Accelerators to do greatly increased damage while indoors. \
Occupies 35% mod capacity."
- progression_minimum = 30 MINUTES
+ // While less deadly than a revolver it does have infinite ammo
+ progression_minimum = 15 MINUTES
item = /obj/item/borg/upgrade/modkit/indoors
cost = 5 //you need two for full damage, so total of 10 for maximum damage
limited_stock = 2 //you can't use more than two!
@@ -220,7 +219,6 @@
name = "Guide to Advanced Mimery Series"
desc = "The classical two part series on how to further hone your mime skills. Upon studying the series, the user should be able to make 3x1 invisible walls, and shoot bullets out of their fingers. \
Obviously only works for Mimes."
- progression_minimum = 20 MINUTES
cost = 12
item = /obj/item/storage/box/syndie_kit/mimery
restricted_roles = list(JOB_MIME)
@@ -238,7 +236,7 @@
/datum/uplink_item/role_restricted/chemical_gun
name = "Reagent Dartgun"
desc = "A heavily modified syringe gun which is capable of synthesizing its own chemical darts using input reagents. Can hold 90u of reagents."
- progression_minimum = 20 MINUTES
+ progression_minimum = 15 MINUTES
item = /obj/item/gun/chem
cost = 12
restricted_roles = list(JOB_CHEMIST, JOB_CHIEF_MEDICAL_OFFICER, JOB_BOTANIST)
@@ -246,7 +244,6 @@
/datum/uplink_item/role_restricted/pie_cannon
name = "Banana Cream Pie Cannon"
desc = "A special pie cannon for a special clown, this gadget can hold up to 20 pies and automatically fabricates one every two seconds!"
- progression_minimum = 10 MINUTES
cost = 10
item = /obj/item/pneumatic_cannon/pie/selfcharge
restricted_roles = list(JOB_CLOWN)
@@ -276,9 +273,6 @@
someone saves them or they manage to crawl out. Be sure not to ram into any walls or vending machines, as the springloaded seats \
are very sensitive. Now with our included lube defense mechanism which will protect you against any angry shitcurity! \
Premium features can be unlocked with a cryptographic sequencer!"
- // It has a low progression cost because it's the sort of item that only works well early in the round
- // Plus, it costs all your TC, and it's not an instant kill tool.
- progression_minimum = 5 MINUTES
item = /obj/vehicle/sealed/car/clowncar
cost = 20
restricted_roles = list(JOB_CLOWN)
@@ -290,9 +284,6 @@
His Grace grants gradual regeneration and complete stun immunity to His wielder, but be wary: if He gets too hungry, He will become impossible to drop and eventually kill you if not fed. \
However, if left alone for long enough, He will fall back to slumber. \
To activate His Grace, simply unlatch Him."
- // It has a low progression cost because it's the sort of item that only works well early in the round
- // Plus, it costs all your TC and will lock your uplink.
- progression_minimum = 5 MINUTES
lock_other_purchases = TRUE
cant_discount = TRUE
item = /obj/item/his_grace
diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm
index 632d4cc2717f44..78dedc0c07261b 100644
--- a/code/modules/uplink/uplink_items/nukeops.dm
+++ b/code/modules/uplink/uplink_items/nukeops.dm
@@ -498,6 +498,16 @@
cost = 10
purchasable_from = UPLINK_NUKE_OPS
+/datum/uplink_item/bundles_tc/cowboy
+ name = "Syndicate Outlaw Kit"
+ desc = "There've been high tales of an outlaw 'round these parts. A fella so ruthless and efficient no ranger could ever capture 'em. \
+ Now you can be just like 'em! \
+ This kit contains armor-lined cowboy equipment, a custom revolver and holster, and a horse with a complimentary apple to tame. \
+ A lighter is also included, though you must supply your own smokes."
+ item = /obj/item/storage/box/syndie_kit/cowboy
+ cost = 18
+ purchasable_from = UPLINK_NUKE_OPS
+
// Mech related gear
/datum/uplink_category/mech
diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm
index 491f8e8e99d6d7..2f205a9d0bd692 100644
--- a/code/modules/uplink/uplink_items/stealthy.dm
+++ b/code/modules/uplink/uplink_items/stealthy.dm
@@ -65,7 +65,6 @@
desc = "This box contains a guide on how to craft masterful works of origami, allowing you to transform normal pieces of paper into \
perfectly aerodynamic (and potentially lethal) paper airplanes."
item = /obj/item/storage/box/syndie_kit/origami_bundle
- progression_minimum = 10 MINUTES
cost = 4
surplus = 0
purchasable_from = ~UPLINK_NUKE_OPS //clown ops intentionally left in, because that seems like some s-tier shenanigans.
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index fdb53c47572cb5..0652c554b8a4af 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -323,7 +323,7 @@
if(!ai.linked_core) // we probably shouldnt gib AIs with a core
unlucky_ai = occupant
ai.investigate_log("has been gibbed by having their mech destroyed.", INVESTIGATE_DEATHS)
- ai.gib() //No wreck, no AI to recover
+ ai.gib(DROP_ALL_REMAINS) //No wreck, no AI to recover
else
mob_exit(ai,silent = TRUE, forced = TRUE) // so we dont ghost the AI
continue
diff --git a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
index dae449d8388b40..e3926aa7d3502e 100644
--- a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
+++ b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
@@ -212,7 +212,7 @@
to_chat(crushed_victim, span_userdanger("[chassis] crashes down on you from above!"))
if(crushed_victim.stat != CONSCIOUS)
crushed_victim.investigate_log("has been gibbed by a falling Savannah Ivanov mech.", INVESTIGATE_DEATHS)
- crushed_victim.gib(FALSE, FALSE, FALSE)
+ crushed_victim.gib(DROP_ALL_REMAINS)
continue
crushed_victim.adjustBruteLoss(80)
diff --git a/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm b/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm
index 6aae4adaaeed79..de96f3ad5f6573 100644
--- a/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm
+++ b/code/modules/vehicles/mecha/equipment/tools/mining_tools.dm
@@ -122,7 +122,7 @@
SEND_SIGNAL(src, COMSIG_MECHA_DRILL_MOB, chassis, target)
else
target.investigate_log("has been gibbed by [src] (attached to [chassis]).", INVESTIGATE_DEATHS)
- target.gib()
+ target.gib(DROP_ALL_REMAINS)
else
//drill makes a hole
var/obj/item/bodypart/target_part = target.get_bodypart(target.get_random_valid_zone(BODY_ZONE_CHEST))
diff --git a/code/modules/vehicles/mecha/mecha_mob_interaction.dm b/code/modules/vehicles/mecha/mecha_mob_interaction.dm
index c5f440ac97e25a..07b13a27ec0540 100644
--- a/code/modules/vehicles/mecha/mecha_mob_interaction.dm
+++ b/code/modules/vehicles/mecha/mecha_mob_interaction.dm
@@ -120,7 +120,7 @@
if(forced)//This should only happen if there are multiple AIs in a round, and at least one is Malf.
if(!AI.linked_core) //if the victim AI has no core
AI.investigate_log("has been gibbed by being forced out of their mech by another AI.", INVESTIGATE_DEATHS)
- AI.gib() //If one Malf decides to steal a mech from another AI (even other Malfs!), they are destroyed, as they have nowhere to go when replaced.
+ AI.gib(DROP_ALL_REMAINS) //If one Malf decides to steal a mech from another AI (even other Malfs!), they are destroyed, as they have nowhere to go when replaced.
AI = null
mecha_flags &= ~SILICON_PILOT
return
diff --git a/code/modules/vehicles/vehicle_key.dm b/code/modules/vehicles/vehicle_key.dm
index e1b45d55f0a80b..f6e5f7c4e2882e 100644
--- a/code/modules/vehicles/vehicle_key.dm
+++ b/code/modules/vehicles/vehicle_key.dm
@@ -41,7 +41,7 @@
switch(user.mind?.get_skill_level(/datum/skill/cleaning))
if(SKILL_LEVEL_NONE to SKILL_LEVEL_NOVICE) //Their mind is too weak to ascend as a janny
user.visible_message(span_suicide("[user] is putting \the [src] in [user.p_their()] mouth and is trying to become one with the janicart, but has no idea where to start! It looks like [user.p_theyre()] trying to commit suicide!"))
- user.gib()
+ user.gib(DROP_ALL_REMAINS)
return MANUAL_SUICIDE
if(SKILL_LEVEL_APPRENTICE to SKILL_LEVEL_JOURNEYMAN) //At least they tried
user.visible_message(span_suicide("[user] is putting \the [src] in [user.p_their()] mouth and has inefficiently become one with the janicart! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/code/modules/vending/autodrobe.dm b/code/modules/vending/autodrobe.dm
index 55e19152528220..02daa0ce7e7dd0 100644
--- a/code/modules/vending/autodrobe.dm
+++ b/code/modules/vending/autodrobe.dm
@@ -66,6 +66,8 @@
"products" = list(
/obj/item/clothing/suit/costume/imperium_monk = 1,
/obj/item/clothing/suit/chaplainsuit/holidaypriest = 1,
+ /obj/item/clothing/suit/chaplainsuit/habit = 1,
+ /obj/item/clothing/head/chaplain/habit_veil = 1,
/obj/item/clothing/suit/chaplainsuit/whiterobe = 1,
/obj/item/clothing/head/wizard/marisa/fake = 1,
/obj/item/clothing/suit/wizrobe/marisa/fake = 1,
diff --git a/code/modules/vending/drinnerware.dm b/code/modules/vending/drinnerware.dm
index 2e00d9d2a01a41..c37750a2d3d8e4 100644
--- a/code/modules/vending/drinnerware.dm
+++ b/code/modules/vending/drinnerware.dm
@@ -14,6 +14,7 @@
/obj/item/kitchen/spoon/soup_ladle = 3,
/obj/item/clothing/suit/apron/chef = 2,
/obj/item/kitchen/rollingpin = 2,
+ /obj/item/kitchen/tongs = 2,
/obj/item/knife/kitchen = 2,
),
),
diff --git a/code/modules/vending/games.dm b/code/modules/vending/games.dm
index 80fb1350841987..e51205c00e4a06 100644
--- a/code/modules/vending/games.dm
+++ b/code/modules/vending/games.dm
@@ -56,6 +56,7 @@
/obj/item/skillchip/sabrage = 2,
/obj/item/skillchip/useless_adapter = 5,
/obj/item/skillchip/wine_taster = 2,
+ /obj/item/skillchip/master_angler = 2,
),
),
list(
diff --git a/code/modules/vending/medical.dm b/code/modules/vending/medical.dm
index 576cbb0b8b284c..ad1c63e7e796f3 100644
--- a/code/modules/vending/medical.dm
+++ b/code/modules/vending/medical.dm
@@ -19,6 +19,7 @@
/obj/item/stack/medical/bone_gel = 4,
/obj/item/cane/white = 2,
/obj/item/clothing/glasses/eyepatch/medical = 2,
+ /obj/item/storage/box/bandages = 2,
)
contraband = list(
/obj/item/storage/box/gum/happiness = 3,
diff --git a/code/modules/vending/medical_wall.dm b/code/modules/vending/medical_wall.dm
index 4fd120bdc48759..66badd4adf2703 100644
--- a/code/modules/vending/medical_wall.dm
+++ b/code/modules/vending/medical_wall.dm
@@ -15,6 +15,7 @@
/obj/item/reagent_containers/medigel/sterilizine = 1,
/obj/item/healthanalyzer/simple = 2,
/obj/item/stack/medical/bone_gel = 2,
+ /obj/item/storage/box/bandages = 1,
)
contraband = list(
/obj/item/reagent_containers/pill/tox = 2,
diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm
index 6a7edd8e854133..9b5af87ab44c6a 100644
--- a/code/modules/vending/security.dm
+++ b/code/modules/vending/security.dm
@@ -29,6 +29,7 @@
/obj/item/clothing/gloves/tackler = 5,
/obj/item/grenade/stingbang = 1,
/obj/item/watertank/pepperspray = 2,
+ /obj/item/storage/belt/holster/energy = 4,
)
refill_canister = /obj/item/vending_refill/security
default_price = PAYCHECK_CREW
diff --git a/code/modules/vending/wardrobes.dm b/code/modules/vending/wardrobes.dm
index 2a5360c1ed2eb7..e43314aa3076b3 100644
--- a/code/modules/vending/wardrobes.dm
+++ b/code/modules/vending/wardrobes.dm
@@ -524,6 +524,8 @@
/obj/item/storage/backpack/cultpack = 1,
/obj/item/storage/fancy/candle_box = 2,
/obj/item/radio/headset/headset_srv = 2,
+ /obj/item/clothing/suit/chaplainsuit/habit = 1,
+ /obj/item/clothing/head/chaplain/habit_veil = 1,
)
contraband = list(
/obj/item/toy/plush/ratplush = 1,
diff --git a/code/modules/wiremod/shell/drone.dm b/code/modules/wiremod/shell/drone.dm
index dce5dca46adfe9..6f7afcfea04962 100644
--- a/code/modules/wiremod/shell/drone.dm
+++ b/code/modules/wiremod/shell/drone.dm
@@ -32,7 +32,7 @@
/mob/living/circuit_drone/updatehealth()
. = ..()
if(health < 0)
- gib(no_brain = TRUE, no_organs = TRUE, no_bodyparts = TRUE)
+ gib()
/mob/living/circuit_drone/welder_act(mob/living/user, obj/item/tool)
. = ..()
diff --git a/code/modules/zombie/items.dm b/code/modules/zombie/items.dm
index 4258dc5a304d00..dca26d366d8762 100644
--- a/code/modules/zombie/items.dm
+++ b/code/modules/zombie/items.dm
@@ -74,12 +74,13 @@
if(target.stat == DEAD)
var/hp_gained = target.maxHealth
target.investigate_log("has been devoured by a zombie.", INVESTIGATE_DEATHS)
- target.gib()
- // zero as argument for no instant health update
- user.adjustBruteLoss(-hp_gained, 0)
- user.adjustToxLoss(-hp_gained, 0)
- user.adjustFireLoss(-hp_gained, 0)
- user.adjustCloneLoss(-hp_gained, 0)
- user.updatehealth()
- user.adjustOrganLoss(ORGAN_SLOT_BRAIN, -hp_gained) // Zom Bee gibbers "BRAAAAISNSs!1!"
+ target.gib(DROP_ALL_REMAINS)
+ var/need_mob_update
+ need_mob_update = user.adjustBruteLoss(-hp_gained, updating_health = FALSE)
+ need_mob_update += user.adjustToxLoss(-hp_gained, updating_health = FALSE)
+ need_mob_update += user.adjustFireLoss(-hp_gained, updating_health = FALSE)
+ need_mob_update += user.adjustCloneLoss(-hp_gained, updating_health = FALSE)
+ need_mob_update += user.adjustOrganLoss(ORGAN_SLOT_BRAIN, -hp_gained) // Zom Bee gibbers "BRAAAAISNSs!1!"
user.set_nutrition(min(user.nutrition + hp_gained, NUTRITION_LEVEL_FULL))
+ if(need_mob_update)
+ user.updatehealth()
diff --git a/config/blanks.json b/config/blanks.json
index 299fa67a594b38..f3b38d67bdb4bf 100644
--- a/config/blanks.json
+++ b/config/blanks.json
@@ -545,7 +545,7 @@
"",
"
By writing and signing this form, you consent to the processing of your personal data by Nanotrasen Corporation.
",
"",
- "
Name of offical to take action:
",
+ "
Name of official to take action:
",
"
[___________________________________]
",
"
Official Decision:
",
"
[___________________________________]
",
diff --git a/config/config.txt b/config/config.txt
index 8d28b656c30c1b..e8abb56fed5046 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -137,11 +137,14 @@ VOTE_PERIOD 600
## players' votes default to "No vote" (otherwise, default to "No change")
# DEFAULT_NO_VOTE
-## disable abandon mob
-NORESPAWN
-
-## Respawn delay (deciseconds), which doesn't allow to return to lobby (default 10 minutes)
-#RESPAWN_DELAY 6000
+## Determines if players can respawn after death
+## 0 = Cannot respawn (default)
+## 1 = Can respawn
+## 2 = Can respawn if choosing a different character
+ALLOW_RESPAWN 0
+
+## Respawn delay (deciseconds), which doesn't allow to return to lobby
+RESPAWN_DELAY 0
## set a hosted by name for unix platforms
HOSTEDBY Yournamehere
diff --git a/config/lavaruinblacklist.txt b/config/lavaruinblacklist.txt
index 4332260d6becc9..b2574fd7fd62fd 100644
--- a/config/lavaruinblacklist.txt
+++ b/config/lavaruinblacklist.txt
@@ -11,8 +11,9 @@
##RESPAWN
#_maps/RandomRuins/LavaRuins/lavaland_surface_seed_vault.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
+_maps/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
#modular_skyrat/modules/mapping/_maps/RandomRuins/LavaRuins/lavaland_surface_syndicate_base1_skyrat.dmm
+#_maps/RandomRuins/LavaRuins/skyrat/lavaland_surface_ash_walker1_skyrat.dmm
#_maps/RandomRuins/AnywhereRuins/golem_ship.dmm
##SIN
@@ -23,13 +24,24 @@
#_maps/RandomRuins/LavaRuins/lavaland_surface_sloth.dmm
##MISC
+#_maps/RandomRuins/AnywhereRuins/fountain_hall.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_automated_trade_outpost.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_elephant_graveyard.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_gaia.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_hermit.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_hierophant.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_library.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_mookvillage.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_phonebooth.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_pizzaparty.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_survivalpod.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_tomb.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_ufo_crash.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_ww_vault.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_automated_trade_outpost.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_xeno_nest.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_survivalpod.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_wwiioutpost.dmm
+<<<<<<< HEAD
#_maps/RandomRuins/LavaRuins/lavaland_surface_tomb.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_hierophant.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_pizzaparty.dmm
@@ -39,4 +51,7 @@
#_maps/RandomRuins/LavaRuins/lavaland_surface_elephant_graveyard.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_library.dmm
#_maps/RandomRuins/AnywhereRuins/fountain_hall.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_phonebooth.dmm
\ No newline at end of file
+#_maps/RandomRuins/LavaRuins/lavaland_surface_phonebooth.dmm
+=======
+#_maps/RandomRuins/LavaRuins/lavaland_surface_xeno_nest.dmm
+>>>>>>> 0f5d14e68b1 (Mook village and basic mook refactor (#78789))
diff --git a/html/changelogs/AutoChangeLog-pr-23524.yml b/html/changelogs/AutoChangeLog-pr-23524.yml
new file mode 100644
index 00000000000000..361042dc62ebe2
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-23524.yml
@@ -0,0 +1,4 @@
+author: "nikothedude"
+delete-after: True
+changes:
+ - rscadd: "Sec/Medhuds can now see a small ! if a person has the DNR quick, as well as see a blurb in examine about it"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23580.yml b/html/changelogs/AutoChangeLog-pr-23580.yml
new file mode 100644
index 00000000000000..291734c3a95454
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-23580.yml
@@ -0,0 +1,4 @@
+author: "burgerenergy"
+delete-after: True
+changes:
+ - qol: "Both versions of Interdyne received some map touch ups. Standouts include; a fax machine, dorm locks, a make shift brig area, a revamped bathroom, botany, and kitchen, as well as backporting Icemoon improvements to the Lavaland version."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23964.yml b/html/changelogs/AutoChangeLog-pr-23964.yml
deleted file mode 100644
index a5a9ca339e284f..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23964.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - refactor: "Hivelords and Legions now use the basic mob framework. Please report any unusual behaviour."
- - rscadd: "Hivelords shed more spawn when they are attacked."
- - rscadd: "Legions have learned how to fling their skulls across long distances."
- - rscadd: "Legions can heal other lavaland mobs with their skulls."
- - rscadd: "Legions are better at preserving corpses they consume, and sometimes make use of their radios."
- - rscadd: "Legions may leave behind an unpleasant surprise after you are rescued from them."
- - balance: "The crew monitoring console will now display you as dead if you are dead, an critically injured if in crit, rather than setting those icons purely based on your current health."
- - qol: "You won't continue burning to a husk if consumed by a snow legion after being set on fire by an ice drake."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23969.yml b/html/changelogs/AutoChangeLog-pr-23969.yml
deleted file mode 100644
index e68c1f16a513ab..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23969.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "A.C.M.O."
-delete-after: True
-changes:
- - bugfix: "Fixes the death sandwich, making it safe to examine."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23970.yml b/html/changelogs/AutoChangeLog-pr-23970.yml
deleted file mode 100644
index 90c3560af2270d..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23970.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - bugfix: "Fix water puddle runtime when washing items"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23971.yml b/html/changelogs/AutoChangeLog-pr-23971.yml
deleted file mode 100644
index 77882c152d7f0a..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23971.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Rhials"
-delete-after: True
-changes:
- - bugfix: "The Polymorph Belt should now update its sprite when active."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23972.yml b/html/changelogs/AutoChangeLog-pr-23972.yml
deleted file mode 100644
index cd5835f48b8c35..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23972.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - refactor: "Snakes have been refactored into basic mobs. This means that they are a bit more intelligent than previous snakes, making them more docile and averse to harming people (unless otherwise provoked). They do chomp all sorts of mice though. You can feed them a dead mouse to make them your friend if you'd want that."
- - sound: "If you listen closely to snakes, you might be able to hear a small hissing sound..."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23973.yml b/html/changelogs/AutoChangeLog-pr-23973.yml
deleted file mode 100644
index c78f143cc15f51..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23973.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - rscadd: "Add drinking water causes drunk mobs to become sober"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23974.yml b/html/changelogs/AutoChangeLog-pr-23974.yml
deleted file mode 100644
index 0cb3b6568f5cc0..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23974.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - code_imp: "adds a gas connector component that allows connection to the atmos piping system without the need of repathing"
- - refactor: "changes the cryo machine to use this new system"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23976.yml b/html/changelogs/AutoChangeLog-pr-23976.yml
deleted file mode 100644
index 5069c636850827..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23976.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - balance: "Head revolutionaries and heads of staff are no longer immediately considered disqualified when going AFK or disconnecting and are given a 2 minute grace period."
- - admin: "Admins now get a log when a head revolutionary or head of staff disconnects or goes AFK during a revolution. They also get the same log 1 minute after to give them a chance to act on the information."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23978.yml b/html/changelogs/AutoChangeLog-pr-23978.yml
deleted file mode 100644
index b42cf16ce625bc..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23978.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - code_imp: "Robot Customers have recently been touched codewise, please report any bugs or unexpected behavior as there really should not be any."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23979.yml b/html/changelogs/AutoChangeLog-pr-23979.yml
deleted file mode 100644
index 3093dac19e2277..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23979.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - admin: "There is now a tool to apply a DNA Infuser entry to any human."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23980.yml b/html/changelogs/AutoChangeLog-pr-23980.yml
deleted file mode 100644
index febc6a692b7b7b..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23980.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - rscadd: "Add candle design to biogenerator"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23981.yml b/html/changelogs/AutoChangeLog-pr-23981.yml
deleted file mode 100644
index 29314e499ecc11..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23981.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - qol: "allowed names to start with a number if AI/Borg"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23982.yml b/html/changelogs/AutoChangeLog-pr-23982.yml
deleted file mode 100644
index 41b052985ddcbf..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23982.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - bugfix: "fixed lobstrosities becoming unmovable when killed during their charge windup"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23983.yml b/html/changelogs/AutoChangeLog-pr-23983.yml
deleted file mode 100644
index 7f6595037bfff6..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23983.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - bugfix: "dead bodies now cool down to room temperature over time"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23984.yml b/html/changelogs/AutoChangeLog-pr-23984.yml
deleted file mode 100644
index 4cea4c40d9ceec..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23984.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - balance: "Diabetics rejoice! Nerfed sugar OD/hyperglycaemic shock to be an immediate KO followed by drowsiness afterwards until the OD is gone."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23985.yml b/html/changelogs/AutoChangeLog-pr-23985.yml
deleted file mode 100644
index eb6c6b29171d01..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23985.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SkyratBot"
-delete-after: True
-changes:
- - bugfix: "The Nuke Op MODsuit AI downloader only works once per purchase, as intended."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-23986.yml b/html/changelogs/AutoChangeLog-pr-23986.yml
deleted file mode 100644
index 6febb0c667cf05..00000000000000
--- a/html/changelogs/AutoChangeLog-pr-23986.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "sergeirocks100"
-delete-after: True
-changes:
- - bugfix: "Undershirts will now look as they should if you have a body type that differs from the gender default."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-24174.yml b/html/changelogs/AutoChangeLog-pr-24174.yml
new file mode 100644
index 00000000000000..cd5b645f5c9e0f
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-24174.yml
@@ -0,0 +1,6 @@
+author: "Hatterhat"
+delete-after: True
+changes:
+ - bugfix: "The CIN replicator medipen pouch and pocket first-aid kit no longer have bag-like functionality (scooping/the action button), which they didn't need to fit in your pockets anyway."
+ - balance: "First-aid pouches (the ones through Cargo for 300 cr) now have five slots; one up from four."
+ - rscadd: "Ammo pouches can now be reskinned into casing pouches, which let them hold ten individual shell casings in your pocket. Yes, this includes shotgun shells. This is also primarily intended for shotgun users."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-24175.yml b/html/changelogs/AutoChangeLog-pr-24175.yml
new file mode 100644
index 00000000000000..6f65e92a12f3f0
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-24175.yml
@@ -0,0 +1,5 @@
+author: "Hatterhat"
+delete-after: True
+changes:
+ - spellcheck: ".50 BMG surplus, incendiary, and marksman have now been given more SR-lore-accurate names."
+ - bugfix: "10mm Reaper can't be printed in ammo benches like it was intended. If you don't know what this means, don't worry about it."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-24283.yml b/html/changelogs/AutoChangeLog-pr-24283.yml
new file mode 100644
index 00000000000000..b2c323707047e7
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-24283.yml
@@ -0,0 +1,10 @@
+author: "LT3"
+delete-after: True
+changes:
+ - rscadd: "Introducing Nanotrasen Wave! A Nanotrasen exclusive, Waveallows your PDA to be charged wirelessly through microwave frequencies. You can Wave-charge your device by placing it inside a compatible microwave and selecting the charge mode."
+ - rscadd: "Microwaves can be upgraded to add wireless charging"
+ - rscadd: "Cell-swappable microwave for the engineer on-the-go"
+ - rscadd: "Microwave now has a wire to swap charge/cook modes"
+ - rscadd: "Furnishings RCD upgrade now includes wireless microwave"
+ - rscadd: "Tramstation and Birdshot engineering break rooms now have microwave and donk pockets. Some microwaves come pre-equipped with wireless charging and an upgraded cell."
+ - bugfix: "The microwave in the snowdin ruin is now real, not a fluff prop"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-24284.yml b/html/changelogs/AutoChangeLog-pr-24284.yml
new file mode 100644
index 00000000000000..78e4247ee9f58a
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-24284.yml
@@ -0,0 +1,4 @@
+author: "SkyratBot"
+delete-after: True
+changes:
+ - refactor: "Gorillas now use the basic mob framework. Please report any unusual side effects."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-24289.yml b/html/changelogs/AutoChangeLog-pr-24289.yml
new file mode 100644
index 00000000000000..14a3193bacce17
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-24289.yml
@@ -0,0 +1,6 @@
+author: "SkyratBot"
+delete-after: True
+changes:
+ - bugfix: "The Holy Hand Grenade's effect on revealing a revenant had its duration accidentally nerfed, it is now back to 10 seconds."
+ - bugfix: "Revenant midrounds should now properly run."
+ - bugfix: "Revenant harvesting should now let you actually pass the final do_after so you can harvest that sweet essence."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-24291.yml b/html/changelogs/AutoChangeLog-pr-24291.yml
new file mode 100644
index 00000000000000..3f65148a845fc0
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-24291.yml
@@ -0,0 +1,4 @@
+author: "SkyratBot"
+delete-after: True
+changes:
+ - qol: "The rollerdome is now better - the dance floor works now, and the bar is groovier."
\ No newline at end of file
diff --git a/html/changelogs/archive/2023-09.yml b/html/changelogs/archive/2023-09.yml
index 976efdac110c16..a45513671cec74 100644
--- a/html/changelogs/archive/2023-09.yml
+++ b/html/changelogs/archive/2023-09.yml
@@ -1154,3 +1154,109 @@
tattle:
- qol: Basic animals now make sounds for audible emotes
- sound: Added new sound effects for chicks, chickens, crabs, and insects
+2023-09-29:
+ A.C.M.O.:
+ - bugfix: Fixes the death sandwich, making it safe to examine.
+ BurgerBB:
+ - bugfix: Scrubbers and Vents will no longer reset their settings on map load.
+ GoldenAlpharex:
+ - server: Added a way for calls to be made to interfere with player ranks on live
+ servers (updating the players if they're connected) from outside of the game.
+ Rhials:
+ - qol: The freedom implant has received minor feedback and other minor usage improvements.
+ - bugfix: The Polymorph Belt should now update its sprite when active.
+ SkyratBot:
+ - qol: allowed names to start with a number if AI/Borg
+ - rscadd: Add drinking water causes drunk mobs to become sober
+ - balance: Diabetics rejoice! Nerfed sugar OD/hyperglycaemic shock to be an immediate
+ KO followed by drowsiness afterwards until the OD is gone.
+ - code_imp: Robot Customers have recently been touched codewise, please report any
+ bugs or unexpected behavior as there really should not be any.
+ - admin: There is now a tool to apply a DNA Infuser entry to any human.
+ - bugfix: The Nuke Op MODsuit AI downloader only works once per purchase, as intended.
+ - code_imp: adds a gas connector component that allows connection to the atmos piping
+ system without the need of repathing
+ - refactor: changes the cryo machine to use this new system
+ - refactor: Hivelords and Legions now use the basic mob framework. Please report
+ any unusual behaviour.
+ - rscadd: Hivelords shed more spawn when they are attacked.
+ - rscadd: Legions have learned how to fling their skulls across long distances.
+ - rscadd: Legions can heal other lavaland mobs with their skulls.
+ - rscadd: Legions are better at preserving corpses they consume, and sometimes make
+ use of their radios.
+ - rscadd: Legions may leave behind an unpleasant surprise after you are rescued
+ from them.
+ - balance: The crew monitoring console will now display you as dead if you are dead,
+ an critically injured if in crit, rather than setting those icons purely based
+ on your current health.
+ - qol: You won't continue burning to a husk if consumed by a snow legion after being
+ set on fire by an ice drake.
+ - bugfix: removes incorrect stack traces when using some admin secrets
+ - balance: Head revolutionaries and heads of staff are no longer immediately considered
+ disqualified when going AFK or disconnecting and are given a 2 minute grace
+ period.
+ - admin: Admins now get a log when a head revolutionary or head of staff disconnects
+ or goes AFK during a revolution. They also get the same log 1 minute after to
+ give them a chance to act on the information.
+ - refactor: Snakes have been refactored into basic mobs. This means that they are
+ a bit more intelligent than previous snakes, making them more docile and averse
+ to harming people (unless otherwise provoked). They do chomp all sorts of mice
+ though. You can feed them a dead mouse to make them your friend if you'd want
+ that.
+ - sound: If you listen closely to snakes, you might be able to hear a small hissing
+ sound...
+ - bugfix: fixed lobstrosities becoming unmovable when killed during their charge
+ windup
+ - bugfix: Splattercasting resets your blood to normal values when you transsform
+ into a vampire.
+ - bugfix: Gaining a new species will set your blood volume down to the normal volume
+ levels if higher than normal.
+ - bugfix: Fix water puddle runtime when washing items
+ - bugfix: the parole status and discharged status are now green and blue respectively
+ in the security record interface
+ - bugfix: Dimensional Anomalies no longer destroy wall-mounted equipment.
+ - code_imp: Your bodytype now decides what gendered sounds you make.
+ - bugfix: Fixed crabs not correctly (kinda) walking sideway.
+ - bugfix: dead bodies now cool down to room temperature over time
+ - rscadd: Add candle design to biogenerator
+ sergeirocks100:
+ - bugfix: Undershirts will now look as they should if you have a body type that
+ differs from the gender default.
+ softcerv:
+ - rscadd: Adds in the ability for certain NIFSofts to be kept between rounds.
+2023-09-30:
+ DrDiasyl aka DrTuxedo:
+ - balance: Holsters can now be clipped to any suit, and house Captain antique gun
+ and HoS gun. You now can buy holsters from the SecTech premium section.
+ Paxilmaniac:
+ - qol: The half mask respirator can have its TTS voice muffling properties toggled
+ with control click
+ - qol: Icecats are now listed in the round end report, so you can see who was up
+ icing they cat
+ - bugfix: Icecats should hopefully spawn with their special language correctly now
+ SkyratBot:
+ - code_imp: removed some redundant code for airlocks
+ - admin: Mob abilities can be granted to arbitrary mobs via the VV menu in a similar
+ way to spells.
+ - bugfix: Lavaland syndicate operatives can no longer trivially use the jetpack
+ on their modsuit to fly over the lava.
+ - bugfix: If two cosmic heretics ascend in the same round, their star gazer survival
+ will be linked to each individual heretic and not shared by just one of them.
+ - bugfix: You can't click the Knock heretic portal to join as a mob while already
+ signed up to become a mob.
+ - balance: Cosmic heretics can't order the Star Gazer around while jaunting.
+ - balance: The Knock Heretic portal cannot summon Flesh Worms, but can summon Fire
+ Sharks.
+ - balance: The Knock Heretic portal will disperse if its creator is killed.
+ - rscadd: SM crystal can now dust someone or something if it falls on it.
+ - bugfix: The reverse revolver now looks like a normal Syndicate revolver on inspection.
+ - bugfix: fixed the stamp in the metastation CMO office always spawning on the floor
+ - bugfix: You can now spray paint the SM without getting dusted
+ Smol42:
+ - rscadd: Added some new hairstyles
+ Zergspower:
+ - bugfix: Crew Monitor works again properly
+ nikothedude:
+ - rscadd: A waterbreathing quirk
+ - qol: Waterbreathing is now documented on species pages of the species that have
+ it
diff --git a/html/changelogs/archive/2023-10.yml b/html/changelogs/archive/2023-10.yml
new file mode 100644
index 00000000000000..265d5d5136b4e9
--- /dev/null
+++ b/html/changelogs/archive/2023-10.yml
@@ -0,0 +1,615 @@
+2023-10-01:
+ Hatterhat:
+ - balance: Bullets have had their base type's wound bonus reduced back to 0, down
+ from 20, because wounds are actually quite punishing. Funnily enough, most bullets
+ already have modified wound bonuses - except c9mm, c10mm, and most incendiaries,
+ so this probably doesn't change much.
+ - balance: .50 (used in the snipers and renamed to .416 or whatever) is now back
+ to TG balance standards. Knockdown on hit, 60 instead of 110 damage, etc. etc.
+ - bugfix: .50 Soporific was removed because disruptor ammo was right there and nobody
+ realized it existed.
+ - bugfix: After review of a missing equipment complaint, Nanotrasen remembered to
+ pay Lopland's quartermasters to put the customary flashbang and teargas grenade
+ boxes into the Void Raptor's armory.
+ Melbert:
+ - qol: Examine blocked out roundstart / latejoin job information.
+ - qol: Captain gets a little bit more information about how their radio works roundstart.
+ - bugfix: Fixed roundstart players not getting radio information.
+ Paxilmaniac:
+ - image: The buttondown shirts (underwear) have been updated with a better look
+ and more contrasted palette
+ SkyratBot:
+ - rscadd: A new export has arrived in the imports section, the Galactic Materials
+ Market! You can use this to buy and sell minerals for profit or cost, as well
+ as stock your station when you don't have any miners.
+ - rscadd: Insert sheets of minerals into the Galactic Materials Market to convert
+ them into a stock block, allowing you to lock in your price for 5 minutes. Wait
+ too long and it'll be subject to market value again!
+ - rscadd: Minerals can be bought on the market either using the station's cargo
+ budget by cargo crew, or privately by everyone else.
+ - rscdel: Any material stacks that can be bought and sold on the market before have
+ been removed from the cargo catalog.
+ - rscadd: Adds Bitrunning to supply department- a semi-offstation role that rewards
+ teamwork.
+ - rscadd: Adds new machines to complement the job- net pod, quantum server, quantum
+ consoles, and the nexacache vendor.
+ - rscadd: Adds several new maps which can be loaded and unloaded at will.
+ - rscadd: Some flair for the new bitrunning vendor.
+ - rscadd: Adds a new antagonist for the virtual domain only. Short lived ghost role
+ that fights bitrunners.
+ - rscdel: Removes the BEPIS machine, moves its tech into the Bitrunning vendor.
+ - bugfix: Fixes missing baseturfs and clowns in mining planet VDOM..
+ - qol: Font settings in the chat panel applies to all text now.
+ - image: new chaplain outfit
+ - bugfix: Blob spores will respond to rallies more reliably (it won't runtime every
+ time they try and pathfind).
+ - bugfix: Blobbernaut pain animation overlays should align with the direction the
+ mob is facing instead of always facing South
+ - refactor: Blob spores, zombies, and blobbernauts now all use the basic mob framework.
+ They should work the same, but please report any issues.
+ - bugfix: Added warden to list of default required enemies for rulesets.
+ - bugfix: Blob Zombies and Blobbernauts have had their attack speed restored to
+ its original value
+ - refactor: Supermatter Spiders have been refactored into basic mobs, on the extremely
+ off chance you spot one and also notice any weird bugs regarding it, please
+ report it.
+ - balance: There are now 3 roundstart cyborg job slots open by default.
+ - rscadd: Quantum servers now talk over supply channel when they're done cooling
+ off. Go outside!
+ - bugfix: You can no longer use dragon swoop to bypass cordons.
+ - bugfix: Netpod brain damage is now properly reduced upon server upgrades.
+ - bugfix: Fixed an bug where swapping bodies in vdom prevented you from disconnecting.
+ - bugfix: Fixed a bug where a quantum server could get locked out of loading new
+ domains.
+ - bugfix: Changed quantum console UI to display "no bandwidth" rather than "none"
+ - bugfix: Actually fixed the hooked item exploit.
+ - rscadd: Heretic Rebalance
+ - balance: Researching the Main Knowledge paths that unlock Side Paths will grant
+ one Side Point that can be used only on those side paths. You can still spend
+ normal knowledge points on them if you wish.
+ - balance: Rune drawing time has been reduced from 30->20 seconds. Codex drawing
+ time has been reduced from 15->8.
+ - balance: 'Codex Cicatrix is now a roundstart knowledge, works as an amber focus
+ when held in-hand and opened, and has had its recipe changed to: 1 of any non-standard
+ pen (literally anything that isn''t the base pen), any book, and either animal
+ hide OR a corpse, any kind.'
+ - code_imp: Added support for using a list inside ritual requirements and a special
+ 'snowflake check' rituals can utilize.
+ - balance: The first non-path knowledge, the Mansus Hand Mark, has had its cost
+ reduced from 2->1 points.
+ - bugfix: Aloe and other baked foods that don't have reagents can be baked again
+ without turning to ash
+ Vekter:
+ - bugfix: Fixes the missing grinder in Birdshot's Virology department
+ jjpark-kb:
+ - bugfix: the ashwalker tendril will allow you to respawn again (the tendril blessing)
+ - bugfix: the round end report will accurately report ashwalker sacrifices
+ nikothedude:
+ - code_imp: Gauze removal is now handled by the gauze's destroy instead of seep_gauze
+ ninjanomnom:
+ - rscdel: An easter egg plushie that was spawning where it shouldn't has been brought
+ back home.
+ - rscadd: The secure closet can now spawn live gibtonite, enjoy your free bomb.
+2023-10-02:
+ SkyratBot:
+ - balance: Sci now has access to the materials & canisters section in their departmental
+ order console
+ - rscadd: Expanded the fishing portal generator. It now comes with several portal
+ options that can be unlocked by performing fish scanning experiments, which
+ also award a modest amount of techweb points.
+ - balance: The fishing portal generator is now buildable and no longer orderable.
+ The board can be printed from cargo, service and science lathes.
+ - balance: Advanced fishing tech is no longer a BEPIS design. It now requires the
+ base fish scanning experiment and 2000 points to be unlocked.
+ - rscadd: The advanced fishing rod now comes with an incorporated experiscanner
+ specific for fish scanning.
+ - rscadd: Added a new skillchip that may change the icon of the "fish" shown in
+ the minigame UI to less generic ones. Reaching master level in fishing also
+ does that.
+ - qol: The experiment handler UI no longer shows unselectable experiments.
+ - bugfix: Security officers can now download the crew manifest PDA app that they
+ start with.
+ - rscadd: Wizards who complete the grand ritual can now gift everyone with eternal
+ life
+ distributivgesetz:
+ - bugfix: Font scaling in TGUI chat has been reverted to its original implementation.
+ softcerv:
+ - rscadd: Adds the mini-soulcatcher, a more lightweight soulcatcher that can be
+ attached to objects
+ - rscadd: Adds in the RSD brain interface, an item that allows for soulcatcher souls,
+ that died within a round and were scanned, to be transferred to a new brain.
+ - rscadd: Adds in the NIFSoft Scryer, a NIFSoft that gives the user a Scryer they
+ can use to communicate with other Scryer users.
+2023-10-03:
+ SkyratBot:
+ - bugfix: fixed misplaced door on syndicate listening post
+ - rscadd: adds boxes of bandages, a quick healing item
+ - bugfix: Spiders, Morphs, Fire Sharks, and Regal Rats no longer have a reduced
+ click speed.
+ - image: Railings have had a visual update.
+ projectkepler-ru:
+ - balance: reverted the nerf on the X-01
+ softcerv:
+ - rscadd: Adds in Purpura Eye, a NIFSoft that allows for the user to hypnotize others.
+ vinylspiders:
+ - bugfix: the pollution system will no longer try (and fail) to pollute a turf from
+ nullspace
+2023-10-04:
+ Coded by Jacquerel, Sprited by Dalmationer:
+ - rscadd: Added tongs to the kitchen, which you can use to manipulate food from
+ further away
+ Fazzie:
+ - rscadd: Added a budget solar crate to the derelict teleporter room
+ - rscadd: Added a solar panel control to the north derelict solar
+ - qol: The derelict's AI coridoor is now shorter and prettier
+ - rscadd: A lot of new content has been added to the beach away mission
+ - qol: It also looks substantially better, too!
+ Motho:
+ - bugfix: An anonymous visitor to the NSV Void Raptor has turned in security's biosuit
+ to the lost and found. Security officers everywhere rejoice.
+ Paxilmaniac:
+ - rscadd: A significantly smaller selection of new SolFed weapons has taken the
+ place of the now missing Armadyne selection.
+ - rscdel: So basically, all of Armadyne is gone.
+ - qol: A large number of weapons now use singular interchangeable magazine types,
+ rather than having seven different 9mm magazines to do the same thing
+ - balance: Any weapon that was replaced is going to have different stats from what
+ they used to be, numbers subject to change
+ - sound: Several new firing sounds from TGMC for the new weapons
+ - image: Whole collection of new ammo, gun, case, and so on sprites by myself
+ - image: Gun case worn on the back sprites done by Zydras
+ - code_imp: Where applicable, containers spawning gun stuff use generate_items_inside,
+ which is much neater than spamming new x(src)
+ SkyratBot:
+ - bugfix: The Syndicate have fired their previous construction company after poor
+ results in recent outposts.
+ - qol: Departmental order consoles now alert their department via radio when their
+ cooldown expires
+ - bugfix: Fix butchered monkeys to transfer reagents and diseases to meat
+ - bugfix: Fix organs having no DNA and become bloody when violently removed.
+ - refactor: Raw Prophets now use the basic mob framework. Please report any unusual
+ behaviour.
+ - bugfix: Cutting open a hand-pressed paper bundle no longer deletes all of the
+ paper.
+ - balance: Kudzu will now be destroyed by adverse weather.
+ - balance: Kudzu will no longer spread over holes.
+ - refactor: fixed many instances of updatehealth() either being called needlessly
+ or not at all within on_mob_life() and in various other parts of the code
+ - refactor: damage procs now return useful information--the actual net change in
+ damage on the mob. added a unit test for this
+ - bugfix: Some icons for selecting character preferences are no longer scaled incorrectly.
+ ninjanomnom:
+ - admin: Appearance vars in VV now display instead of being left blank
+2023-10-05:
+ FIoppie:
+ - sound: '*flap now makes a fluttering noise for moth wings'
+ - sound: Moths now have a death sound
+ - qol: '*tremble emote now is just "trembles!" instead of "trembles in fear!"'
+ LT3:
+ - image: Added colourable arm and leg wraps
+ Melbert:
+ - qol: Moved a lot of maintenance spawnpoints out of non-maintenance rooms. Some
+ antags (paradox clone, fugitives, nightmares, spiders) are now less likely to
+ spawn in obvious places like the morgue, tech storage, or dorms rooms.
+ Motho:
+ - image: Sector 13's station air alarms and fire alarms have been updated to be
+ more in line with other frontier station models.
+ SkyratBot:
+ - admin: Admins can turn off dynamic rulesets (or force them on despite not meeting
+ the qualification criteria) on a per-round basis.
+ - rscadd: The funds the syndicate have been saving by restricting galley access
+ has been suddenly funneled into a singular mosaic pattern in the experiments
+ wing.
+ - balance: CQC legsweeps now cause knockdown instead of paralysis.
+ - balance: CQC kicks now knockout a target on the floor for ten seconds if they
+ reach stam crit. Helmet protection shortens the knockout length.
+ - bugfix: Your heart will no longer be deleted if an admin heals you while you have
+ corazargh in your system.
+ - refactor: The cursed heart has been streamlined a bit, and now gives you a visual
+ cooldown for when you can beat your heart again.
+ Wallem:
+ - rscadd: Adds The Hand of Midas, an ancient Egyptian matchlock pistol.
+ honkpocket:
+ - bugfix: Extra magazines for all the Foam Force guns are now purchasable from cargo
+ imports
+ - rscadd: The Foam Force dart collector MOD modules are now purchasable from cargo
+ imports
+ - sound: added a unique racking sfx for the sol 'Renoster' shotgun
+ softcerv:
+ - qol: NIFSofts now use the same TGUI as their parent NIF.
+ tf-4:
+ - bugfix: Fixed some weirdness with mould mob attacks.
+ - qol: Mold mobs delete themselves on death.
+2023-10-06:
+ LT3:
+ - image: Job icon for departmental guards, DS2 and other SR specific jobs are properly
+ aligned
+ Majkl-J:
+ - bugfix: Table to the cafe food processor
+ RatFromTheJungle:
+ - balance: Interdyne now spawns with Sidano SMG's, instead of .50 snipers
+ - bugfix: you can now actually wear the 'LIZARED' top, yippie!
+ SkyratBot:
+ - bugfix: The Galactic Materials Market now offers things for sale as it should.
+ - rscadd: Added blackout, happens when you drink...ALOT
+ - bugfix: plumbing reaction chamber now balances the ph of it's solution correctly
+ to the best of it's ability so no guarantees
+ - code_imp: converted plumbing reaction chamber & mixing chamber UI files to Typescript
+ - refactor: plumbing mixing chamber now also accepts an TGUI input list to input
+ it's chemicals
+ - image: We have received a new shipment of IDs, as the old ones were found out
+ to be haunted.
+ - image: Laser tag red team ID has received a massive nerf
+ - image: Station budget cards have gotten a facelift
+ - image: Emags and Doorjacks
+ - bugfix: Numbered prisoner IDs will now be legible
+ Wallem:
+ - rscadd: Buffs the Active Sonar module with a radial scan, and makes the power
+ costs more in-line with other modules.
+ softcerv:
+ - bugfix: the ghost role NIF boxes now contain the Purpura Eye NIFSoft
+ vinylspiders:
+ - bugfix: fixes gender shaping and height offsets on underwear
+ - bugfix: female gender shaping now works with digi jumpsuits
+ - bugfix: fixed rainbow jumpsuit digi sprite
+2023-10-07:
+ GoldenAlpharex:
+ - bugfix: The round end report will no longer expose people's ckeys for the achievements
+ they obtained through the round, nor when they're contractor support agents.
+ LT3:
+ - bugfix: Adjusted Void Raptor shutters and firelocks in the pharmacy, hallway and
+ HoP line
+ - balance: Hypovial capacity now matches bottle capacity
+ - bugfix: Broken and placeholder hypovials can no longer be printed in the ChemMaster
+ Melbert:
+ - qol: AI, cyborg, and PAI camera (photo taking) behavior now uses balloon alerts
+ and has sound effects associated
+ - refactor: Refactored AI, cyborg, and PAI camera (photo taking) code
+ - bugfix: fixed being unable to print photos as a cyborg when below 50% toner, even
+ though photos only take 5%
+ SkyratBot:
+ - bugfix: Engineering borgs can no longer grab and drop their own iron/glass sheet
+ module.
+ - bugfix: It is no longer possible to chasm yourself on the geode. Again.
+ - rscadd: Fish analyzers can now be used to perform fish scanning experiments.
+ - balance: They can now be singularly bought as a goodie pack for 125 cr each, instead
+ of a crate of three for 500 cr.
+ - bugfix: Borgs will no longer become permanently upside-down if tipped over by
+ multiple people at the same time.
+ - balance: Despite earlier reports suggesting that the famous lethality of the Regal
+ Condor was largely a myth, there has been rumors that the gun has once again
+ started to display its true killing potential on any station that it 'manifests'.
+ - bugfix: The AI can no longer turn you off if you shapeshift into a robot.
+ - bugfix: Blood once again appears as small drops instead of splatters during minor
+ bleeding.
+ - bugfix: you are now made a ghost faster if you get gibbed
+ - bugfix: fixed bad food not having bad food reagents
+ - rscadd: The laser carbine, a weak but fully automatic sidegrade to the normal
+ laser gun, can now be ordered from cargo.
+ - bugfix: Adminheal will now properly clear negative mutations as intended.
+ - bugfix: Ice whelps can now use spells given to them by admins, and people who
+ have polymorphed into ice whelps can now polymorph back to normal.
+ - bugfix: Fixed silent catwalks.
+ - rscadd: Fake moustaches are now poorly slapped on top of what you're wearing
+ jjpark-kb:
+ - rscadd: ashwalker nest is now in the NE corner of lavaland
+ neocloudy:
+ - bugfix: MetaStation disposal pipes from Cargo to Disposals/the rest of the station
+ are working again.
+ nikothedude:
+ - rscadd: Synthetic wounds! Blunt, Pierce, Slash, Burn, Muscle. See PR 23733 for
+ more information
+ - rscadd: Robodrobe now has 2 chilled hercuri sprays for treating synthetic burn
+ wounds
+ - rscadd: Robodrobe now has 2 pairs of black gloves to let robotics painlessly meld
+ T3 synthetic blunt wounds
+ - balance: 'Synthetic damage multiplier: 1.3 -> 1.0'
+ - rscadd: '3 new cargo packs to science: 2 chilled hercuri, a synth trauma kit,
+ and synth medicine'
+ - balance: Quadruple amputee can now be picked with frail
+ - rscadd: Interdyne/DS2 now have advanced synthetic trauma kits in their medbays
+2023-10-08:
+ LT3:
+ - image: Text alignment on ID cards slightly adjusted
+ Melbert:
+ - bugfix: Fixed an error from reading an ID card closely when you can't read
+ - config: Adds a config option for player respawning that enables respawns, but
+ forces you pick a new character.
+ - config: '"NORESPAWN" has been replaced with "ALLOW_RESPAWN 0". Unlimited respawns
+ is "ALLOW_RESPAWN 1" and character limited respawns is "ALLOW_RESPAWN 2".'
+ SkyratBot:
+ - bugfix: Fix bodies now lose fire stacks while husked.
+ - bugfix: Flesh Worms will move smoothly more consistently.
+ - balance: You can now remove and replace power cells from PDAs (with screwdriver).
+ - balance: PDAs now drain their power cells harder, and also take into account active
+ programs & their flashlight being on.
+ - balance: PDAs running out of charge now turn their flashlights off.
+ - qol: allows janitor keys to be stored in janitor wintercoats and janibets
+ - bugfix: PDA flashlights wont cause the cell to constantly drain faster and faster.
+ - bugfix: People who are irremediably bald can still grow a beard with barber aid.
+ - qol: Added slapcrafting to unloaded tech shells, click on them with ingredients
+ to quickly craft your shell.
+ - qol: gives empty fireaxe and mech removal crowbars cabinets directional helpers
+ - bugfix: fixed a PDA's messenger TGUI issue with handling of destroyed recipients.
+ - qol: '"prison" intercoms have been renamed to "receive-only" intercoms to make
+ it clearer they cannot transmit.'
+ - refactor: Rust Walkers, Ash Spirits, Flesh Stalkers, and The Maid in the Mirror
+ now use the basic mob framework. Please report any unusual behaviour.
+ - spellcheck: '"offical" has been officially corrected to "official" in several
+ official locations.'
+ - refactor: Sloths are now basic mobs, however their overall sluggish behavior shouldn't
+ have changed much- let us know if anything is broken.
+ - refactor: Refactored goats into basic mobs! Not much should have changed beyond
+ their endless desire to retaliate should you attack them, they're still just
+ as good as chomping away plant life as ever.
+ - qol: Miners can now tag monster spawners (necropolis tendrils, animal dens, demonic
+ portals, and netherworld links) by using their mining scanner on it, which updates
+ their GPS tag (and/or gives them one) to give it a numerical designation and
+ a short identifier for what it's spawning.
+ oranges:
+ - rscadd: Dogs now react to centrist grillers more realistically
+2023-10-09:
+ Nerev4r:
+ - image: The crew's knowledge of origami and papercrafting has been extended to
+ making paper masks. Find them in the loadout, or just make them!
+ SkyratBot:
+ - qol: Supermatter shards can now be fastened with right click too. Now, just don't
+ forget to use a wrench.
+ - bugfix: Slaughter/Laughter demon melee cooldowns have been fixed and now attack
+ at the regular player character attack speed
+ - balance: The chemical gun and PKA pressure mod traitor items are now purchasable
+ within 15 minutes of the round starting rather than 20/30.
+ - balance: All preset bundle kits, the cash briefcase, the makarov, the revolver,
+ the throwing weapon kit, c4, the detomatix cartridge, the large EMP bomb, gorillas,
+ advanced mimery tome, pie cannon, clown car, His Grace, and the origami kit
+ are now all purchasable at the start of a round.
+ - refactor: ice demons have been refactored into basic mbos. please report any bugs
+ - rscadd: ice demons now have a unique trophy
+ - bugfix: Spider types get properly checked again.
+ nikothedude:
+ - bugfix: Borers work now
+2023-10-10:
+ Fazzie:
+ - qol: NT's logo on Centcom's landing pad looks better
+ - qol: Centcom's Cargo and other rooms had their items rearanged to look marginally
+ better. Like you're every gonna see them!
+ - bugfix: The Thunderdome on Centcom now has up-to-date cooking machinery
+ GoldenAlpharex:
+ - qol: "You can now bind the Shift Layer Up/Down verbs to keybinds! Look for \"\
+ shiftlayerup\" and \"shiftlayerdown\" respectively in your Game Preferences\
+ \ >\_Keybindings to bind them, as they aren't bound by default (for now)!"
+ Hatterhat:
+ - qol: Internal health analyzer no longer displays both health and chem scans at
+ the same time; LMB for health, RMB for chems.
+ Melber:
+ - bugfix: Wearing bread (or roses, or other non-mask things) no longer prevents
+ you from TTS speaking.
+ Melbert:
+ - bugfix: Robotic bodyparts not attached to people are now properly affected by
+ EMPs.
+ - bugfix: Virtual Drink Glasses now look correct.
+ Motho:
+ - rscadd: Bitrunners can now have alternative job titles. FTU urges that these titles
+ are purely cosmetic and not representative of bitrunning ability.
+ - rscadd: Barbers, Botanists, Warehouse Techs, Coroners, Curators, Cyborgs, Geneticists,
+ Mimes, Nanotrasen Consultants, Roboticists, and Virologists enjoy new alternative
+ job titles.
+ - rscadd: Certain jobs now have Trainee/Newbie alternative job titles ordered at
+ the very bottom of the title selection dialog. If you or your character are
+ new to the job/department, set your title so your colleagues are aware!
+ - rscdel: Removed Engineering Trainee.
+ - qol: Alphabetized alt-titles excluding two key areas. The base title, and the
+ newbie title.
+ Nerev4r:
+ - image: One new long ring tail!
+ SkyratBot:
+ - bugfix: Warm donk-pockets should now have omnizine in them again.
+ - code_imp: COMSIG_GLOB_LIGHT_MECHANISM_COMPLETED is now COMSIG_GLOB_PUZZLE_COMPLETED
+ - qol: The autopsy tray (and surgery trays) can now hold the autopsy scanner
+ - bugfix: Metastation disposals will no longer infinitely loop garbage around the
+ station.
+ - bugfix: fixed gibbing from having too much blood not working in some cases
+ - refactor: Heavily refactored mirrors to be less ass cancer 1998 code. Player facing
+ changes are that mirrors now use a radial menu, women can get beards in magic
+ mirrors, made the magic mirror 'change sex' option Woke (it supports the 4 official
+ genders and physique as well)
+ - bugfix: Fixed Pride Mirror teleporting you into the space on the first use. Now
+ it waits until you officially cancel and say 'I am Done' so you can customize
+ yourself to your liking.
+ - bugfix: Cowardly mobs will consistently run away from you instead of getting tired
+ and just sort of standing there after an initial burst of movement.
+ - bugfix: Virtual domain gondola meat will no longer have a small chance to turn
+ you into a weaker gondola variant
+ - bugfix: Added extra checks to bitrunning domain cleanup so avatars are deleted
+ properly.
+ - rscadd: Quantum servers now look for a new machine called a byteforge to spawn
+ loot on- no longer on an invisible landmark. This should make the rooms rebuildable
+ after disasters.
+ - rscadd: '*Most* bitrunning machinery is now researchable and buildable via circuits
+ in the engineering protolathe.'
+ - refactor: Refactor gib code to be more robust.
+ - qol: Gibbing a mob will result in all items being dropped instead of getting deleted.
+ There are a few exceptions (like admin gib self) where this will not take place.
+ - code_imp: made an eensy teensie weensie change to some supermatter boilerplate
+ - code_imp: moved some global procs and vars related to reagents to its own dedicated
+ file. removed some unused procs and macros
+ - code_imp: heavy auto docs for a lot of procs
+ - refactor: adds reagent sanity and bound check code
+ - refactor: multiple reagents are more uniformly distributed when transferring them
+ between beakers or dropper & in every other reagent dependent operation
+ - code_imp: exploration drone adventures are now file-based and not database-based
+ - bugfix: Images are once more displayed as images in vv instead of as an appearance
+ TheSS13Melon:
+ - bugfix: Bitrunner now shows up with other cargo jobs on the suit sensors menu.
+ Wallem:
+ - rscadd: Nuclear Operatives now have ready access to ancient cowboy technology
+ in the form of the Outlaw Bundle. Now you too can roll into town on your horse.
+ honkpocket:
+ - bugfix: the OPFOR loadout 'Syndicate insurgent bundle' MODsuit is subtyped correctly
+ - bugfix: the OPFOR loadout 'Blood-Red MODsuit' is subtyped correctly
+ - balance: changes the mask in the 'Syndicate insurgent bundle' from SWAT to Syndicate
+ nikothedude:
+ - qol: SAD patients can now reject the treatment at the last step, preventing any
+ changes from being made
+ - balance: Synth tend wounds repeatable step now takes 2.5 seconds from 1
+ - balance: 2 new synth tend wounds upgrades available to research
+ - bugfix: Synth tend wounds now gives proper feedback
+ - bugfix: Flipped tables now properly block movement
+ - balance: Defibrilators now EMP synths and apply a temporary brain trauma for 90
+ seconds to them
+ - balance: Synth revival surgery time and steps vastly reduced/streamlined
+ ninjanomnom:
+ - rscadd: Pipes now have a colored visual display that shows their contents at a
+ glance.
+ softcerv:
+ - spellcheck: renames the purpura NIFSofts to libidine
+2023-10-11:
+ GoldenAlpharex:
+ - bugfix: Frogs no longer make obnoxious sounds anymore (again).
+ Melbert:
+ - bugfix: Miner's Salve, Sterilizine, and Space Cleaner now all properly affect
+ burn wounds
+ Paxilmaniac:
+ - bugfix: The quartermaster's Rengo rifle will no longer have floating bayonets
+ SkyratBot:
+ - bugfix: moved a garbage spawner on Tramstation that was causing random runtimes
+ due to sometimes spawning in space depending on which module got loaded
+ - bugfix: fixes a runtime in organ on_death()
+ - bugfix: fixed some faulty research connections in between heretic's blade and
+ rust paths.
+ - bugfix: Heretic mobs will not be summoned with AI enabled, and won't turn into
+ small animals instead of summoning a flesh stalker.
+ - bugfix: Fixed some issues in the security camera UI - pressing next or back will
+ now loop through the cameras
+ - bugfix: Fixed some style issues in the camera console where selected cams weren't
+ showing as selected
+ - bugfix: Camera console search works again
+ - bugfix: you can no longer polymorph belt into a holoparasite
+ - refactor: Revenants, the mob that's split between planes of Life and Death, have
+ been refactored into a basic mob. While this alone shouldn't touch behavior,
+ a lot of the backend code has been gutted and refactored to try and furnish
+ a better antagonist experience. This might mean that some weird stuff can come
+ up and around, report something if it's utterly broken.
+ - code_imp: In order to better facilitate some code, you do not ghost outside of
+ a revenant on death, you simply get transferred into the ectoplasm. You should
+ still be able to speak with your ghost friends on how hard you got wrecked or
+ if you'll be able to resurrect though.
+ - code_imp: The timing on revenant stuff such as being revealed, stunned, and inhibited
+ (by holy weapons) should be tweaked a bit to allow better management. This should
+ mean that getting unstunned and such should be a bit more precise now.
+ - qol: Revenant instructions are now relayed in a neat little examine block.
+ - bugfix: Wound promotion and demotion no longer removes gauze from the limb
+ - balance: The Changeling Space Suit has been replaced by a new ability which makes
+ you passively spaceproof without replacing your clothing.
+ - admin: Editing the atmos sensitivity variables on a basic mob during the game
+ will now actually do something.
+ - qol: Adds a base physical description proc to gameplay species, displays it on
+ magic mirrors. It will give a description of not the lore of the species but
+ in what way they differ from base species.
+ - bugfix: Fixes a bad subtype on magical mirrors.
+ - bugfix: Magical mirrors now give the user ADVANCEDTOOLUSER and LITERACY if they
+ lack either of them, so monkey wizards aren't softlocked.
+ - bugfix: Borg modules can no longer be sold by pirates.
+ - balance: Unholy water acts as a coagulant for cultists.
+ - bugfix: bitrunners will no longer be lumped in with assistants on the crew monitor
+ console's display
+ - bugfix: count station food verb now counts food only onstation
+ - bugfix: Antiglow now probably has negative glow power.
+ - refactor: Harvester constructs have been updated to the basic mob framework. This
+ should have very little impact on their behavior, but please report any issues.
+ - bugfix: Fixes a few runtimes with TTS and skips some code if TTS isn't enabled.
+ - sound: Add burning sound loop to bonfires and fireplaces
+ - code_imp: Improved fireplaces to only process when lit
+ - bugfix: using a magic mirror to change gender or skintone will now update your
+ icon properly to match your selection
+ - rscadd: Adds practice carbines to all firing ranges. They don't deal damage.
+ - balance: Reviver Implant now able to revive dead people.
+ nikothedude:
+ - bugfix: Chest/Heads can now be augmented in the augment menu
+ - bugfix: Augments menu color wheels now properly recognize existing color
+ - rscadd: Science can now print advanced tools
+ - rscadd: Robotics can now print advanced medical tools and health analyzers
+ - rscadd: Roboticists can now randomly get advanced engi/medical tools in the mail
+ - rscadd: Table flipping now throws everything on the table
+2023-10-12:
+ GoldenAlpharex:
+ - bugfix: Bitrunners and Coroners now have Akula outfits.
+ - bugfix: Shaft Miners now have the Cargo Akula outfit, rather than the default
+ one.
+ Hatterhat:
+ - balance: Blueshield's armor is now on par with a regular security vest's, with
+ comparatively improved fire/acid and barely improved bomb armor. (None of their
+ equipment covers their legs.)
+ - image: Blueshield's vest is now reskinnable, with three variants; the old slim
+ variant, a sec-vest variant, and a marine variant.
+ - image: Blueshield's earpiece is no longer a bright blue tumor on the side of their
+ head.
+ LT3:
+ - bugfix: After the untimely loss of too many novice HoPs, the Icebox "New IDs and
+ You" instructions have been moved from the icemoon wastes to the HoP's office,
+ ending this rite of passage
+ - bugfix: Added some missing firelocks in the pharmacy area. Icebox pharmacy now
+ has a shower
+ OrionTheFox:
+ - image: returns Lopland Security's blue ID trims, and updated a few modular ID
+ trims for DS2/NRI
+ SkyratBot:
+ - code_imp: removed round robin method of transferring reagents which would result
+ in some missing reagents after transferring.
+ - code_imp: added some more rounding for reagent operations.
+ - code_imp: cleaned up some plumbing & reaction chamber code
+ - code_imp: improves the performance of `update_total()` , `clear_reagents()` &
+ `del_reagent()` procs
+ - bugfix: plumbing setups will no longer output 0 or more than maximum available
+ volume of reagents.
+ - bugfix: removing, copying, transfering reagents is now done proportionally and
+ not equally again.
+ - refactor: examining individual reagents up close will display their volumes up
+ to 4 decimal places for accuracy.
+ - qol: droppers & beakers round the amount of reagents transferred before displaying
+ them to chat for easy readibility
+ - sound: the blood cult's rise to power is now accompanied by several new sound
+ effects
+ - rscadd: Adds a new lavaland ruin where you can find a unique egg.
+ - image: you can now change the style of lipstick to be higher or lower on the face
+ by alt-clicking the lipstick tube
+ - balance: Flesh Spiders heal automatically over time if they go a short time without
+ taking damage, instead of healing large chunks by clicking themselves and waiting
+ two seconds.
+ - qol: Spider egg clusters which only hatch into one kind of spider don't ask you
+ to select that one type from a radial menu with one option on it.
+ - qol: As a Flesh Spider, the game now tells you how you can heal yourself.
+ - bugfix: You cannot order with cargo budget if you don't have cargo access in the
+ Galactic Market
+ - bugfix: Private & Cargo orders no longer get mixed together in the same crate
+ if you order them interchangeably so no more embezzlement in the Galactic Market
+ - bugfix: Orders made with cargo budget come in a regular cargo crate thus allowing
+ you to open them without QM cargo budget card in the Galactic Market
+ - qol: Orders made in the Galactic Market will deduct money from your account/cargo
+ budget only after the order has been confirmed in the cargo request console
+ & after the shuttle arrives with your order. This way you drain the budget only
+ after your orders were successfully delivered and not before hand itself
+ - qol: You can now see what drones and gorillas are holding by examining them.
+ - admin: It's now easier to give handless mobs hands by applying the "dextrous"
+ element.
+ - balance: Spiders and Bears can now climb railings (you know if... they'd rather
+ do that than destroy them).
+ - refactor: venus human traps are basicmobs now
+ - balance: venus human traps have 100 health
+ - balance: venus human traps take damage out of range of kudzu, heal near kudzu,
+ are slightly slower, attack slower, and their damage output is slightly more
+ random
+ - balance: also venus human trap tangle ability now needs you to actually move backwards
+ to pull victims
+ - admin: Gondola supplypods are functional again.
+ - bugfix: Examining twice experiment handlers with an active fish-related experiment
+ now gives a comprehensible, correctly spaced list of scanned species rather
+ than something like "pufferfishguppyslimefishchasmchrab".
+ - bugfix: No more "line snapped" balloon messages everytime the fishing minigame
+ is over
+ - bugfix: Getting to the Master level of the fishing skill now correctly gives you
+ that slight helping hand to identify yet-to-be-caught fishes.
+ nikothedude:
+ - qol: Table flipping feedback is now in the form of balloon alerts
+ - rscadd: Robo can now print standard and alien surgical tools
+ - bugfix: Defibs now dont screw over organics if the user was robotic
diff --git a/icons/area/areas_station.dmi b/icons/area/areas_station.dmi
index b07ea38a1592b0..cbfe463efa5169 100644
Binary files a/icons/area/areas_station.dmi and b/icons/area/areas_station.dmi differ
diff --git a/icons/effects/96x96.dmi b/icons/effects/96x96.dmi
index 38d1d44a000d13..31f26c3e6e11ea 100644
Binary files a/icons/effects/96x96.dmi and b/icons/effects/96x96.dmi differ
diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi
index ae695c3227f3de..12e3ce9f7d5946 100644
Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ
diff --git a/icons/effects/bitrunning.dmi b/icons/effects/bitrunning.dmi
new file mode 100644
index 00000000000000..bfdc7c63436c2f
Binary files /dev/null and b/icons/effects/bitrunning.dmi differ
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index 29e59dc6c7148d..b01986a9522d0e 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/hud/fishing_hud.dmi b/icons/hud/fishing_hud.dmi
index b68acee09b76af..58c478d071064c 100644
Binary files a/icons/hud/fishing_hud.dmi and b/icons/hud/fishing_hud.dmi differ
diff --git a/icons/hud/radial.dmi b/icons/hud/radial.dmi
index 897cb3a872e292..42d5c451018aec 100644
Binary files a/icons/hud/radial.dmi and b/icons/hud/radial.dmi differ
diff --git a/icons/hud/radial_fishing.dmi b/icons/hud/radial_fishing.dmi
new file mode 100644
index 00000000000000..65fd55176b7c82
Binary files /dev/null and b/icons/hud/radial_fishing.dmi differ
diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi
index a1fc01434e4564..0fa8ec218500ed 100755
Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ
diff --git a/icons/mob/clothing/belt.dmi b/icons/mob/clothing/belt.dmi
index 546e3da0f86aa6..d1a1777e4e4d8f 100644
Binary files a/icons/mob/clothing/belt.dmi and b/icons/mob/clothing/belt.dmi differ
diff --git a/icons/mob/clothing/head/chaplain.dmi b/icons/mob/clothing/head/chaplain.dmi
index efb6ec3c9e9082..100b7ee922fb91 100644
Binary files a/icons/mob/clothing/head/chaplain.dmi and b/icons/mob/clothing/head/chaplain.dmi differ
diff --git a/icons/mob/clothing/head/plasmaman_head.dmi b/icons/mob/clothing/head/plasmaman_head.dmi
index 9846cf02200117..1917ae7bcf538b 100644
Binary files a/icons/mob/clothing/head/plasmaman_head.dmi and b/icons/mob/clothing/head/plasmaman_head.dmi differ
diff --git a/icons/mob/clothing/suits/chaplain.dmi b/icons/mob/clothing/suits/chaplain.dmi
index 4b6368fb291d0e..8806bf5f679b44 100644
Binary files a/icons/mob/clothing/suits/chaplain.dmi and b/icons/mob/clothing/suits/chaplain.dmi differ
diff --git a/icons/mob/clothing/suits/jacket.dmi b/icons/mob/clothing/suits/jacket.dmi
index cd924e847eb3e4..a6f25d91c59bfe 100644
Binary files a/icons/mob/clothing/suits/jacket.dmi and b/icons/mob/clothing/suits/jacket.dmi differ
diff --git a/icons/mob/clothing/under/cargo.dmi b/icons/mob/clothing/under/cargo.dmi
index 4bf30a67a2d7bd..180f0e4ec876fb 100644
Binary files a/icons/mob/clothing/under/cargo.dmi and b/icons/mob/clothing/under/cargo.dmi differ
diff --git a/icons/mob/clothing/under/plasmaman.dmi b/icons/mob/clothing/under/plasmaman.dmi
index 41cbfb4482b48b..fcc8f008cd7aa8 100644
Binary files a/icons/mob/clothing/under/plasmaman.dmi and b/icons/mob/clothing/under/plasmaman.dmi differ
diff --git a/icons/mob/effects/debuff_overlays.dmi b/icons/mob/effects/debuff_overlays.dmi
new file mode 100644
index 00000000000000..383ce22aabec7f
Binary files /dev/null and b/icons/mob/effects/debuff_overlays.dmi differ
diff --git a/icons/mob/huds/hud.dmi b/icons/mob/huds/hud.dmi
index 9a602eeb806abd..d71ba4b0940a67 100644
Binary files a/icons/mob/huds/hud.dmi and b/icons/mob/huds/hud.dmi differ
diff --git a/icons/mob/human/human_face.dmi b/icons/mob/human/human_face.dmi
index 6985cf07eee49a..6530b300aa676e 100644
Binary files a/icons/mob/human/human_face.dmi and b/icons/mob/human/human_face.dmi differ
diff --git a/icons/mob/inhands/clothing/hats_lefthand.dmi b/icons/mob/inhands/clothing/hats_lefthand.dmi
index 7111ee4d7480b0..191c85cf4825c8 100644
Binary files a/icons/mob/inhands/clothing/hats_lefthand.dmi and b/icons/mob/inhands/clothing/hats_lefthand.dmi differ
diff --git a/icons/mob/inhands/clothing/hats_righthand.dmi b/icons/mob/inhands/clothing/hats_righthand.dmi
index 96756fc44db9cd..8038e7474ee870 100644
Binary files a/icons/mob/inhands/clothing/hats_righthand.dmi and b/icons/mob/inhands/clothing/hats_righthand.dmi differ
diff --git a/icons/mob/inhands/clothing/suits_lefthand.dmi b/icons/mob/inhands/clothing/suits_lefthand.dmi
index 757fb8b8593c72..8b9fa5256a9327 100644
Binary files a/icons/mob/inhands/clothing/suits_lefthand.dmi and b/icons/mob/inhands/clothing/suits_lefthand.dmi differ
diff --git a/icons/mob/inhands/clothing/suits_righthand.dmi b/icons/mob/inhands/clothing/suits_righthand.dmi
index c749a2ed98a8e0..c88f4d224444fc 100644
Binary files a/icons/mob/inhands/clothing/suits_righthand.dmi and b/icons/mob/inhands/clothing/suits_righthand.dmi differ
diff --git a/icons/mob/inhands/equipment/medical_lefthand.dmi b/icons/mob/inhands/equipment/medical_lefthand.dmi
index 7ce3674c86aadf..feaed1690786ec 100644
Binary files a/icons/mob/inhands/equipment/medical_lefthand.dmi and b/icons/mob/inhands/equipment/medical_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/medical_righthand.dmi b/icons/mob/inhands/equipment/medical_righthand.dmi
index 6ceb5efe4d83cd..15ccf5c090e599 100644
Binary files a/icons/mob/inhands/equipment/medical_righthand.dmi and b/icons/mob/inhands/equipment/medical_righthand.dmi differ
diff --git a/icons/mob/simple/animal.dmi b/icons/mob/simple/animal.dmi
index 6e8b31b7877c40..87fe3a21506b1d 100644
Binary files a/icons/mob/simple/animal.dmi and b/icons/mob/simple/animal.dmi differ
diff --git a/icons/mob/simple/jungle/mook.dmi b/icons/mob/simple/jungle/mook.dmi
index c9265b22a0ad20..fbc38d29d99deb 100644
Binary files a/icons/mob/simple/jungle/mook.dmi and b/icons/mob/simple/jungle/mook.dmi differ
diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi
index 38b78cf468f1f1..f68e3db4a6cb91 100644
Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ
diff --git a/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi b/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi
index 2be68ef4c6696e..808fdc59d9bae9 100644
Binary files a/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi and b/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi differ
diff --git a/icons/obj/aquarium.dmi b/icons/obj/aquarium.dmi
index 3a27c83c906a83..19e2e68c4f8f33 100644
Binary files a/icons/obj/aquarium.dmi and b/icons/obj/aquarium.dmi differ
diff --git a/icons/obj/card.dmi b/icons/obj/card.dmi
index a5c4e8283010bd..a5e34e9cc27cbf 100644
Binary files a/icons/obj/card.dmi and b/icons/obj/card.dmi differ
diff --git a/icons/obj/clothing/glasses.dmi b/icons/obj/clothing/glasses.dmi
index 20f24dd22402f8..fd898d3105fd88 100644
Binary files a/icons/obj/clothing/glasses.dmi and b/icons/obj/clothing/glasses.dmi differ
diff --git a/icons/obj/clothing/head/chaplain.dmi b/icons/obj/clothing/head/chaplain.dmi
index d95436fdd2d7ac..ed6f6248b317c6 100644
Binary files a/icons/obj/clothing/head/chaplain.dmi and b/icons/obj/clothing/head/chaplain.dmi differ
diff --git a/icons/obj/clothing/head/plasmaman_hats.dmi b/icons/obj/clothing/head/plasmaman_hats.dmi
index adcf9129c45830..f593a08b88c300 100644
Binary files a/icons/obj/clothing/head/plasmaman_hats.dmi and b/icons/obj/clothing/head/plasmaman_hats.dmi differ
diff --git a/icons/obj/clothing/suits/chaplain.dmi b/icons/obj/clothing/suits/chaplain.dmi
index 64474a04d31821..730e47cd6fa949 100644
Binary files a/icons/obj/clothing/suits/chaplain.dmi and b/icons/obj/clothing/suits/chaplain.dmi differ
diff --git a/icons/obj/clothing/suits/jacket.dmi b/icons/obj/clothing/suits/jacket.dmi
index c63f262f104944..dc507017cd25d9 100644
Binary files a/icons/obj/clothing/suits/jacket.dmi and b/icons/obj/clothing/suits/jacket.dmi differ
diff --git a/icons/obj/clothing/under/cargo.dmi b/icons/obj/clothing/under/cargo.dmi
index fc04a897d5ea86..63e40538899f7d 100644
Binary files a/icons/obj/clothing/under/cargo.dmi and b/icons/obj/clothing/under/cargo.dmi differ
diff --git a/icons/obj/clothing/under/plasmaman.dmi b/icons/obj/clothing/under/plasmaman.dmi
index 4277c43d54b405..4d416d5b05f1f2 100644
Binary files a/icons/obj/clothing/under/plasmaman.dmi and b/icons/obj/clothing/under/plasmaman.dmi differ
diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi
index d89ee6e5d6408f..fe74b6c11c5c7d 100644
Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ
diff --git a/icons/obj/economy.dmi b/icons/obj/economy.dmi
index dc90265b6e9019..04abc41cae1753 100644
Binary files a/icons/obj/economy.dmi and b/icons/obj/economy.dmi differ
diff --git a/icons/obj/fishing.dmi b/icons/obj/fishing.dmi
index 39bcc853442005..f7ab9fc1ad9c2a 100644
Binary files a/icons/obj/fishing.dmi and b/icons/obj/fishing.dmi differ
diff --git a/icons/obj/food/egg.dmi b/icons/obj/food/egg.dmi
index c7661fca918f0c..58908d8247913d 100644
Binary files a/icons/obj/food/egg.dmi and b/icons/obj/food/egg.dmi differ
diff --git a/icons/obj/machines/bepis.dmi b/icons/obj/machines/bepis.dmi
deleted file mode 100644
index f348c2e1b05596..00000000000000
Binary files a/icons/obj/machines/bepis.dmi and /dev/null differ
diff --git a/icons/obj/machines/bitrunning.dmi b/icons/obj/machines/bitrunning.dmi
new file mode 100644
index 00000000000000..b3f8ad63a6c993
Binary files /dev/null and b/icons/obj/machines/bitrunning.dmi differ
diff --git a/icons/obj/machines/computer.dmi b/icons/obj/machines/computer.dmi
index 10974f97bac685..5ffa3445db6922 100644
Binary files a/icons/obj/machines/computer.dmi and b/icons/obj/machines/computer.dmi differ
diff --git a/icons/obj/machines/microwave.dmi b/icons/obj/machines/microwave.dmi
index 427a5d9daaee6c..7a72ba5d3dca3a 100644
Binary files a/icons/obj/machines/microwave.dmi and b/icons/obj/machines/microwave.dmi differ
diff --git a/icons/obj/medical/stack_medical.dmi b/icons/obj/medical/stack_medical.dmi
index d12949da595f1a..c4ec905786c699 100644
Binary files a/icons/obj/medical/stack_medical.dmi and b/icons/obj/medical/stack_medical.dmi differ
diff --git a/icons/obj/mining_zones/artefacts.dmi b/icons/obj/mining_zones/artefacts.dmi
index f3f7d00e4eef8f..d4c603834d21be 100644
Binary files a/icons/obj/mining_zones/artefacts.dmi and b/icons/obj/mining_zones/artefacts.dmi differ
diff --git a/icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi b/icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi
new file mode 100644
index 00000000000000..0262adcaeb2419
Binary files /dev/null and b/icons/obj/pipes_n_cables/!pipe_gas_overlays.dmi differ
diff --git a/icons/obj/pipes_n_cables/!pipes_bitmask.dmi b/icons/obj/pipes_n_cables/!pipes_bitmask.dmi
new file mode 100644
index 00000000000000..97643036fbe3bb
Binary files /dev/null and b/icons/obj/pipes_n_cables/!pipes_bitmask.dmi differ
diff --git a/icons/obj/pipes_n_cables/pipe_template_pieces.dmi b/icons/obj/pipes_n_cables/pipe_template_pieces.dmi
new file mode 100644
index 00000000000000..d0d2f7ff7bb809
Binary files /dev/null and b/icons/obj/pipes_n_cables/pipe_template_pieces.dmi differ
diff --git a/icons/obj/pipes_n_cables/pipes_bitmask.dmi b/icons/obj/pipes_n_cables/pipes_bitmask.dmi
deleted file mode 100644
index 7a382fb55c5e40..00000000000000
Binary files a/icons/obj/pipes_n_cables/pipes_bitmask.dmi and /dev/null differ
diff --git a/icons/obj/railings.dmi b/icons/obj/railings.dmi
index 28332e21324550..3dbbd7c8318e70 100644
Binary files a/icons/obj/railings.dmi and b/icons/obj/railings.dmi differ
diff --git a/icons/obj/service/kitchen.dmi b/icons/obj/service/kitchen.dmi
index cb47ddf35a2a6b..aeafe2591e9bdf 100644
Binary files a/icons/obj/service/kitchen.dmi and b/icons/obj/service/kitchen.dmi differ
diff --git a/icons/obj/storage/box.dmi b/icons/obj/storage/box.dmi
index 7ff4067c288600..d660e1b7bfe388 100644
Binary files a/icons/obj/storage/box.dmi and b/icons/obj/storage/box.dmi differ
diff --git a/icons/obj/storage/storage.dmi b/icons/obj/storage/storage.dmi
index cdaf8cae339f71..a55606fa3b33ba 100644
Binary files a/icons/obj/storage/storage.dmi and b/icons/obj/storage/storage.dmi differ
diff --git a/icons/obj/weapons/guns/energy.dmi b/icons/obj/weapons/guns/energy.dmi
index 97b75335b91a0e..e45c5ee4869ed7 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 fe3eb6ae895f63..7cab0cdfc25926 100644
Binary files a/icons/obj/weapons/guns/magic.dmi and b/icons/obj/weapons/guns/magic.dmi differ
diff --git a/icons/obj/weapons/guns/projectiles.dmi b/icons/obj/weapons/guns/projectiles.dmi
index e1c70c4f5ade42..3c2d3ff452cd0c 100644
Binary files a/icons/obj/weapons/guns/projectiles.dmi and b/icons/obj/weapons/guns/projectiles.dmi differ
diff --git a/icons/turf/floors.dmi b/icons/turf/floors.dmi
index 8a1575fcec6407..6ddc178b98cdb3 100644
Binary files a/icons/turf/floors.dmi and b/icons/turf/floors.dmi differ
diff --git a/modular_skyrat/master_files/code/datums/components/grillable.dm b/modular_skyrat/master_files/code/datums/components/grillable.dm
new file mode 100644
index 00000000000000..42146559af1ff5
--- /dev/null
+++ b/modular_skyrat/master_files/code/datums/components/grillable.dm
@@ -0,0 +1,9 @@
+/datum/component/grillable
+ /// What type of pollutant we spread around as we are grilleed, can be none
+ var/pollutant_type
+
+/datum/component/grillable/Initialize(cook_result, required_cook_time, positive_result, use_large_steam_sprite, list/added_reagents, pollutant_type)
+ . = ..()
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+ src.pollutant_type = pollutant_type
diff --git a/modular_skyrat/master_files/code/datums/id_trim/jobs.dm b/modular_skyrat/master_files/code/datums/id_trim/jobs.dm
index b26e823b0e0436..f0bcde700876ed 100644
--- a/modular_skyrat/master_files/code/datums/id_trim/jobs.dm
+++ b/modular_skyrat/master_files/code/datums/id_trim/jobs.dm
@@ -1,8 +1,8 @@
// MODULAR ID TRIM ACCESS OVERRIDES GO HERE!!
+//(Most) of Security has inverted IDs, with custom blue-on-black icons. This is to distinguish them from their head, who has a white-on-blue icon
/datum/id_trim/job/head_of_security
- trim_icon = 'modular_skyrat/master_files/icons/obj/card.dmi'
- subdepartment_color = COLOR_ASSEMBLY_BLACK // This actually is the shade of grey formerly used by the static icons! Didn't have to add anything extra! Just thought that was neat.
+ subdepartment_color = COLOR_ASSEMBLY_BLACK
/datum/id_trim/job/warden
trim_icon = 'modular_skyrat/master_files/icons/obj/card.dmi'
@@ -53,10 +53,10 @@
department_color = COLOR_COMMAND_BLUE
subdepartment_color = COLOR_CENTCOM_BLUE // Not the other way around. I think.
sechud_icon_state = SECHUD_BLUESHIELD
- extra_access = list(ACCESS_SECURITY, ACCESS_BRIG, ACCESS_COURT, ACCESS_CARGO, ACCESS_GATEWAY) // Someone needs to come back and order these alphabetically, this is a nightmare
+ extra_access = list(ACCESS_BRIG, ACCESS_CARGO, ACCESS_COURT, ACCESS_GATEWAY, ACCESS_SECURITY)
minimal_access = list(
- ACCESS_DETECTIVE, ACCESS_BRIG_ENTRANCE, ACCESS_MEDICAL, ACCESS_CONSTRUCTION, ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS, ACCESS_RESEARCH,
- ACCESS_RC_ANNOUNCE, ACCESS_COMMAND, ACCESS_WEAPONS,
+ ACCESS_BRIG_ENTRANCE, ACCESS_COMMAND, ACCESS_CONSTRUCTION, ACCESS_DETECTIVE, ACCESS_ENGINEERING,
+ ACCESS_MAINT_TUNNELS, ACCESS_MEDICAL, ACCESS_RC_ANNOUNCE, ACCESS_RESEARCH, ACCESS_WEAPONS,
)
minimal_wildcard_access = list(ACCESS_CAPTAIN)
template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
@@ -68,13 +68,15 @@
subdepartment_color = COLOR_GREEN
sechud_icon_state = SECHUD_NT_CONSULTANT
extra_access = list()
- minimal_access = list(ACCESS_SECURITY, ACCESS_BRIG_ENTRANCE, ACCESS_COURT, ACCESS_WEAPONS,
- ACCESS_MEDICAL, ACCESS_PSYCHOLOGY, ACCESS_ENGINEERING, ACCESS_CHANGE_IDS, ACCESS_AI_UPLOAD, ACCESS_EVA, ACCESS_COMMAND,
- ACCESS_ALL_PERSONAL_LOCKERS, ACCESS_MAINT_TUNNELS, ACCESS_BAR, ACCESS_JANITOR, ACCESS_CONSTRUCTION, ACCESS_MORGUE,
- ACCESS_CREMATORIUM, ACCESS_KITCHEN, ACCESS_HYDROPONICS, ACCESS_LAWYER,
- ACCESS_MECH_ENGINE, ACCESS_MECH_SCIENCE, ACCESS_MECH_SECURITY, ACCESS_MECH_MEDICAL,
- ACCESS_THEATRE, ACCESS_CHAPEL_OFFICE, ACCESS_LIBRARY, ACCESS_RESEARCH, ACCESS_VAULT, ACCESS_MINING_STATION,
- ACCESS_HOP, ACCESS_RC_ANNOUNCE, ACCESS_KEYCARD_AUTH, ACCESS_GATEWAY, ACCESS_MINERAL_STOREROOM, ACCESS_AUX_BASE, ACCESS_TELEPORTER, ACCESS_CENT_GENERAL)
+ minimal_access = list(
+ ACCESS_AI_UPLOAD, ACCESS_ALL_PERSONAL_LOCKERS, ACCESS_AUX_BASE, ACCESS_BAR, ACCESS_BRIG_ENTRANCE,
+ ACCESS_CENT_GENERAL, ACCESS_CHANGE_IDS, ACCESS_CHAPEL_OFFICE, ACCESS_COMMAND, ACCESS_CONSTRUCTION,
+ ACCESS_CREMATORIUM, ACCESS_COURT, ACCESS_ENGINEERING, ACCESS_EVA, ACCESS_GATEWAY, ACCESS_HOP, ACCESS_HYDROPONICS,
+ ACCESS_JANITOR, ACCESS_KEYCARD_AUTH, ACCESS_KITCHEN, ACCESS_LAWYER, ACCESS_LIBRARY, ACCESS_MAINT_TUNNELS,
+ ACCESS_MEDICAL, ACCESS_MECH_ENGINE, ACCESS_MECH_MEDICAL, ACCESS_MECH_SCIENCE, ACCESS_MECH_SECURITY,
+ ACCESS_MINING_STATION, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_PSYCHOLOGY, ACCESS_RC_ANNOUNCE,
+ ACCESS_RESEARCH, ACCESS_SECURITY, ACCESS_TELEPORTER, ACCESS_THEATRE, ACCESS_VAULT, ACCESS_WEAPONS
+ )
minimal_wildcard_access = list(ACCESS_CAPTAIN, ACCESS_CENT_GENERAL)
template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
@@ -86,9 +88,11 @@
subdepartment_color = COLOR_ASSEMBLY_BLACK
sechud_icon_state = SECHUD_CORRECTIONS_OFFICER
extra_access = list()
- minimal_access = list(ACCESS_SECURITY, ACCESS_BRIG_ENTRANCE, ACCESS_BRIG, ACCESS_COURT,
- ACCESS_MAINT_TUNNELS, ACCESS_WEAPONS)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOS, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_BRIG, ACCESS_BRIG_ENTRANCE, ACCESS_COURT,
+ ACCESS_MAINT_TUNNELS, ACCESS_SECURITY, ACCESS_WEAPONS
+ )
+ template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS, ACCESS_HOS)
job = /datum/job/corrections_officer
/datum/id_trim/job/barber
@@ -99,6 +103,6 @@
subdepartment_color = COLOR_SERVICE_LIME
sechud_icon_state = SECHUD_BARBER
extra_access = list()
- minimal_access = list(ACCESS_THEATRE, ACCESS_MAINT_TUNNELS, ACCESS_BARBER, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_access = list(ACCESS_BARBER, ACCESS_MAINT_TUNNELS, ACCESS_SERVICE, ACCESS_THEATRE)
+ template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS, ACCESS_HOP)
job = /datum/job/barber
diff --git a/modular_skyrat/master_files/code/datums/storage/subtypes/pockets.dm b/modular_skyrat/master_files/code/datums/storage/subtypes/pockets.dm
index 491eb74443a5f3..75a2f01aa9927f 100644
--- a/modular_skyrat/master_files/code/datums/storage/subtypes/pockets.dm
+++ b/modular_skyrat/master_files/code/datums/storage/subtypes/pockets.dm
@@ -4,18 +4,8 @@
. = ..()
add_holdable(list(
- // Adds pistol magazines from guncargo companies in their respective order
- /obj/item/ammo_box/magazine/multi_sprite/pdh,
- /obj/item/ammo_box/magazine/multi_sprite/ladon,
- /obj/item/ammo_box/magazine/multi_sprite/firefly,
- /obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper,
- /obj/item/ammo_box/magazine/multi_sprite/mk58,
- /obj/item/ammo_box/magazine/m45,
- /obj/item/ammo_box/magazine/pepperball, // boot pepper
- /obj/item/ammo_box/magazine/multi_sprite/g17,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_snub,
- /obj/item/ammo_box/magazine/multi_sprite/makarov,
+ /obj/item/ammo_box/magazine/c35sol_pistol,
+ /obj/item/ammo_box/magazine/c585trappiste_pistol,
/obj/item/ammo_box/magazine/m9mm_aps,
/obj/item/ammo_box/magazine/toy/pistol,
))
diff --git a/modular_skyrat/master_files/code/datums/traits/good.dm b/modular_skyrat/master_files/code/datums/traits/good.dm
index 22987f7705d197..08f265145a9ea5 100644
--- a/modular_skyrat/master_files/code/datums/traits/good.dm
+++ b/modular_skyrat/master_files/code/datums/traits/good.dm
@@ -68,6 +68,16 @@
right_arm.unarmed_miss_sound = initial(right_arm.unarmed_miss_sound)
right_arm.unarmed_sharpness = initial(right_arm.unarmed_sharpness)
+/datum/quirk/water_breathing
+ name = "Water breathing"
+ desc = "You are able to breathe underwater!"
+ value = 2
+ mob_trait = TRAIT_WATER_BREATHING
+ gain_text = span_notice("You become acutely aware of the moisture in your lungs and in the air. It feels nice.")
+ lose_text = span_danger("You suddenly realize the moisture in your lungs feels really weird, and you almost choke on it!")
+ medical_record_text = "Patient possesses biology compatible with aquatic respiration."
+ icon = FA_ICON_FISH
+
// AdditionalEmotes *turf quirks
/datum/quirk/water_aspect
name = "Water aspect (Emotes)"
diff --git a/modular_skyrat/master_files/code/datums/traits/neutral.dm b/modular_skyrat/master_files/code/datums/traits/neutral.dm
index 6ca79ee52b8593..0ab2f6e90b7cb2 100644
--- a/modular_skyrat/master_files/code/datums/traits/neutral.dm
+++ b/modular_skyrat/master_files/code/datums/traits/neutral.dm
@@ -1,5 +1,16 @@
#define TRAIT_HYDRA_HEADS "hydrahead" // We still dont have a centralised trait file
+GLOBAL_VAR_INIT(DNR_trait_overlay, generate_DNR_trait_overlay())
+
+/// Instantiates GLOB.DNR_trait_overlay by creating a new mutable_appearance instance of the overlay.
+/proc/generate_DNR_trait_overlay()
+ RETURN_TYPE(/mutable_appearance)
+
+ var/mutable_appearance/DNR_trait_overlay = mutable_appearance('modular_skyrat/modules/indicators/icons/DNR_trait_overlay.dmi', "DNR", FLY_LAYER)
+ DNR_trait_overlay.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
+ return DNR_trait_overlay
+
+
// SKYRAT NEUTRAL TRAITS
/datum/quirk/excitable
name = "Excitable!"
@@ -31,6 +42,47 @@
mob_trait = TRAIT_DNR
icon = FA_ICON_SKULL_CROSSBONES
+/datum/quirk/dnr/add(client/client_source)
+ . = ..()
+
+ quirk_holder.update_dnr_hud()
+
+/datum/quirk/dnr/remove()
+ var/mob/living/old_holder = quirk_holder
+
+ . = ..()
+
+ old_holder.update_dnr_hud()
+
+/mob/living/prepare_data_huds()
+ . = ..()
+
+ update_dnr_hud()
+
+/// Adds the DNR HUD element if src has TRAIT_DNR. Removes it otherwise.
+/mob/living/proc/update_dnr_hud()
+ var/image/dnr_holder = hud_list?[DNR_HUD]
+ if(isnull(dnr_holder))
+ return
+
+ var/icon/temporary_icon = icon(icon, icon_state, dir)
+ dnr_holder.pixel_y = temporary_icon.Height() - world.icon_size
+
+ if(HAS_TRAIT(src, TRAIT_DNR))
+ set_hud_image_active(DNR_HUD)
+ dnr_holder.icon_state = "hud_dnr"
+ else
+ set_hud_image_inactive(DNR_HUD)
+
+/mob/living/carbon/human/examine(mob/user)
+ . = ..()
+
+ if(stat != DEAD && HAS_TRAIT(src, TRAIT_DNR) && (HAS_TRAIT(user, TRAIT_SECURITY_HUD) || HAS_TRAIT(user, TRAIT_MEDICAL_HUD)))
+ . += "\n[span_boldwarning("This individual is unable to be revived, and may be permanently dead if allowed to die!")]"
+
+/datum/atom_hud/data/human/dnr
+ hud_icons = list(DNR_HUD)
+
// uncontrollable laughter
/datum/quirk/item_quirk/joker
name = "Pseudobulbar Affect"
diff --git a/modular_skyrat/master_files/code/game/objects/items/cards_ids.dm b/modular_skyrat/master_files/code/game/objects/items/cards_ids.dm
index 64f40cb5f86ba2..9b43fccfc3ab28 100644
--- a/modular_skyrat/master_files/code/game/objects/items/cards_ids.dm
+++ b/modular_skyrat/master_files/code/game/objects/items/cards_ids.dm
@@ -3,30 +3,13 @@
name = "generic silver identification card"
icon = 'modular_skyrat/master_files/icons/obj/card.dmi'
icon_state = "card_silvergen"
- assigned_icon_state = "assigned_silver"
+ assigned_icon_state = null
/obj/item/card/id/advanced/gold/generic
name = "generic gold identification card"
icon = 'modular_skyrat/master_files/icons/obj/card.dmi'
icon_state = "card_goldgen"
- assigned_icon_state = "assigned_gold"
-
-// COLOURABLE
-/obj/item/card/id/advanced/colourable
- name = "colourable identification card"
- desc = "A failed prototype for customizable ID cards, it looks.. strange." // Read: I'm too lazy to implement this properly
- icon_state = "id_card"
- assigned_icon_state = null // Built into the sprite itself.
- greyscale_config = /datum/greyscale_config/id_card
- greyscale_colors = "#FF0000#00FF00#0000FF"
-
-/obj/item/card/id/advanced/colourable/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/gags_recolorable)
-
-/obj/item/card/id/advanced/colourable/examine(mob/user)
- . = ..()
- . += span_info("You could change its colours with a spray can!")
+ assigned_icon_state = null
// DS2
/obj/item/card/id/advanced/prisoner/ds2
diff --git a/modular_skyrat/master_files/code/game/objects/structures/mirror.dm b/modular_skyrat/master_files/code/game/objects/structures/mirror.dm
new file mode 100644
index 00000000000000..ac17e8afd414b7
--- /dev/null
+++ b/modular_skyrat/master_files/code/game/objects/structures/mirror.dm
@@ -0,0 +1,9 @@
+// Magic Mirror Character Application
+/obj/structure/mirror/magic/attack_hand(mob/living/carbon/human/user)
+ var/user_input = tgui_alert(user, "Would you like to apply your loaded character?","Confirm", list("Yes!", "No"))
+
+ if(user_input == "Yes!")
+ user?.client?.prefs?.safe_transfer_prefs_to(user)
+ return TRUE
+
+ return ..()
diff --git a/modular_skyrat/master_files/code/modules/antagonists/traitor/objectives/kill_pet.dm b/modular_skyrat/master_files/code/modules/antagonists/traitor/objectives/kill_pet.dm
index 68865d44b21956..b3486f40ba72e3 100644
--- a/modular_skyrat/master_files/code/modules/antagonists/traitor/objectives/kill_pet.dm
+++ b/modular_skyrat/master_files/code/modules/antagonists/traitor/objectives/kill_pet.dm
@@ -9,9 +9,9 @@
JOB_CHIEF_MEDICAL_OFFICER = /mob/living/simple_animal/pet/cat/runtime,
JOB_CHIEF_ENGINEER = /mob/living/simple_animal/parrot/poly,
JOB_QUARTERMASTER = list(
- /mob/living/simple_animal/sloth/citrus,
- /mob/living/simple_animal/sloth/paperwork,
- /mob/living/simple_animal/hostile/gorilla/cargo_domestic,
+ /mob/living/basic/sloth/citrus,
+ /mob/living/basic/sloth/paperwork,
+ /mob/living/basic/gorilla/cargorilla,
),
// Non-heads like the warden, these are automatically medium-risk at minimum
// They are also the only two modular additions so far
diff --git a/modular_skyrat/master_files/code/modules/bitrunning/orders/tech.dm b/modular_skyrat/master_files/code/modules/bitrunning/orders/tech.dm
new file mode 100644
index 00000000000000..6c9a0626517b47
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/bitrunning/orders/tech.dm
@@ -0,0 +1,3 @@
+/datum/orderable_item/bepis/flashdark
+ item_path = /obj/item/flashlight/flashdark
+ cost_per_order = 750
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/middleware/limbs_and_markings.dm b/modular_skyrat/master_files/code/modules/client/preferences/middleware/limbs_and_markings.dm
index 09a174b1ff319c..1099d7a3e79cd9 100644
--- a/modular_skyrat/master_files/code/modules/client/preferences/middleware/limbs_and_markings.dm
+++ b/modular_skyrat/master_files/code/modules/client/preferences/middleware/limbs_and_markings.dm
@@ -41,8 +41,8 @@
"r_arm" = TRUE,
"l_leg" = TRUE,
"r_leg" = TRUE,
- "chest" = FALSE, // TODO: figure out why head/chest augs dont render, needed for IPC head on non IPC body
- "head" = FALSE,
+ "chest" = TRUE,
+ "head" = TRUE,
"l_hand" = FALSE,
"r_hand" = FALSE,
)
@@ -73,9 +73,7 @@
if(!visuals_only)
return
- // If you ever add chest and head augments, please add the body zones to this list.
- // Removing them for now for optimization purposes.
- for(var/body_zone in list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG))
+ for(var/body_zone in list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG, BODY_ZONE_CHEST, BODY_ZONE_HEAD))
if(body_zone in visited_body_zones)
continue
@@ -164,7 +162,7 @@
usr,
"Select new color",
null,
- preferences.body_markings[limb_slot][marking_entry_name],
+ preferences.body_markings[limb_slot][marking_entry_name][1],
) as color | null
if(!new_color)
return TRUE
diff --git a/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm b/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm
index f7ffc6fddd6280..be228d89f102d5 100644
--- a/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm
+++ b/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm
@@ -213,7 +213,6 @@
var/static/list/undershirt_to_bra = list(
"Bra, Sports" = "Bra, Sports",
"Sports Bra (Alt)" = "Sports Bra (Alt)",
- "LIZARED Top" = "LIZARED Top",
"Bra" = "Bra",
"Bra - Alt" = "Bra - Alt",
"Bra - Thin" = "Bra - Thin",
diff --git a/modular_skyrat/master_files/code/modules/clothing/under/color.dm b/modular_skyrat/master_files/code/modules/clothing/under/color.dm
index 07327d062b82cb..094dddaabd3585 100644
--- a/modular_skyrat/master_files/code/modules/clothing/under/color.dm
+++ b/modular_skyrat/master_files/code/modules/clothing/under/color.dm
@@ -1,6 +1,9 @@
/obj/item/clothing/under/color
greyscale_config_worn_monkey = /datum/greyscale_config/jumpsuit/worn/monkey
+/obj/item/clothing/under/color/rainbow
+ worn_icon_digi = 'modular_skyrat/master_files/icons/mob/clothing/under/color_digi.dmi'
+
/**
* Random jumpsuit is the preferred style of the wearer if loaded as an outfit.
* This is cleaner than creating a ../skirt variant as skirts are precached into SSwardrobe
diff --git a/modular_skyrat/master_files/code/modules/clothing/under/jobs/centcom.dm b/modular_skyrat/master_files/code/modules/clothing/under/jobs/centcom.dm
index 9c48b5a2736dc8..05aa24276d8a5c 100644
--- a/modular_skyrat/master_files/code/modules/clothing/under/jobs/centcom.dm
+++ b/modular_skyrat/master_files/code/modules/clothing/under/jobs/centcom.dm
@@ -13,12 +13,6 @@
*/
//Check modular_skyrat\modules\nanotrasen_naval_command\code\clothing.dm for more of these! (Or, currently, ALL of these.)
-/*
-* ARMADYNE
-*/
-//Check modular_skyrat\modules\sec_haul\code\peacekeeper\armadyne_clothing.dm for these (ORION TODO: debate moving them all into this one file - uniforms, at least)
-//(TODO applies to NT as well)
-
/*
* LOPLAND
*/
diff --git a/modular_skyrat/master_files/code/modules/jobs/job_types/_job_attire.dm b/modular_skyrat/master_files/code/modules/jobs/job_types/_job_attire.dm
index a5de93b45dc3b5..05bc14211e3a42 100644
--- a/modular_skyrat/master_files/code/modules/jobs/job_types/_job_attire.dm
+++ b/modular_skyrat/master_files/code/modules/jobs/job_types/_job_attire.dm
@@ -8,6 +8,9 @@
/datum/job/bartender
akula_outfit = /datum/outfit/akula
+/datum/job/bitrunner
+ akula_outfit = /datum/outfit/akula/cargo_technician
+
/datum/job/botanist
akula_outfit = /datum/outfit/akula
@@ -84,7 +87,7 @@
akula_outfit = /datum/outfit/akula/security_officer
/datum/job/shaft_miner
- akula_outfit = /datum/outfit/akula
+ akula_outfit = /datum/outfit/akula/cargo_technician
/datum/job/station_engineer
akula_outfit = /datum/outfit/akula/station_engineer
@@ -139,6 +142,9 @@
/datum/job/barber
akula_outfit = /datum/outfit/akula
+/datum/job/coroner
+ akula_outfit = /datum/outfit/akula/doctor
+
/datum/job/corrections_officer
akula_outfit = /datum/outfit/akula/security_officer
diff --git a/modular_skyrat/master_files/code/modules/jobs/job_types/roboticist.dm b/modular_skyrat/master_files/code/modules/jobs/job_types/roboticist.dm
index d90c994c6ad40d..35e4f206de12cd 100644
--- a/modular_skyrat/master_files/code/modules/jobs/job_types/roboticist.dm
+++ b/modular_skyrat/master_files/code/modules/jobs/job_types/roboticist.dm
@@ -3,3 +3,24 @@
satchel = /obj/item/storage/backpack/satchel/science/robo
duffelbag = /obj/item/storage/backpack/duffelbag/science/robo
messenger = /obj/item/storage/backpack/messenger/science/robo
+
+/datum/job/roboticist
+ description = "Build cyborgs, mechs, AIs, and maintain them all. Create MODsuits for those that wish. Try to remind medical that you're \
+ actually a lot better at treating synthetic crew members than them."
+
+/datum/job/roboticist/New()
+ . = ..()
+
+ mail_goodies += list(
+ /obj/item/healthanalyzer/advanced = 15,
+ /obj/item/screwdriver/power/science = 6,
+ /obj/item/crowbar/power/science = 6,
+ /obj/item/weldingtool/experimental = 2, // a lot rarer since its relatively powerful
+ /obj/item/scalpel/advanced = 6,
+ /obj/item/retractor/advanced = 6,
+ /obj/item/cautery/advanced = 6,
+ /obj/item/storage/pill_bottle/liquid_solder = 6,
+ /obj/item/storage/pill_bottle/system_cleaner = 6,
+ /obj/item/storage/pill_bottle/nanite_slurry = 6,
+ /obj/item/reagent_containers/spray/hercuri/chilled = 8,
+ )
diff --git a/modular_skyrat/master_files/code/modules/mob/living/basic/icemoon/ice_demon.dm b/modular_skyrat/master_files/code/modules/mob/living/basic/icemoon/ice_demon.dm
new file mode 100644
index 00000000000000..0c8f44b3118159
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/mob/living/basic/icemoon/ice_demon.dm
@@ -0,0 +1,3 @@
+// crusher loot /obj/item/crusher_trophy/ice_demon_cube -> /obj/item/crusher_trophy/demon_core
+/mob/living/basic/mining/ice_demon
+ crusher_loot = /obj/item/crusher_trophy/demon_core
diff --git a/modular_skyrat/master_files/code/modules/mob/living/carbon/death.dm b/modular_skyrat/master_files/code/modules/mob/living/carbon/death.dm
index b2d6c97565213a..e7c3348e585964 100644
--- a/modular_skyrat/master_files/code/modules/mob/living/carbon/death.dm
+++ b/modular_skyrat/master_files/code/modules/mob/living/carbon/death.dm
@@ -1,18 +1,11 @@
// By temporarily removing the unspillable organs before calling the parent proc we can avoid Skyrat edits and make this less likely to break in the future
-/mob/living/carbon/spill_organs(no_brain, no_organs, no_bodyparts, gibbed = FALSE)
+/mob/living/carbon/spill_organs(drop_bitflags)
var/list/held_organs = list()
for(var/obj/item/organ/organ as anything in organs)
if(!organ.drop_when_organ_spilling)
held_organs.Add(organ)
organs.Remove(organ)
- // Organs always get spilled when the mob is gibbed
- if(gibbed)
- for(var/deleting_organ in held_organs)
- qdel(deleting_organ)
-
- return ..()
-
. = ..()
// put the unspillable organs back
diff --git a/modular_skyrat/master_files/code/modules/mob/living/carbon/human/death.dm b/modular_skyrat/master_files/code/modules/mob/living/carbon/human/death.dm
index b40e30ca8f86a5..7889b4f391ceca 100644
--- a/modular_skyrat/master_files/code/modules/mob/living/carbon/human/death.dm
+++ b/modular_skyrat/master_files/code/modules/mob/living/carbon/human/death.dm
@@ -1,8 +1,8 @@
// Pocket contents fly out when gibbed
-/mob/living/carbon/human/gib(no_brain, no_organs, no_bodyparts, safe_gib = FALSE)
- if(safe_gib) // we are just going to drop everything regardless
+/mob/living/carbon/human/gib(drop_bitflags=NONE)
+ if(drop_bitflags & DROP_ITEMS) // we are just going to drop everything regardless
return ..()
- if(no_bodyparts) // don't drop any items when the mob is being reduced to a paste
+ if(!(drop_bitflags & DROP_BODYPARTS)) // don't drop any items when the mob is being reduced to a paste
return ..()
var/obj/item/left_pocket = l_store
diff --git a/modular_skyrat/master_files/code/modules/mob/living/carbon/human/species_type/podpeople.dm b/modular_skyrat/master_files/code/modules/mob/living/carbon/human/species_type/podpeople.dm
new file mode 100644
index 00000000000000..012665829fd23d
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/mob/living/carbon/human/species_type/podpeople.dm
@@ -0,0 +1,6 @@
+// Character creation podpeople
+/datum/species/pod/get_species_description()
+ return "Plant lore!"
+
+/datum/species/pod/get_species_lore()
+ return list("You're a plant!")
diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
index b2b9eaeecba58c..30df82a7f1158e 100644
--- a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
+++ b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
@@ -32,3 +32,14 @@
/datum/species/proc/apply_supplementary_body_changes(mob/living/carbon/human/target, datum/preferences/preferences, visuals_only = FALSE)
return
+
+/datum/species/create_pref_traits_perks()
+ . = ..()
+
+ if (TRAIT_WATER_BREATHING in inherent_traits)
+ . += list(list(
+ SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK,
+ SPECIES_PERK_ICON = FA_ICON_FISH,
+ SPECIES_PERK_NAME = "Waterbreathing",
+ SPECIES_PERK_DESC = "[plural_form] can breathe in water, making pools a lot safer to be in!",
+ ))
diff --git a/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm b/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm
index e263c4062c7815..b09de049865002 100644
--- a/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm
+++ b/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm
@@ -206,12 +206,14 @@
. = ..()
design_ids += list(
"borg_upgrade_snacks",
+ "mini_soulcatcher",
)
/datum/techweb_node/neural_programming/New()
. = ..()
design_ids += list(
"soulcatcher_device",
+ "rsd_interface",
)
/datum/techweb_node/cyborg_upg_util/New()
diff --git a/modular_skyrat/master_files/icons/donator/mob/clothing/hands.dmi b/modular_skyrat/master_files/icons/donator/mob/clothing/hands.dmi
index 44e58131a33a19..93cadcbdbdbaf8 100644
Binary files a/modular_skyrat/master_files/icons/donator/mob/clothing/hands.dmi and b/modular_skyrat/master_files/icons/donator/mob/clothing/hands.dmi differ
diff --git a/modular_skyrat/master_files/icons/donator/obj/clothing/gloves.dmi b/modular_skyrat/master_files/icons/donator/obj/clothing/gloves.dmi
index 9c82af8e8b3fa1..62bfc6d5433351 100644
Binary files a/modular_skyrat/master_files/icons/donator/obj/clothing/gloves.dmi and b/modular_skyrat/master_files/icons/donator/obj/clothing/gloves.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi b/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi
index 0e3d125038e8e7..eac81c9cca9f41 100644
Binary files a/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi and b/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/ears.dmi b/modular_skyrat/master_files/icons/mob/clothing/ears.dmi
index eb135d3fdc3b14..0626710027cb3b 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/ears.dmi and b/modular_skyrat/master_files/icons/mob/clothing/ears.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/mask.dmi b/modular_skyrat/master_files/icons/mob/clothing/mask.dmi
index db9c50ab844015..07bef00e9c4a28 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/mask.dmi and b/modular_skyrat/master_files/icons/mob/clothing/mask.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi b/modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi
index 199a17cdbb20a0..24004a9b2604bb 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi and b/modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi b/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi
index 9a8042cf0a0f6e..8d18e50d6233e3 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi and b/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi b/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi
index c4ecb9e4f81203..faceea6def3269 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi and b/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/under/color_digi.dmi b/modular_skyrat/master_files/icons/mob/clothing/under/color_digi.dmi
index 1d7d6c0e9330c9..1366a6ba1ad53e 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/under/color_digi.dmi and b/modular_skyrat/master_files/icons/mob/clothing/under/color_digi.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi b/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi
index 3d9209905cb3c3..ff6f4218c27e25 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi and b/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/huds/hud.dmi b/modular_skyrat/master_files/icons/mob/huds/hud.dmi
index 276bdf638ffe15..235a45ecd4918e 100644
Binary files a/modular_skyrat/master_files/icons/mob/huds/hud.dmi and b/modular_skyrat/master_files/icons/mob/huds/hud.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi b/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi
index fdfc3fee1c8bdd..681457fe596f1c 100644
Binary files a/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi and b/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/sprite_accessory/tails_big.dmi b/modular_skyrat/master_files/icons/mob/sprite_accessory/tails_big.dmi
index f0c56e67183968..e77a1d1d77d126 100644
Binary files a/modular_skyrat/master_files/icons/mob/sprite_accessory/tails_big.dmi and b/modular_skyrat/master_files/icons/mob/sprite_accessory/tails_big.dmi differ
diff --git a/modular_skyrat/master_files/icons/obj/card.dmi b/modular_skyrat/master_files/icons/obj/card.dmi
index cc5b4eb3a0a571..9130183d19d8f4 100644
Binary files a/modular_skyrat/master_files/icons/obj/card.dmi and b/modular_skyrat/master_files/icons/obj/card.dmi differ
diff --git a/modular_skyrat/master_files/icons/obj/clothing/masks.dmi b/modular_skyrat/master_files/icons/obj/clothing/masks.dmi
index c9e746f0506286..7deb40b8d859a7 100644
Binary files a/modular_skyrat/master_files/icons/obj/clothing/masks.dmi and b/modular_skyrat/master_files/icons/obj/clothing/masks.dmi differ
diff --git a/modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi b/modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi
index a3274e254b1943..e0a71694d5b716 100644
Binary files a/modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi and b/modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi differ
diff --git a/modular_skyrat/modules/GAGS/greyscale_configs.dm b/modular_skyrat/modules/GAGS/greyscale_configs.dm
index 36870626d5dc36..9ee547d060eba9 100644
--- a/modular_skyrat/modules/GAGS/greyscale_configs.dm
+++ b/modular_skyrat/modules/GAGS/greyscale_configs.dm
@@ -1056,18 +1056,41 @@ digi
// WRAPS
-/datum/greyscale_config/wraps
+/datum/greyscale_config/clothwraps
name = "Cloth Wraps"
icon_file = 'modular_skyrat/modules/GAGS/icons/shoes.dmi'
json_config = 'modular_skyrat/modules/GAGS/json_configs/wraps/wraps.json'
-/datum/greyscale_config/wraps/worn
+/datum/greyscale_config/clothwraps/worn
name = "Cloth Wraps (Worn)"
json_config = 'modular_skyrat/modules/GAGS/json_configs/wraps/wraps_worn.json'
-/datum/greyscale_config/wraps/worn/digi
+/datum/greyscale_config/clothwraps/worn/digi
name = "Cloth Wraps (Worn, Digi)"
+/datum/greyscale_config/legwraps
+ name = "Leg Wraps"
+ icon_file = 'modular_skyrat/modules/GAGS/icons/shoes.dmi'
+ json_config = 'modular_skyrat/modules/GAGS/json_configs/wraps/legwraps.json'
+
+/datum/greyscale_config/legwraps/worn
+ name = "Leg Wraps (Worn)"
+ json_config = 'modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn.json'
+
+/datum/greyscale_config/legwraps/worn/digi
+ name = "Leg Wraps (Worn, Digi)"
+ json_config = 'modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn_digi.json'
+
+/datum/greyscale_config/armwraps
+ name = "Cloth Arm Wraps"
+ icon_file = 'modular_skyrat/master_files/icons/donator/obj/clothing/gloves.dmi'
+ json_config = 'modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps.json'
+
+/datum/greyscale_config/armwraps/worn
+ name = "Cloth Arm Wraps (Worn)"
+ icon_file = 'modular_skyrat/master_files/icons/donator/mob/clothing/hands.dmi'
+ json_config = 'modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps_worn.json'
+
// MISC SHOES
/datum/greyscale_config/heels
diff --git a/modular_skyrat/modules/GAGS/icons/shoes.dmi b/modular_skyrat/modules/GAGS/icons/shoes.dmi
index 154b6f312ba683..243f4ff5ec444d 100644
Binary files a/modular_skyrat/modules/GAGS/icons/shoes.dmi and b/modular_skyrat/modules/GAGS/icons/shoes.dmi differ
diff --git a/modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps.json b/modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps.json
new file mode 100644
index 00000000000000..cfe620714a3787
--- /dev/null
+++ b/modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps.json
@@ -0,0 +1,10 @@
+{
+ "arm_wraps": [
+ {
+ "type": "icon_state",
+ "icon_state": "arm_wraps",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ }
+ ]
+}
diff --git a/modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps_worn.json b/modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps_worn.json
new file mode 100644
index 00000000000000..cfe620714a3787
--- /dev/null
+++ b/modular_skyrat/modules/GAGS/json_configs/arm_wraps/arm_wraps_worn.json
@@ -0,0 +1,10 @@
+{
+ "arm_wraps": [
+ {
+ "type": "icon_state",
+ "icon_state": "arm_wraps",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ }
+ ]
+}
diff --git a/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps.json b/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps.json
new file mode 100644
index 00000000000000..0e605054f908dc
--- /dev/null
+++ b/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps.json
@@ -0,0 +1,10 @@
+{
+ "legwrap": [
+ {
+ "type": "icon_state",
+ "icon_state": "legwrap_obj",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ }
+ ]
+}
diff --git a/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn.json b/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn.json
new file mode 100644
index 00000000000000..8ddd9e1dc3b1d5
--- /dev/null
+++ b/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn.json
@@ -0,0 +1,10 @@
+{
+ "legwrap": [
+ {
+ "type": "icon_state",
+ "icon_state": "legwrap_mob",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ }
+ ]
+}
diff --git a/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn_digi.json b/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn_digi.json
new file mode 100644
index 00000000000000..833a458c85397a
--- /dev/null
+++ b/modular_skyrat/modules/GAGS/json_configs/wraps/legwraps_worn_digi.json
@@ -0,0 +1,10 @@
+{
+ "legwrap": [
+ {
+ "type": "icon_state",
+ "icon_state": "legwrap_mob_digi",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ }
+ ]
+}
diff --git a/modular_skyrat/modules/aesthetics/airalarm/code/airalarm.dm b/modular_skyrat/modules/aesthetics/airalarm/code/airalarm.dm
index f3675f8e226900..6792118bbaab8c 100644
--- a/modular_skyrat/modules/aesthetics/airalarm/code/airalarm.dm
+++ b/modular_skyrat/modules/aesthetics/airalarm/code/airalarm.dm
@@ -1,6 +1,5 @@
/obj/machinery/airalarm
- icon = 'modular_skyrat/modules/aesthetics/airalarm/icons/airalarm.dmi'
- var/light_mask = "alarm-light-mask"
+ icon = 'icons/obj/machines/wallmounts.dmi'
/obj/machinery/airalarm/update_appearance(updates)
. = ..()
diff --git a/modular_skyrat/modules/aesthetics/firealarm/icons/firealarm.dmi b/modular_skyrat/modules/aesthetics/firealarm/icons/firealarm.dmi
index 20adc064d3254f..ce8c37b6004928 100644
Binary files a/modular_skyrat/modules/aesthetics/firealarm/icons/firealarm.dmi and b/modular_skyrat/modules/aesthetics/firealarm/icons/firealarm.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/guns/code/guns.dm b/modular_skyrat/modules/aesthetics/guns/code/guns.dm
index ccc43793c335f1..ac20f9f5db997b 100644
--- a/modular_skyrat/modules/aesthetics/guns/code/guns.dm
+++ b/modular_skyrat/modules/aesthetics/guns/code/guns.dm
@@ -49,7 +49,7 @@
/datum/material/bluespace = SMALL_MATERIAL_AMOUNT * 0.2, \
)
-// for .35 Sol Ripper. one day, anon. one day
+// for .35 Sol Ripper
#define AMMO_MATS_RIPPER list( \
/datum/material/iron = SMALL_MATERIAL_AMOUNT * 1.6, \
/datum/material/glass = SMALL_MATERIAL_AMOUNT * 0.4, \
@@ -75,33 +75,6 @@
lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
-/obj/item/gun/ballistic/shotgun/riot
- name = "\improper Peacekeeper shotgun"
- desc = "A Nanotrasen-made riot control shotgun fitted with an extended tube and a fixed tactical stock."
- icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns.dmi'
- worn_icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi'
- lefthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi'
- righthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi'
- inhand_icon_state = "riot_shotgun"
- inhand_x_dimension = 32
- inhand_y_dimension = 32
- can_suppress = TRUE
- suppressed_sound = 'modular_skyrat/modules/aesthetics/guns/sound/suppressed_shotgun.ogg'
- suppressed_volume = 100
- vary_fire_sound = TRUE
- fire_sound = 'modular_skyrat/modules/aesthetics/guns/sound/shotgun_light.ogg'
-
-/obj/item/gun/ballistic/shotgun/riot/syndicate
- name = "\improper Peacebreaker shotgun"
- desc = "A Scarborough riot control shotgun fitted with a crimson furnishing and a wooden tactical stock. You swear you've seen this model elsewhere before..."
- icon_state = "riotshotgun_syndie"
- inhand_icon_state = "riot_shotgun_syndie"
- can_be_sawn_off = FALSE
- can_suppress = FALSE
-
-/obj/item/gun/ballistic/shotgun/riot/syndicate/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_SCARBOROUGH)
-
/obj/item/gun/ballistic/shotgun/automatic/combat
name = "\improper Peacekeeper combat shotgun"
desc = "A semi-automatic Nanotrasen Peacekeeper shotgun with tactical furnishing and heavier internals meant for sustained fire. Lacks a threaded barrel."
@@ -327,31 +300,11 @@
desc = "One of countless obsolete ballistic rifles that still sees use as a cheap deterrent. Uses 10mm ammo and its bulky frame prevents one-hand firing."
icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns.dmi'
-/obj/item/gun/ballistic/automatic/ar/modular/model75
- name = "\improper NT ARG-75"
- desc = "A contemporary rifle manufactured by NT chambered for .310 Strilka. It's equipped with a heavy duty integrally suppressed barrel, CQB scope and a topmounted laser sight."
- icon_state = "arg75"
- icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns.dmi'
- fire_sound = 'sound/weapons/gun/pistol/shot_suppressed.ogg'
- fire_delay = 5
- fire_sound_volume = 90
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/ostwind/arg75
-
-/obj/item/gun/ballistic/automatic/ar/modular/model75/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_NANOTRASEN)
-
-/obj/item/ammo_box/magazine/multi_sprite/ostwind/arg75
- name = "\improper ARG-75 magazine"
- desc = "A twenty round double-stack magazine for the NT ARG-75 rifle. Chambered in .310 Strilka."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pcr"
- ammo_type = /obj/item/ammo_casing/strilka310
- caliber = CALIBER_STRILKA310
- max_ammo = 20
-
// GUBMAN3 - FULL BULLET RENAME
// i loathe the above
+// overrides for 10mm ammo in modular_skyrat\modules\sec_haul\code\guns\bullets.dm
+
// overrides for .310 Strilka-derived ammo, e.g. lionhunter ammo, because you don't want to give security the ability to print infinite wallhack ammo, right?
/obj/item/ammo_casing/strilka310/lionhunter
name = "hunter's rifle round"
@@ -378,23 +331,47 @@
// overrides for tgcode .50cal, used in their sniper/anti-materiel rifles
/obj/item/ammo_casing/p50
- name = ".416 Stabilis polymer casing"
+ name = ".416 Stabilis casing"
desc = "A .416 bullet casing."
advanced_print_req = TRUE // you are NOT printing more ammo for this without effort.
// then again the offstations with ammo printers and sniper rifles come with an ammo disk anyway, so
-/obj/item/ammo_casing/p50/soporific
- name = ".416 Stabilis tranquilizer casing"
- desc = "A .416 bullet casing that specialises in sending the target to sleep rather than hell.\
+/obj/item/ammo_casing/p50/surplus
+ name = ".416 Stabilis surplus casing"
+ desc = "A .416 bullet casing. Intentionally underloaded, but still quite painful to be shot with.\
+
\
+ SURPLUS/UNDERLOAD: Lacks armor penetration capabilities, contact-stun, or innate dismemberment ability. Still incredibly painful to be hit by."
+ projectile_type = /obj/projectile/bullet/p50/surplus
+
+/obj/item/ammo_casing/p50/disruptor
+ name = ".416 Stabilis disruptor casing"
+ desc = "A .416 bullet casing. Specializes in sending the target to sleep rather than hell, unless they're synthetic. Then they probably go to hell anyway.\
+
\
+ DISRUPTOR: Forces humanoid targets to sleep, does heavy damage against cyborgs, EMPs struck targets."
+
+/obj/item/ammo_casing/p50/incendiary
+ name = ".416 Stabilis precision incendiary casing"
+ desc = "A .416 bullet casing. Made with an agitated-plasma tip, for making people regret being alive.\
\
- SOPORIFIC: Forces targets to sleep, deals no damage."
- projectile_type = /obj/projectile/bullet/p50/soporific
+ PRECISION INCENDIARY: Lacks innate dismemberment ability and contact-stun, suffers against mechanized armor. Sets people on fire."
+ projectile_type = /obj/projectile/bullet/p50/incendiary
/obj/item/ammo_casing/p50/penetrator
- name = ".416 Stabilis APFSDS ++P bullet casing"
- desc = "A .416 round casing designed to go through basically everything. A label warns not to use the round if the weapon cannot handle pressures greater than 85000 PSI.\
+ name = ".416 Stabilis penetrator sabot casing"
+ desc = "A .416 bullet casing. Loaded with a hardened sabot and packed with extra propellant. \
+ Designed to go through basically everything. A label warns of overpressure risk, and to not use the round if \
+ a given weapon cannot handle pressures greater than 85000 PSI.\
\
- PENETRATOR: Goes through every surface, and every mob. Goes through everything. Yes, really."
+ PENETRATOR: Goes through basically everything. Lacks innate dismemberment ability and contact-stun capabilities."
+
+/obj/item/ammo_casing/p50/marksman
+ name = ".416 Stabilis marksman hyperkinetic casing"
+ desc = "A .416 bullet casing. Loaded with a hyperkinetic bullet that ignores mundane things like \"travel time\" \
+ and a concerning amount of experimental propellant. A label warns of overpressure risk, and to not use the round if \
+ a given weapon cannot handle pressures greater than 95000 PSI.\
+
\
+ MARKSMAN: Bullets have no travel time, and can ricochet once. Does slightly less damage, lacks innate dismemberment and contact-stun capabilities."
+ projectile_type = /obj/projectile/bullet/p50/marksman
// overrides for tgcode 4.6x30mm, used in the WT-550
/obj/item/ammo_casing/c46x30mm
@@ -528,8 +505,11 @@
/obj/projectile/bullet/incendiary/c46x30mm
name = "8mm incendiary bullet"
-/obj/projectile/bullet/p50/soporific // COMMON BULLET IS ALREADY OVERRIDEN IN MODULAR > BULLETREBALANCE > CODE > sniper.dm
- name = ".416 tranquilizer"
+/obj/projectile/bullet/p50
+ name = ".416 Stabilis bullet"
+
+/obj/projectile/bullet/p50/disruptor
+ name = ".416 disruptor bullet"
/obj/projectile/bullet/p50/penetrator
name = ".416 penetrator bullet"
diff --git a/modular_skyrat/modules/aesthetics/guns/icons/guns.dmi b/modular_skyrat/modules/aesthetics/guns/icons/guns.dmi
index 3933f899038732..deb9cd5931e232 100644
Binary files a/modular_skyrat/modules/aesthetics/guns/icons/guns.dmi and b/modular_skyrat/modules/aesthetics/guns/icons/guns.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi b/modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi
index bc77592a879a2b..471407b916e9ff 100644
Binary files a/modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi and b/modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi b/modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi
index 0b1ab1864f49f7..131c63fca3bc39 100644
Binary files a/modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi and b/modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi b/modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi
index 76ae0487b9f6fa..7907e543b89427 100644
Binary files a/modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi and b/modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/kitchen/microwave.dmi b/modular_skyrat/modules/aesthetics/kitchen/microwave.dmi
index d5560e1bc077a1..e1c2860da6e1dd 100644
Binary files a/modular_skyrat/modules/aesthetics/kitchen/microwave.dmi and b/modular_skyrat/modules/aesthetics/kitchen/microwave.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/tools/tools.dmi b/modular_skyrat/modules/aesthetics/tools/tools.dmi
index e40038a5dcf9f4..518fa54cba94fe 100644
Binary files a/modular_skyrat/modules/aesthetics/tools/tools.dmi and b/modular_skyrat/modules/aesthetics/tools/tools.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/tools/tools_lefthand.dmi b/modular_skyrat/modules/aesthetics/tools/tools_lefthand.dmi
index bc1a97aa3e2d5e..256b736f8cca66 100644
Binary files a/modular_skyrat/modules/aesthetics/tools/tools_lefthand.dmi and b/modular_skyrat/modules/aesthetics/tools/tools_lefthand.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/tools/tools_righthand.dmi b/modular_skyrat/modules/aesthetics/tools/tools_righthand.dmi
index d7355344627784..3ed4928ee11851 100644
Binary files a/modular_skyrat/modules/aesthetics/tools/tools_righthand.dmi and b/modular_skyrat/modules/aesthetics/tools/tools_righthand.dmi differ
diff --git a/modular_skyrat/modules/alternative_job_titles/code/alt_job_titles.dm b/modular_skyrat/modules/alternative_job_titles/code/alt_job_titles.dm
index d7abb4e00f0116..11de552190337a 100644
--- a/modular_skyrat/modules/alternative_job_titles/code/alt_job_titles.dm
+++ b/modular_skyrat/modules/alternative_job_titles/code/alt_job_titles.dm
@@ -12,48 +12,60 @@
/datum/job/ai
alt_titles = list(
"AI",
+ "Automated Overseer",
"Station Intelligence",
- "Automated Overseer"
)
/datum/job/assistant
alt_titles = list(
"Assistant",
- "Civilian",
- "Tourist",
+ "Artist",
"Businessman",
"Businesswoman",
- "Trader",
+ "Civilian",
"Entertainer",
"Freelancer",
- "Artist",
- "Off-Duty Staff",
+ "Tourist",
+ "Trader",
"Off-Duty Crew",
+ "Off-Duty Staff",
)
/datum/job/atmospheric_technician
alt_titles = list(
"Atmospheric Technician",
- "Life Support Technician",
"Emergency Fire Technician",
"Firefighter",
+ "Life Support Technician",
)
/datum/job/barber
alt_titles = list(
"Barber",
+ "Aethestician",
+ "Colorist",
"Salon Manager",
"Salon Technician",
"Stylist",
- "Colorist",
)
/datum/job/bartender
alt_titles = list(
"Bartender",
- "Mixologist",
- "Barkeeper",
"Barista",
+ "Barkeeper",
+ "Mixologist",
+ )
+
+/datum/job/bitrunner
+ alt_titles = list(
+ "Bitrunner",
+ "Bitdomain Technician",
+ "Data Retrieval Specialist",
+ "Netdiver",
+ "Pod Jockey",
+ "Union Bitrunner",
+ "Junior Runner",
)
/datum/job/blueshield
@@ -66,11 +78,13 @@
/datum/job/botanist
alt_titles = list(
"Botanist",
- "Hydroponicist",
- "Gardener",
"Botanical Researcher",
- "Herbalist",
"Florist",
+ "Gardener",
+ "Herbalist",
+ "Hydroponicist",
+ "Mycologist",
+ "Junior Botanist",
)
/datum/job/bouncer
@@ -89,33 +103,35 @@
/datum/job/captain
alt_titles = list(
"Captain",
- "Station Commander",
"Commanding Officer",
"Site Manager",
+ "Station Commander",
)
/datum/job/cargo_technician
alt_titles = list(
"Warehouse Technician",
+ "Commodities Trader",
"Deck Worker",
+ "Inventory Associate",
"Mailman",
+ "Receiving Clerk",
"Union Associate",
- "Inventory Associate",
)
/datum/job/chaplain
alt_titles = list(
"Chaplain",
- "Priest",
- "Preacher",
- "Reverend",
- "Oracle",
- "Pontifex",
- "Magister",
"High Priest",
"Imam",
- "Rabbi",
+ "Magister",
"Monk",
+ "Oracle",
+ "Preacher",
+ "Priest",
+ "Pontifex",
+ "Rabbi",
+ "Reverend",
)
/datum/job/chemist
@@ -136,25 +152,25 @@
/datum/job/chief_medical_officer
alt_titles = list(
"Chief Medical Officer",
- "Medical Director",
- "Head of Medical",
"Chief Physician",
+ "Head of Medical",
"Head Physician",
+ "Medical Director",
)
/datum/job/clown
alt_titles = list(
"Clown",
+ "Comedian",
"Jester",
"Joker",
- "Comedian",
)
/datum/job/cook
alt_titles = list(
"Cook",
- "Chef",
"Butcher",
+ "Chef",
"Culinary Artist",
"Sous-Chef",
)
@@ -162,16 +178,19 @@
/datum/job/coroner
alt_titles = list(
"Coroner",
- "Mortician",
+ "Forensic Pathologist",
"Funeral Director",
+ "Medical Examiner",
+ "Mortician",
)
/datum/job/curator
alt_titles = list(
"Curator",
- "Librarian",
- "Journalist",
"Archivist",
+ "Conservator",
+ "Journalist",
+ "Librarian",
)
/datum/job/customs_agent
@@ -183,26 +202,27 @@
/datum/job/cyborg
alt_titles = list(
"Cyborg",
- "Robot",
"Android",
+ "Robot",
)
/datum/job/detective
alt_titles = list(
"Detective",
+ "Forensic Scientist",
"Forensic Technician",
"Private Investigator",
- "Forensic Scientist",
)
/datum/job/doctor
alt_titles = list(
"Medical Doctor",
- "Surgeon",
- "Nurse",
"General Practitioner",
"Medical Resident",
+ "Nurse",
"Physician",
+ "Surgeon",
+ "Medical Student",
)
/datum/job/engineering_guard //see orderly
@@ -210,58 +230,61 @@
/datum/job/geneticist
alt_titles = list(
"Geneticist",
+ "Gene Tailor",
"Mutation Researcher",
)
/datum/job/head_of_personnel
alt_titles = list(
"Head of Personnel",
- "Executive Officer",
- "Employment Officer",
"Crew Supervisor",
+ "Employment Officer",
+ "Executive Officer",
)
/datum/job/head_of_security
alt_titles = list(
"Head of Security",
- "Security Commander",
"Chief Constable",
"Chief of Security",
+ "Security Commander",
"Sheriff",
)
/datum/job/janitor
alt_titles = list(
"Janitor",
- "Custodian",
- "Custodial Technician",
- "Sanitation Technician",
- "Maintenance Technician",
"Concierge",
+ "Custodial Technician",
+ "Custodian",
"Maid",
+ "Maintenance Technician",
+ "Sanitation Technician",
)
/datum/job/lawyer
alt_titles = list(
"Lawyer",
- "Internal Affairs Agent",
- "Human Resources Agent",
- "Defence Attorney",
- "Public Defender",
"Barrister",
- "Prosecutor",
+ "Defense Attorney",
+ "Human Resources Agent",
+ "Internal Affairs Agent",
"Legal Clerk",
+ "Prosecutor",
+ "Public Defender",
)
/datum/job/mime
alt_titles = list(
"Mime",
+ "Mummer",
"Pantomimist",
)
/datum/job/nanotrasen_consultant
alt_titles = list(
"Nanotrasen Consultant",
+ "Nanotrasen Advisor",
"Nanotrasen Diplomat",
)
@@ -285,44 +308,42 @@
"Maximum Security Prisoner",
"SuperMax Security Prisoner",
"Protective Custody Prisoner",
- "Convict",
- "Felon",
- "Inmate",
)
/datum/job/psychologist
alt_titles = list(
"Psychologist",
+ "Counsellor",
"Psychiatrist",
"Therapist",
- "Counsellor",
)
/datum/job/quartermaster
alt_titles = list(
"Quartermaster",
- "Union Requisitions Officer",
"Deck Chief",
- "Warehouse Supervisor",
- "Supply Foreman",
"Head of Supply",
"Logistics Coordinator",
+ "Supply Foreman",
+ "Union Requisitions Officer",
+ "Warehouse Supervisor",
)
/datum/job/research_director
alt_titles = list(
"Research Director",
- "Silicon Administrator",
- "Lead Researcher",
"Biorobotics Director",
- "Research Supervisor",
"Chief Science Officer",
+ "Lead Researcher",
+ "Research Supervisor",
+ "Silicon Administrator",
)
/datum/job/roboticist
alt_titles = list(
"Roboticist",
"Biomechanical Engineer",
+ "Machinist",
"Mechatronic Engineer",
"Apprentice Roboticist",
)
@@ -332,24 +353,24 @@
/datum/job/scientist
alt_titles = list(
"Scientist",
+ "Anomalist",
"Circuitry Designer",
- "Xenobiologist",
"Cytologist",
- "Plasma Researcher",
- "Anomalist",
+ "Graduate Student",
"Lab Technician",
- "Theoretical Physicist",
"Ordnance Technician",
+ "Plasma Researcher",
+ "Theoretical Physicist",
"Xenoarchaeologist",
+ "Xenobiologist",
"Research Assistant",
- "Graduate Student",
)
/datum/job/security_officer
alt_titles = list(
"Security Officer",
- "Security Operative",
"Peacekeeper",
+ "Security Operative",
"Security Cadet",
)
@@ -357,26 +378,27 @@
alt_titles = list(
"Union Miner",
"Excavator",
- "Spelunker",
"Drill Technician",
"Prospector",
+ "Spelunker",
+ "Apprentice Miner",
)
/datum/job/station_engineer
alt_titles = list(
"Station Engineer",
- "Emergency Damage Control Technician",
"Electrician",
+ "Emergency Damage Control Technician",
"Engine Technician",
"EVA Technician",
"Mechanic",
"Apprentice Engineer",
- "Engineering Trainee",
)
/datum/job/virologist
alt_titles = list(
"Virologist",
+ "Epidemiologist",
"Pathologist",
"Junior Pathologist",
)
@@ -385,7 +407,7 @@
alt_titles = list(
"Warden",
"Brig Sergeant",
- "Dispatch Officer",
"Brig Governor",
+ "Dispatch Officer",
"Jailer",
)
diff --git a/modular_skyrat/modules/alternative_job_titles/code/job.dm b/modular_skyrat/modules/alternative_job_titles/code/job.dm
index 2f47a4c1f0533c..13edfe45aa4c12 100644
--- a/modular_skyrat/modules/alternative_job_titles/code/job.dm
+++ b/modular_skyrat/modules/alternative_job_titles/code/job.dm
@@ -11,6 +11,9 @@
if(!player_client)
return
+ if(!ishuman(equipping))
+ return
+
var/chosen_title = player_client.prefs.alt_job_titles[job.title] || job.title
var/obj/item/card/id/card = equipping.wear_id
diff --git a/modular_skyrat/modules/ammo_workbench/code/design_disks.dm b/modular_skyrat/modules/ammo_workbench/code/design_disks.dm
index f7955d053e009f..26e6302c6da792 100644
--- a/modular_skyrat/modules/ammo_workbench/code/design_disks.dm
+++ b/modular_skyrat/modules/ammo_workbench/code/design_disks.dm
@@ -1,5 +1,5 @@
/obj/item/disk/ammo_workbench
- name = "Armadyne Munitions blueprint datadisk"
+ name = "munitions blueprint datadisk"
desc = "You shouldn't be seeing this!"
/// For doing things when installed/downloaded onto an ammo bench.
@@ -9,8 +9,8 @@
/obj/item/disk/ammo_workbench/advanced
name = "advanced munitions datadisk"
- desc = "An Armadyne datadisk filled with advanced munition fabrication data for the ammunition workbench, including lethal ammotypes if not previously enabled. \
- Armadyne's munitions division does not take responsibility for any incidents that occur if safeties were circumvented beforehand."
+ desc = "An datadisk filled with advanced munition fabrication data for the ammunition workbench, including lethal ammotypes if not previously enabled. \
+ No parties are liable for any incidents that occur if safeties were circumvented beforehand."
/obj/item/disk/ammo_workbench/advanced/on_bench_install(obj/machinery/ammo_workbench/ammobench)
ammobench.allowed_harmful = TRUE
diff --git a/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm b/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm
index eaa80cc59417d6..18d3baa3b51f0c 100644
--- a/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm
+++ b/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm
@@ -99,7 +99,7 @@
viewable_living.gib()
continue
- if(viewable_living.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (viewable_living.key || viewable_living.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
+ if(viewable_living.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (viewable_living.ckey || viewable_living.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
revive_ashwalker(viewable_living)
continue
@@ -129,6 +129,8 @@
else
living_observers.add_mood_event("oogabooga", /datum/mood_event/sacrifice_bad)
+ ashies.sacrifices_made++
+
/**
* Proc that will spawn the egg that will revive the ashwalker
* This is also the Skyrat replacement for /proc/remake_walker
diff --git a/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm b/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm
index ba1dd931c3a225..7b38a565698b21 100644
--- a/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm
+++ b/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm
@@ -198,7 +198,7 @@
/datum/ash_ritual/summon_icemoon_creature/ritual_success(obj/effect/ash_rune/success_rune)
. = ..()
var/mob_type = pick(
- /mob/living/simple_animal/hostile/asteroid/ice_demon,
+ /mob/living/basic/mining/ice_demon,
/mob/living/basic/mining/ice_whelp,
/mob/living/basic/mining/lobstrosity,
/mob/living/simple_animal/hostile/asteroid/polarbear,
diff --git a/modular_skyrat/modules/assault_operatives/code/armaments/_armament_primary.dm b/modular_skyrat/modules/assault_operatives/code/armaments/_armament_primary.dm
index ff55e715b0e983..e2ef34a2972ef0 100644
--- a/modular_skyrat/modules/assault_operatives/code/armaments/_armament_primary.dm
+++ b/modular_skyrat/modules/assault_operatives/code/armaments/_armament_primary.dm
@@ -7,8 +7,8 @@
#define OPS_SUBCATEGORY_SHOTGUN "Shotguns"
#define OPS_SUBCATEGORY_SHOTGUN_AMMO "Speciality Shotgun Ammo"
-#define OPS_SUBCATEGORY_SNIPER "Marksman Rifles"
-#define OPS_SUBCATEGORY_SNIPER_AMMO "Speciality Marksman Rifle Ammo"
+#define OPS_SUBCATEGORY_SNIPER "Grenade Launchers"
+#define OPS_SUBCATEGORY_SNIPER_AMMO "Speciality Grenade Launcher Ammo"
/datum/armament_entry/assault_operatives/primary
category = "Long Arms"
@@ -20,50 +20,67 @@
subcategory = OPS_SUBCATEGORY_RIFLE
/datum/armament_entry/assault_operatives/primary/rifle/assault_ops_rifle
- item_type = /obj/item/gun/ballistic/automatic/assault_ops_rifle
+ item_type = /obj/item/gun/ballistic/automatic/sol_rifle/evil
/datum/armament_entry/assault_operatives/primary/rifle_ammo
subcategory = OPS_SUBCATEGORY_RIFLE_AMMO
max_purchase = 10
cost = 1
-/datum/armament_entry/assault_operatives/primary/rifle_ammo/rubber
- name = "\improper IGE-110 rubber magazine"
- description = "Rifle ammo that is more likely to exhaust whoever its shot at, rather than killing them."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_rifle/rubber
+/datum/armament_entry/assault_operatives/primary/rifle_ammo/standard
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/starts_empty
+ cost = 0
-/datum/armament_entry/assault_operatives/primary/rifle_ammo/ap
- name = "\improper IGE-110 armor piercing magazine"
- description = "Rifle ammo built specifically to penetrate through armor."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_rifle/ap
+/datum/armament_entry/assault_operatives/primary/rifle_ammo/drum
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/drum/starts_empty
+ cost = 0
+
+/datum/armament_entry/assault_operatives/primary/rifle_ammo/c40sol
+ item_type = /obj/item/ammo_box/c40sol
+
+/datum/armament_entry/assault_operatives/primary/rifle_ammo/c40sol_disabler
+ item_type = /obj/item/ammo_box/c40sol/fragmentation
+
+/datum/armament_entry/assault_operatives/primary/rifle_ammo/c40sol_pierce
+ item_type = /obj/item/ammo_box/c40sol/pierce
+
+/datum/armament_entry/assault_operatives/primary/rifle_ammo/c40sol_incendiary
+ item_type = /obj/item/ammo_box/c40sol/incendiary
/datum/armament_entry/assault_operatives/primary/submachinegun
subcategory = OPS_SUBCATEGORY_SMG
/datum/armament_entry/assault_operatives/primary/submachinegun/assault_ops_smg
- item_type = /obj/item/gun/ballistic/automatic/assault_ops_smg
+ item_type = /obj/item/gun/ballistic/automatic/sol_smg/evil
/datum/armament_entry/assault_operatives/primary/submachinegun_ammo
subcategory = OPS_SUBCATEGORY_SMG_AMMO
max_purchase = 10
cost = 1
-/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/rubber
- name = "\improper IGE-260 rubber magazine"
- description = "Submachine gun ammo that is more likely to exhaust whoever its shot at, rather than killing them."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_smg/rubber
+/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/standard
+ item_type = /obj/item/ammo_box/magazine/c35sol_pistol/starts_empty
+ cost = 0
+
+/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/extended
+ item_type = /obj/item/ammo_box/magazine/c35sol_pistol/stendo/starts_empty
+ cost = 0
+
+/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/c35sol
+ item_type = /obj/item/ammo_box/c35sol
-/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/hp
- name = "\improper IGE-260 hollowpoint magazine"
- description = "Submachine gun ammo that hurts unarmored targets more, in exchange for worse performance against armor."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_smg/hp
+/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/c35sol_disabler
+ item_type = /obj/item/ammo_box/c35sol/incapacitator
+
+/datum/armament_entry/assault_operatives/primary/submachinegun_ammo/c35sol_pierce
+ item_type = /obj/item/ammo_box/c35sol/ripper
/datum/armament_entry/assault_operatives/primary/shotgun
subcategory = OPS_SUBCATEGORY_SHOTGUN
/datum/armament_entry/assault_operatives/primary/shotgun/assault_ops_shotgun
- item_type = /obj/item/gun/ballistic/automatic/assault_ops_shotgun
+ item_type = /obj/item/gun/ballistic/shotgun/riot/sol/evil
/datum/armament_entry/assault_operatives/primary/shotgun_ammo
subcategory = OPS_SUBCATEGORY_SHOTGUN_AMMO
@@ -71,50 +88,53 @@
cost = 1
/datum/armament_entry/assault_operatives/primary/shotgun_ammo/rubber
- name = "\improper IGE-340 rubbershot magazine"
- description = "Shotgun ammo that's much like buckshot, but more likely to exhaust whoever its shot at rather than killing them."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/rubbershot
+ item_type = /obj/item/ammo_box/advanced/s12gauge/rubber
/datum/armament_entry/assault_operatives/primary/shotgun_ammo/flechette
- name = "\improper IGE-340 flechette magazine"
- description = "Shotgun ammo that fires armor piercing flechettes that can cause some nasty wounds."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/flechette
+ item_type = /obj/item/ammo_box/advanced/s12gauge/flechette
/datum/armament_entry/assault_operatives/primary/shotgun_ammo/hollowpoint
- name = "\improper IGE-340 hollowpoint slug magazine"
- description = "Shotgun ammo that fires a large hollowpoint slug that hurts unarmored targets a lot more, in exchange for worse performance against armor."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/hollowpoint
+ item_type = /obj/item/ammo_box/advanced/s12gauge/hp
/datum/armament_entry/assault_operatives/primary/shotgun_ammo/beehive
- name = "\improper IGE-340 'beehive' magazine"
- description = "Shotgun ammo that fires a spread of smart-bouncing pellets, that are more likely to exhaust whoever its shot at rather than killing them."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/beehive
+ item_type = /obj/item/ammo_box/advanced/s12gauge/beehive
-/datum/armament_entry/assault_operatives/primary/shotgun_ammo/dragonsbreath
- name = "\improper IGE-340 dragonsbreath magazine"
- description = "Shotgun ammo that fires a spread of incendiary projectiles, creating a wall of fire whichever direction they are shot in."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/dragonsbreath
+/datum/armament_entry/assault_operatives/primary/shotgun_ammo/incendiary
+ item_type = /obj/item/ammo_box/advanced/s12gauge/incendiary
/datum/armament_entry/assault_operatives/primary/sniper
subcategory = OPS_SUBCATEGORY_SNIPER
-/datum/armament_entry/assault_operatives/primary/sniper/assault_ops_sniper
- item_type = /obj/item/gun/ballistic/rifle/boltaction/assault_ops_sniper
+/datum/armament_entry/assault_operatives/primary/sniper/assault_ops_gl
+ item_type = /obj/item/gun/ballistic/automatic/sol_grenade_launcher/evil
/datum/armament_entry/assault_operatives/primary/sniper_ammo
subcategory = OPS_SUBCATEGORY_SNIPER_AMMO
max_purchase = 10
cost = 1
-/datum/armament_entry/assault_operatives/primary/sniper_ammo/eepy
- name = "\improper IGE-410 soporific magazine"
- description = "Sniper ammo that will put whoever it hits right to sleep, rather than killing them."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper/sleepytime
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/standard
+ item_type = /obj/item/ammo_box/magazine/c980_grenade/starts_empty
+ cost = 0
+
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/drum
+ item_type = /obj/item/ammo_box/magazine/c980_grenade/drum/starts_empty
+ cost = 0
+
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/practice
+ item_type = /obj/item/ammo_box/c980grenade
+
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/smoke
+ item_type = /obj/item/ammo_box/c980grenade/smoke
+
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/shrapnel
+ item_type = /obj/item/ammo_box/c980grenade/shrapnel
+
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/phosphor
+ item_type = /obj/item/ammo_box/c980grenade/shrapnel/phosphor
-/datum/armament_entry/assault_operatives/primary/sniper_ammo/penetrator
- name = "\improper IGE-410 penetrator magazine"
- description = "Sniper ammo that is capable of penetrating through multiple walls and people at once."
- item_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper/penetrator
+/datum/armament_entry/assault_operatives/primary/sniper_ammo/riot
+ item_type = /obj/item/ammo_box/c980grenade/riot
#undef OPS_SUBCATEGORY_RIFLE
#undef OPS_SUBCATEGORY_RIFLE_AMMO
diff --git a/modular_skyrat/modules/assault_operatives/code/armaments/_armament_secondary.dm b/modular_skyrat/modules/assault_operatives/code/armaments/_armament_secondary.dm
index 0022abd9868095..36460f04d630d9 100644
--- a/modular_skyrat/modules/assault_operatives/code/armaments/_armament_secondary.dm
+++ b/modular_skyrat/modules/assault_operatives/code/armaments/_armament_secondary.dm
@@ -11,8 +11,8 @@
/datum/armament_entry/assault_operatives/secondary/lethal
subcategory = OPS_SUBCATEGORY_LETHAL_SIDE
-/datum/armament_entry/assault_operatives/secondary/lethal/ansem
- item_type = /obj/item/gun/ballistic/automatic/pistol/clandestine/assault_ops
+/datum/armament_entry/assault_operatives/secondary/lethal/pistol
+ item_type = /obj/item/gun/ballistic/automatic/pistol/sol/evil
/datum/armament_entry/assault_operatives/secondary/lethal/energy_sword
item_type = /obj/item/melee/energy/sword/saber
@@ -21,7 +21,7 @@
subcategory = OPS_SUBCATEGORY_NONLETHAL_SIDE
/datum/armament_entry/assault_operatives/secondary/nonlethal/taze_me_bro
- item_type = /obj/item/gun/energy/e_gun/advtaser/assault_ops
+ item_type = /obj/item/gun/energy/e_gun/advtaser
/datum/armament_entry/assault_operatives/secondary/nonlethal/baton
item_type = /obj/item/melee/baton/telescopic
diff --git a/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm b/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm
deleted file mode 100644
index 357c25e02f961a..00000000000000
--- a/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm
+++ /dev/null
@@ -1,246 +0,0 @@
-// La Pistola
-
-/obj/item/gun/ballistic/automatic/pistol/clandestine/assault_ops
- name = "\improper IGE-040 pistol"
- desc = "A pistol chambered in 10mm magnum and painted in an ominous matte black. Strangely, the gun also seems to lack any form of manufacturer markings."
-
-/obj/item/gun/ballistic/automatic/pistol/clandestine/assault_ops/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_REMOVED)
-
-/obj/item/gun/energy/e_gun/advtaser/assault_ops
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/energy/e_gun/advtaser/assault_ops/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_REMOVED)
-
-// Rifle
-
-/obj/item/gun/ballistic/automatic/assault_ops_rifle
- name = "\improper IGE-110 rifle"
- desc = "A bullpup rifle chambered in 5.6x40mm and painted in an ominous matte black. Strangely, the gun also seems to lack any form of manufacturer markings."
-
- icon_state = "ige_assault"
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns.dmi'
- inhand_icon_state = "ige_assault"
- righthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_lefthand.dmi'
- worn_icon_state = "ige_assault"
- worn_icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_worn.dmi'
-
- base_pixel_x = -8
- pixel_x = -8
-
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_rifle
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/sfrifle_fire.ogg'
- can_suppress = TRUE
- suppressor_x_offset = 4
- suppressed_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg'
- burst_size = 2
- fire_delay = 3
- pin = /obj/item/firing_pin/implant/pindicate
-
-/obj/item/gun/ballistic/automatic/assault_ops_rifle/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/scope, range_modifier = 1.5)
-
-/obj/item/gun/ballistic/automatic/assault_ops_rifle/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_REMOVED)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_rifle
- name = "\improper IGE-110 magazine"
- desc = "A twenty round magazine built for 5.6x40mm, intended for use in the IGE-110 rifle."
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/magazines.dmi'
- icon_state = "ige_assault_mag"
- ammo_type = /obj/item/ammo_casing/realistic/a762x39
- caliber = "a762x39"
- max_ammo = 20
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_AP)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_rifle/rubber
- ammo_type = /obj/item/ammo_casing/realistic/a762x39/civilian/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_rifle/ap
- ammo_type = /obj/item/ammo_casing/realistic/a762x39/ap
- round_type = AMMO_TYPE_AP
-
-// SMG
-
-/obj/item/gun/ballistic/automatic/assault_ops_smg
- name = "\improper IGE-260 submachine gun"
- desc = "A toploader submachine gun chambered in 9x25mm and painted in an ominous matte black. Strangely, the gun also seems to lack any form of manufacturer markings."
-
- icon_state = "ige_smg"
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns.dmi'
- inhand_icon_state = "ige_smg"
- righthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_lefthand.dmi'
- worn_icon_state = "ige_smg"
- worn_icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_worn.dmi'
-
- base_pixel_x = -8
- pixel_x = -8
-
- w_class = WEIGHT_CLASS_NORMAL
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_smg
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/smg_fire.ogg'
- can_suppress = TRUE
- burst_size = 1
- fire_delay = 0.8
- projectile_damage_multiplier = 0.6
- actions_types = list()
- pin = /obj/item/firing_pin/implant/pindicate
-
-/obj/item/gun/ballistic/automatic/assault_ops_smg/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/assault_ops_smg/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_REMOVED)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_smg
- name = "\improper IGE-260 magazine"
- desc = "A forty round magazine built for 9x25mm, intended for use in the IGE-260 submachine gun."
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/magazines.dmi'
- icon_state = "ige_smg_mag"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 40
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_HOLLOWPOINT)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_smg/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_smg/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-// Shotgun
-
-/obj/item/gun/ballistic/automatic/assault_ops_shotgun
- name = "\improper IGE-340 semi-automatic shotgun"
- desc = "A magazine fed semi-automatic shotgun chambered in 12 GA and painted in an ominous matte black. Strangely, the gun also seems to lack any form of manufacturer markings."
-
- icon_state = "ige_shotgun"
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns.dmi'
- inhand_icon_state = "ige_shotgun"
- righthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_lefthand.dmi'
- worn_icon_state = "ige_shotgun"
- worn_icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_worn.dmi'
-
- base_pixel_x = -8
- pixel_x = -8
-
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/shotgun_bm.ogg'
- can_suppress = TRUE
- suppressor_x_offset = 4
- suppressed_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg'
- burst_size = 1
- fire_delay = 1.5
- actions_types = list()
- pin = /obj/item/firing_pin/implant/pindicate
-
-/obj/item/gun/ballistic/automatic/assault_ops_shotgun/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_REMOVED)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun
- name = "\improper IGE-340 magazine"
- desc = "A seven round magazine built for 12 GA, intended for use in the IGE-340 shotgun."
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/magazines.dmi'
- icon_state = "ige_shotgun_mag"
- ammo_type = /obj/item/ammo_casing/shotgun
- caliber = CALIBER_SHOTGUN
- max_ammo = 7
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_AP, AMMO_TYPE_HOLLOWPOINT, AMMO_TYPE_IHDF, AMMO_TYPE_INCENDIARY)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/rubbershot
- ammo_type = /obj/item/ammo_casing/shotgun/rubbershot
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/flechette
- ammo_type = /obj/item/ammo_casing/shotgun/flechette
- round_type = AMMO_TYPE_AP
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/hollowpoint
- ammo_type = /obj/item/ammo_casing/shotgun/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/beehive
- ammo_type = /obj/item/ammo_casing/shotgun/beehive
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_shotgun/dragonsbreath
- ammo_type = /obj/item/ammo_casing/shotgun/dragonsbreath
- round_type = AMMO_TYPE_INCENDIARY
-
-// Sniper
-
-/obj/item/gun/ballistic/rifle/boltaction/assault_ops_sniper
- name = "\improper IGE-410-S marksman rifle"
- desc = "A magazine fed bolt-action rifle with a short enough barrel that your shoulder hurts just looking at it. Chambered in .416 Stabilis, it is painted in an ominous matte black and seems to lack any form of manufacturer markings."
-
- icon_state = "ige_sniper"
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns.dmi'
- inhand_icon_state = "ige_sniper"
- righthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_lefthand.dmi'
- worn_icon_state = "ige_sniper"
- worn_icon = 'modular_skyrat/modules/assault_operatives/icons/guns/guns_worn.dmi'
-
- base_pixel_x = -8
- pixel_x = -8
-
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
- internal_magazine = FALSE
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/sniper_fire.ogg'
- can_suppress = TRUE
- suppressor_x_offset = 6
- suppressed_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg'
- burst_size = 1
- fire_delay = 10
- recoil = 3
- can_be_sawn_off = FALSE
- can_jam = FALSE
- pin = /obj/item/firing_pin/implant/pindicate
-
-/obj/item/gun/ballistic/rifle/boltaction/assault_ops_sniper/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/scope, range_modifier = 2.5)
-
-/obj/item/gun/ballistic/rifle/boltaction/assault_ops_sniper/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_REMOVED)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper
- name = "\improper IGE-410 magazine"
- desc = "A five round magazine built for .416 Stabilis, intended for use in the IGE-410 sniper."
- icon = 'modular_skyrat/modules/assault_operatives/icons/guns/magazines.dmi'
- icon_state = "ige_sniper_mag"
- ammo_type = /obj/item/ammo_casing/p50
- caliber = CALIBER_50BMG
- max_ammo = 5
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_AP)
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper/sleepytime
- ammo_type = /obj/item/ammo_casing/p50/soporific
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper/penetrator
- ammo_type = /obj/item/ammo_casing/p50/penetrator
- round_type = AMMO_TYPE_AP
diff --git a/modular_skyrat/modules/black_mesa/code/armaments/_armament_primary.dm b/modular_skyrat/modules/black_mesa/code/armaments/_armament_primary.dm
index 9c5344691d1448..80462185a0e0ea 100644
--- a/modular_skyrat/modules/black_mesa/code/armaments/_armament_primary.dm
+++ b/modular_skyrat/modules/black_mesa/code/armaments/_armament_primary.dm
@@ -41,7 +41,7 @@
magazine_cost = 4
/datum/armament_entry/hecu/primary/shotgun/shotgun_highcap
- item_type = /obj/item/gun/ballistic/shotgun/m23
+ item_type = /obj/item/gun/ballistic/shotgun/riot/sol
max_purchase = 2
cost = 5
@@ -55,7 +55,7 @@
mags_to_spawn = 2
/datum/armament_entry/hecu/primary/special/sniper_rifle
- item_type = /obj/item/gun/ballistic/automatic/cfa_rifle
+ item_type = /obj/item/gun/ballistic/automatic/sol_rifle/marksman
max_purchase = 1
cost = 16
diff --git a/modular_skyrat/modules/black_mesa/code/armaments/_armaments_secondary.dm b/modular_skyrat/modules/black_mesa/code/armaments/_armaments_secondary.dm
index da2f9575ed06e6..2098c72a7185dc 100644
--- a/modular_skyrat/modules/black_mesa/code/armaments/_armaments_secondary.dm
+++ b/modular_skyrat/modules/black_mesa/code/armaments/_armaments_secondary.dm
@@ -12,8 +12,8 @@
item_type = /obj/item/gun/ballistic/automatic/pistol/m1911
max_purchase = 4
-/datum/armament_entry/hecu/secondary/pistol/glock
- item_type = /obj/item/gun/ballistic/automatic/pistol/g17/mesa
+/datum/armament_entry/hecu/secondary/pistol/pistol
+ item_type = /obj/item/gun/ballistic/automatic/pistol/sol
max_purchase = 4
mags_to_spawn = 3
diff --git a/modular_skyrat/modules/black_mesa/code/armaments/armament_miscellaneous.dm b/modular_skyrat/modules/black_mesa/code/armaments/armament_miscellaneous.dm
index 8ec1c5e39a64aa..1916b4e90fc403 100644
--- a/modular_skyrat/modules/black_mesa/code/armaments/armament_miscellaneous.dm
+++ b/modular_skyrat/modules/black_mesa/code/armaments/armament_miscellaneous.dm
@@ -53,6 +53,6 @@
cost = 1
/datum/armament_entry/hecu/misc/hudglasses
- item_type = /obj/item/clothing/glasses/hud/security/sunglasses/peacekeeper/armadyne
+ item_type = /obj/item/clothing/glasses/hud/security/sunglasses/peacekeeper
max_purchase = 6
cost = 2
diff --git a/modular_skyrat/modules/black_mesa/code/drops.dm b/modular_skyrat/modules/black_mesa/code/drops.dm
index a415f790b94c6a..30ab8bf0cf0a27 100644
--- a/modular_skyrat/modules/black_mesa/code/drops.dm
+++ b/modular_skyrat/modules/black_mesa/code/drops.dm
@@ -1,8 +1,8 @@
/obj/effect/spawner/random/hecu_smg
name = "HECU SMG drops"
spawn_all_loot = FALSE
- loot = list(/obj/item/gun/ballistic/automatic/cfa_wildcat = 15,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat = 25,
+ loot = list(/obj/item/gun/ballistic/automatic/sol_smg = 15,
+ /obj/item/ammo_box/magazine/c35sol_pistol/stendo = 25,
/obj/item/clothing/mask/gas/hecu2 = 15,
/obj/item/clothing/head/helmet = 15,
/obj/item/clothing/suit/armor/vest = 15,
diff --git a/modular_skyrat/modules/black_mesa/code/ghost_spawners.dm b/modular_skyrat/modules/black_mesa/code/ghost_spawners.dm
index 8efff44325d329..416def1973aa42 100644
--- a/modular_skyrat/modules/black_mesa/code/ghost_spawners.dm
+++ b/modular_skyrat/modules/black_mesa/code/ghost_spawners.dm
@@ -56,9 +56,9 @@
belt = /obj/item/storage/belt/security/full
back = /obj/item/storage/backpack
backpack_contents = list(/obj/item/radio,
- /obj/item/gun/ballistic/automatic/pistol/g17/mesa,
- /obj/item/ammo_box/magazine/multi_sprite/ladon,
- /obj/item/ammo_box/magazine/multi_sprite/ladon,
+ /obj/item/gun/ballistic/automatic/pistol/sol,
+ /obj/item/ammo_box/magazine/c35sol_pistol,
+ /obj/item/ammo_box/magazine/c35sol_pistol,
)
id = /obj/item/card/id
id_trim = /datum/id_trim/security_guard
diff --git a/modular_skyrat/modules/black_mesa/code/mobs/human_mobs.dm b/modular_skyrat/modules/black_mesa/code/mobs/human_mobs.dm
index b83e961a701391..514c9aabe7cb92 100644
--- a/modular_skyrat/modules/black_mesa/code/mobs/human_mobs.dm
+++ b/modular_skyrat/modules/black_mesa/code/mobs/human_mobs.dm
@@ -162,7 +162,7 @@
icon_living = "security_guard_ranged"
casingtype = /obj/item/ammo_casing/c9mm
projectilesound = 'sound/weapons/gun/pistol/shot.ogg'
- loot = list(/obj/effect/gibspawner/human, /obj/item/clothing/suit/armor/vest/blueshirt, /obj/item/gun/ballistic/automatic/pistol/g17/mesa)
+ loot = list(/obj/effect/gibspawner/human, /obj/item/clothing/suit/armor/vest/blueshirt, /obj/item/gun/ballistic/automatic/pistol/sol)
rapid_melee = 1
/mob/living/simple_animal/hostile/blackmesa/blackops
diff --git a/modular_skyrat/modules/blueshield/code/blueshield.dm b/modular_skyrat/modules/blueshield/code/blueshield.dm
index 183bc4263c5d67..ed0cd1e27307eb 100644
--- a/modular_skyrat/modules/blueshield/code/blueshield.dm
+++ b/modular_skyrat/modules/blueshield/code/blueshield.dm
@@ -37,7 +37,6 @@
/obj/item/stack/spacecash/c500 = 3,
/obj/item/disk/nuclear/fake/obvious = 2,
/obj/item/clothing/head/collectable/captain = 4,
- /obj/projectile/bullet/b460 = 1
)
veteran_only = TRUE
@@ -53,9 +52,6 @@
shoes = /obj/item/clothing/shoes/jackboots
ears = /obj/item/radio/headset/headset_bs/alt
glasses = /obj/item/clothing/glasses/hud/security/sunglasses
- backpack_contents = list(
- /obj/item/storage/box/gunset/blueshield = 1,
- )
implants = list(/obj/item/implant/mindshield)
backpack = /obj/item/storage/backpack/blueshield
satchel = /obj/item/storage/backpack/satchel/blueshield
diff --git a/modular_skyrat/modules/blueshield/code/closet.dm b/modular_skyrat/modules/blueshield/code/closet.dm
index 1ceaf37325b6f2..8c7a3c195c9c14 100644
--- a/modular_skyrat/modules/blueshield/code/closet.dm
+++ b/modular_skyrat/modules/blueshield/code/closet.dm
@@ -29,4 +29,5 @@
new /obj/item/restraints/handcuffs(src)
new /obj/item/clothing/glasses/hud/security/sunglasses(src)
new /obj/item/storage/medkit/tactical/blueshield(src)
+ new /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano(src)
new /obj/item/storage/bag/garment/blueshield(src)
diff --git a/modular_skyrat/modules/blueshield/code/clothing.dm b/modular_skyrat/modules/blueshield/code/clothing.dm
index d25af6a26dd6b5..0875af9051048c 100644
--- a/modular_skyrat/modules/blueshield/code/clothing.dm
+++ b/modular_skyrat/modules/blueshield/code/clothing.dm
@@ -21,8 +21,8 @@
worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/ears.dmi'
icon_state = "bshield_headset"
worn_icon_state = "bshield_headset"
- keyslot = new /obj/item/encryptionkey/heads/blueshield
- keyslot2 = new /obj/item/encryptionkey/headset_cent
+ keyslot = /obj/item/encryptionkey/heads/blueshield
+ keyslot2 = /obj/item/encryptionkey/headset_cent
/obj/item/radio/headset/headset_bs/alt
icon_state = "bshield_headset_alt"
@@ -39,16 +39,9 @@
icon = 'modular_skyrat/master_files/icons/obj/clothing/head/plasmaman_hats.dmi'
worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/head/plasmaman_head.dmi'
icon_state = "bs_envirohelm"
- armor_type = /datum/armor/helmet_plasmaman_blueshield
-
-/datum/armor/helmet_plasmaman_blueshield
- melee = 30
- bullet = 20
- laser = 20
- energy = 20
- bomb = 25
- bio = 100
- fire = 100
+ armor_type = /datum/armor/head_helmet/plasmaman/blueshield
+
+/datum/armor/head_helmet/plasmaman/blueshield
acid = 90
/obj/item/clothing/under/plasmaman/blueshield
@@ -77,15 +70,10 @@
greyscale_colors = "#3A4E7D#DEB63D"
//alternate_worn_icon_digi = 'modular_skyrat/icons/mob/head_muzzled.dmi'
icon_state = "beret_badge_police"
- armor_type = /datum/armor/beret_blueshield
+ armor_type = /datum/armor/head_helmet/blueshield
supports_variations_flags = CLOTHING_SNOUTED_VARIATION_NO_NEW_ICON
-/datum/armor/beret_blueshield
- melee = 35
- bullet = 25
- laser = 25
- energy = 15
- bomb = 25
+/datum/armor/head_helmet/blueshield
fire = 75
acid = 75
@@ -132,13 +120,30 @@
desc = "A tight-fitting kevlar-lined vest with a blue badge on the chest of it."
icon_state = "blueshieldarmor"
body_parts_covered = CHEST
- armor_type = /datum/armor/vest_blueshield
-
-/datum/armor/vest_blueshield
- melee = 35
- bullet = 25
- laser = 25
- energy = 25
+ armor_type = /datum/armor/suit_armor/blueshield
+ uses_advanced_reskins = TRUE
+ unique_reskin = list(
+ "Slim" = list(
+ RESKIN_ICON = 'modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi',
+ RESKIN_ICON_STATE = "blueshieldarmor",
+ RESKIN_WORN_ICON = 'modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi',
+ RESKIN_WORN_ICON_STATE = "blueshieldarmor",
+ ),
+ "Marine" = list(
+ RESKIN_ICON = 'modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi',
+ RESKIN_ICON_STATE = "bs_marine",
+ RESKIN_WORN_ICON = 'modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi',
+ RESKIN_WORN_ICON_STATE = "bs_marine",
+ ),
+ "Bulky" = list(
+ RESKIN_ICON = 'modular_skyrat/master_files/icons/obj/clothing/suits/armor.dmi',
+ RESKIN_ICON_STATE = "vest_black",
+ RESKIN_WORN_ICON = 'modular_skyrat/master_files/icons/mob/clothing/suits/armor.dmi',
+ RESKIN_WORN_ICON_STATE = "vest_black",
+ ),
+ )
+
+/datum/armor/suit_armor/blueshield
bomb = 30
fire = 75
acid = 75
@@ -148,6 +153,7 @@
desc = "An expensive kevlar-lined jacket with a golden badge on the chest and \"NT\" emblazoned on the back. It weighs surprisingly little, despite how heavy it looks."
icon_state = "blueshield"
body_parts_covered = CHEST|ARMS
+ unique_reskin = null
/obj/item/clothing/suit/armor/vest/blueshield/jacket/Initialize(mapload)
. = ..()
@@ -159,16 +165,7 @@
desc = "A comfy kevlar-lined coat with blue highlights, fit to keep the blueshield armored and warm."
hoodtype = /obj/item/clothing/head/hooded/winterhood/skyrat/blueshield
allowed = list(/obj/item/melee/baton/security/loaded)
- armor_type = /datum/armor/wintercoat_blueshield
-
-/datum/armor/wintercoat_blueshield
- melee = 35
- bullet = 25
- laser = 25
- energy = 25
- bomb = 30
- fire = 75
- acid = 75
+ armor_type = /datum/armor/suit_armor/blueshield
/obj/item/clothing/suit/hooded/wintercoat/skyrat/blueshield/Initialize(mapload)
. = ..()
@@ -177,13 +174,4 @@
/obj/item/clothing/head/hooded/winterhood/skyrat/blueshield
icon_state = "hood_blueshield"
desc = "A comfy kevlar-lined hood to go with the comfy kevlar-lined coat."
- armor_type = /datum/armor/winterhood_blueshield
-
-/datum/armor/winterhood_blueshield
- melee = 35
- bullet = 25
- laser = 25
- energy = 15
- bomb = 25
- fire = 75
- acid = 75
+ armor_type = /datum/armor/suit_armor/blueshield
diff --git a/modular_skyrat/modules/blueshield/code/encryptionkey.dm b/modular_skyrat/modules/blueshield/code/encryptionkey.dm
index 952cb3e8c2ea72..deddf9bc4eb8ee 100644
--- a/modular_skyrat/modules/blueshield/code/encryptionkey.dm
+++ b/modular_skyrat/modules/blueshield/code/encryptionkey.dm
@@ -1,5 +1,6 @@
/obj/item/encryptionkey/heads/blueshield
name = "\proper the blueshield's encryption key"
- icon = 'modular_skyrat/modules/blueshield/icons/radio.dmi'
- icon_state = "bshield_cypherkey"
+ icon_state = "cypherkey_centcom"
channels = list(RADIO_CHANNEL_COMMAND = 1, RADIO_CHANNEL_SECURITY = 1)
+ greyscale_config = /datum/greyscale_config/encryptionkey_centcom
+ greyscale_colors = "#1d2657#dca01b"
diff --git a/modular_skyrat/modules/blueshield/code/weapons.dm b/modular_skyrat/modules/blueshield/code/weapons.dm
deleted file mode 100644
index 089dedefb15480..00000000000000
--- a/modular_skyrat/modules/blueshield/code/weapons.dm
+++ /dev/null
@@ -1,92 +0,0 @@
-// CMG gunset - IN USE
-/obj/item/storage/box/gunset/blueshield
- name = "Blueshield's CMG-2 gunset"
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/storage/box/gunset/blueshield/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/cmg/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg/lethal(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg/lethal(src)
- new /obj/item/suppressor/nanotrasen(src)
-
-//suppressor for the CMG
-/obj/item/suppressor/nanotrasen
- name = "NT-S suppressor"
- desc = "A Nanotrasen brand small-arms suppressor, including a large NT logo stamped on the side."
-
-
-// -----------
-// Unused guns:
-// -----------
-
-//Energy Revolver
-/obj/item/gun/energy/e_gun/revolver //The virgin gun.
- name = "energy revolver"
- desc = "An advanced energy revolver with the capacity to shoot both electrodes and lasers."
- force = 7
- ammo_type = list(/obj/item/ammo_casing/energy/electrode, /obj/item/ammo_casing/energy/laser)
- ammo_x_offset = 1
- charge_sections = 4
- fire_delay = 4
- icon = 'modular_skyrat/modules/blueshield/icons/energy.dmi'
- icon_state = "bsgun"
- inhand_icon_state = "minidisable"
- lefthand_file = 'modular_skyrat/modules/blueshield/icons/guns_lefthand.dmi'
- righthand_file = 'modular_skyrat/modules/blueshield/icons/guns_righthand.dmi'
- obj_flags = UNIQUE_RENAME
- cell_type = /obj/item/stock_parts/cell/blueshield
- pin = /obj/item/firing_pin/implant/mindshield
- selfcharge = TRUE
-
-/obj/item/stock_parts/cell/blueshield
- name = "internal revolver power cell"
- maxcharge = 1500
- chargerate = 300
-
-//PDW-9 taser pistol
-/obj/item/gun/energy/e_gun/revolver/pdw9 //The chad gun.
- name = "PDW-9 taser pistol"
- desc = "A military grade energy sidearm, used by many militia forces throughout the local sector. It comes with an internally recharging battery which is slow to recharge."
- ammo_x_offset = 2
- icon_state = "pdw9pistol"
- inhand_icon_state = null
- cell_type = /obj/item/stock_parts/cell/pdw9
-
-/obj/item/stock_parts/cell/pdw9
- name = "internal pistol power cell"
- maxcharge = 1000
- chargerate = 300
- var/obj/item/gun/energy/e_gun/revolver/pdw9/parent
-
-/obj/item/stock_parts/cell/pdw9/Initialize(mapload)
- . = ..()
- parent = loc
-
-/obj/item/stock_parts/cell/pdw9/process()
- . = ..()
- parent.update_icon()
-
-//Allstar SC-3 PDW 'Hellfire'
-/obj/item/gun/energy/laser/hellgun/blueshield
- name = "\improper Allstar SC-3 PDW 'Hellfire'"
- desc = "A prototype energy carbine, despite NT's ban on hellfire weaponry due to negative press. \
- Allstar continued to work on it, compacting it into a small form-factor for personal defense. \
- As part of the Asset Retention Program created by Nanotrasen, Allstar's prototype began to be put into use."
- icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns.dmi'
- worn_icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi'
- lefthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi'
- righthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi'
- icon_state = "hellfirepdw"
- worn_icon_state = "hellfirepdw"
- ammo_type = list(/obj/item/ammo_casing/energy/laser/hellfire/bs)
-
-/obj/item/gun/energy/laser/hellgun/blueshield/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ALLSTAR)
-
-/obj/item/ammo_casing/energy/laser/hellfire/bs
- projectile_type = /obj/projectile/beam/laser/hellfire
- e_cost = 83 //Lets it squeeze out a few more shots
- select_name = "maim"
diff --git a/modular_skyrat/modules/blueshield/icons/radio.dmi b/modular_skyrat/modules/blueshield/icons/radio.dmi
index 94581e4346f385..9c5184305e1536 100644
Binary files a/modular_skyrat/modules/blueshield/icons/radio.dmi and b/modular_skyrat/modules/blueshield/icons/radio.dmi differ
diff --git a/modular_skyrat/modules/bulletrebalance/code/sniper.dm b/modular_skyrat/modules/bulletrebalance/code/sniper.dm
deleted file mode 100644
index 632f80251fd92e..00000000000000
--- a/modular_skyrat/modules/bulletrebalance/code/sniper.dm
+++ /dev/null
@@ -1,22 +0,0 @@
-/obj/projectile/bullet/p50
- name =".416 Stabilis bullet"
- speed = 0.2 //This means it's insanely fast, not insanely slow
- damage = 110 //You have 135 health, which does not make this an instant crit
- paralyze = 0 //Knocks you on your ass hard enough as-is, we won't need the paralyze stat
- dismemberment = 30
- armour_penetration = 61 //Bulletproof armor alone will not stop this
- wound_bonus = 90 //Theoretically guaranteed wound
-
-/obj/projectile/bullet/p50/soporific
- name = ".416 Stabilis tranquilizer casing"
- damage_type = STAMINA
- dismemberment = 0
- catastropic_dismemberment = FALSE
- object_damage = 0
-
-/obj/projectile/bullet/p50/soporific/on_hit(atom/target, blocked = FALSE)
- . = ..()
- if((blocked != 100) && isliving(target))
- var/mob/living/living_guy = target
- living_guy.Sleeping(40 SECONDS) //Yes, its really 40 seconds of sleep, I hope you had your morning coffee.
-
diff --git a/modular_skyrat/modules/clock_cult/code/items/weaponry.dm b/modular_skyrat/modules/clock_cult/code/items/weaponry.dm
index 9704cd0d4ef99b..92bd53531dd78f 100644
--- a/modular_skyrat/modules/clock_cult/code/items/weaponry.dm
+++ b/modular_skyrat/modules/clock_cult/code/items/weaponry.dm
@@ -291,6 +291,7 @@
icon = 'modular_skyrat/modules/clock_cult/icons/weapons/ammo.dmi'
icon_state = "762_brass"
ammo_type = /obj/item/ammo_casing/strilka310/lionhunter/clock
+ unique_reskin = NONE
max_ammo = 3
multiple_sprites = AMMO_BOX_PER_BULLET
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/bolt_nanotrasen_firearms.dm b/modular_skyrat/modules/company_imports/code/armament_datums/bolt_nanotrasen_firearms.dm
deleted file mode 100644
index 0d744d4b38ec80..00000000000000
--- a/modular_skyrat/modules/company_imports/code/armament_datums/bolt_nanotrasen_firearms.dm
+++ /dev/null
@@ -1,112 +0,0 @@
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons
- category = BOLT_NANOTRASEN_DEFENSE_NAME
- company_bitflag = CARGO_COMPANY_BOLT_NANOTRASEN
-
-// Basic armor vests
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/armor
- subcategory = "Light Body Armor"
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/armor/slim_vest
- name = "type I vest - slim"
- item_type = /obj/item/clothing/suit/armor/vest
- cost = PAYCHECK_CREW * 3
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/armor/normal_vest
- name = "type I vest - normal"
- item_type = /obj/item/clothing/suit/armor/vest/alt
- cost = PAYCHECK_CREW * 3
-
-// Fully non-lethal weapons
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/nonlethal
- subcategory = "Non-Lethal Weapons"
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/nonlethal/responder
- item_type = /obj/item/gun/energy/disabler/bolt_disabler
- cost = PAYCHECK_CREW * 5
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/nonlethal/pepperball
- item_type = /obj/item/gun/ballistic/automatic/pistol/pepperball
- cost = PAYCHECK_CREW * 5
-
-// Lethal pistols, requires some company interest first
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/lethal_sidearm
- subcategory = "Lethal Sidearms"
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/lethal_sidearm/detective_revolver
- item_type = /obj/item/gun/ballistic/revolver/c38/detective
- cost = PAYCHECK_COMMAND * 4
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/lethal_sidearm/g17
- item_type = /obj/item/gun/ballistic/automatic/pistol/g17
- cost = PAYCHECK_COMMAND * 4
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/lethal_sidearm/mk58
- item_type = /obj/item/gun/ballistic/automatic/pistol/mk58
- cost = PAYCHECK_COMMAND * 4
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/lethal_sidearm/m1911
- item_type = /obj/item/gun/ballistic/automatic/pistol/m1911
- cost = PAYCHECK_COMMAND * 4
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines
- subcategory = "Sidearm Magazines"
- cost = PAYCHECK_CREW
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/c38speedloader
- item_type = /obj/item/ammo_box/c38
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/c38speedloader_rubber
- item_type = /obj/item/ammo_box/c38/match/bouncy
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/g17
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g17
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/g17_rubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g17/rubber
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/mk58
- item_type = /obj/item/ammo_box/magazine/multi_sprite/mk58
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/mk58_rubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/mk58/rubber
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/sidearm_magazines/m1911
- item_type = /obj/item/ammo_box/magazine/m45
-
-// Lethal anything that's not a pistol, requires high company interest
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm
- subcategory = "Lethal Longarms"
- restricted = TRUE
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm/riot_shotgun
- item_type = /obj/item/gun/ballistic/shotgun/riot
- cost = PAYCHECK_COMMAND * 6
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm/m23
- item_type = /obj/item/gun/ballistic/shotgun/m23
- cost = PAYCHECK_COMMAND * 8
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm/wt550
- item_type = /obj/item/gun/ballistic/automatic/wt550
- cost = PAYCHECK_COMMAND * 6
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm/cmg
- item_type = /obj/item/gun/ballistic/automatic/cmg
- cost = PAYCHECK_COMMAND * 6
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm_magazines
- subcategory = "Longarm Magazines"
- cost = PAYCHECK_CREW
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm_magazines/wt550
- item_type = /obj/item/ammo_box/magazine/wt550m9
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm_magazines/cmg
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cmg/lethal
-
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/longarm_magazines/cmg_rubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cmg
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm b/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm
index 40f8ef775f7bf0..d6e5be0049e3e2 100644
--- a/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm
+++ b/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm
@@ -226,21 +226,3 @@
/datum/armament_entry/company_import/deforest/cyber_organs/augments/breathing_tube
name = "breathing tube implant"
item_type = /obj/item/organ/internal/cyberimp/mouth/breathing_tube
-
-// Personal Defense Weapons (For when the pharmacist must become the harmacist)
-
-/datum/armament_entry/company_import/deforest/defense
- subcategory = "Personal Defense Equipment"
- contraband = TRUE
-
-/datum/armament_entry/company_import/deforest/defense/firefly
- item_type = /obj/item/gun/ballistic/automatic/pistol/firefly
- cost = PAYCHECK_COMMAND * 4
-
-/datum/armament_entry/company_import/deforest/defense/firefly_mag
- item_type = /obj/item/ammo_box/magazine/multi_sprite/firefly
- cost = PAYCHECK_CREW
-
-/datum/armament_entry/company_import/deforest/defense/firefly_mag_rubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/firefly/rubber
- cost = PAYCHECK_CREW
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/microstar_energy.dm b/modular_skyrat/modules/company_imports/code/armament_datums/microstar_energy.dm
index 35c89c03b1feb8..060a149e5dfa73 100644
--- a/modular_skyrat/modules/company_imports/code/armament_datums/microstar_energy.dm
+++ b/modular_skyrat/modules/company_imports/code/armament_datums/microstar_energy.dm
@@ -15,7 +15,7 @@
item_type = /obj/item/gun/energy/e_gun/mini
cost = PAYCHECK_CREW * 5
-/datum/armament_entry/company_import/nanotrasen_bolt_weapons/lethal_sidearm/energy_holster
+/datum/armament_entry/company_import/microstar/lethal_sidearm/energy_holster
item_type = /obj/item/storage/belt/holster/energy/thermal
cost = PAYCHECK_COMMAND * 6
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/put_a_donk_on_it.dm b/modular_skyrat/modules/company_imports/code/armament_datums/put_a_donk_on_it.dm
index 97d8307efd9a42..534801d0031a2f 100644
--- a/modular_skyrat/modules/company_imports/code/armament_datums/put_a_donk_on_it.dm
+++ b/modular_skyrat/modules/company_imports/code/armament_datums/put_a_donk_on_it.dm
@@ -99,6 +99,18 @@
item_type = /obj/item/gun/ballistic/automatic/l6_saw/toy/unrestricted
cost = PAYCHECK_COMMAND * 5
+/datum/armament_entry/company_import/donk/mod_modules
+ subcategory = "Donk Co. MOD modules"
+ cost = PAYCHECK_COMMAND
+
+/datum/armament_entry/company_import/donk/mod_modules/dart_collector_safe
+ item_type = /obj/item/mod/module/recycler/donk/safe
+ cost = PAYCHECK_COMMAND
+
+/datum/armament_entry/company_import/donk/mod_modules/dart_collector
+ item_type = /obj/item/mod/module/recycler/donk
+ cost = PAYCHECK_COMMAND * 4
+
/datum/armament_entry/company_import/donk/foamforce_ammo
subcategory = "Foam Force (TM) Dart Accessories"
cost = PAYCHECK_CREW
@@ -111,14 +123,14 @@
item_type = /obj/item/ammo_box/foambox/riot
cost = PAYCHECK_COMMAND * 1.5
-/datum/armament_entry/company_import/donk/foamforce_ammo
+/datum/armament_entry/company_import/donk/foamforce_ammo/pistol_mag
item_type = /obj/item/ammo_box/magazine/toy/pistol
-/datum/armament_entry/company_import/donk/foamforce_ammo
+/datum/armament_entry/company_import/donk/foamforce_ammo/smg_mag
item_type = /obj/item/ammo_box/magazine/toy/smg
-/datum/armament_entry/company_import/donk/foamforce_ammo
+/datum/armament_entry/company_import/donk/foamforce_ammo/smgm45_mag
item_type = /obj/item/ammo_box/magazine/toy/smgm45
-/datum/armament_entry/company_import/donk/foamforce_ammo
+/datum/armament_entry/company_import/donk/foamforce_ammo/m762_mag
item_type = /obj/item/ammo_box/magazine/toy/m762
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/sol_defense.dm b/modular_skyrat/modules/company_imports/code/armament_datums/sol_defense.dm
new file mode 100644
index 00000000000000..0df662cd959bfb
--- /dev/null
+++ b/modular_skyrat/modules/company_imports/code/armament_datums/sol_defense.dm
@@ -0,0 +1,115 @@
+/datum/armament_entry/company_import/sol_defense
+ category = SOL_DEFENSE_DEFENSE_NAME
+ company_bitflag = CARGO_COMPANY_SOL_DEFENSE
+
+// Basic armor vests
+
+/datum/armament_entry/company_import/sol_defense/armor
+ subcategory = "Light Body Armor"
+
+/datum/armament_entry/company_import/sol_defense/armor/slim_vest
+ name = "type I vest - slim"
+ item_type = /obj/item/clothing/suit/armor/vest
+ cost = PAYCHECK_CREW * 3
+
+/datum/armament_entry/company_import/sol_defense/armor/normal_vest
+ name = "type I vest - normal"
+ item_type = /obj/item/clothing/suit/armor/vest/alt
+ cost = PAYCHECK_CREW * 3
+
+/datum/armament_entry/company_import/sol_defense/case
+ subcategory = "Weapon Cases"
+
+/datum/armament_entry/company_import/sol_defense/case/trappiste
+ item_type = /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/empty
+ cost = PAYCHECK_COMMAND
+
+/datum/armament_entry/company_import/sol_defense/case/carwo
+ item_type = /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/empty
+ cost = PAYCHECK_COMMAND * 2
+
+/datum/armament_entry/company_import/sol_defense/sidearm
+ subcategory = "Sidearms"
+
+/datum/armament_entry/company_import/sol_defense/sidearm/eland
+ item_type = /obj/item/gun/ballistic/revolver/sol
+ cost = PAYCHECK_COMMAND * 4
+
+/datum/armament_entry/company_import/sol_defense/sidearm/wespe
+ item_type = /obj/item/gun/ballistic/automatic/pistol/sol
+ cost = PAYCHECK_COMMAND * 4
+
+/datum/armament_entry/company_import/sol_defense/sidearm/skild
+ item_type = /obj/item/gun/ballistic/automatic/pistol/trappiste
+ cost = PAYCHECK_COMMAND * 6
+
+/datum/armament_entry/company_import/sol_defense/sidearm/takbok
+ item_type = /obj/item/gun/ballistic/revolver/takbok
+ cost = PAYCHECK_COMMAND * 6
+
+// Lethal anything that's not a pistol, requires high company interest
+
+/datum/armament_entry/company_import/sol_defense/longarm
+ subcategory = "Longarms"
+ restricted = TRUE
+
+/datum/armament_entry/company_import/sol_defense/longarm/renoster
+ item_type = /obj/item/gun/ballistic/shotgun/riot/sol
+ cost = PAYCHECK_COMMAND * 6
+
+/datum/armament_entry/company_import/sol_defense/longarm/sindano
+ item_type = /obj/item/gun/ballistic/automatic/sol_smg
+ cost = PAYCHECK_COMMAND * 6
+
+/datum/armament_entry/company_import/sol_defense/longarm/elite
+ item_type = /obj/item/gun/ballistic/automatic/sol_rifle/marksman
+ cost = PAYCHECK_COMMAND * 12
+
+/datum/armament_entry/company_import/sol_defense/longarm/infanterie
+ item_type = /obj/item/gun/ballistic/automatic/sol_rifle
+ cost = PAYCHECK_COMMAND * 14
+ contraband = TRUE
+
+/datum/armament_entry/company_import/sol_defense/longarm/outomaties
+ item_type = /obj/item/gun/ballistic/automatic/sol_rifle/machinegun
+ cost = PAYCHECK_COMMAND * 23
+ contraband = TRUE
+
+/datum/armament_entry/company_import/sol_defense/longarm/kiboko
+ item_type = /obj/item/gun/ballistic/automatic/sol_grenade_launcher
+ cost = PAYCHECK_COMMAND * 46
+ contraband = TRUE
+
+/datum/armament_entry/company_import/sol_defense/magazines
+ subcategory = "Magazines"
+ cost = PAYCHECK_CREW
+
+/datum/armament_entry/company_import/sol_defense/magazines/c35_mag
+ item_type = /obj/item/ammo_box/magazine/c35sol_pistol/starts_empty
+
+/datum/armament_entry/company_import/sol_defense/magazines/c35_extended
+ item_type = /obj/item/ammo_box/magazine/c35sol_pistol/stendo/starts_empty
+
+/datum/armament_entry/company_import/sol_defense/magazines/c585_mag
+ item_type = /obj/item/ammo_box/magazine/c585trappiste_pistol/spawns_empty
+
+/datum/armament_entry/company_import/sol_defense/magazines/sol_rifle_short
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/starts_empty
+
+/datum/armament_entry/company_import/sol_defense/magazines/sol_rifle_standard
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/standard/starts_empty
+ cost = PAYCHECK_COMMAND
+
+/datum/armament_entry/company_import/sol_defense/magazines/sol_rifle_drum
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/drum/starts_empty
+ cost = PAYCHECK_COMMAND * 3
+ contraband = TRUE
+
+/datum/armament_entry/company_import/sol_defense/magazines/sol_grenade_standard
+ item_type = /obj/item/ammo_box/magazine/c980_grenade/starts_empty
+ cost = PAYCHECK_COMMAND * 2
+
+/datum/armament_entry/company_import/sol_defense/magazines/sol_grenade_drum
+ item_type = /obj/item/ammo_box/magazine/c980_grenade/drum/starts_empty
+ cost = PAYCHECK_CREW * 3
+ contraband = TRUE
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/vitezstvi_ammo.dm b/modular_skyrat/modules/company_imports/code/armament_datums/vitezstvi_ammo.dm
index 3e6df12d1246e8..d6083b39cbc956 100644
--- a/modular_skyrat/modules/company_imports/code/armament_datums/vitezstvi_ammo.dm
+++ b/modular_skyrat/modules/company_imports/code/armament_datums/vitezstvi_ammo.dm
@@ -11,7 +11,7 @@
item_type = /obj/item/circuitboard/machine/ammo_workbench
cost = PAYCHECK_COMMAND * 5
-/datum/armament_entry/company_import/vitezstvi/ammo_bench
+/datum/armament_entry/company_import/vitezstvi/ammo_bench/ammo_disk
item_type = /obj/item/disk/ammo_workbench/advanced
cost = PAYCHECK_COMMAND * 5
@@ -19,26 +19,28 @@
item_type = /obj/item/circuitboard/machine/dish_drive/bullet
cost = PAYCHECK_COMMAND * 2
-// Boxes of non-shotgun ammo
+// Weapon accessories
-/datum/armament_entry/company_import/vitezstvi/ammo_boxes
- subcategory = "Ammunition Boxes"
- cost = PAYCHECK_CREW
+/datum/armament_entry/company_import/vitezstvi/accessory
+ subcategory = "Weapon Accessories"
-/datum/armament_entry/company_import/vitezstvi/ammo_boxes/pepperball
- item_type = /obj/item/ammo_box/advanced/pepperballs
+/datum/armament_entry/company_import/vitezstvi/accessory/suppressor
+ item_type = /obj/item/suppressor
+ cost = PAYCHECK_COMMAND
-/datum/armament_entry/company_import/vitezstvi/ammo_boxes/m1911_lethals
- item_type = /obj/item/ammo_box/c45
+/datum/armament_entry/company_import/vitezstvi/accessory/small_case
+ item_type = /obj/item/storage/toolbox/guncase/skyrat/pistol/empty
+ cost = PAYCHECK_COMMAND
-/datum/armament_entry/company_import/vitezstvi/ammo_boxes/wt550_lethals
- item_type = /obj/item/ammo_box/c46x30mm
+/datum/armament_entry/company_import/vitezstvi/accessory/large_case
+ item_type = /obj/item/storage/toolbox/guncase/skyrat/empty
+ cost = PAYCHECK_COMMAND * 2
-/datum/armament_entry/company_import/vitezstvi/ammo_boxes/wt550_piercing
- item_type = /obj/item/ammo_box/c46x30mm/ap
+// Boxes of non-shotgun ammo
-/datum/armament_entry/company_import/vitezstvi/ammo_boxes/wt550_rubber
- item_type = /obj/item/ammo_box/c46x30mm/rubber
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes
+ subcategory = "Ammunition Boxes"
+ cost = PAYCHECK_CREW
/datum/armament_entry/company_import/vitezstvi/ammo_boxes/peacekeeper_lethal
item_type = /obj/item/ammo_box/c9mm
@@ -73,6 +75,36 @@
/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sabel_blank
item_type = /obj/item/ammo_box/c56mm/blank
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol35
+ item_type = /obj/item/ammo_box/c35sol
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol35_disabler
+ item_type = /obj/item/ammo_box/c35sol/incapacitator
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol35_ripper
+ item_type = /obj/item/ammo_box/c35sol/ripper
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol40
+ item_type = /obj/item/ammo_box/c40sol
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol40_disabler
+ item_type = /obj/item/ammo_box/c40sol/fragmentation
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol40_flame
+ item_type = /obj/item/ammo_box/c40sol/incendiary
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/sol40_pierce
+ item_type = /obj/item/ammo_box/c40sol/pierce
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/trappiste585
+ item_type = /obj/item/ammo_box/c585trappiste
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/trappiste585_disabler
+ item_type = /obj/item/ammo_box/c585trappiste/incapacitator
+
+/datum/armament_entry/company_import/vitezstvi/ammo_boxes/trappiste585_hollowpoint
+ item_type = /obj/item/ammo_box/c585trappiste/hollowpoint
+
// Revolver speedloaders
/datum/armament_entry/company_import/vitezstvi/speedloader
@@ -121,3 +153,26 @@
/datum/armament_entry/company_import/vitezstvi/shot_shells/confetti
item_type = /obj/item/ammo_box/advanced/s12gauge/honk
description = "A box of 35 confetti shells, firing a spread of harmless confetti everywhere, yippie!"
+
+// Boxes of kiboko launcher ammo
+
+/datum/armament_entry/company_import/vitezstvi/grenade_shells
+ subcategory = "Grenade Shells"
+ cost = PAYCHECK_COMMAND
+
+/datum/armament_entry/company_import/vitezstvi/grenade_shells/practice
+ item_type = /obj/item/ammo_box/c980grenade
+
+/datum/armament_entry/company_import/vitezstvi/grenade_shells/smoke
+ item_type = /obj/item/ammo_box/c980grenade/smoke
+
+/datum/armament_entry/company_import/vitezstvi/grenade_shells/riot
+ item_type = /obj/item/ammo_box/c980grenade/riot
+
+/datum/armament_entry/company_import/vitezstvi/grenade_shells/shrapnel
+ item_type = /obj/item/ammo_box/c980grenade/shrapnel
+ contraband = TRUE
+
+/datum/armament_entry/company_import/vitezstvi/grenade_shells/phosphor
+ item_type = /obj/item/ammo_box/c980grenade/shrapnel/phosphor
+ contraband = TRUE
diff --git a/modular_skyrat/modules/company_imports/code/company_datums.dm b/modular_skyrat/modules/company_imports/code/company_datums.dm
index cdbfddaa03ddd0..263c8214ffc8df 100644
--- a/modular_skyrat/modules/company_imports/code/company_datums.dm
+++ b/modular_skyrat/modules/company_imports/code/company_datums.dm
@@ -82,8 +82,8 @@
// A coalition between nt and bolt to sell personal defense equipment and weapons
/datum/cargo_company/nanotrasen_bolt_weapons
- name = BOLT_NANOTRASEN_DEFENSE_NAME
- company_flag = CARGO_COMPANY_BOLT_NANOTRASEN
+ name = SOL_DEFENSE_DEFENSE_NAME
+ company_flag = CARGO_COMPANY_SOL_DEFENSE
cost = 6000
cost_change_lower = -1000
cost_change_upper = 4000
diff --git a/modular_skyrat/modules/company_imports/code/objects/vitezstvi/ammo_boxes.dm b/modular_skyrat/modules/company_imports/code/objects/vitezstvi/ammo_boxes.dm
index 20ebab5002f3ca..fc7fb6015317f9 100644
--- a/modular_skyrat/modules/company_imports/code/objects/vitezstvi/ammo_boxes.dm
+++ b/modular_skyrat/modules/company_imports/code/objects/vitezstvi/ammo_boxes.dm
@@ -74,25 +74,6 @@
name = "ammo box (.34 incendiary)"
ammo_type = /obj/item/ammo_casing/c34_incendiary
-/obj/item/ammo_box/c12mm
- name = "ammo box (12mm)"
- icon = 'modular_skyrat/modules/company_imports/icons/ammo.dmi'
- icon_state = "ammo_12mm"
- ammo_type = /obj/item/ammo_casing/c12mm
- max_ammo = 20
-
-/obj/item/ammo_box/c12mm/ap
- name = "ammo box (12mm AP)"
- ammo_type = /obj/item/ammo_casing/c12mm/ap
-
-/obj/item/ammo_box/c12mm/hp
- name = "ammo box (12mm HP)"
- ammo_type = /obj/item/ammo_casing/c12mm/hp
-
-/obj/item/ammo_box/c12mm/fire
- name = "ammo box (12mm incendiary)"
- ammo_type = /obj/item/ammo_casing/c12mm/fire
-
/obj/item/ammo_box/c56mm
name = "ammo box (5.6mm civilian)"
desc = "5.6x40mm ammunition specifically made for civilian use like recreation, hunting, self-defense or LARP. While the package itself lacks any real identification \
diff --git a/modular_skyrat/modules/contractor/code/datums/contractor_datum.dm b/modular_skyrat/modules/contractor/code/datums/contractor_datum.dm
index f7d4e09f41e5fd..5346d412702b1d 100644
--- a/modular_skyrat/modules/contractor/code/datums/contractor_datum.dm
+++ b/modular_skyrat/modules/contractor/code/datums/contractor_datum.dm
@@ -18,7 +18,8 @@
// Special case for reinforcements, we want to show their ckey and name on round end.
if (istype(contractor_purchase, /datum/contractor_item/contractor_partner))
var/datum/contractor_item/contractor_partner/partner = contractor_purchase
- contractor_support_unit += " [partner.partner_mind.key] played [partner.partner_mind.current.name], their contractor support unit."
+ var/mob/living/carbon/human/partner_mob = partner.partner_mind.current
+ contractor_support_unit += " [partner_mob.name] was [partner_mob.p_their()] contractor support unit."
if (length(contractor_hub.purchased_items))
result += " (used [total_spent_rep] Rep) "
diff --git a/modular_skyrat/modules/cortical_borer/code/cortical_borer_abilities.dm b/modular_skyrat/modules/cortical_borer/code/cortical_borer_abilities.dm
index 8a239f9c9a5328..fae00851ad41d9 100644
--- a/modular_skyrat/modules/cortical_borer/code/cortical_borer_abilities.dm
+++ b/modular_skyrat/modules/cortical_borer/code/cortical_borer_abilities.dm
@@ -50,7 +50,7 @@
name = "Open Chemical Injector"
button_icon_state = "chemical"
-/datum/action/cooldown/borer/inject_chemical/Trigger(trigger_flags)
+/datum/action/cooldown/borer/inject_chemical/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -141,7 +141,7 @@
name = "Open Evolution Tree"
button_icon_state = "newability"
-/datum/action/cooldown/borer/evolution_tree/Trigger(trigger_flags)
+/datum/action/cooldown/borer/evolution_tree/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -230,7 +230,7 @@
name = "Learn Focus"
button_icon_state = "getfocus"
-/datum/action/cooldown/borer/learn_focus/Trigger(trigger_flags)
+/datum/action/cooldown/borer/learn_focus/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -268,7 +268,7 @@
button_icon_state = "bloodchem"
chemical_evo_points = 5
-/datum/action/cooldown/borer/learn_bloodchemical/Trigger(trigger_flags)
+/datum/action/cooldown/borer/learn_bloodchemical/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -314,7 +314,7 @@
button_icon_state = "bloodlevel"
chemical_evo_points = 1
-/datum/action/cooldown/borer/upgrade_chemical/Trigger(trigger_flags)
+/datum/action/cooldown/borer/upgrade_chemical/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -349,7 +349,7 @@
button_icon_state = "level"
stat_evo_points = 1
-/datum/action/cooldown/borer/upgrade_stat/Trigger(trigger_flags)
+/datum/action/cooldown/borer/upgrade_stat/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -379,7 +379,7 @@
name = "Toggle Hiding"
button_icon_state = "hide"
-/datum/action/cooldown/borer/toggle_hiding/Trigger(trigger_flags)
+/datum/action/cooldown/borer/toggle_hiding/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -401,7 +401,7 @@
cooldown_time = 12 SECONDS
button_icon_state = "fear"
-/datum/action/cooldown/borer/fear_human/Trigger(trigger_flags)
+/datum/action/cooldown/borer/fear_human/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -464,7 +464,7 @@
cooldown_time = 5 SECONDS
button_icon_state = "blood"
-/datum/action/cooldown/borer/check_blood/Trigger(trigger_flags)
+/datum/action/cooldown/borer/check_blood/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -485,7 +485,7 @@
cooldown_time = 10 SECONDS
button_icon_state = "host"
-/datum/action/cooldown/borer/choosing_host/Trigger(trigger_flags)
+/datum/action/cooldown/borer/choosing_host/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -593,7 +593,7 @@
cooldown_time = 30 SECONDS
button_icon_state = "speak"
-/datum/action/cooldown/borer/force_speak/Trigger(trigger_flags)
+/datum/action/cooldown/borer/force_speak/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -628,7 +628,7 @@
button_icon_state = "reproduce"
chemical_cost = 100
-/datum/action/cooldown/borer/produce_offspring/Trigger(trigger_flags)
+/datum/action/cooldown/borer/produce_offspring/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -692,7 +692,7 @@
button_icon_state = "revive"
chemical_cost = 200
-/datum/action/cooldown/borer/revive_host/Trigger(trigger_flags)
+/datum/action/cooldown/borer/revive_host/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -732,7 +732,7 @@
button_icon_state = "willing"
chemical_cost = 150
-/datum/action/cooldown/borer/willing_host/Trigger(trigger_flags)
+/datum/action/cooldown/borer/willing_host/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
@@ -765,7 +765,7 @@
button_icon_state = "hiding"
chemical_cost = 100
-/datum/action/cooldown/borer/stealth_mode/Trigger(trigger_flags)
+/datum/action/cooldown/borer/stealth_mode/Trigger(trigger_flags, atom/target)
var/mob/living/basic/cortical_borer/cortical_owner = owner
var/in_stealth = (cortical_owner.upgrade_flags & BORER_STEALTH_MODE)
if(in_stealth)
@@ -794,7 +794,7 @@
button_icon_state = "reproduce"
chemical_cost = 150
-/datum/action/cooldown/borer/empowered_offspring/Trigger(trigger_flags)
+/datum/action/cooldown/borer/empowered_offspring/Trigger(trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
diff --git a/modular_skyrat/modules/customization/modules/client/augment/limbs.dm b/modular_skyrat/modules/customization/modules/client/augment/limbs.dm
index e5f8f4f14e7beb..8ae5fed1f044d9 100644
--- a/modular_skyrat/modules/customization/modules/client/augment/limbs.dm
+++ b/modular_skyrat/modules/customization/modules/client/augment/limbs.dm
@@ -10,15 +10,16 @@
var/obj/item/bodypart/new_limb = path
var/body_zone = initial(new_limb.body_zone)
var/obj/item/bodypart/old_limb = augmented.get_bodypart(body_zone)
+
+ old_limb.limb_id = initial(new_limb.limb_id)
+ old_limb.base_limb_id = initial(new_limb.limb_id)
+ old_limb.is_dimorphic = initial(new_limb.is_dimorphic)
+
if(uses_robotic_styles && prefs.augment_limb_styles[slot])
var/chosen_style = GLOB.robotic_styles_list[prefs.augment_limb_styles[slot]]
- old_limb.limb_id = initial(new_limb.limb_id)
- old_limb.base_limb_id = initial(new_limb.limb_id)
old_limb.set_icon_static(chosen_style)
old_limb.current_style = prefs.augment_limb_styles[slot]
else
- old_limb.limb_id = initial(new_limb.limb_id)
- old_limb.base_limb_id = initial(new_limb.limb_id)
old_limb.set_icon_static(initial(new_limb.icon))
old_limb.should_draw_greyscale = FALSE
@@ -30,6 +31,8 @@
var/chosen_style = GLOB.robotic_styles_list[prefs.augment_limb_styles[slot]]
new_limb.set_icon_static(chosen_style)
new_limb.current_style = prefs.augment_limb_styles[slot]
+ for (var/obj/item/organ/external/external_organ as anything in old_limb.external_organs)
+ external_organ.transfer_to_limb(new_limb)
new_limb.replace_limb(augmented)
qdel(old_limb)
diff --git a/modular_skyrat/modules/customization/modules/clothing/hands/hands.dm b/modular_skyrat/modules/customization/modules/clothing/hands/hands.dm
index 1d395d906ae9bf..667796dbce4fe0 100644
--- a/modular_skyrat/modules/customization/modules/clothing/hands/hands.dm
+++ b/modular_skyrat/modules/customization/modules/clothing/hands/hands.dm
@@ -31,5 +31,16 @@
max_heat_protection_temperature = GLOVES_MAX_TEMP_PROTECT
resistance_flags = NONE
+/obj/item/clothing/gloves/bracer/wraps
+ name = "cloth arm wraps"
+ desc = "Cloth bracers, the colour all left up to the choice of the wearer."
+ icon = 'modular_skyrat/master_files/icons/donator/obj/clothing/gloves.dmi'
+ icon_state = "arm_wraps"
+ inhand_icon_state = "greyscale_gloves"
+ greyscale_config = /datum/greyscale_config/armwraps
+ greyscale_config_worn = /datum/greyscale_config/armwraps/worn
+ greyscale_colors = "#FFFFFF"
+ flags_1 = IS_PLAYER_COLORABLE_1
+
/obj/item/clothing/gloves
worn_icon_teshari = TESHARI_HANDS_ICON
diff --git a/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm b/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm
index 9174700e738381..47ae6a48e89c2a 100644
--- a/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm
+++ b/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm
@@ -94,6 +94,19 @@
greyscale_config_worn_vox = /datum/greyscale_config/respirator/worn/vox
greyscale_config_worn_teshari = /datum/greyscale_config/respirator/worn/teshari
+/obj/item/clothing/mask/gas/respirator/examine(mob/user)
+ . = ..()
+ . += span_notice("You can toggle its ability to muffle your TTS voice with control click.")
+
+/obj/item/clothing/mask/gas/respirator/CtrlClick(mob/living/user)
+ if(!isliving(user))
+ return
+ if(user.get_active_held_item() != src)
+ to_chat(user, span_warning("You must hold the [src] in your hand to do this!"))
+ return
+ voice_filter = voice_filter ? null : initial(voice_filter)
+ to_chat(user, span_notice("Mask voice muffling [voice_filter ? "enabled" : "disabled"]."))
+
/obj/item/clothing/mask/gas/clown_hat/vox
desc = "A true prankster's facial attire. A clown is incomplete without his wig and mask. This one's got an easily accessible feeding port to be more suitable for the Vox crewmembers."
icon = 'modular_skyrat/master_files/icons/mob/clothing/species/vox/mask.dmi'
diff --git a/modular_skyrat/modules/customization/modules/clothing/masks/paper.dm b/modular_skyrat/modules/customization/modules/clothing/masks/paper.dm
new file mode 100644
index 00000000000000..97b52485b4b306
--- /dev/null
+++ b/modular_skyrat/modules/customization/modules/clothing/masks/paper.dm
@@ -0,0 +1,76 @@
+/obj/item/clothing/mask/paper
+ name = "paper mask"
+ desc = "It's true. Once you wear a mask for so long, you forget about who you are. Wonder if that happens with shitty paper ones."
+ icon = 'modular_skyrat/master_files/icons/obj/clothing/masks.dmi'
+ worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/mask.dmi'
+ icon_state = "mask_paper"
+ clothing_flags = MASKINTERNALS
+ flags_inv = HIDEFACIALHAIR|HIDESNOUT
+ w_class = WEIGHT_CLASS_SMALL
+ /// Whether or not the mask is currently being layered over (or under!) hair.
+ var/wear_over_hair = TRUE
+ unique_reskin = list(
+ "Blank" = "mask_paper",
+ "Neutral" = "mask_neutral",
+ "Eye" = "mask_eye",
+ "Sleep" = "mask_sleep",
+ "Heart" = "mask_heart",
+ "Core" = "mask_core",
+ "Plus" = "mask_plus",
+ "Square" = "mask_square",
+ "Bullseye" = "mask_bullseye",
+ "Vertical" = "mask_vertical",
+ "Horizontal" = "mask_horizontal",
+ "X" = "mask_x",
+ "Bug" = "mask_bug",
+ "Double" = "mask_double",
+ "Mark" = "mask_mark",
+ "Line" = "mask_line",
+ "Minus" = "mask_minus",
+ "Four" = "mask_four",
+ "Diamond" = "mask_diamond",
+ "Cat" = "mask_cat",
+ "Big Eye" = "mask_bigeye",
+ "Good" = "mask_good",
+ "Bad" = "mask_bad",
+ "Happy" = "mask_happy",
+ "Sad" = "mask_sad",
+ )
+
+/obj/item/clothing/mask/paper/Initialize(mapload)
+ . = ..()
+ register_context()
+ if(wear_over_hair)
+ alternate_worn_layer = BACK_LAYER
+
+/obj/item/clothing/mask/paper/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Change Mask Face"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/item/clothing/mask/paper/reskin_obj(mob/user)
+ . = ..()
+ user.update_worn_mask()
+ current_skin = null //so we can infinitely reskin
+
+/obj/item/clothing/mask/paper/proc/adjust_mask(mob/living/carbon/human/user)
+ if(!istype(user))
+ return
+ if(!user.incapacitated())
+ wear_over_hair = !wear_over_hair
+ if(wear_over_hair)
+ alternate_worn_layer = BACK_LAYER
+ to_chat(user, "You sweep your hair over the mask.")
+ else
+ alternate_worn_layer = initial(alternate_worn_layer)
+ to_chat(user, "You sweep your hair under the mask.")
+
+ user.update_body_parts()
+ user.update_inv_ears(0)
+ user.update_worn_mask()
+
+/obj/item/clothing/mask/paper/verb/toggle()
+ set category = "Object"
+ set name = "Adjust Mask"
+ set src in usr
+ adjust_mask(usr)
diff --git a/modular_skyrat/modules/customization/modules/clothing/shoes/shoes.dm b/modular_skyrat/modules/customization/modules/clothing/shoes/shoes.dm
index 682d2236a1e8f9..07d7cc81ea0414 100644
--- a/modular_skyrat/modules/customization/modules/clothing/shoes/shoes.dm
+++ b/modular_skyrat/modules/customization/modules/clothing/shoes/shoes.dm
@@ -95,13 +95,24 @@
worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/feet.dmi'
icon_state = "blackjack"
-/obj/item/clothing/shoes/wraps/colourable
+/obj/item/clothing/shoes/wraps/cloth
name = "cloth foot wraps"
desc = "Boxer tape or bandages wrapped like a mummy, all left up to the choice of the wearer."
icon_state = "clothwrap"
- greyscale_config = /datum/greyscale_config/wraps
- greyscale_config_worn = /datum/greyscale_config/wraps/worn
- greyscale_config_worn_digi = /datum/greyscale_config/wraps/worn/digi
+ greyscale_config = /datum/greyscale_config/clothwraps
+ greyscale_config_worn = /datum/greyscale_config/clothwraps/worn
+ greyscale_config_worn_digi = /datum/greyscale_config/clothwraps/worn/digi
+ greyscale_colors = "#FFFFFF"
+ body_parts_covered = FALSE
+ flags_1 = IS_PLAYER_COLORABLE_1
+
+/obj/item/clothing/shoes/wraps/colourable
+ name = "colourable foot wraps"
+ desc = "Ankle coverings. These ones have a customisable colour design."
+ icon_state = "legwrap"
+ greyscale_config = /datum/greyscale_config/legwraps
+ greyscale_config_worn = /datum/greyscale_config/legwraps/worn
+ greyscale_config_worn_digi = /datum/greyscale_config/legwraps/worn/digi
greyscale_colors = "#FFFFFF"
body_parts_covered = FALSE
flags_1 = IS_PLAYER_COLORABLE_1
@@ -196,3 +207,14 @@
greyscale_config_worn_better_vox = /datum/greyscale_config/boots/worn/newvox
greyscale_config_worn_vox = /datum/greyscale_config/boots/worn/oldvox
flags_1 = IS_PLAYER_COLORABLE_1
+
+/obj/item/clothing/shoes/wraps/cloth
+ name = "cloth foot wraps"
+ desc = "Boxer tape or bandages wrapped like a mummy, all left up to the choice of the wearer."
+ icon_state = "clothwrap"
+ greyscale_config = /datum/greyscale_config/clothwraps
+ greyscale_config_worn = /datum/greyscale_config/clothwraps/worn
+ greyscale_config_worn_digi = /datum/greyscale_config/clothwraps/worn/digi
+ greyscale_colors = "#FFFFFF"
+ body_parts_covered = FALSE
+ flags_1 = IS_PLAYER_COLORABLE_1
diff --git a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm
index 172ea352ac2b5d..cbb1565392f3c9 100644
--- a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm
+++ b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm
@@ -479,6 +479,146 @@
name = "Hairfre"
icon_state = "hair_hairfre"
+/datum/sprite_accessory/hair/skyrat/bobcut_over_eye_1
+ name = "Bobcut over eye 1"
+ icon_state = "hair_bobcut_over_eye_1"
+
+/datum/sprite_accessory/hair/skyrat/bobcut_over_eye_2
+ name = "Bobcut over eye 2"
+ icon_state = "hair_bobcut_over_eye_2"
+
+/datum/sprite_accessory/hair/skyrat/bobcut_over_eye_3
+ name = "Bobcut over eye 3"
+ icon_state = "hair_bobcut_over_eye_3"
+
+/datum/sprite_accessory/hair/skyrat/bonnie
+ name = "Bonnie"
+ icon_state = "hair_bonnie"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_short
+ name = "Bonnie short"
+ icon_state = "hair_bonnie_short"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_long
+ name = "Bonnie long"
+ icon_state = "hair_bonnie_long"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_2
+ name = "Bonnie 2"
+ icon_state = "hair_bonnie_2"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_2_long
+ name = "Bonnie long 2"
+ icon_state = "hair_bonnie_2_long"
+
+/datum/sprite_accessory/hair/skyrat/bonie_2_short
+ name = "Bonnie short 2"
+ icon_state = "hair_bonnie_2_short"
+
+/datum/sprite_accessory/hair/skyrat/dawn
+ name = "Dawn"
+ icon_state = "hair_dawn"
+
+/datum/sprite_accessory/hair/skyrat/fluffy
+ name = "Fluffy"
+ icon_state = "hair_fluffy"
+
+/datum/sprite_accessory/hair/skyrat/fluffy_long
+ name = "Fluffy long"
+ icon_state = "hair_fluffy_long"
+
+/datum/sprite_accessory/hair/skyrat/khmuro
+ name = "Khmuro"
+ icon_state = "hair_khmuro"
+
+/datum/sprite_accessory/hair/skyrat/kobeni_1
+ name = "Kobeni 1"
+ icon_state = "hair_kobeni_1"
+
+/datum/sprite_accessory/hair/skyrat/kobeni_2
+ name = "Kobeni 2"
+ icon_state = "hair_kobeni_2"
+
+/datum/sprite_accessory/hair/skyrat/low_bun
+ name = "Low bun"
+ icon_state = "hair_low_bun"
+
+/datum/sprite_accessory/hair/skyrat/low_ponytail
+ name = "Low ponytail"
+ icon_state = "hair_low_ponytail"
+
+/datum/sprite_accessory/hair/skyrat/morning
+ name = "Morning"
+ icon_state = "hair_morning"
+
+/datum/sprite_accessory/hair/skyrat/over_ear_1
+ name = "Over ear 1"
+ icon_state = "hair_over_ear_1"
+
+/datum/sprite_accessory/hair/skyrat/over_ear_2
+ name = "Over ear 2"
+ icon_state = "hair_over_ear_2"
+
+/datum/sprite_accessory/hair/skyrat/over_eye
+ name = "Over eye"
+ icon_state = "hair_over_eye"
+
+/datum/sprite_accessory/hair/skyrat/ponytail
+ name = "Fluffy ponytail"
+ icon_state = "hair_ponytail"
+
+/datum/sprite_accessory/hair/skyrat/ponytail_short
+ name = "Short fluffy ponytail"
+ icon_state = "hair_ponytail_short"
+
+/datum/sprite_accessory/hair/skyrat/simple
+ name = "Simple"
+ icon_state = "hair_simple"
+
+/datum/sprite_accessory/hair/skyrat/simple_long
+ name = "Simple long"
+ icon_state = "hair_simple_long"
+
+/datum/sprite_accessory/hair/skyrat/simple_short
+ name = "Simple short"
+ icon_state = "hair_simple_short"
+
+/datum/sprite_accessory/hair/skyrat/strict
+ name = "Strict"
+ icon_state = "hair_strict"
+
+/datum/sprite_accessory/hair/skyrat/strict_long
+ name = "Strict long"
+ icon_state = "hair_strict_long"
+
+/datum/sprite_accessory/hair/skyrat/strict_short
+ name = "Strict short"
+ icon_state = "hair_strict_short"
+
+/datum/sprite_accessory/hair/skyrat/thin_ponytail
+ name = "Thin ponytail"
+ icon_state = "hair_thin_ponytail"
+
+/datum/sprite_accessory/hair/skyrat/thin_ponytail_long
+ name = "Long thin ponytail"
+ icon_state = "hair_thin_ponytail_long"
+
+/datum/sprite_accessory/hair/skyrat/twintails_2
+ name = "Twintails 2"
+ icon_state = "hair_twintails_2"
+
+/datum/sprite_accessory/hair/skyrat/twintails_2_long
+ name = "Long twintails"
+ icon_state = "hair_twintails_2_long"
+
+/datum/sprite_accessory/hair/skyrat/twintails_2_short
+ name = "Short twintails"
+ icon_state = "hair_twintails_2_short"
+
+/datum/sprite_accessory/hair/skyrat/upwards
+ name = "Upwards"
+ icon_state = "hair_upwards"
+
// Facial hair
/datum/sprite_accessory/facial_hair/skyrat
diff --git a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/tails.dm b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/tails.dm
index 0a23f98f2541f1..f3d9d346de5999 100644
--- a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/tails.dm
+++ b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/tails.dm
@@ -309,6 +309,11 @@
name = "Shade (Striped)"
icon_state = "shadekinlongstriped_large"
+/datum/sprite_accessory/tails/mammal/wagging/big/ringtail
+ name = "Ring Tail (Long)"
+ icon_state = "bigring_large"
+ color_src = USE_MATRIXED_COLORS
+
/datum/sprite_accessory/tails/mammal/wagging/akula/akula
name = "Akula"
icon_state = "akula"
diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
index 76ebf34b68250d..b87946aaf37c88 100644
--- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
+++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
@@ -124,13 +124,19 @@ GLOBAL_LIST_EMPTY(customizable_races)
if(species_human.underwear && !(species_human.underwear_visibility & UNDERWEAR_HIDE_UNDIES))
var/datum/sprite_accessory/underwear/underwear = GLOB.underwear_list[species_human.underwear]
var/mutable_appearance/underwear_overlay
+ var/female_sprite_flags = FEMALE_UNIFORM_FULL // the default gender shaping
if(underwear)
var/icon_state = underwear.icon_state
if(underwear.has_digitigrade && (species_human.bodytype & BODYTYPE_DIGITIGRADE))
icon_state += "_d"
- underwear_overlay = mutable_appearance(underwear.icon, icon_state, -BODY_LAYER)
+ female_sprite_flags = FEMALE_UNIFORM_TOP_ONLY // for digi gender shaping
+ if(species_human.dna.species.sexes && species_human.physique == FEMALE && (underwear.gender == MALE))
+ underwear_overlay = wear_female_version(icon_state, underwear.icon, BODY_LAYER, female_sprite_flags)
+ else
+ underwear_overlay = mutable_appearance(underwear.icon, icon_state, -BODY_LAYER)
if(!underwear.use_static)
underwear_overlay.color = species_human.underwear_color
+ underwear_overlay.pixel_y += height_offset
standing += underwear_overlay
if(species_human.bra && !(species_human.underwear_visibility & UNDERWEAR_HIDE_BRA))
@@ -142,7 +148,7 @@ GLOBAL_LIST_EMPTY(customizable_races)
bra_overlay = mutable_appearance(bra.icon, icon_state, -BODY_LAYER)
if(!bra.use_static)
bra_overlay.color = species_human.bra_color
-
+ bra_overlay.pixel_y += height_offset
standing += bra_overlay
if(species_human.undershirt && !(species_human.underwear_visibility & UNDERWEAR_HIDE_SHIRT))
@@ -155,6 +161,7 @@ GLOBAL_LIST_EMPTY(customizable_races)
undershirt_overlay = mutable_appearance(undershirt.icon, undershirt.icon_state, -BODY_LAYER)
if(!undershirt.use_static)
undershirt_overlay.color = species_human.undershirt_color
+ undershirt_overlay.pixel_y += height_offset
standing += undershirt_overlay
if(species_human.socks && species_human.num_legs >= 2 && !(mutant_bodyparts["taur"]) && !(species_human.underwear_visibility & UNDERWEAR_HIDE_SOCKS))
diff --git a/modular_skyrat/modules/emotes/code/scream_emote.dm b/modular_skyrat/modules/emotes/code/scream_emote.dm
index dd07e81cec84bd..50619d1bd851c3 100644
--- a/modular_skyrat/modules/emotes/code/scream_emote.dm
+++ b/modular_skyrat/modules/emotes/code/scream_emote.dm
@@ -24,7 +24,7 @@
return 'modular_skyrat/modules/emotes/sound/voice/scream_silicon.ogg'
if(ismonkey(user))
return 'modular_skyrat/modules/emotes/sound/voice/scream_monkey.ogg'
- if(istype(user, /mob/living/simple_animal/hostile/gorilla))
+ if(istype(user, /mob/living/basic/gorilla))
return 'sound/creatures/gorilla.ogg'
if(isalien(user))
return 'sound/voice/hiss6.ogg'
diff --git a/modular_skyrat/modules/encounters/code/nri_raiders.dm b/modular_skyrat/modules/encounters/code/nri_raiders.dm
index ce89ac660a29e1..aa3b406bc01b77 100644
--- a/modular_skyrat/modules/encounters/code/nri_raiders.dm
+++ b/modular_skyrat/modules/encounters/code/nri_raiders.dm
@@ -128,7 +128,7 @@ GLOBAL_VAR(first_officer)
belt = /obj/item/storage/belt/security/nri
back = /obj/item/storage/backpack/satchel/leather
- backpack_contents = list(/obj/item/storage/box/nri_survival_pack/raider = 1, /obj/item/ammo_box/magazine/m9mm_aps = 3, /obj/item/gun/ballistic/automatic/pistol/ladon/nri = 1, /obj/item/crucifix = 1, /obj/item/clothing/mask/gas/hecu2 = 1, /obj/item/modular_computer/pda/security = 1)
+ backpack_contents = list(/obj/item/storage/box/nri_survival_pack/raider = 1, /obj/item/ammo_box/magazine/m9mm_aps = 3, /obj/item/gun/ballistic/automatic/pistol/nri = 1, /obj/item/crucifix = 1, /obj/item/clothing/mask/gas/hecu2 = 1, /obj/item/modular_computer/pda/security = 1)
l_pocket = /obj/item/folder/blue/nri_cop
r_pocket = /obj/item/storage/pouch/ammo
@@ -343,40 +343,6 @@ GLOBAL_VAR(first_officer)
command_name = "NRI Enforcer-Class Starship Telegram"
report_sound = ANNOUNCER_NRI_RAIDERS
-/obj/item/gun/ballistic/automatic/pistol/automag
- name = "\improper Automag"
- desc = "A .44 AMP handgun with a sleek metallic finish."
- icon_state = "automag"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/automag.dmi'
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/automag
- can_suppress = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/automag.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
-
-/obj/item/ammo_box/magazine/automag
- name = "handgun magazine (.44 AMP)"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "automag"
- base_icon_state = "automag"
- ammo_type = /obj/item/ammo_casing/c44
- caliber = CALIBER_44
- max_ammo = 7
- multiple_sprites = AMMO_BOX_PER_BULLET
-
-/obj/item/ammo_casing/c44
- name = ".44 AMP bullet casing"
- desc = "A .44 AMP bullet casing."
- caliber = CALIBER_44
- projectile_type = /obj/projectile/bullet/c44
-
-/obj/projectile/bullet/c44
- name = ".44 AMP bullet"
- damage = 40
- wound_bonus = 30
-
/obj/item/storage/belt/military/nri/captain/pirate_officer/PopulateContents()
generate_items_inside(list(
/obj/item/knife/combat = 1,
diff --git a/modular_skyrat/modules/exp_corps/code/expeditionary_trooper.dm b/modular_skyrat/modules/exp_corps/code/expeditionary_trooper.dm
index 54fc933848ae6c..fdcbef42cabfef 100644
--- a/modular_skyrat/modules/exp_corps/code/expeditionary_trooper.dm
+++ b/modular_skyrat/modules/exp_corps/code/expeditionary_trooper.dm
@@ -29,4 +29,4 @@
new /obj/item/clothing/suit/armor/vest/expeditionary_corps(src)
new /obj/item/storage/belt/military/expeditionary_corps/marksman(src)
new /obj/item/storage/backpack/duffelbag/expeditionary_corps(src)
- new /obj/item/storage/box/gunset/ladon(src)
+ new /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild(src)
diff --git a/modular_skyrat/modules/exp_corps/code/gear.dm b/modular_skyrat/modules/exp_corps/code/gear.dm
index 05c5a989ac8163..5d3668f3f6d6b7 100644
--- a/modular_skyrat/modules/exp_corps/code/gear.dm
+++ b/modular_skyrat/modules/exp_corps/code/gear.dm
@@ -96,6 +96,7 @@
/obj/item/storage/pouch/ammo/marksman
name = "marksman's knife pouch"
+ unique_reskin = NONE
/obj/item/storage/pouch/ammo/marksman/Initialize(mapload)
. = ..()
diff --git a/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm b/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm
index 035e1597f4e541..67461a949b7e4d 100644
--- a/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm
+++ b/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm
@@ -181,11 +181,11 @@
new /obj/item/knife/combat(src)
new /obj/item/switchblade(src)
new /obj/item/switchblade(src)
- new /obj/item/gun/ballistic/automatic/cfa_wildcat(src)
- new /obj/item/gun/ballistic/automatic/cfa_wildcat(src)
+ new /obj/item/gun/ballistic/automatic/sol_smg(src)
+ new /obj/item/gun/ballistic/automatic/sol_smg(src)
for(var/i in 1 to 2)
- new /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat(src)
+ new /obj/item/ammo_box/magazine/c35sol_pistol/stendo(src)
+ new /obj/item/ammo_box/magazine/c35sol_pistol/stendo(src)
new /obj/item/gun/ballistic/automatic/akm(src)
for(var/i in 1 to 2)
new /obj/item/ammo_box/magazine/akm(src)
diff --git a/modular_skyrat/modules/food_replicator/code/replicator_designs/replicator_medical.dm b/modular_skyrat/modules/food_replicator/code/replicator_designs/replicator_medical.dm
index 31b14284387efe..85fa2a2ee4f982 100644
--- a/modular_skyrat/modules/food_replicator/code/replicator_designs/replicator_medical.dm
+++ b/modular_skyrat/modules/food_replicator/code/replicator_designs/replicator_medical.dm
@@ -3,7 +3,7 @@
id = "slavic_cfap"
build_type = BIOGENERATOR
materials = list(/datum/material/biomass = 250)
- build_path = /obj/item/storage/bag/pocket_medkit
+ build_path = /obj/item/storage/pouch/cin_medkit
category = list(
RND_CATEGORY_INITIAL,
RND_CATEGORY_NRI_MEDICAL,
@@ -14,7 +14,7 @@
id = "slavic_medipouch"
build_type = BIOGENERATOR
materials = list(/datum/material/biomass = 250)
- build_path = /obj/item/storage/bag/medipen
+ build_path = /obj/item/storage/pouch/cin_medipens
category = list(
RND_CATEGORY_INITIAL,
RND_CATEGORY_NRI_MEDICAL,
diff --git a/modular_skyrat/modules/food_replicator/code/storage.dm b/modular_skyrat/modules/food_replicator/code/storage.dm
index 09ae7bece12b30..fe7ffc41a28038 100644
--- a/modular_skyrat/modules/food_replicator/code/storage.dm
+++ b/modular_skyrat/modules/food_replicator/code/storage.dm
@@ -1,21 +1,19 @@
-/obj/item/storage/bag/medipen
+/obj/item/storage/pouch/cin_medipens
name = "colonial medipen pouch"
desc = "A pouch for your (medi-)pens that goes in your pocket."
icon = 'modular_skyrat/modules/food_replicator/icons/pouch.dmi'
icon_state = "medipen_pouch"
- slot_flags = ITEM_SLOT_POCKETS
- w_class = WEIGHT_CLASS_BULKY
- resistance_flags = FLAMMABLE
+ w_class = WEIGHT_CLASS_NORMAL
-/obj/item/storage/bag/medipen/update_icon_state()
+/obj/item/storage/pouch/cin_medipens/update_icon_state()
icon_state = "[initial(icon_state)]_[contents.len]"
return ..()
-/obj/item/storage/bag/medipen/Initialize(mapload)
+/obj/item/storage/pouch/cin_medipens/Initialize(mapload)
. = ..()
update_appearance()
-/obj/item/storage/bag/medipen/Initialize(mapload)
+/obj/item/storage/pouch/cin_medipens/Initialize(mapload)
. = ..()
atom_storage.max_specific_storage = WEIGHT_CLASS_TINY
atom_storage.max_total_storage = 4
@@ -23,17 +21,16 @@
atom_storage.numerical_stacking = FALSE
atom_storage.can_hold = typecacheof(list(/obj/item/reagent_containers/hypospray/medipen, /obj/item/pen, /obj/item/flashlight/pen))
-/obj/item/storage/bag/pocket_medkit
+/obj/item/storage/pouch/cin_medkit
name = "colonial first aid kit"
- desc = "A medical pouch that goes in your pocket. Can be used to store things unrelated to medicine, except for guns, ammo and raw materials."
+ desc = "A medical case that goes in your pocket. Can be used to store things unrelated to medicine, except for guns, ammo and raw materials."
icon = 'modular_skyrat/modules/food_replicator/icons/pouch.dmi'
icon_state = "cfak"
- slot_flags = ITEM_SLOT_POCKETS
- w_class = WEIGHT_CLASS_BULKY
- resistance_flags = FLAMMABLE
+ w_class = WEIGHT_CLASS_NORMAL
-/obj/item/storage/bag/pocket_medkit/Initialize(mapload)
+/obj/item/storage/pouch/cin_medkit/Initialize(mapload)
. = ..()
+ atom_storage.numerical_stacking = TRUE
atom_storage.max_specific_storage = WEIGHT_CLASS_SMALL
atom_storage.max_total_storage = 4
atom_storage.max_slots = 4
diff --git a/modular_skyrat/modules/goofsec/code/department_guards.dm b/modular_skyrat/modules/goofsec/code/department_guards.dm
index 7a5828b5fa26de..c93b74ffa185c5 100644
--- a/modular_skyrat/modules/goofsec/code/department_guards.dm
+++ b/modular_skyrat/modules/goofsec/code/department_guards.dm
@@ -200,7 +200,7 @@
l_pocket = /obj/item/restraints/handcuffs/cable/pink
backpack_contents = list(
/obj/item/melee/baton/security/loaded/departmental/science = 1,
- /obj/item/storage/box/gunset/pepperball = 1,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/pepperball = 1,
)
backpack = /obj/item/storage/backpack/science
@@ -304,7 +304,7 @@
l_pocket = /obj/item/restraints/handcuffs/cable/blue
backpack_contents = list(
/obj/item/melee/baton/security/loaded/departmental/medical = 1,
- /obj/item/storage/box/gunset/pepperball = 1,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/pepperball = 1,
)
backpack = /obj/item/storage/backpack/medic
@@ -402,7 +402,7 @@
l_pocket = /obj/item/restraints/handcuffs/cable/yellow
backpack_contents = list(
/obj/item/melee/baton/security/loaded/departmental/engineering = 1,
- /obj/item/storage/box/gunset/pepperball = 1,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/pepperball = 1,
)
backpack = /obj/item/storage/backpack/industrial
@@ -507,7 +507,7 @@
l_pocket = /obj/item/restraints/handcuffs/cable/orange
backpack_contents = list(
/obj/item/melee/baton/security/loaded/departmental/cargo = 1,
- /obj/item/storage/box/gunset/pepperball = 1,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/pepperball = 1,
)
backpack = /obj/item/storage/backpack
@@ -604,7 +604,7 @@
r_pocket = /obj/item/assembly/flash/handheld
backpack_contents = list(
/obj/item/melee/baton/security/loaded/departmental/service = 1,
- /obj/item/storage/box/gunset/pepperball = 1,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/pepperball = 1,
)
glasses = /obj/item/clothing/glasses/sunglasses
diff --git a/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm b/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm
index ea06dc1163c22b..f1bece74ab47a7 100644
--- a/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm
+++ b/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm
@@ -762,6 +762,8 @@
/obj/item/gun/energy/dueling,
/obj/item/gun/energy/laser/thermal,
/obj/item/gun/ballistic/rifle/boltaction, //fits if you make it an obrez
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
/obj/item/storage/belt/holster/detective
@@ -785,6 +787,8 @@
/obj/item/gun/energy/dueling,
/obj/item/gun/energy/laser/thermal,
/obj/item/gun/ballistic/rifle/boltaction, //fits if you make it an obrez
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
/*
diff --git a/modular_skyrat/modules/gun_safety/code/safety_additions.dm b/modular_skyrat/modules/gun_safety/code/safety_additions.dm
index 67b6fe8561ed11..af8f3665315775 100644
--- a/modular_skyrat/modules/gun_safety/code/safety_additions.dm
+++ b/modular_skyrat/modules/gun_safety/code/safety_additions.dm
@@ -25,9 +25,6 @@
/obj/item/gun/ballistic/automatic/pistol/deagle/ctf/give_gun_safeties()
return
-/obj/item/gun/ballistic/automatic/pistol/g17/mesa/give_gun_safeties()
- return
-
/obj/item/gun/ballistic/revolver/grenadelauncher/give_gun_safeties()
return
diff --git a/modular_skyrat/modules/horrorform/code/horror_form.dm b/modular_skyrat/modules/horrorform/code/horror_form.dm
index b99f4360c356eb..9e6ea71ad7335f 100644
--- a/modular_skyrat/modules/horrorform/code/horror_form.dm
+++ b/modular_skyrat/modules/horrorform/code/horror_form.dm
@@ -14,6 +14,7 @@
req_stat = UNCONSCIOUS
/datum/action/changeling/horror_form/sting_action(mob/living/carbon/human/user)
+ ..()
if(!user || HAS_TRAIT(user, TRAIT_NO_TRANSFORM))
return 0
user.visible_message(span_warning("[user] writhes and contorts, their body expanding to inhuman proportions!"), \
diff --git a/modular_skyrat/modules/hyposprays/code/hypovials.dm b/modular_skyrat/modules/hyposprays/code/hypovials.dm
index edc7b41c85b5f3..a4781bfea8fe7f 100644
--- a/modular_skyrat/modules/hyposprays/code/hypovials.dm
+++ b/modular_skyrat/modules/hyposprays/code/hypovials.dm
@@ -45,32 +45,32 @@
//Fit in all hypos
/obj/item/reagent_containers/cup/vial/small
name = "hypovial"
- desc = "A small, 60u capacity vial compatible with most hyposprays."
- volume = 60
- possible_transfer_amounts = list(1,2,5,10,20,30,40,50,60)
+ desc = "A small, 50u capacity vial compatible with most hyposprays."
+ volume = 50
+ possible_transfer_amounts = list(1,2,5,10,15,25,50)
//Fit in CMO hypo only
/obj/item/reagent_containers/cup/vial/large
name = "large hypovial"
icon_state = "hypoviallarge"
- desc = "A large, 120u capacity vial that fits only in the most deluxe hyposprays."
- volume = 120
+ desc = "A large, 100u capacity vial that fits only in the most deluxe hyposprays."
+ volume = 100
type_suffix = "-l"
- possible_transfer_amounts = list(1,2,5,10,20,30,40,50,100,120)
+ possible_transfer_amounts = list(1,2,5,10,20,30,40,50,100)
//Hypos that are in the CMO's kit round start
/obj/item/reagent_containers/cup/vial/large/deluxe
name = "deluxe hypovial"
- list_reagents = list(/datum/reagent/medicine/omnizine = 20, /datum/reagent/medicine/leporazine = 20, /datum/reagent/medicine/atropine = 20)
+ list_reagents = list(/datum/reagent/medicine/omnizine = 15, /datum/reagent/medicine/leporazine = 15, /datum/reagent/medicine/atropine = 15)
/obj/item/reagent_containers/cup/vial/large/salglu
name = "large green hypovial (salglu)"
- list_reagents = list(/datum/reagent/medicine/salglu_solution = 60)
+ list_reagents = list(/datum/reagent/medicine/salglu_solution = 50)
/obj/item/reagent_containers/cup/vial/large/synthflesh
name = "large orange hypovial (synthflesh)"
- list_reagents = list(/datum/reagent/medicine/c2/synthflesh = 60)
+ list_reagents = list(/datum/reagent/medicine/c2/synthflesh = 50)
/obj/item/reagent_containers/cup/vial/large/multiver
name = "large black hypovial (multiver)"
- list_reagents = list(/datum/reagent/medicine/c2/multiver = 60)
+ list_reagents = list(/datum/reagent/medicine/c2/multiver = 50)
diff --git a/modular_skyrat/modules/implants/code/augments_chest.dm b/modular_skyrat/modules/implants/code/augments_chest.dm
index 230793228741a5..87e817da3795fe 100644
--- a/modular_skyrat/modules/implants/code/augments_chest.dm
+++ b/modular_skyrat/modules/implants/code/augments_chest.dm
@@ -1,3 +1,7 @@
+// for readability's sake, define here to match the healthscan() proc's use of it
+// if someone updates that upstream, fix that here too, wouldja?
+#define SCANNER_VERBOSE 1
+
/obj/item/organ/internal/cyberimp/chest/scanner
name = "internal health analyzer"
desc = "An advanced health analyzer implant, designed to directly interface with a host's body and relay scan information to the brain on command."
@@ -6,14 +10,21 @@
icon_state = "internal_HA"
implant_overlay = null
implant_color = null
- actions_types = list(/datum/action/item_action/organ_action/use)
+ actions_types = list(/datum/action/item_action/organ_action/use/internal_analyzer)
w_class = WEIGHT_CLASS_SMALL
-/obj/item/organ/internal/cyberimp/chest/scanner/ui_action_click(owner, action)
- if(istype(action, /datum/action/item_action/organ_action/use))
- if(organ_flags & ORGAN_FAILING)
- to_chat(owner, span_warning("Your health analyzer relays an error! It can't interface with your body in its current condition!"))
- return
- else
- healthscan(owner, owner, 1, TRUE)
- chemscan(owner, owner)
+/datum/action/item_action/organ_action/use/internal_analyzer
+ desc = "LMB: Health scan. RMB: Chemical scan. Requires implanted analyzer to not be failing due to EMPs or other causes. Does not provide treatment assistance."
+
+/datum/action/item_action/organ_action/use/internal_analyzer/Trigger(trigger_flags)
+ . = ..()
+ var/obj/item/organ/internal/cyberimp/chest/scanner/our_scanner = target
+ if(our_scanner.organ_flags & ORGAN_FAILING)
+ to_chat(owner, span_warning("Your health analyzer relays an error! It can't interface with your body in its current condition!"))
+ return
+ if(trigger_flags & TRIGGER_SECONDARY_ACTION)
+ chemscan(owner, owner)
+ else
+ healthscan(owner, owner, SCANNER_VERBOSE, TRUE)
+
+#undef SCANNER_VERBOSE
diff --git a/modular_skyrat/modules/indicators/icons/DNR_trait_overlay.dmi b/modular_skyrat/modules/indicators/icons/DNR_trait_overlay.dmi
new file mode 100644
index 00000000000000..950b5503c00eb0
Binary files /dev/null and b/modular_skyrat/modules/indicators/icons/DNR_trait_overlay.dmi differ
diff --git a/modular_skyrat/modules/layer_shift/code/mob_movement.dm b/modular_skyrat/modules/layer_shift/code/mob_movement.dm
index 2bc2ed4cdee594..740b3e2289ed82 100644
--- a/modular_skyrat/modules/layer_shift/code/mob_movement.dm
+++ b/modular_skyrat/modules/layer_shift/code/mob_movement.dm
@@ -6,34 +6,73 @@
//#define MOB_LAYER 4 // This is a byond standard define
#define MOB_LAYER_SHIFT_MAX 4.05
-/mob/living/verb/layershift_up()
+/mob/living/verb/shift_layer_up()
set name = "Shift Layer Upwards"
set category = "IC"
if(incapacitated())
to_chat(src, span_warning("You can't do that right now!"))
- return
+ return FALSE
if(layer >= MOB_LAYER_SHIFT_MAX)
to_chat(src, span_warning("You cannot increase your layer priority any further."))
- return
+ return FALSE
layer = min(((layer * MOB_LAYER_MULTIPLIER) + MOB_LAYER_SHIFT_INCREMENT) / MOB_LAYER_MULTIPLIER, MOB_LAYER_SHIFT_MAX)
var/layer_priority = round(layer * MOB_LAYER_MULTIPLIER - MOB_LAYER * MOB_LAYER_MULTIPLIER, MOB_LAYER_SHIFT_INCREMENT) // Just for text feedback
to_chat(src, span_notice("Your layer priority is now [layer_priority]."))
-/mob/living/verb/layershift_down()
+ return TRUE
+
+
+/mob/living/verb/shift_layer_down()
set name = "Shift Layer Downwards"
set category = "IC"
if(incapacitated())
to_chat(src, span_warning("You can't do that right now!"))
- return
+ return FALSE
if(layer <= MOB_LAYER_SHIFT_MIN)
to_chat(src, span_warning("You cannot decrease your layer priority any further."))
- return
+ return FALSE
layer = max(((layer * MOB_LAYER_MULTIPLIER) - MOB_LAYER_SHIFT_INCREMENT) / MOB_LAYER_MULTIPLIER, MOB_LAYER_SHIFT_MIN)
var/layer_priority = round(layer * MOB_LAYER_MULTIPLIER - MOB_LAYER * MOB_LAYER_MULTIPLIER, MOB_LAYER_SHIFT_INCREMENT) // Just for text feedback
to_chat(src, span_notice("Your layer priority is now [layer_priority]."))
+
+ return TRUE
+
+
+/datum/emote/living/shift_layer_up
+ key = "shiftlayerup"
+ key_third_person = "shiftlayerup"
+ message = null
+ mob_type_blacklist_typecache = list(/mob/living/brain)
+ cooldown = 0.25 SECONDS
+
+/datum/emote/living/shift_layer_up/run_emote(mob/user, params, type_override, intentional)
+ if(!can_run_emote(user))
+ to_chat(user, span_warning("You can't change layer at this time."))
+ return FALSE
+
+ var/mob/living/layer_shifter = user
+
+ return layer_shifter.shift_layer_up()
+
+
+/datum/emote/living/shift_layer_down
+ key = "shiftlayerdown"
+ key_third_person = "shiftlayerdown"
+ message = null
+ mob_type_blacklist_typecache = list(/mob/living/brain)
+ cooldown = 0.25 SECONDS
+
+/datum/emote/living/shift_layer_down/run_emote(mob/user, params, type_override, intentional)
+ if(!can_run_emote(user))
+ to_chat(user, span_warning("You can't change layer at this time."))
+ return FALSE
+
+ var/mob/living/layer_shifter = user
+
+ return layer_shifter.shift_layer_down()
diff --git a/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm b/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm
index 7b6e248a278fc2..c41311e46945d4 100644
--- a/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm
+++ b/modular_skyrat/modules/loadouts/loadout_items/donator/personal/donator_personal.dm
@@ -667,7 +667,7 @@
/datum/loadout_item/pocket_items/masvedishcigar
name = "Holocigar"
item_path = /obj/item/clothing/mask/holocigarette/masvedishcigar
- ckeywhitelist = list("masvedish")
+ ckeywhitelist = list("masvedish", "lutowski", "lawful", "anyacers", "apolloafk", "avianaviator", "notdhu")
/datum/loadout_item/suit/lt3_armor
name = "Silver Jacket Mk II"
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm
index 68d10776be68e0..8adc97dd42b574 100644
--- a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_gloves.dm
@@ -82,6 +82,10 @@ GLOBAL_LIST_INIT(loadout_gloves, generate_loadout_items(/datum/loadout_item/glov
name = "Maid Arm Covers"
item_path = /obj/item/clothing/gloves/maid
+/datum/loadout_item/gloves/armwraps
+ name = "Colourable Arm Wraps"
+ item_path = /obj/item/clothing/gloves/bracer/wraps
+
/*
* RINGS
*/
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm
index a92764d30745f8..d0052806a13de3 100644
--- a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_masks.dm
@@ -130,6 +130,10 @@ GLOBAL_LIST_INIT(loadout_masks, generate_loadout_items(/datum/loadout_item/mask)
name = "Joy Mask"
item_path = /obj/item/clothing/mask/joy
+/datum/loadout_item/mask/paper
+ name = "Paper Mask"
+ item_path = /obj/item/clothing/mask/paper
+
/datum/loadout_item/mask/lollipop
name = "Lollipop"
item_path = /obj/item/food/lollipop
diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm
index d24031b78e2cb7..1ac2cfcb6b45b8 100644
--- a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm
+++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_shoes.dm
@@ -158,9 +158,13 @@ GLOBAL_LIST_INIT(loadout_shoes, generate_loadout_items(/datum/loadout_item/shoes
name = "Blue Leg Wraps"
item_path = /obj/item/clothing/shoes/wraps/blue
+/datum/loadout_item/shoes/cuffs/colourable
+ name = "Colourable Leg Wraps"
+ item_path = /obj/item/clothing/shoes/wraps/colourable
+
/datum/loadout_item/shoes/clothwrap
name = "Colourable Cloth Wraps"
- item_path = /obj/item/clothing/shoes/wraps/colourable
+ item_path = /obj/item/clothing/shoes/wraps/cloth
/*
* MISC
diff --git a/modular_skyrat/modules/mapping/code/lockers/cargodiselost/cargodiselockers.dm b/modular_skyrat/modules/mapping/code/lockers/cargodiselost/cargodiselockers.dm
index 00c5d6e8b627b0..2d9be517a0b19e 100644
--- a/modular_skyrat/modules/mapping/code/lockers/cargodiselost/cargodiselockers.dm
+++ b/modular_skyrat/modules/mapping/code/lockers/cargodiselost/cargodiselockers.dm
@@ -15,8 +15,8 @@
new /obj/item/ammo_box/magazine/akm(src)
new /obj/item/ammo_box/magazine/akm(src)
new /obj/item/ammo_box/magazine/akm(src)
- new /obj/item/ammo_box/magazine/multi_sprite/makarov(src)
- new /obj/item/ammo_box/magazine/multi_sprite/makarov(src)
+ new /obj/item/ammo_box/magazine/c35sol_pistol(src)
+ new /obj/item/ammo_box/magazine/c35sol_pistol(src)
new /obj/item/ammo_box/strilka310(src)
new /obj/item/ammo_box/strilka310(src)
new /obj/item/ammo_box/strilka310(src)
@@ -25,7 +25,7 @@
/obj/structure/closet/secure_closet/personal/cabinet/freighterboss/PopulateContents()
. = ..()
- new /obj/item/gun/ballistic/automatic/cfa_rifle(src)
+ new /obj/item/gun/ballistic/automatic/sol_rifle/marksman(src)
new /obj/item/storage/pouch/ammo(src)
new /obj/item/clothing/suit/armor/bulletproof(src)
new /obj/item/storage/belt/utility/syndicate(src)
@@ -33,6 +33,6 @@
new /obj/item/clothing/gloves/combat(src)
new /obj/item/storage/backpack/duffelbag/syndie(src)
new /obj/item/radio(src)
- new /obj/item/ammo_box/magazine/cm68(src)
- new /obj/item/ammo_box/magazine/cm68(src)
- new /obj/item/ammo_box/magazine/cm68(src)
+ new /obj/item/ammo_box/magazine/c40sol_rifle(src)
+ new /obj/item/ammo_box/magazine/c40sol_rifle(src)
+ new /obj/item/ammo_box/magazine/c40sol_rifle(src)
diff --git a/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm b/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm
index df1aa1cfe3f490..82eb8a853146d9 100644
--- a/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm
+++ b/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm
@@ -71,13 +71,12 @@
/obj/structure/closet/secure_closet/interdynefob/munitions_locker/PopulateContents()
..()
- new /obj/item/ammo_box/c9mm(src)
- new /obj/item/ammo_box/c9mm(src)
- new /obj/item/ammo_box/magazine/m9mm(src)
- new /obj/item/ammo_box/magazine/m9mm(src)
- new /obj/item/ammo_box/magazine/m9mm(src)
- new /obj/item/ammo_box/magazine/m9mm(src)
- new /obj/item/ammo_box/advanced/s12gauge(src)
- new /obj/item/ammo_box/advanced/s12gauge(src)
- new /obj/item/ammo_box/advanced/s12gauge/rubber(src)
- new /obj/item/ammo_box/advanced/s12gauge/rubber(src)
+ generate_items_inside(list(
+ /obj/item/ammo_box/magazine/c35sol_pistol = 6,
+ /obj/item/ammo_box/magazine/c35sol_pistol/stendo = 2,
+ /obj/item/ammo_box/c35sol = 2,
+ /obj/item/ammo_box/magazine/c40sol_rifle/standard = 2,
+ /obj/item/ammo_box/c40sol = 2,
+ /obj/item/ammo_box/advanced/s12gauge = 2,
+ /obj/item/ammo_box/advanced/s12gauge/rubber = 2,
+ ),src)
diff --git a/modular_skyrat/modules/mapping/code/shuttles.dm b/modular_skyrat/modules/mapping/code/shuttles.dm
index 9f5293d9a1dca5..1ad2e883eec562 100644
--- a/modular_skyrat/modules/mapping/code/shuttles.dm
+++ b/modular_skyrat/modules/mapping/code/shuttles.dm
@@ -121,19 +121,14 @@
implants = list(/obj/item/implant/weapons_auth)
belt = /obj/item/storage/belt/military
r_pocket = /obj/item/storage/pouch/ammo
- l_pocket = /obj/item/gun/energy/disabler/bolt_disabler
+ l_pocket = /obj/item/gun/energy/e_gun/mini
id = /obj/item/card/id/advanced/chameleon
id_trim = /datum/id_trim/chameleon/operative
skillchips = list(/obj/item/skillchip/job/engineer)
backpack_contents = list(
/obj/item/storage/box/survival/engineer/radio,
/obj/item/melee/baton/telescopic,
- /obj/item/gun/ballistic/automatic/pistol/cfa_snub/empty,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_snub,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_snub,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_snub/ap,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_snub/rubber,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_snub/rubber,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/wespe,
/obj/item/grenade/c4,
/obj/item/grenade/smokebomb
)
@@ -151,18 +146,14 @@
implants = list(/obj/item/implant/weapons_auth)
belt = /obj/item/storage/belt/military
r_pocket = /obj/item/storage/pouch/ammo
- l_pocket = /obj/item/gun/energy/disabler/bolt_disabler
+ l_pocket = /obj/item/gun/energy/e_gun/mini
id = /obj/item/card/id/advanced/chameleon
id_trim = /datum/id_trim/chameleon/operative
skillchips = list(/obj/item/skillchip/job/engineer)
backpack_contents = list(
/obj/item/storage/box/survival/engineer/radio,
/obj/item/melee/baton/telescopic,
- /obj/item/gun/ballistic/automatic/pistol/cfa_ruby/empty,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/ap,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/ap,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/rubber,
- /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/rubber,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
/obj/item/megaphone/command
)
diff --git a/modular_skyrat/modules/medical/attributions.txt b/modular_skyrat/modules/medical/attributions.txt
new file mode 100644
index 00000000000000..f57c3fa0db566f
--- /dev/null
+++ b/modular_skyrat/modules/medical/attributions.txt
@@ -0,0 +1,2 @@
+robotic_slash_T1.ogg, robotic_slash_T2.ogg, and robotic_slash_T3.ogg adapated from CGEffex's Bug Zapper2.wav (CC Attribution 4.0)
+https://freesound.org/people/CGEffex/sounds/107004/
diff --git a/modular_skyrat/modules/medical/code/cargo/packs.dm b/modular_skyrat/modules/medical/code/cargo/packs.dm
new file mode 100644
index 00000000000000..0de1f24eb16700
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/cargo/packs.dm
@@ -0,0 +1,36 @@
+/datum/supply_pack/science/chilled_hercuri
+ name = "Chilled Hercuri Pack"
+ desc = "Contains 2 pre-chilled bottles of hercuri, 100u each. Useful for dealing with severely burnt synthetics!"
+ cost = CARGO_CRATE_VALUE * 2.5
+ contains = list(/obj/item/reagent_containers/spray/hercuri/chilled = 2)
+ crate_name = "chilled hercuri crate"
+
+ access_view = FALSE
+ access = FALSE
+ access_any = FALSE
+
+/datum/supply_pack/science/synth_treatment_kits
+ name = "Synthetic Treatment Kits"
+ desc = "Contains 2 treatment kits for synthetic lifeforms, filled with everything you need to treat an inorganic wound!"
+ cost = CARGO_CRATE_VALUE * 4.5
+ contains = list(/obj/item/storage/backpack/duffelbag/synth_treatment_kit = 2)
+ crate_name = "synthetic treatment kits crate"
+
+ access_view = FALSE
+ access = FALSE
+ access_any = FALSE
+
+/datum/supply_pack/science/synth_healing_chems
+ name = "Synthetic Medicine Pack"
+ desc = "Contains a variety of synthetic-exclusive medicine. 2 pill bottles of liquid solder, 2 of nanite slurry, 2 of system cleaner."
+ cost = CARGO_CRATE_VALUE * 7 // rarely made, so it should be expensive(?)
+ contains = list(
+ /obj/item/storage/pill_bottle/liquid_solder = 2,
+ /obj/item/storage/pill_bottle/nanite_slurry = 2,
+ /obj/item/storage/pill_bottle/system_cleaner = 2
+ )
+ crate_name = "synthetic medicine crate"
+
+ access_view = FALSE
+ access = FALSE
+ access_any = FALSE
diff --git a/modular_skyrat/modules/medical/code/medkit.dm b/modular_skyrat/modules/medical/code/medkit.dm
new file mode 100644
index 00000000000000..8ea40ae54bd4ef
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/medkit.dm
@@ -0,0 +1,177 @@
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit
+ name = "synthetic treatment kit"
+ desc = "A \"surgical\" duffel bag containing everything you need to treat the worst and best of inorganic wounds."
+ icon = 'modular_skyrat/master_files/icons/obj/clothing/backpacks.dmi'
+ worn_icon = 'modular_skyrat/master_files/icons/mob/clothing/back.dmi'
+ lefthand_file = 'modular_skyrat/master_files/icons/mob/inhands/clothing/backpack_lefthand.dmi'
+ righthand_file = 'modular_skyrat/master_files/icons/mob/inhands/clothing/backpack_righthand.dmi'
+ icon_state = "duffel_robo"
+ inhand_icon_state = "duffel_robo"
+
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit/PopulateContents() // yes, this is all within the storage capacity
+ // Slash/Pierce wound tools - can reduce intensity of electrical damage (wires can fix generic burn damage)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/wirecutters(src)
+ // Blunt/Brute tools
+ new /obj/item/weldingtool/largetank(src) // Used for repairing blunt damage or heating metal at T3 blunt
+ new /obj/item/screwdriver(src) // Used for fixing T1 blunt or securing internals of T2/3 blunt
+ new /obj/item/bonesetter(src)
+ // Clothing items
+ new /obj/item/clothing/head/utility/welding(src)
+ new /obj/item/clothing/gloves/color/black(src) // Protects from T3 mold metal step
+ new /obj/item/clothing/glasses/hud/diagnostic(src) // When worn, generally improves wound treatment quality
+ // Reagent containers
+ new /obj/item/reagent_containers/spray/hercuri/chilled(src) // Highly effective (specifically coded to be) against burn wounds
+ // Generic medical items
+ new /obj/item/stack/medical/gauze/twelve(src)
+ new /obj/item/healthanalyzer(src)
+ new /obj/item/healthanalyzer/simple(src) // Buffs wound treatment and gives details of wounds it scans
+ // "Ghetto" tools, things you shouldnt ideally use but you might have to
+ new /obj/item/stack/medical/bone_gel(src) // Ghetto T2/3 option for securing internals
+ new /obj/item/plunger(src) // Can be used to mold heated metal at T3
+
+// a treatment kit with extra space and more tools/upgraded tools, like a crowbar, insuls, a reinforced plunger, a crowbar and wrench
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit/trauma
+ name = "synthetic trauma kit"
+ desc = "A \"surgical\" duffel bag containing everything you need to treat the worst and best of inorganic wounds. This one has extra tools and space \
+ for treatment of the WORST of the worst! However, it's highly specialized interior means it can ONLY hold synthetic repair tools."
+ storage_type = /datum/storage/duffel/synth_trauma_kit
+
+/datum/storage/duffel/synth_trauma_kit
+ exception_max = 6
+ max_slots = 27
+ max_total_storage = 35
+
+/datum/storage/duffel/synth_trauma_kit/New(atom/parent, max_slots, max_specific_storage, max_total_storage, numerical_stacking, allow_quick_gather, allow_quick_empty, collection_mode, attack_hand_interact)
+ . = ..()
+
+ var/static/list/exception_cache = typecacheof(list(
+ // Mainly just stacks, with the exception of pill bottles and sprays
+ /obj/item/stack/cable_coil,
+ /obj/item/stack/medical/gauze,
+ /obj/item/reagent_containers/spray,
+ /obj/item/stack/medical/bone_gel,
+ /obj/item/rcd_ammo,
+ /obj/item/storage/pill_bottle,
+ ))
+
+ var/static/list/can_hold_list = list(
+ // Stacks
+ /obj/item/stack/cable_coil,
+ /obj/item/stack/medical/gauze,
+ /obj/item/stack/medical/bone_gel,
+ // Reagent containers, for synth medicine
+ /obj/item/reagent_containers/spray,
+ /obj/item/storage/pill_bottle,
+ /obj/item/reagent_containers/pill,
+ /obj/item/reagent_containers/cup,
+ /obj/item/reagent_containers/syringe,
+ // Tools, including tools you might not want to use but might have to (hemostat/retractor/etc)
+ /obj/item/screwdriver,
+ /obj/item/wrench,
+ /obj/item/crowbar,
+ /obj/item/weldingtool,
+ /obj/item/bonesetter,
+ /obj/item/wirecutters,
+ /obj/item/hemostat,
+ /obj/item/retractor,
+ /obj/item/cautery,
+ /obj/item/plunger,
+ // RCD stuff - RCDs can easily treat the 1st step of T3 blunt
+ /obj/item/construction/rcd,
+ /obj/item/rcd_ammo,
+ // Clothing items
+ /obj/item/clothing/gloves,
+ /obj/item/clothing/glasses/hud/health,
+ /obj/item/clothing/glasses/hud/diagnostic,
+ /obj/item/clothing/glasses/welding,
+ /obj/item/clothing/glasses/sunglasses, // still provides some welding protection
+ /obj/item/clothing/head/utility/welding,
+ /obj/item/clothing/mask/gas/welding,
+ // Generic health items
+ /obj/item/healthanalyzer,
+ )
+ exception_hold = exception_cache
+
+ // We keep the type list and the typecache list separate...
+ var/static/list/can_hold_cache = typecacheof(can_hold_list)
+ can_hold = can_hold_cache
+
+ //...So we can run this without it generating a line for every subtype.
+ can_hold_description = generate_hold_desc(can_hold_list)
+
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit/trauma/PopulateContents() // yes, this is all within the storage capacity
+ // Slash/Pierce wound tools - can reduce intensity of electrical damage (wires can fix generic burn damage)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/wirecutters(src)
+ // Blunt/Brute tools
+ new /obj/item/weldingtool/hugetank(src) // Used for repairing blunt damage or heating metal at T3 blunt
+ new /obj/item/screwdriver(src) // Used for fixing T1 blunt or securing internals of T2/3 blunt
+ new /obj/item/wrench(src) // Same as screwdriver for T2/3
+ new /obj/item/crowbar(src) // Ghetto fixing option for T2/3 blunt
+ new /obj/item/bonesetter(src)
+ // Clothing items
+ new /obj/item/clothing/head/utility/welding(src)
+ new /obj/item/clothing/gloves/color/black(src) // Protects from T3 mold metal step
+ new /obj/item/clothing/gloves/color/yellow(src) // Protects from electrical damage and crowbarring a blunt wound
+ new /obj/item/clothing/glasses/hud/diagnostic(src) // When worn, generally improves wound treatment quality
+ // Reagent containers
+ new /obj/item/reagent_containers/spray/hercuri/chilled(src) // Highly effective (specifically coded to be) against burn wounds
+ // Generic medical items
+ new /obj/item/stack/medical/gauze/twelve(src)
+ new /obj/item/healthanalyzer(src)
+ new /obj/item/healthanalyzer/simple(src) // Buffs wound treatment and gives details of wounds it scans
+ // "Ghetto" tools, things you shouldnt ideally use but you might have to
+ new /obj/item/stack/medical/bone_gel(src) // Ghetto T2/3 option for securing internals
+ new /obj/item/plunger/reinforced(src) // Can be used to mold heated metal at T3
+
+// advanced tools, an RCD, chems, etc etc. dont give this one to the crew early in the round
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit/trauma/advanced
+ name = "advanced synth trauma kit"
+ desc = "An \"advanced\" \"surgical\" duffel bag containing absolutely everything you need to treat the worst and best of inorganic wounds. \
+ This one has extra tools and space for treatment of the ones even worse than the WORST of the worst! However, its highly specialized interior \
+ means it can ONLY hold synthetic repair tools."
+
+ storage_type = /datum/storage/duffel/synth_trauma_kit/advanced
+
+/datum/storage/duffel/synth_trauma_kit/advanced
+ exception_max = 10
+ max_slots = 31
+ max_total_storage = 48
+
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit/trauma/advanced/PopulateContents() // yes, this is all within the storage capacity
+ // Slash/Pierce wound tools - can reduce intensity of electrical damage (wires can fix generic burn damage)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/stack/cable_coil(src)
+ new /obj/item/crowbar/power(src) // jaws of life - wirecutters and crowbar
+ // Blunt/Brute tools
+ new /obj/item/weldingtool/experimental(src) // Used for repairing blunt damage or heating metal at T3 blunt
+ new /obj/item/screwdriver/power(src) // drill - screwdriver and wrench
+ new /obj/item/construction/rcd/loaded(src) // lets you instantly heal T3 blunt step 1
+ new /obj/item/bonesetter(src)
+ // Clothing items
+ new /obj/item/clothing/head/utility/welding(src)
+ new /obj/item/clothing/gloves/combat(src) // insulated AND heat-resistant
+ new /obj/item/clothing/glasses/hud/diagnostic(src) // When worn, generally improves wound treatment quality
+ // Reagent containers
+ new /obj/item/reagent_containers/spray/hercuri/chilled(src) // Highly effective (specifically coded to be) against burn wounds
+ new /obj/item/reagent_containers/spray/hercuri/chilled(src) // 2 of them
+ new /obj/item/storage/pill_bottle/nanite_slurry(src) // Heals blunt/burn
+ new /obj/item/storage/pill_bottle/liquid_solder(src) // Heals brain damage
+ new /obj/item/storage/pill_bottle/system_cleaner(src) // Heals toxin damage and purges chems
+ // Generic medical items
+ new /obj/item/stack/medical/gauze/twelve(src)
+ new /obj/item/healthanalyzer/advanced(src) // advanced, not a normal analyzer
+ new /obj/item/healthanalyzer/simple(src) // Buffs wound treatment and gives details of wounds it scans
+ // "Ghetto" tools, things you shouldn't ideally use but you might have to
+ new /obj/item/stack/medical/bone_gel(src) // Ghetto T2/3 option for securing internals
+ new /obj/item/plunger/reinforced(src) // Can be used to mold heated metal at T3 blunt
+
+/obj/item/storage/backpack/duffelbag/synth_treatment_kit/trauma/advanced/unzipped
+ zipped_up = FALSE
diff --git a/modular_skyrat/modules/medical/code/sprays.dm b/modular_skyrat/modules/medical/code/sprays.dm
new file mode 100644
index 00000000000000..8e2371e3f28549
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/sprays.dm
@@ -0,0 +1,12 @@
+/obj/item/reagent_containers/spray/hercuri/chilled
+ name = "chilled hercuri spray" // effective at cooling low-temperature burns but also is more efficienct at cooling high-temperature
+ desc = "A medical spray bottle. This one contains hercuri, a medicine used to negate the effects of dangerous high-temperature environments. \
+ This one comes pre-chilled, making it especially good at cooling synthetic burns! \n\
+ It has a bold warning label near the nozzle: ONLY USE IN EMERGENCIES! WILL CAUSE FREEZING! SECONDARY EFFECT ONLY USEFUL ON LIVING SYNTHS! INEFFECTIVE ON DECEASED! \n\
+ There's a smaller warning label on the body of the spray: IN EVENT OF RUNAWAY ENDOTHERMY, APPLY SYSTEM CLEANER!"
+ var/starting_temperature = 100
+
+/obj/item/reagent_containers/spray/hercuri/chilled/add_initial_reagents()
+ . = ..()
+
+ reagents.chem_temp = starting_temperature
diff --git a/modular_skyrat/modules/medical/code/wounds/medical.dm b/modular_skyrat/modules/medical/code/wounds/medical.dm
index 80662956e5f350..5a3723fff4bfb2 100644
--- a/modular_skyrat/modules/medical/code/wounds/medical.dm
+++ b/modular_skyrat/modules/medical/code/wounds/medical.dm
@@ -4,8 +4,6 @@
/obj/item/stack/medical/gauze
/// The amount of direct hits our limb can take before we fall off.
var/integrity = 2
- /// The bodypart we are attached to. Nullable if we aren't applied to anything.
- var/obj/item/bodypart/bodypart
/// If we are splinting a limb, this is the overlay prefix we will use.
var/splint_prefix = "splint"
/// If we are bandaging a limb, this is the overlay prefix we will use.
@@ -14,25 +12,16 @@
var/can_splint = TRUE
/obj/item/bodypart/apply_gauze(obj/item/stack/gauze)
- RegisterSignal(src, COMSIG_BODYPART_GAUZED, PROC_REF(got_gauzed))
-
. = ..()
- UnregisterSignal(src, COMSIG_BODYPART_GAUZED)
+ owner?.update_bandage_overlays()
-/// Signal handler that allows us to modularly detect if we were applied to a limb or not.
-/obj/item/bodypart/proc/got_gauzed(datum/signal_source, obj/item/stack/medical/gauze/new_gauze)
- SIGNAL_HANDLER
+/obj/item/stack/medical/gauze/Destroy()
+ var/mob/living/carbon/previously_gauzed = gauzed_bodypart?.owner
- if (istype(current_gauze, /obj/item/stack/medical/gauze))
- var/obj/item/stack/medical/gauze/applied_gauze = current_gauze
- applied_gauze.set_limb(src) // new_gauze isnt actually the gauze that was applied weirdly
+ . = ..()
-/obj/item/stack/medical/gauze/Destroy()
- bodypart?.current_gauze = null
- bodypart?.owner?.update_bandage_overlays()
- set_limb(null)
- return ..()
+ previously_gauzed?.update_bandage_overlays()
/**
* rip_off() called when someone rips it off
@@ -43,7 +32,7 @@
/obj/item/stack/medical/gauze/proc/rip_off()
if (is_pristine())
. = new src.type(null, 1)
- bodypart?.owner?.update_bandage_overlays()
+
qdel(src)
/// Returns either [splint_prefix] or [gauze_prefix] depending on if we are splinting or not. Suffixes it with a digitigrade flag if applicable for the limb.
@@ -56,8 +45,8 @@
else
prefix = gauze_prefix
- var/suffix = bodypart.body_zone
- if(bodypart.bodytype & BODYTYPE_DIGITIGRADE)
+ var/suffix = gauzed_bodypart.body_zone
+ if(gauzed_bodypart.bodytype & BODYTYPE_DIGITIGRADE)
suffix += "_digitigrade"
return "[prefix]_[suffix]"
@@ -69,7 +58,7 @@
if (!can_splint)
return FALSE
- for (var/datum/wound/iterated_wound as anything in bodypart.wounds)
+ for (var/datum/wound/iterated_wound as anything in gauzed_bodypart.wounds)
if (iterated_wound.wound_flags & SPLINT_OVERLAY)
return TRUE
@@ -95,35 +84,30 @@
/obj/item/stack/medical/gauze/proc/get_hit()
integrity--
if(integrity <= 0)
- if(bodypart.owner)
- to_chat(bodypart.owner, span_warning("The [name] on your [bodypart.name] tears and falls off!"))
+ if(gauzed_bodypart.owner)
+ to_chat(gauzed_bodypart.owner, span_warning("The [name] on your [gauzed_bodypart.name] tears and falls off!"))
qdel(src)
/obj/item/stack/medical/gauze/Topic(href, href_list)
. = ..()
if(href_list["remove"])
- if(!bodypart.owner)
+ if(!gauzed_bodypart.owner)
return
if(!iscarbon(usr))
return
- if(!in_range(usr, bodypart.owner))
+ if(!in_range(usr, gauzed_bodypart.owner))
return
- var/mob/living/carbon/C = usr
- var/self = (C == bodypart.owner)
- C.visible_message(span_notice("[C] begins removing [name] from [self ? "[bodypart.owner.p_Their()]" : "[bodypart.owner]'s" ] [bodypart.name]..."), span_notice("You begin to remove [name] from [self ? "your" : "[bodypart.owner]'s"] [bodypart.name]..."))
- if(!do_after(C, (self ? SELF_AID_REMOVE_DELAY : OTHER_AID_REMOVE_DELAY), target=bodypart.owner))
+ var/mob/living/carbon/carbon_user = usr
+ var/self = (carbon_user == gauzed_bodypart.owner)
+ carbon_user.visible_message(span_notice("[carbon_user] begins removing [name] from [self ? "[gauzed_bodypart.owner.p_Their()]" : "[gauzed_bodypart.owner]'s" ] [gauzed_bodypart.name]..."), span_notice("You begin to remove [name] from [self ? "your" : "[gauzed_bodypart.owner]'s"] [gauzed_bodypart.name]..."))
+ if(!do_after(carbon_user, (self ? SELF_AID_REMOVE_DELAY : OTHER_AID_REMOVE_DELAY), target = gauzed_bodypart.owner))
return
if(QDELETED(src))
return
- C.visible_message(span_notice("[C] removes [name] from [self ? "[bodypart.owner.p_Their()]" : "[bodypart.owner]'s" ] [bodypart.name]."), span_notice("You remove [name] from [self ? "your" : "[bodypart.owner]'s" ] [bodypart.name]."))
+ carbon_user.visible_message(span_notice("[carbon_user] removes [name] from [self ? "[gauzed_bodypart.owner.p_Their()]" : "[gauzed_bodypart.owner]'s" ] [gauzed_bodypart.name]."), span_notice("You remove [name] from [self ? "your" : "[gauzed_bodypart.owner]'s" ] [gauzed_bodypart.name]."))
var/obj/item/gotten = rip_off()
- if(gotten && !C.put_in_hands(gotten))
- gotten.forceMove(get_turf(C))
-
-/// Sets bodypart to limb, and then updates owner's bandage overlays. Limb is nullable.
-/obj/item/stack/medical/gauze/proc/set_limb(limb)
- bodypart = limb
- bodypart?.owner?.update_bandage_overlays()
+ if(gotten && !carbon_user.put_in_hands(gotten))
+ gotten.forceMove(get_turf(carbon_user))
/// Returns the name of ourself when used in a "owner is [usage_prefix] by [name]" examine_more situation/
/obj/item/stack/proc/get_gauze_description()
diff --git a/modular_skyrat/modules/medical/code/wounds/muscle.dm b/modular_skyrat/modules/medical/code/wounds/muscle.dm
index c1f9c3fc4c3515..35cc54c7099b07 100644
--- a/modular_skyrat/modules/medical/code/wounds/muscle.dm
+++ b/modular_skyrat/modules/medical/code/wounds/muscle.dm
@@ -33,13 +33,14 @@
Overwriting of base procs
*/
/datum/wound/muscle/wound_injury(datum/wound/old_wound = null, attack_direction)
- if(limb.held_index && victim.get_item_for_held_index(limb.held_index) && (disabling || prob(30 * severity)))
- var/obj/item/I = victim.get_item_for_held_index(limb.held_index)
- if(istype(I, /obj/item/offhand))
- I = victim.get_inactive_held_item()
+ var/obj/item/held_item = victim.get_item_for_held_index(limb.held_index || 0)
+ if(held_item && (disabling || prob(30 * severity)))
+ if(istype(held_item, /obj/item/offhand))
+ held_item = victim.get_inactive_held_item()
- if(I && victim.dropItemToGround(I))
- victim.visible_message(span_danger("[victim] drops [I] in shock!"), span_warning("The force on your [parse_zone(limb.body_zone)] causes you to drop [I]!"), vision_distance=COMBAT_MESSAGE_RANGE)
+ if(held_item && victim.dropItemToGround(held_item))
+ victim.visible_message(span_danger("[victim] drops [held_item] in shock!"), \
+ span_warning("The force on your [parse_zone(limb.body_zone)] causes you to drop [held_item]!"), vision_distance=COMBAT_MESSAGE_RANGE)
return ..()
@@ -173,3 +174,9 @@
id = "torn muscle"
/datum/status_effect/wound/muscle/severe
id = "ruptured tendon"
+
+/datum/status_effect/wound/muscle/robotic/moderate
+ id = "worn servo"
+
+/datum/status_effect/wound/muscle/robotic/severe
+ id = "severed hydraulic"
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt.dm b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt.dm
new file mode 100644
index 00000000000000..52995365b01fb6
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt.dm
@@ -0,0 +1,398 @@
+/// The multiplier put against our movement effects if our victim has the determined reagent
+#define ROBOTIC_WOUND_DETERMINATION_MOVEMENT_EFFECT_MOD 0.7
+/// The multiplier of stagger intensity on hit if our victim has the determined reagent
+#define ROBOTIC_WOUND_DETERMINATION_STAGGER_MOVEMENT_MULT 0.7
+
+/// The multiplier put against our movement effects if our limb is grasped
+#define ROBOTIC_BLUNT_GRASPED_MOVEMENT_MULT 0.7
+
+/datum/wound/blunt/robotic
+ name = "Robotic Blunt (Screws and bolts) Wound"
+ wound_flags = (ACCEPTS_GAUZE|SPLINT_OVERLAY|CAN_BE_GRASPED)
+
+ default_scar_file = METAL_SCAR_FILE
+
+ /// If we suffer severe head booboos, we can get brain traumas tied to them
+ var/datum/brain_trauma/active_trauma
+ /// What brain trauma group, if any, we can draw from for head wounds
+ var/brain_trauma_group
+ /// If we deal brain traumas, when is the next one due?
+ var/next_trauma_cycle
+ /// How long do we wait +/- 20% for the next trauma?
+ var/trauma_cycle_cooldown
+
+ /// The ratio stagger score will be multiplied against for determining the final chance of moving away from the attacker.
+ var/stagger_movement_chance_ratio = 1
+ /// The ratio stagger score will be multiplied against for determining the amount of pixelshifting we will do when we are hit.
+ var/stagger_shake_shift_ratio = 0.05
+
+ /// The ratio of stagger score to shake duration during a stagger() call
+ var/stagger_score_to_shake_duration_ratio = 0.1
+
+ /// In the stagger aftershock, the stagger score will be multiplied against for determining the chance of dropping held items.
+ var/stagger_drop_chance_ratio = 1.25
+ /// In the stagger aftershock, the stagger score will be multiplied against for determining the chance of falling over.
+ var/stagger_fall_chance_ratio = 1
+
+ /// In the stagger aftershock, the stagger score will be multiplied against for determining how long we are knocked down for.
+ var/stagger_aftershock_knockdown_ratio = 0.5
+ /// In the stagger after shock, the stagger score will be multiplied against this (if caused by movement) for determining how long we are knocked down for.
+ var/stagger_aftershock_knockdown_movement_ratio = 0.1
+
+ /// If the victim stops moving before the aftershock, aftershock effects will be multiplied against this.
+ var/aftershock_stopped_moving_score_mult = 0.1
+
+ /// The ratio damage applied will be multiplied against for determining our stagger score.
+ var/chest_attacked_stagger_mult = 2.5
+ /// The minimum score an attack must do to trigger a stagger.
+ var/chest_attacked_stagger_minimum_score = 5
+ /// The ratio of damage to stagger chance on hit.
+ var/chest_attacked_stagger_chance_ratio = 2
+
+ /// The base score given to stagger() when we successfully stagger on a move.
+ var/base_movement_stagger_score = 30
+ /// The base chance of moving to trigger stagger().
+ var/chest_movement_stagger_chance = 1
+
+ /// The base duration of a stagger()'s sprite shaking.
+ var/base_stagger_shake_duration = 1.5 SECONDS
+ /// The base duration of a stagger()'s sprite shaking if caused by movement.
+ var/base_stagger_movement_shake_duration = 1.5 SECONDS
+
+ /// The ratio of stagger score to camera shake chance.
+ var/stagger_camera_shake_chance_ratio = 0.75
+ /// The base duration of a stagger's aftershock's camerashake.
+ var/base_aftershock_camera_shake_duration = 1.5 SECONDS
+ /// The base strength of a stagger's aftershock's camerashake.
+ var/base_aftershock_camera_shake_strength = 0.5
+
+ /// The amount of x and y pixels we will be shaken around by during a movement stagger.
+ var/movement_stagger_shift = 1
+
+ /// If we are currently oscillating. If true, we cannot stagger().
+ var/oscillating = FALSE
+
+ /// % chance for hitting our limb to fix something.
+ var/percussive_maintenance_repair_chance = 10
+ /// Damage must be under this to proc percussive maintenance.
+ var/percussive_maintenance_damage_max = 7
+ /// Damage must be over this to proc percussive maintenance.
+ var/percussive_maintenance_damage_min = 0
+
+ /// The time, in world time, that we will be allowed to do another movement shake. Useful because it lets us prioritize attacked shakes over movement shakes.
+ var/time_til_next_movement_shake_allowed = 0
+
+ /// The percent our limb must get to max possible damage by burn damage alone to count as malleable if it has no T2 burn wound.
+ var/limb_burn_percent_to_max_threshold_for_malleable = 0.8 // must be 75% to max damage by burn damage alone
+
+ /// The last time our victim has moved. Used for determining if we should increase or decrease the chance of having stagger aftershock.
+ var/last_time_victim_moved = 0
+
+ processes = TRUE
+
+/datum/wound_pregen_data/blunt_metal
+ abstract = TRUE
+ required_limb_biostate = BIO_METAL
+ wound_series = WOUND_SERIES_METAL_BLUNT_BASIC
+ required_wounding_types = list(WOUND_BLUNT)
+
+/datum/wound_pregen_data/blunt_metal/generate_scar_priorities()
+ return list("[BIO_METAL]")
+
+/datum/wound/blunt/robotic/set_victim(new_victim)
+ if(victim)
+ UnregisterSignal(victim, COMSIG_MOVABLE_MOVED)
+ UnregisterSignal(victim, COMSIG_MOB_AFTER_APPLY_DAMAGE)
+ if(new_victim)
+ RegisterSignal(new_victim, COMSIG_MOVABLE_MOVED, PROC_REF(victim_moved))
+ RegisterSignal(new_victim, COMSIG_MOB_AFTER_APPLY_DAMAGE, PROC_REF(victim_attacked))
+
+ return ..()
+
+/datum/wound/blunt/robotic/get_limb_examine_description()
+ return span_warning("This limb looks loosely held together.")
+
+// this wound is unaffected by cryoxadone and pyroxadone
+/datum/wound/blunt/robotic/on_xadone(power)
+ return
+
+/datum/wound/blunt/robotic/wound_injury(datum/wound/old_wound, attack_direction)
+ . = ..()
+
+ // hook into gaining/losing gauze so crit bone wounds can re-enable/disable depending if they're slung or not
+ if(limb.body_zone == BODY_ZONE_HEAD && brain_trauma_group)
+ processes = TRUE
+ active_trauma = victim.gain_trauma_type(brain_trauma_group, TRAUMA_RESILIENCE_WOUND)
+ next_trauma_cycle = world.time + (rand(100-WOUND_BONE_HEAD_TIME_VARIANCE, 100+WOUND_BONE_HEAD_TIME_VARIANCE) * 0.01 * trauma_cycle_cooldown)
+
+ var/obj/item/held_item = victim.get_item_for_held_index(limb.held_index || 0)
+ if(held_item && (disabling || prob(30 * severity)))
+ if(istype(held_item, /obj/item/offhand))
+ held_item = victim.get_inactive_held_item()
+ if(held_item && victim.dropItemToGround(held_item))
+ victim.visible_message(span_danger("[victim] drops [held_item] in shock!"), span_warning("The force on your [limb.plaintext_zone] causes you to drop [held_item]!"), vision_distance=COMBAT_MESSAGE_RANGE)
+
+/datum/wound/blunt/robotic/remove_wound(ignore_limb, replaced)
+ . = ..()
+
+ QDEL_NULL(active_trauma)
+
+/datum/wound/blunt/robotic/handle_process(seconds_per_tick, times_fired)
+ . = ..()
+
+ if (!victim || IS_IN_STASIS(victim))
+ return
+
+ if (limb.body_zone == BODY_ZONE_HEAD && brain_trauma_group && world.time > next_trauma_cycle)
+ if (active_trauma)
+ QDEL_NULL(active_trauma)
+ else
+ active_trauma = victim.gain_trauma_type(brain_trauma_group, TRAUMA_RESILIENCE_WOUND)
+ next_trauma_cycle = world.time + (rand(100-WOUND_BONE_HEAD_TIME_VARIANCE, 100+WOUND_BONE_HEAD_TIME_VARIANCE) * 0.01 * trauma_cycle_cooldown)
+
+/// If true, allows our superstructure to be modified if we are T3. RCDs can always fix our superstructure.
+/datum/wound/blunt/robotic/proc/limb_malleable()
+ if (!isnull(get_overheat_wound()))
+ return TRUE
+ var/burn_damage_to_max = (limb.burn_dam / limb.max_damage) // only exists for the weird case where it cant get a overheat wound
+ if (burn_damage_to_max >= limb_burn_percent_to_max_threshold_for_malleable)
+ return TRUE
+ return FALSE
+
+/// If we have one, returns a robotic overheat wound of severe severity or higher. Null otherwise.
+/datum/wound/blunt/robotic/proc/get_overheat_wound()
+ RETURN_TYPE(/datum/wound/burn/robotic/overheat)
+ for (var/datum/wound/found_wound as anything in limb.wounds)
+ var/datum/wound_pregen_data/pregen_data = found_wound.get_pregen_data()
+ if (pregen_data.wound_series == WOUND_SERIES_METAL_BURN_OVERHEAT && found_wound.severity >= WOUND_SEVERITY_SEVERE) // meh solution but whateva
+ return found_wound
+ return null
+
+/// If our victim is lying down and is attacked in the chest, effective oscillation damage is multiplied against this.
+#define OSCILLATION_ATTACKED_LYING_DOWN_EFFECT_MULT 0.5
+
+/// If the attacker is wearing a diag hud, chance of percussive maintenance succeeding is multiplied against this.
+#define PERCUSSIVE_MAINTENANCE_DIAG_HUD_CHANCE_MULT 1.5
+/// If our wound has been scanned by a wound analyzer, chance of percussive maintenance succeeding is multiplied against this.
+#define PERCUSSIVE_MAINTENANCE_WOUND_SCANNED_CHANCE_MULT 1.5
+/// If the attacker is NOT our victim, chance of percussive maintenance succeeding is multiplied against this.
+#define PERCUSSIVE_MAINTENANCE_ATTACKER_NOT_VICTIM_CHANCE_MULT 2.5
+
+/// Signal handler proc to when our victim has damage applied via apply_damage(), which is a external attack.
+/datum/wound/blunt/robotic/proc/victim_attacked(datum/source, damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
+ SIGNAL_HANDLER
+
+ if (def_zone != limb.body_zone) // use this proc since receive damage can also be called for like, chems and shit
+ return
+ if(!victim)
+ return
+
+ var/effective_damage = (damage - blocked)
+
+ var/obj/item/stack/gauze = limb.current_gauze
+ if(gauze)
+ effective_damage *= gauze.splint_factor
+
+ switch (limb.body_zone)
+
+ if(BODY_ZONE_CHEST)
+ var/oscillation_mult = 1
+ if (victim.body_position == LYING_DOWN)
+ oscillation_mult *= OSCILLATION_ATTACKED_LYING_DOWN_EFFECT_MULT
+ var/oscillation_damage = effective_damage
+ var/stagger_damage = oscillation_damage * chest_attacked_stagger_mult
+ if (victim.has_status_effect(/datum/status_effect/determined))
+ oscillation_damage *= ROBOTIC_WOUND_DETERMINATION_STAGGER_MOVEMENT_MULT
+ if ((stagger_damage >= chest_attacked_stagger_minimum_score) && prob(oscillation_damage * chest_attacked_stagger_chance_ratio))
+ stagger(stagger_damage * oscillation_mult, attack_direction, attacking_item, shift = stagger_damage * stagger_shake_shift_ratio)
+
+ if(!uses_percussive_maintenance() || damage < percussive_maintenance_damage_min || damage > percussive_maintenance_damage_max || damagetype != BRUTE || sharpness)
+ return
+ var/success_chance_mult = 1
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ success_chance_mult *= PERCUSSIVE_MAINTENANCE_WOUND_SCANNED_CHANCE_MULT
+ var/mob/living/user
+ if (isatom(attacking_item))
+ var/atom/attacking_atom = attacking_item
+ user = attacking_atom.loc // nullable
+
+ if (istype(user))
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ success_chance_mult *= PERCUSSIVE_MAINTENANCE_DIAG_HUD_CHANCE_MULT
+
+ if (user != victim)
+ success_chance_mult *= PERCUSSIVE_MAINTENANCE_ATTACKER_NOT_VICTIM_CHANCE_MULT // encourages people to get other people to beat the shit out of their limbs
+ if (prob(percussive_maintenance_repair_chance * success_chance_mult))
+ handle_percussive_maintenance_success(attacking_item, user)
+ else
+ handle_percussive_maintenance_failure(attacking_item, user)
+
+#undef OSCILLATION_ATTACKED_LYING_DOWN_EFFECT_MULT
+#undef PERCUSSIVE_MAINTENANCE_DIAG_HUD_CHANCE_MULT
+#undef PERCUSSIVE_MAINTENANCE_WOUND_SCANNED_CHANCE_MULT
+#undef PERCUSSIVE_MAINTENANCE_ATTACKER_NOT_VICTIM_CHANCE_MULT
+
+/// The percent, in decimal, of a stagger's shake() duration, that will be used in a addtimer() to queue aftershock().
+#define STAGGER_PERCENT_OF_SHAKE_DURATION_TO_AFTERSHOCK_DELAY 0.65 // 1 = happens at the end, .5 = happens halfway through
+
+/// Causes an oscillation, which 1. has a chance to move our victim away from the attacker, and 2. after a delay, calls aftershock().
+/datum/wound/blunt/robotic/proc/stagger(stagger_score, attack_direction, obj/item/attacking_item, from_movement, shake_duration = base_stagger_shake_duration, shift, knockdown_ratio = stagger_aftershock_knockdown_ratio)
+ if (oscillating)
+ return
+
+ var/self_message = "Your [limb.plaintext_zone] oscillates"
+ var/message = "[victim]'s [limb.plaintext_zone] oscillates"
+ if (attacking_item)
+ message += " from the impact"
+ else if (from_movement)
+ message += " from the movement"
+ message += "!"
+ self_message += "! You might be able to avoid an aftershock by stopping and waiting..."
+
+ if (isnull(attack_direction))
+ attack_direction = get_dir(victim, attacking_item)
+
+ if (!isnull(attack_direction) && prob(stagger_score * stagger_movement_chance_ratio))
+ to_chat(victim, span_warning("The force of the blow sends you reeling!"))
+ var/turf/target_loc = get_step(victim, attack_direction)
+ victim.Move(target_loc)
+
+ victim.visible_message(span_warning(message), ignored_mobs = victim)
+ to_chat(victim, span_warning(self_message))
+ victim.balloon_alert(victim, "oscillation! stop moving")
+
+ victim.Shake(pixelshiftx = shift, pixelshifty = shift, duration = shake_duration)
+ var/aftershock_delay = (shake_duration * STAGGER_PERCENT_OF_SHAKE_DURATION_TO_AFTERSHOCK_DELAY)
+ var/knockdown_time = stagger_score * knockdown_ratio
+ addtimer(CALLBACK(src, PROC_REF(aftershock), stagger_score, attack_direction, attacking_item, world.time, knockdown_time), aftershock_delay)
+ oscillating = TRUE
+
+#undef STAGGER_PERCENT_OF_SHAKE_DURATION_TO_AFTERSHOCK_DELAY
+
+#define AFTERSHOCK_GRACE_THRESHOLD_PERCENT 0.33 // lower mult = later grace period = more forgiving
+
+/**
+ * Timer proc from stagger().
+ *
+ * Based on chance, causes items to be dropped, knockdown to be applied, and/or screenshake to occur.
+ * Chance is massively reduced if the victim isn't moving.
+ */
+/datum/wound/blunt/robotic/proc/aftershock(stagger_score, attack_direction, obj/item/attacking_item, stagger_starting_time, knockdown_time)
+ if (!still_exists())
+ return FALSE
+
+ var/message = "The oscillations from your [limb.plaintext_zone] spread, "
+ var/limb_message = "causing "
+ var/limb_affected
+
+ var/stopped_moving_grace_threshold = (world.time - ((world.time - stagger_starting_time) * AFTERSHOCK_GRACE_THRESHOLD_PERCENT))
+ var/victim_stopped_moving = (last_time_victim_moved <= stopped_moving_grace_threshold)
+ if (victim_stopped_moving)
+ stagger_score *= aftershock_stopped_moving_score_mult
+
+ if (prob(stagger_score * stagger_drop_chance_ratio))
+ limb_message += "your hands"
+ victim.drop_all_held_items()
+ limb_affected = TRUE
+
+ if (prob(stagger_score * stagger_fall_chance_ratio))
+ if (limb_affected)
+ limb_message += " and "
+ limb_message += "your legs"
+ victim.Knockdown(knockdown_time)
+ limb_affected = TRUE
+
+ if (prob(stagger_score * stagger_camera_shake_chance_ratio))
+ if (limb_affected)
+ limb_message += " and "
+ limb_message += "your head"
+ shake_camera(victim, base_aftershock_camera_shake_duration, base_aftershock_camera_shake_strength)
+ limb_affected = TRUE
+
+ if (limb_affected)
+ message += "[limb_message] to shake uncontrollably!"
+ else
+ message += "but pass harmlessly"
+ if (victim_stopped_moving)
+ message += " thanks to your stillness"
+ message += "."
+
+ to_chat(victim, span_danger(message))
+ victim.balloon_alert(victim, "oscillation over")
+
+ oscillating = FALSE
+
+#undef AFTERSHOCK_GRACE_THRESHOLD_PERCENT
+
+/// Called when percussive maintenance succeeds at its random roll.
+/datum/wound/blunt/robotic/proc/handle_percussive_maintenance_success(attacking_item, mob/living/user)
+ victim.visible_message(span_green("[victim]'s [limb.plaintext_zone] rattles from the impact, but looks a lot more secure!"), \
+ span_green("Your [limb.plaintext_zone] rattles into place!"))
+ remove_wound()
+
+/// Called when percussive maintenance fails at its random roll.
+/datum/wound/blunt/robotic/proc/handle_percussive_maintenance_failure(attacking_item, mob/living/user)
+ to_chat(victim, span_warning("Your [limb.plaintext_zone] rattles around, but you don't sense any sign of improvement."))
+
+/// If our victim has no gravity, the effects of movement are multiplied by this.
+#define VICTIM_MOVED_NO_GRAVITY_EFFECT_MULT 0.5
+/// If our victim is resting, or is walking and isnt forced to move, the effects of movement are multiplied by this.
+#define VICTIM_MOVED_CAREFULLY_EFFECT_MULT 0.25
+
+/// Signal handler proc that applies movements affect to our victim if they were moved.
+/datum/wound/blunt/robotic/proc/victim_moved(datum/source, atom/old_loc, dir, forced, list/old_locs)
+ SIGNAL_HANDLER
+
+ var/overall_mult = 1
+
+ var/obj/item/stack/gauze = limb.current_gauze
+ if (gauze)
+ overall_mult *= gauze.splint_factor
+ if (!victim.has_gravity(get_turf(victim)))
+ overall_mult *= VICTIM_MOVED_NO_GRAVITY_EFFECT_MULT
+ else if (victim.body_position == LYING_DOWN || (!forced && victim.move_intent == MOVE_INTENT_WALK))
+ overall_mult *= VICTIM_MOVED_CAREFULLY_EFFECT_MULT
+ if (victim.has_status_effect(/datum/status_effect/determined))
+ overall_mult *= ROBOTIC_WOUND_DETERMINATION_MOVEMENT_EFFECT_MOD
+ if (limb.grasped_by)
+ overall_mult *= ROBOTIC_BLUNT_GRASPED_MOVEMENT_MULT
+
+ overall_mult *= get_buckled_movement_consequence_mult(victim.buckled)
+
+ if (limb.body_zone == BODY_ZONE_CHEST)
+ var/stagger_chance = chest_movement_stagger_chance * overall_mult
+ if (prob(stagger_chance))
+ stagger(base_movement_stagger_score, shake_duration = base_stagger_movement_shake_duration, from_movement = TRUE, shift = movement_stagger_shift, knockdown_ratio = stagger_aftershock_knockdown_movement_ratio)
+
+ last_time_victim_moved = world.time
+
+#undef VICTIM_MOVED_NO_GRAVITY_EFFECT_MULT
+#undef VICTIM_MOVED_CAREFULLY_EFFECT_MULT
+
+/// If our victim is buckled to a generic object, movement effects will be multiplied against this.
+#define VICTIM_BUCKLED_BASE_MOVEMENT_EFFECT_MULT 0.5
+/// If our victim is buckled to a medical bed (e.g. rollerbed), movement effects will be multiplied against this.
+#define VICTIM_BUCKLED_ROLLER_BED_MOVEMENT_EFFECT_MULT 0.05
+
+/// Returns a multiplier to our movement effects based on what our victim is buckled to.
+/datum/wound/blunt/robotic/proc/get_buckled_movement_consequence_mult(atom/movable/buckled_to)
+ if (!buckled_to)
+ return 1
+
+ if (istype(buckled_to, /obj/structure/bed/medical))
+ return VICTIM_BUCKLED_ROLLER_BED_MOVEMENT_EFFECT_MULT
+ else
+ return VICTIM_BUCKLED_BASE_MOVEMENT_EFFECT_MULT
+
+#undef VICTIM_BUCKLED_BASE_MOVEMENT_EFFECT_MULT
+#undef VICTIM_BUCKLED_ROLLER_BED_MOVEMENT_EFFECT_MULT
+
+/// If this wound can be treated in its current state by just hitting it with a low force object. Exists for conditional logic, e.g. "Should we respond
+/// to percussive maintenance right now?". Critical blunt uses this to only react when the limb is malleable and superstructure is broken.
+/datum/wound/blunt/robotic/proc/uses_percussive_maintenance()
+ return FALSE
+
+#undef ROBOTIC_WOUND_DETERMINATION_MOVEMENT_EFFECT_MOD
+#undef ROBOTIC_WOUND_DETERMINATION_STAGGER_MOVEMENT_MULT
+
+#undef ROBOTIC_BLUNT_GRASPED_MOVEMENT_MULT
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T1.dm b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T1.dm
new file mode 100644
index 00000000000000..9cd2ae0adbbc0f
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T1.dm
@@ -0,0 +1,65 @@
+/datum/wound/blunt/robotic/moderate
+ name = "Loosened Screws"
+ desc = "Various semi-external fastening instruments have loosened, causing components to jostle, inhibiting limb control."
+ treat_text = "Recommend topical re-fastening of instruments with a screwdriver, though percussive maintenance via low-force bludgeoning may suffice - \
+ albeit at risk of worsening the injury."
+ examine_desc = "appears to be loosely secured"
+ occur_text = "jostles awkwardly and seems to slightly unfasten"
+ severity = WOUND_SEVERITY_MODERATE
+ simple_treat_text = "Bandaging the wound will reduce the impact until it's screws are secured - which is faster if done by \
+ someone else, a roboticist, an engineer, or with a diagnostic HUD."
+ homemade_treat_text = "In a pinch, percussive maintenance can reset the screws - the chance of which is increased if done by someone else or \
+ with a diagnostic HUD!"
+ status_effect_type = /datum/status_effect/wound/blunt/robotic/moderate
+ treatable_tools = list(TOOL_SCREWDRIVER)
+ interaction_efficiency_penalty = 1.2
+ limp_slowdown = 2.5
+ limp_chance = 30
+ threshold_penalty = 20
+ can_scar = FALSE
+ a_or_from = "from"
+
+/datum/wound_pregen_data/blunt_metal/loose_screws
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/blunt/robotic/moderate
+ viable_zones = list(BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
+ threshold_minimum = 30
+
+/datum/wound/blunt/robotic/moderate/uses_percussive_maintenance()
+ return TRUE
+
+/datum/wound/blunt/robotic/moderate/treat(obj/item/potential_treater, mob/user)
+ if (potential_treater.tool_behaviour == TOOL_SCREWDRIVER)
+ fasten_screws(potential_treater, user)
+ return TRUE
+
+ return ..()
+
+/// The main treatment for T1 blunt. Uses a screwdriver, guaranteed to always work, better with a diag hud. Removes the wound.
+/datum/wound/blunt/robotic/moderate/proc/fasten_screws(obj/item/screwdriver_tool, mob/user)
+ if (!screwdriver_tool.tool_start_check())
+ return
+
+ var/delay_mult = 1
+
+ if (user == victim)
+ delay_mult *= 3
+
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ delay_mult *= 0.5
+
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.5
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ victim.visible_message(span_notice("[user] begins fastening the screws of [their_or_other] [limb.plaintext_zone]..."), \
+ span_notice("You begin fastening the screws of [your_or_other] [limb.plaintext_zone]..."))
+
+ if (!screwdriver_tool.use_tool(target = victim, user = user, delay = (10 SECONDS * delay_mult), volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return
+
+ victim.visible_message(span_green("[user] finishes fastening [their_or_other] [limb.plaintext_zone]!"), \
+ span_green("You finish fastening [your_or_other] [limb.plaintext_zone]!"))
+
+ remove_wound()
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T2.dm b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T2.dm
new file mode 100644
index 00000000000000..68bac1fe12bf05
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T2.dm
@@ -0,0 +1,52 @@
+/datum/wound/blunt/robotic/secures_internals/severe
+ name = "Detached Fastenings"
+ desc = "Various fastening devices are extremely loose and solder has disconnected at multiple points, causing significant jostling of internal components and \
+ noticable limb dysfunction."
+ treat_text = "Fastening of bolts and screws by a qualified technician (though bone gel may suffice in the absence of one) followed by re-soldering."
+ examine_desc = "jostles with every move, solder visibly broken"
+ occur_text = "visibly cracks open, solder flying everywhere"
+ severity = WOUND_SEVERITY_SEVERE
+
+ simple_treat_text = "If on the chest, walk, grasp it, splint, rest or buckle yourself to something to reduce movement effects. \
+ Afterwards, get someone else, ideally a robo/engi to screwdriver/wrench it, and then re-solder it!"
+ homemade_treat_text = "If unable to screw/wrench, bone gel can, over time, secure inner components at risk of corrossion. \
+ Alternatively, crowbar the limb open to expose the internals - this will make it easier to re-secure them, but has a high risk of shocking you, \
+ so use insulated gloves. This will cripple the limb, so use it only as a last resort!"
+
+ wound_flags = (ACCEPTS_GAUZE|MANGLES_EXTERIOR|SPLINT_OVERLAY|CAN_BE_GRASPED)
+ treatable_by = list(/obj/item/stack/medical/bone_gel)
+ status_effect_type = /datum/status_effect/wound/blunt/robotic/severe
+ treatable_tools = list(TOOL_WELDER, TOOL_CROWBAR)
+
+ interaction_efficiency_penalty = 2
+ limp_slowdown = 6
+ limp_chance = 60
+
+ brain_trauma_group = BRAIN_TRAUMA_MILD
+ trauma_cycle_cooldown = 1.5 MINUTES
+
+ threshold_penalty = 40
+
+ base_movement_stagger_score = 40
+
+ chest_attacked_stagger_chance_ratio = 5
+ chest_attacked_stagger_mult = 3
+
+ chest_movement_stagger_chance = 3
+
+ stagger_aftershock_knockdown_ratio = 0.3
+ stagger_aftershock_knockdown_movement_ratio = 0.2
+
+ a_or_from = "from"
+
+ ready_to_secure_internals = TRUE
+ ready_to_resolder = FALSE
+
+ scar_keyword = "bluntsevere"
+
+/datum/wound_pregen_data/blunt_metal/fastenings
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/blunt/robotic/secures_internals/severe
+
+ threshold_minimum = 65
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T3.dm b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T3.dm
new file mode 100644
index 00000000000000..aa85498108aa5c
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/blunt/robotic_blunt_T3.dm
@@ -0,0 +1,385 @@
+/datum/wound/blunt/robotic/secures_internals/critical
+ name = "Collapsed Superstructure"
+ desc = "The superstructure has totally collapsed in one or more locations, causing extreme internal oscillation with every move and massive limb dysfunction"
+ treat_text = "Reforming of superstructure via either RCD or manual molding, followed by typical treatment of loosened internals. \
+ To manually mold, the limb must be aggressively grabbed and welded held to it to make it malleable (though attacking it til thermal overload may be adequate) \
+ followed by firmly grasping and molding the limb with heat-resistant gloves."
+ occur_text = "caves in on itself, damaged solder and shrapnel flying out in a miniature explosion"
+ examine_desc = "has caved in, with internal components visible through gaps in the metal"
+ severity = WOUND_SEVERITY_CRITICAL
+
+ disabling = TRUE
+
+ simple_treat_text = "If on the chest, walk, grasp it, splint, rest or buckle yourself to something to reduce movement effects. \
+ Afterwards, get someone, ideally a robo/engi to firmly grasp the limb and hold a welder to it. Then, have them use their hands to mold the metal - \
+ careful though, it's hot! An RCD can skip all this, but is hard to come by. Afterwards, have them screw/wrench and then re-solder the limb!"
+
+ homemade_treat_text = "The metal can be made malleable by repeated application of a welder, to a severe burn. Afterwards, a plunger can reset the metal, \
+ as can percussive maintenance. After the metal is reset, if unable to screw/wrench, bone gel can, over time, secure inner components at risk of corrossion. \
+ Alternatively, crowbar the limb open to expose the internals - this will make it easier to re-secure them, but has a high risk of shocking you, \
+ so use insulated gloves. This will cripple the limb, so use it only as a last resort!"
+
+ interaction_efficiency_penalty = 2.8
+ limp_slowdown = 8
+ limp_chance = 80
+ threshold_penalty = 60
+
+ brain_trauma_group = BRAIN_TRAUMA_SEVERE
+ trauma_cycle_cooldown = 2.5 MINUTES
+
+ scar_keyword = "bluntcritical"
+
+ status_effect_type = /datum/status_effect/wound/blunt/robotic/critical
+
+ sound_effect = 'sound/effects/wounds/crack2.ogg'
+
+ wound_flags = (ACCEPTS_GAUZE|MANGLES_EXTERIOR|SPLINT_OVERLAY|CAN_BE_GRASPED)
+ treatable_by = list(/obj/item/stack/medical/bone_gel)
+ status_effect_type = /datum/status_effect/wound/blunt/robotic/critical
+ treatable_tools = list(TOOL_WELDER, TOOL_CROWBAR)
+
+ base_movement_stagger_score = 55
+
+ base_aftershock_camera_shake_duration = 1.75 SECONDS
+ base_aftershock_camera_shake_strength = 1
+
+ chest_attacked_stagger_chance_ratio = 6.5
+ chest_attacked_stagger_mult = 4
+
+ chest_movement_stagger_chance = 14
+
+ aftershock_stopped_moving_score_mult = 0.3
+
+ stagger_aftershock_knockdown_ratio = 0.5
+ stagger_aftershock_knockdown_movement_ratio = 0.3
+
+ percussive_maintenance_repair_chance = 3
+ percussive_maintenance_damage_max = 6
+
+ regen_time_needed = 60 SECONDS
+ gel_damage = 20
+
+ ready_to_secure_internals = FALSE
+ ready_to_resolder = FALSE
+
+ a_or_from = "a"
+
+ /// Has the first stage of our treatment been completed? E.g. RCDed, manually molded...
+ var/superstructure_remedied = FALSE
+
+/datum/wound_pregen_data/blunt_metal/superstructure
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/blunt/robotic/secures_internals/critical
+ threshold_minimum = 125
+
+/datum/wound/blunt/robotic/secures_internals/critical/item_can_treat(obj/item/potential_treater)
+ if(!superstructure_remedied)
+ if(istype(potential_treater, /obj/item/construction/rcd))
+ return TRUE
+ if(limb_malleable() && istype(potential_treater, /obj/item/plunger))
+ return TRUE
+ return ..()
+
+/datum/wound/blunt/robotic/secures_internals/critical/check_grab_treatments(obj/item/potential_treater, mob/user)
+ if(potential_treater.tool_behaviour == TOOL_WELDER && (!superstructure_remedied && !limb_malleable()))
+ return TRUE
+ return ..()
+
+/datum/wound/blunt/robotic/secures_internals/critical/treat(obj/item/item, mob/user)
+ if(!superstructure_remedied)
+ if(istype(item, /obj/item/construction/rcd))
+ return rcd_superstructure(item, user)
+ if(uses_percussive_maintenance() && istype(item, /obj/item/plunger))
+ return plunge(item, user)
+ if(item.tool_behaviour == TOOL_WELDER && !limb_malleable() && isliving(victim.pulledby))
+ var/mob/living/living_puller = victim.pulledby
+ if (living_puller.grab_state >= GRAB_AGGRESSIVE) // only let other people do this
+ return heat_metal(item, user)
+ return ..()
+
+/datum/wound/blunt/robotic/secures_internals/critical/try_handling(mob/living/carbon/human/user)
+ if(user.pulling != victim || user.zone_selected != limb.body_zone)
+ return FALSE
+
+ if(superstructure_remedied || !limb_malleable())
+ return FALSE
+
+ if(user.grab_state < GRAB_AGGRESSIVE)
+ to_chat(user, span_warning("You must have [victim] in an aggressive grab to manipulate [victim.p_their()] [lowertext(name)]!"))
+ return TRUE
+
+ user.visible_message(span_danger("[user] begins softly pressing against [victim]'s collapsed [limb.plaintext_zone]..."), \
+ span_notice("You begin softly pressing against [victim]'s collapsed [limb.plaintext_zone]..."), \
+ ignored_mobs = victim)
+ to_chat(victim, span_userdanger("[user] begins pressing against your collapsed [limb.plaintext_zone]!"))
+
+ var/delay_mult = 1
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.75
+
+ if(!do_after(user, 8 SECONDS, target = victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return
+ mold_metal(user)
+ return TRUE
+
+/// If the user turns combat mode on after they start to mold metal, our limb takes this much brute damage.
+#define MOLD_METAL_SABOTAGE_BRUTE_DAMAGE 30 // really punishing
+/// Our limb takes this much brute damage on a failed mold metal attempt.
+#define MOLD_METAL_FAILURE_BRUTE_DAMAGE 5
+/// If the user's hand is unprotected from heat when they mold metal, we do this much burn damage to it.
+#define MOLD_METAL_HAND_BURNT_BURN_DAMAGE 5
+/// Gloves must be above or at this threshold to cause the user to not be burnt apon trying to mold metal.
+#define MOLD_METAL_HEAT_RESISTANCE_THRESHOLD 1000 // less than the black gloves max resist
+/**
+ * Standard treatment for 1st step of T3, after the limb has been made malleable. Done via aggrograb.
+ * High chance to work, very high with robo/engi wires and diag hud.
+ * Can be sabotaged by switching to combat mode.
+ * Deals brute to the limb on failure.
+ * Burns the hand of the user if its not insulated.
+ */
+/datum/wound/blunt/robotic/secures_internals/critical/proc/mold_metal(mob/living/carbon/human/user)
+ var/chance = 60
+
+ var/knows_wires = FALSE
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ chance *= 2
+ knows_wires = TRUE
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ chance *= 1.25
+ knows_wires = TRUE
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ chance *= 2
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ if (knows_wires)
+ chance *= 1.25
+ else
+ chance *= 2
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+
+ if ((user != victim && user.combat_mode))
+ user.visible_message(span_bolddanger("[user] molds [their_or_other] [limb.plaintext_zone] into a really silly shape! What a goofball!"), \
+ span_danger("You maliciously mold [victim]'s [limb.plaintext_zone] into a weird shape, damaging it in the process!"), ignored_mobs = victim)
+ to_chat(victim, span_userdanger("[user] molds your [limb.plaintext_zone] into a weird shape, damaging it in the process!"))
+
+ limb.receive_damage(brute = MOLD_METAL_SABOTAGE_BRUTE_DAMAGE, wound_bonus = CANT_WOUND, damage_source = user)
+ else if (prob(chance))
+ user.visible_message(span_green("[user] carefully molds [their_or_other] [limb.plaintext_zone] into the proper shape!"), \
+ span_green("You carefully mold [victim]'s [limb.plaintext_zone] into the proper shape!"), ignored_mobs = victim)
+ to_chat(victim, span_green("[user] carefully molds your [limb.plaintext_zone] into the proper shape!"))
+ to_chat(user, span_green("[capitalize(your_or_other)] [limb.plaintext_zone] has been molded into the proper shape! Your next step is to use a screwdriver/wrench to secure your internals."))
+ set_superstructure_status(TRUE)
+ else
+ user.visible_message(span_danger("[user] accidentally molds [their_or_other] [limb.plaintext_zone] into the wrong shape!"), \
+ span_danger("You accidentally mold [your_or_other] [limb.plaintext_zone] into the wrong shape!"), ignored_mobs = victim)
+ to_chat(victim, span_userdanger("[user] accidentally molds your [limb.plaintext_zone] into the wrong shape!"))
+
+ limb.receive_damage(brute = MOLD_METAL_FAILURE_BRUTE_DAMAGE, damage_source = user, wound_bonus = CANT_WOUND)
+
+ var/sufficiently_insulated_gloves = FALSE
+ var/obj/item/clothing/gloves/worn_gloves = user.gloves
+ if ((worn_gloves?.heat_protection & HANDS) && worn_gloves?.max_heat_protection_temperature && worn_gloves.max_heat_protection_temperature >= MOLD_METAL_HEAT_RESISTANCE_THRESHOLD)
+ sufficiently_insulated_gloves = TRUE
+
+ if (sufficiently_insulated_gloves || HAS_TRAIT(user, TRAIT_RESISTHEAT) || HAS_TRAIT(user, TRAIT_RESISTHEATHANDS))
+ return
+
+ to_chat(user, span_danger("You burn your hand on [victim]'s [limb.plaintext_zone]!"))
+ var/obj/item/bodypart/affecting = user.get_bodypart("[(user.active_hand_index % 2 == 0) ? "r" : "l" ]_arm")
+ affecting?.receive_damage(burn = MOLD_METAL_HAND_BURNT_BURN_DAMAGE, damage_source = limb)
+
+#undef MOLD_METAL_SABOTAGE_BRUTE_DAMAGE
+#undef MOLD_METAL_FAILURE_BRUTE_DAMAGE
+#undef MOLD_METAL_HAND_BURNT_BURN_DAMAGE
+#undef MOLD_METAL_HEAT_RESISTANCE_THRESHOLD
+
+/**
+ * A "safe" way to give our victim a T2 burn wound. Requires an aggrograb, and a welder. This is required to mold metal, the 1st step of treatment.
+ * Guaranteed to work. After a delay, causes a T2 burn wound with no damage.
+ * Can be sabotaged by enabling combat mode to cause a T3.
+ */
+/datum/wound/blunt/robotic/secures_internals/critical/proc/heat_metal(obj/item/welder, mob/living/user)
+ if (!welder.tool_use_check())
+ return TRUE
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+
+ user?.visible_message(span_danger("[user] carefully holds [welder] to [their_or_other] [limb.plaintext_zone], slowly heating it..."), \
+ span_warning("You carefully hold [welder] to [your_or_other] [limb.plaintext_zone], slowly heating it..."), ignored_mobs = victim)
+
+ var/delay_mult = 1
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.75
+
+ if (!welder.use_tool(target = victim, user = user, delay = 10 SECONDS * delay_mult, volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ var/wound_path = /datum/wound/burn/robotic/overheat/severe
+ if (user != victim && user.combat_mode)
+ wound_path = /datum/wound/burn/robotic/overheat/critical // it really isnt that bad, overheat wounds are a bit funky
+ user.visible_message(span_danger("[user] heats [victim]'s [limb.plaintext_zone] aggressively, overheating it far beyond the necessary point!"), \
+ span_danger("You heat [victim]'s [limb.plaintext_zone] aggressively, overheating it far beyond the necessary point!"), ignored_mobs = victim)
+ to_chat(victim, span_userdanger("[user] heats your [limb.plaintext_zone] aggressively, overheating it far beyond the necessary point!"))
+
+ var/datum/wound/burn/robotic/overheat/overheat_wound = new wound_path
+ overheat_wound.apply_wound(limb, wound_source = welder)
+
+ to_chat(user, span_green("[capitalize(your_or_other)] [limb.plaintext_zone] is now heated, allowing it to be molded! Your next step is to have someone physically reset the superstructure with their hands."))
+ return TRUE
+
+/// Cost of an RCD to quickly fix our broken in raw matter
+#define ROBOTIC_T3_BLUNT_WOUND_RCD_COST 25
+/// Cost of an RCD to quickly fix our broken in silo material
+#define ROBOTIC_T3_BLUNT_WOUND_RCD_SILO_COST ROBOTIC_T3_BLUNT_WOUND_RCD_COST / 4
+
+/// The "premium" treatment for 1st step of T3. Requires an RCD. Guaranteed to work, but can cause damage if delay is high.
+/datum/wound/blunt/robotic/secures_internals/critical/proc/rcd_superstructure(obj/item/construction/rcd/treating_rcd, mob/user)
+ if (!treating_rcd.tool_use_check())
+ return TRUE
+
+ var/has_enough_matter = (treating_rcd.get_matter(user) > ROBOTIC_T3_BLUNT_WOUND_RCD_COST)
+ var/silo_has_enough_materials = (treating_rcd.get_silo_iron() > ROBOTIC_T3_BLUNT_WOUND_RCD_SILO_COST)
+
+ if (!silo_has_enough_materials && has_enough_matter)
+ return TRUE
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+
+ var/base_time = 10 SECONDS
+ var/delay_mult = 1
+ var/knows_wires = FALSE
+ if (victim == user)
+ delay_mult *= 3 // real slow
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.75
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ delay_mult *= 0.5
+ knows_wires = TRUE
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ delay_mult *= 0.5 // engis are accustomed to using RCDs
+ knows_wires = TRUE
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ if (knows_wires)
+ delay_mult *= 0.85
+ else
+ delay_mult *= 0.5
+
+ var/final_time = (base_time * delay_mult)
+ var/misused = (final_time > base_time) // if we damage the limb when we're done
+
+ if (user)
+ var/misused_text = (misused ? "unsteadily " : "")
+
+ var/message = "[user]'s RCD whirs to life as it begins [misused_text]replacing the damaged superstructure of [their_or_other] [limb.plaintext_zone]..."
+ var/self_message = "Your RCD whirs to life as it begins [misused_text]replacing the damaged superstructure of [your_or_other] [limb.plaintext_zone]..."
+
+ if (misused) // warning span if misused, notice span otherwise
+ message = span_danger(message)
+ self_message = span_danger(self_message)
+ else
+ message = span_notice(message)
+ self_message = span_notice(self_message)
+
+ user.visible_message(message, self_message)
+
+ if (!treating_rcd.use_tool(target = victim, user = user, delay = final_time, volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+ playsound(get_turf(treating_rcd), 'sound/machines/ping.ogg', 75) // celebration! we did it
+ set_superstructure_status(TRUE)
+
+ var/use_amount = (silo_has_enough_materials ? ROBOTIC_T3_BLUNT_WOUND_RCD_SILO_COST : ROBOTIC_T3_BLUNT_WOUND_RCD_COST)
+ treating_rcd.useResource(use_amount, user)
+
+ if (user)
+ var/misused_text = (misused ? ", though it replaced a bit more than it should've..." : "!")
+ var/message = "[user]'s RCD lets out a small ping as it finishes replacing the superstructure of [their_or_other] [limb.plaintext_zone][misused_text]"
+ var/self_message = "Your RCD lets out a small ping as it finishes replacing the superstructure of [your_or_other] [limb.plaintext_zone][misused_text]"
+ if (misused)
+ message = span_danger(message)
+ self_message = span_danger(self_message)
+ else
+ message = span_green(message)
+ self_message = span_green(self_message)
+
+ user.visible_message(message, self_message)
+ if (misused)
+ limb.receive_damage(brute = 10, damage_source = treating_rcd, wound_bonus = CANT_WOUND)
+ // the double message is fine here, since the first message also tells you if you fucked up and did some damage
+ to_chat(user, span_green("The superstructure has been reformed! Your next step is to secure the internals via a screwdriver/wrench."))
+ return TRUE
+
+#undef ROBOTIC_T3_BLUNT_WOUND_RCD_COST
+#undef ROBOTIC_T3_BLUNT_WOUND_RCD_SILO_COST
+
+/**
+ * Goofy but practical, this is the superior ghetto self-tend of T3's first step compared to percussive maintenance.
+ * Still requires the limb to be malleable, but has a high chance of success and doesn't burn your hand, but gives worse bonuses for wires/HUD.
+ */
+/datum/wound/blunt/robotic/secures_internals/critical/proc/plunge(obj/item/plunger/treating_plunger, mob/user)
+ if (!treating_plunger.tool_use_check())
+ return TRUE
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ user?.visible_message(span_notice("[user] begins plunging at the dents on [their_or_other] [limb.plaintext_zone] with [treating_plunger]..."), \
+ span_green("You begin plunging at the dents on [your_or_other] [limb.plaintext_zone] with [treating_plunger]..."))
+
+ var/delay_mult = 1
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.75
+
+ delay_mult /= treating_plunger.plunge_mod
+
+ if (!treating_plunger.use_tool(target = victim, user = user, delay = 8 SECONDS * delay_mult, volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ var/success_chance = 80
+ if (victim == user)
+ success_chance *= 0.6
+
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ success_chance *= 1.25
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ success_chance *= 1.1
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ success_chance *= 1.25 // its kinda alien to do this, so even people with the wires get the full bonus of diag huds
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ success_chance *= 1.5
+
+ if (prob(success_chance))
+ user?.visible_message(span_green("[victim]'s [limb.plaintext_zone] lets out a sharp POP as [treating_plunger] forces it into its normal position!"), \
+ span_green("[victim]'s [limb.plaintext_zone] lets out a sharp POP as your [treating_plunger] forces it into its normal position!"))
+ to_chat(user, span_green("[capitalize(your_or_other)] [limb.plaintext_zone]'s structure has been reset to it's proper position! Your next step is to secure it with a screwdriver/wrench, though bone gel would also work."))
+ set_superstructure_status(TRUE)
+ else
+ user?.visible_message(span_danger("[victim]'s [limb.plaintext_zone] splinters from [treating_plunger]'s plunging!"), \
+ span_danger("[capitalize(your_or_other)] [limb.plaintext_zone] splinters from your [treating_plunger]'s plunging!"))
+ limb.receive_damage(brute = 5, damage_source = treating_plunger)
+
+ return TRUE
+
+/datum/wound/blunt/robotic/secures_internals/critical/handle_percussive_maintenance_success(attacking_item, mob/living/user)
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ victim.visible_message(span_green("[victim]'s [limb.plaintext_zone] gets smashed into a proper shape!"), \
+ span_green("Your [limb.plaintext_zone] gets smashed into a proper shape!"))
+
+ var/user_message = "[capitalize(your_or_other)] [limb.plaintext_zone]'s superstructure has been reset! Your next step is to screwdriver/wrench the internals, \
+ though if you're desperate enough to use percussive maintenance, you might want to either use a crowbar or bone gel..."
+ to_chat(user, span_green(user_message))
+
+ set_superstructure_status(TRUE)
+
+/datum/wound/blunt/robotic/secures_internals/critical/handle_percussive_maintenance_failure(attacking_item, mob/living/user)
+ to_chat(victim, span_danger("Your [limb.plaintext_zone] only deforms more from the impact..."))
+ limb.receive_damage(brute = 1, damage_source = attacking_item, wound_bonus = CANT_WOUND)
+
+/datum/wound/blunt/robotic/secures_internals/critical/uses_percussive_maintenance()
+ return (!superstructure_remedied && limb_malleable())
+
+/// Transitions our steps by setting both superstructure and secure internals readiness.
+/datum/wound/blunt/robotic/secures_internals/critical/proc/set_superstructure_status(remedied)
+ superstructure_remedied = remedied
+ ready_to_secure_internals = remedied
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/blunt/secures_internals.dm b/modular_skyrat/modules/medical/code/wounds/synth/blunt/secures_internals.dm
new file mode 100644
index 00000000000000..9dd515813efe60
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/blunt/secures_internals.dm
@@ -0,0 +1,388 @@
+/// A subtype of blunt wounds that has a "secure internals" step
+/datum/wound/blunt/robotic/secures_internals
+ /// Our current counter for gel + gauze regeneration
+ var/regen_time_elapsed = 0 SECONDS
+ /// Time needed for gel to secure internals.
+ var/regen_time_needed = 30 SECONDS
+
+ /// If we have used bone gel to secure internals.
+ var/gelled = FALSE
+ /// Total brute damage taken over the span of [regen_time_needed] deciseconds when we gel our limb.
+ var/gel_damage = 10 // brute in total
+
+ /// If we are ready to begin screwdrivering or gelling our limb.
+ var/ready_to_secure_internals = FALSE
+ /// If our external plating has been torn open and we can access our internals without a tool
+ var/crowbarred_open = FALSE
+ /// If internals are secured, and we are ready to weld our limb closed and end the wound
+ var/ready_to_resolder = TRUE
+
+/datum/wound/blunt/robotic/secures_internals/handle_process(seconds_per_tick, times_fired)
+ . = ..()
+
+ if (!victim || IS_IN_STASIS(victim))
+ return
+
+ if (gelled)
+ regen_time_elapsed += ((seconds_per_tick SECONDS) / 2)
+ if(victim.body_position == LYING_DOWN)
+ if(SPT_PROB(30, seconds_per_tick))
+ regen_time_elapsed += 1 SECONDS
+ if(victim.IsSleeping() && SPT_PROB(30, seconds_per_tick))
+ regen_time_elapsed += 1 SECONDS
+
+ var/effective_damage = ((gel_damage / (regen_time_needed / 10)) * seconds_per_tick)
+ var/obj/item/stack/gauze = limb.current_gauze
+ if (gauze)
+ effective_damage *= gauze.splint_factor
+ limb.receive_damage(effective_damage, wound_bonus = CANT_WOUND, damage_source = src)
+ if(effective_damage && prob(33))
+ var/gauze_text = (gauze?.splint_factor ? ", although the [gauze] helps to prevent some of the leakage" : "")
+ to_chat(victim, span_danger("Your [limb.plaintext_zone] sizzles as some gel leaks and warps the exterior metal[gauze_text]..."))
+
+ if(regen_time_elapsed > regen_time_needed)
+ if(!victim || !limb)
+ qdel(src)
+ return
+ to_chat(victim, span_green("The gel within your [limb.plaintext_zone] has fully hardened, allowing you to re-solder it!"))
+ gelled = FALSE
+ ready_to_resolder = TRUE
+ ready_to_secure_internals = FALSE
+ set_disabling(FALSE)
+
+/datum/wound/blunt/robotic/secures_internals/modify_desc_before_span(desc)
+ . = ..()
+
+ var/use_exclamation = FALSE
+
+ if (!limb.current_gauze) // gauze covers it up
+ if (crowbarred_open)
+ . += ", [span_notice("and is violently torn open, internals visible to the outside")]"
+ use_exclamation = TRUE
+ if (gelled)
+ . += ", [span_notice("with fizzling blue surgical gel leaking out of the cracks")]"
+ use_exclamation = TRUE
+ if (use_exclamation)
+ . += "!"
+
+/datum/wound/blunt/robotic/secures_internals/get_scanner_description(mob/user)
+ . = ..()
+
+ var/to_add = get_wound_status()
+ if (!isnull(to_add))
+ . += "\nWound status: [to_add]"
+
+/datum/wound/blunt/robotic/secures_internals/get_simple_scanner_description(mob/user)
+ . = ..()
+
+ var/to_add = get_wound_status()
+ if (!isnull(to_add))
+ . += "\nWound status: [to_add]"
+
+/// Returns info specific to the dynamic state of the wound.
+/datum/wound/blunt/robotic/secures_internals/proc/get_wound_status(mob/user)
+ if (crowbarred_open)
+ . += "The limb has been torn open, allowing ease of access to internal components, but also disabling it. "
+ if (gelled)
+ . += "Bone gel has been applied, causing progressive corrosion of the metal, but eventually securing the internals. "
+
+/datum/wound/blunt/robotic/secures_internals/item_can_treat(obj/item/potential_treater, mob/user)
+ if (potential_treater.tool_behaviour == TOOL_WELDER || potential_treater.tool_behaviour == TOOL_CAUTERY)
+ if (ready_to_resolder)
+ return TRUE
+
+ if (ready_to_secure_internals)
+ if (item_can_secure_internals(potential_treater))
+ return TRUE
+
+ return ..()
+
+/datum/wound/blunt/robotic/secures_internals/treat(obj/item/potential_treater, mob/user)
+ if (ready_to_secure_internals)
+ if (istype(potential_treater, /obj/item/stack/medical/bone_gel))
+ return apply_gel(potential_treater, user)
+ else if (!crowbarred_open && potential_treater.tool_behaviour == TOOL_CROWBAR)
+ return crowbar_open(potential_treater, user)
+ else if (item_can_secure_internals(potential_treater))
+ return secure_internals_normally(potential_treater, user)
+ else if (ready_to_resolder && (potential_treater.tool_behaviour == TOOL_WELDER) || (potential_treater.tool_behaviour == TOOL_CAUTERY))
+ return resolder(potential_treater, user)
+
+ return ..()
+
+/// Returns TRUE if the item can be used in our 1st step (2nd if T3) of repairs.
+/datum/wound/blunt/robotic/secures_internals/proc/item_can_secure_internals(obj/item/potential_treater)
+ return (potential_treater.tool_behaviour == TOOL_SCREWDRIVER || potential_treater.tool_behaviour == TOOL_WRENCH || istype(potential_treater, /obj/item/stack/medical/bone_gel))
+
+#define CROWBAR_OPEN_SELF_TEND_DELAY_MULT 2
+#define CROWBAR_OPEN_KNOWS_ROBO_WIRES_DELAY_MULT 0.5
+#define CROWBAR_OPEN_KNOWS_ENGI_WIRES_DELAY_MULT 0.5
+#define CROWBAR_OPEN_HAS_DIAG_HUD_DELAY_MULT 0.5
+#define CROWBAR_OPEN_WOUND_SCANNED_DELAY_MULT 0.5
+/// If our limb is essential, damage dealt to it by tearing it open will be multiplied against this.
+#define CROWBAR_OPEN_ESSENTIAL_LIMB_DAMAGE_MULT 1.5
+
+/// The "power" put into electrocute_act whenever someone gets shocked when they crowbar open our limb
+#define CROWBAR_OPEN_SHOCK_POWER 20
+/// The brute damage done to this limb (doubled on essential limbs) when it is crowbarred open
+#define CROWBAR_OPEN_BRUTE_DAMAGE 20
+
+/**
+ * Available during the "secure internals" step of T2 and T3. Requires a crowbar. Low-quality ghetto option.
+ *
+ * Tears open the limb, exposing internals. This massively increases the chance of secure internals succeeding, and removes the self-tend malice.
+ *
+ * Deals significant damage to the limb, and shocks the user (causing failure) if victim is alive, this limb is wired, and user is not insulated.
+ */
+/datum/wound/blunt/robotic/secures_internals/proc/crowbar_open(obj/item/crowbarring_item, mob/living/user)
+ if (!crowbarring_item.tool_start_check())
+ return TRUE
+
+ var/delay_mult = 1
+ if (user == victim)
+ delay_mult *= CROWBAR_OPEN_SELF_TEND_DELAY_MULT
+
+ var/knows_wires = FALSE
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ delay_mult *= CROWBAR_OPEN_KNOWS_ROBO_WIRES_DELAY_MULT
+ knows_wires = TRUE
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ delay_mult *= CROWBAR_OPEN_KNOWS_ENGI_WIRES_DELAY_MULT
+ knows_wires = TRUE
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ if (knows_wires)
+ delay_mult *= (CROWBAR_OPEN_HAS_DIAG_HUD_DELAY_MULT * 1.5)
+ else
+ delay_mult *= CROWBAR_OPEN_HAS_DIAG_HUD_DELAY_MULT
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= CROWBAR_OPEN_WOUND_SCANNED_DELAY_MULT
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+
+ var/self_message = span_warning("You start prying open [your_or_other] [limb.plaintext_zone] with [crowbarring_item]...")
+
+ user?.visible_message(span_bolddanger("[user] starts prying open [their_or_other] [limb.plaintext_zone] with [crowbarring_item]!"), self_message, ignored_mobs = list(victim))
+
+ var/victim_message
+ if (user != victim) // this exists so we can do a userdanger
+ victim_message = span_userdanger("[user] starts prying open your [limb.plaintext_zone] with [crowbarring_item]!")
+ else
+ victim_message = self_message
+ to_chat(victim, victim_message)
+
+ playsound(get_turf(crowbarring_item), 'sound/machines/airlock_alien_prying.ogg', 30, TRUE)
+ if (!crowbarring_item.use_tool(target = victim, user = user, delay = (7 SECONDS * delay_mult), volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ var/limb_can_shock = (victim.stat != DEAD && limb.biological_state & BIO_WIRED)
+ var/stunned = FALSE
+
+ var/message
+
+ if (user && limb_can_shock)
+ var/electrocute_flags = (SHOCK_KNOCKDOWN|SHOCK_NO_HUMAN_ANIM|SHOCK_SUPPRESS_MESSAGE)
+ var/stun_chance = 100
+
+ if (HAS_TRAIT(user, TRAIT_SHOCKIMMUNE))
+ stun_chance = 0
+
+ else if (iscarbon(user)) // doesn't matter if we're shock immune, it's set to 0 anyway
+ var/mob/living/carbon/carbon_user = user
+ if (carbon_user.gloves)
+ stun_chance *= carbon_user.gloves.siemens_coefficient
+
+ if (ishuman(user))
+ var/mob/living/carbon/human/human_user = user
+ stun_chance *= human_user.physiology.siemens_coeff
+ stun_chance *= carbon_user.dna.species.siemens_coeff
+
+ if (stun_chance && prob(stun_chance))
+ electrocute_flags &= ~SHOCK_KNOCKDOWN
+ electrocute_flags &= ~SHOCK_NO_HUMAN_ANIM
+ stunned = TRUE
+
+ message = span_boldwarning("[user] is shocked by [their_or_other] [limb.plaintext_zone], [user.p_their()] crowbar slipping as [user.p_they()] briefly convulse!")
+ self_message = span_userdanger("You are shocked by [your_or_other] [limb.plaintext_zone], causing your crowbar to slip out!")
+ if (user != victim)
+ victim_message = span_userdanger("[user] is shocked by your [limb.plaintext_zone] in [user.p_their()] efforts to tear it open!")
+
+ var/shock_damage = CROWBAR_OPEN_SHOCK_POWER
+ if (limb.current_gauze)
+ shock_damage *= limb.current_gauze.splint_factor // always good to let gauze do something
+ user.electrocute_act(shock_damage, limb, flags = electrocute_flags)
+
+ if (!stunned)
+ var/other_shock_text = ""
+ var/self_shock_text = ""
+ if (!limb_can_shock)
+ other_shock_text = ", and is striken by golden bolts of electricity"
+ self_shock_text = ", but are immediately shocked by the electricity contained within"
+ message = span_boldwarning("[user] tears open [their_or_other] [limb.plaintext_zone] with [user.p_their()] crowbar[other_shock_text]!")
+ self_message = span_warning("You tear open [your_or_other] [limb.plaintext_zone] with your crowbar[self_shock_text]!")
+ if (user != victim)
+ victim_message = span_userdanger("Your [limb.plaintext_zone] fragments and splinters as [user] tears it open with [user.p_their()] crowbar!")
+
+ playsound(get_turf(crowbarring_item), 'sound/effects/bang.ogg', 35, TRUE) // we did it!
+ to_chat(user, span_green("You've torn [your_or_other] [limb.plaintext_zone] open, heavily damaging it but making it a lot easier to screwdriver the internals!"))
+ var/damage = CROWBAR_OPEN_BRUTE_DAMAGE
+ if (limb_essential()) // can't be disabled
+ damage *= CROWBAR_OPEN_ESSENTIAL_LIMB_DAMAGE_MULT
+ limb.receive_damage(brute = CROWBAR_OPEN_BRUTE_DAMAGE, wound_bonus = CANT_WOUND, damage_source = crowbarring_item)
+ set_torn_open(TRUE)
+
+ if (user == victim)
+ victim_message = self_message
+
+ user.visible_message(message, self_message, ignored_mobs = list(victim))
+ to_chat(victim, victim_message)
+ return TRUE
+
+#undef CROWBAR_OPEN_SELF_TEND_DELAY_MULT
+#undef CROWBAR_OPEN_KNOWS_ROBO_WIRES_DELAY_MULT
+#undef CROWBAR_OPEN_KNOWS_ENGI_WIRES_DELAY_MULT
+#undef CROWBAR_OPEN_HAS_DIAG_HUD_DELAY_MULT
+#undef CROWBAR_OPEN_WOUND_SCANNED_DELAY_MULT
+#undef CROWBAR_OPEN_ESSENTIAL_LIMB_DAMAGE_MULT
+
+#undef CROWBAR_OPEN_BRUTE_DAMAGE
+#undef CROWBAR_OPEN_SHOCK_POWER
+
+/// Sets [crowbarred_open] to the new value. If we werent originally disabling, or if we arent currently and we're torn open, we set disabling to true.
+/datum/wound/blunt/robotic/secures_internals/proc/set_torn_open(torn_open_state)
+ // if we aren't disabling but we were torn open, OR if we aren't disabling by default
+ var/should_update_disabling = ((!disabling && torn_open_state) || !initial(disabling))
+
+ crowbarred_open = torn_open_state
+ if(should_update_disabling)
+ set_disabling(torn_open_state)
+
+/// If, on a secure internals attempt, we have less than this chance to succeed, we warn the user.
+#define SECURE_INTERNALS_CONFUSED_CHANCE_THRESHOLD 25
+#define SECURE_INTERNALS_FAILURE_BRUTE_DAMAGE 5
+
+/**
+ * The primary way of performing the secure internals step for T2/T3. Uses a screwdriver/wrench. Very hard to do by yourself, or without a diag hud/wire knowledge.
+ * Roboticists/engineers have a very high chance of succeeding.
+ * Deals some brute damage on failure, but moves to the final step of treatment (re-soldering) on success.
+ *
+ * If [crowbarred_open], made far more likely and remove the self-tend malice.
+ */
+/datum/wound/blunt/robotic/secures_internals/proc/secure_internals_normally(obj/item/securing_item, mob/user)
+ if (!securing_item.tool_start_check())
+ return TRUE
+
+ var/chance = 10
+ var/delay_mult = 1
+
+ if (user == victim)
+ if (!crowbarred_open)
+ chance *= 0.2
+ delay_mult *= 2
+
+ var/knows_wires = FALSE
+ if (crowbarred_open)
+ chance *= 4 // even self-tends get a high chance of success if torn open!
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ chance *= 8 // almost guaranteed if its not self surgery - guaranteed with diag hud
+ delay_mult *= 0.75
+ knows_wires = TRUE
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ chance *= 5.5
+ delay_mult *= 0.85
+ knows_wires = TRUE
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ if (knows_wires)
+ chance *= 1.25 // ((10 * 8) * 1.25) = 100%
+ else
+ chance *= 4
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ chance *= 1.5 // youre not intended to fix this by yourself this way
+ delay_mult *= 0.8
+
+ var/confused = (chance < SECURE_INTERNALS_CONFUSED_CHANCE_THRESHOLD) // generate chance beforehand, so we can use this var
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ user?.visible_message(span_notice("[user] begins the delicate operation of securing the internals of [their_or_other] [limb.plaintext_zone]..."), \
+ span_notice("You begin the delicate operation of securing the internals of [your_or_other] [limb.plaintext_zone]..."))
+ if (confused)
+ to_chat(user, span_warning("You are confused by the layout of [your_or_other] [limb.plaintext_zone]! A diagnostic hud would help, as would knowing robo/engi wires! You could also tear the limb open with a crowbar, or get someone else to help."))
+
+ if (!securing_item.use_tool(target = victim, user = user, delay = (10 SECONDS * delay_mult), volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ if (prob(chance))
+ user?.visible_message(span_green("[user] finishes securing the internals of [their_or_other] [limb.plaintext_zone]!"), \
+ span_green("You finish securing the internals of [your_or_other] [limb.plaintext_zone]!"))
+ to_chat(user, span_green("[capitalize(your_or_other)] [limb.plaintext_zone]'s internals are now secure! Your next step is to weld/cauterize it."))
+ ready_to_secure_internals = FALSE
+ ready_to_resolder = TRUE
+ else
+ user?.visible_message(span_danger("[user] screws up and accidentally damages [their_or_other] [limb.plaintext_zone]!"))
+ limb.receive_damage(brute = SECURE_INTERNALS_FAILURE_BRUTE_DAMAGE, damage_source = securing_item, wound_bonus = CANT_WOUND)
+
+ return TRUE
+
+#undef SECURE_INTERNALS_CONFUSED_CHANCE_THRESHOLD
+#undef SECURE_INTERNALS_FAILURE_BRUTE_DAMAGE
+
+/**
+ * "Premium" ghetto option of the secure internals step for T2/T3. Requires bone gel. Guaranteed to work.
+ * Deals damage over time and disables the limb, but finishes the step afterwards.
+ */
+/datum/wound/blunt/robotic/secures_internals/proc/apply_gel(obj/item/stack/medical/bone_gel/gel, mob/user)
+ if (gelled)
+ to_chat(user, span_warning("[user == victim ? "Your" : "[victim]'s"] [limb.plaintext_zone] is already filled with bone gel!"))
+ return TRUE
+
+ var/delay_mult = 1
+ if (victim == user)
+ delay_mult *= 0.5
+
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.75
+
+ user.visible_message(span_danger("[user] begins hastily applying [gel] to [victim]'s [limb.plaintext_zone]..."), span_warning("You begin hastily applying [gel] to [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone], disregarding the acidic effect it seems to have on the metal..."))
+
+ if (!do_after(user, (8 SECONDS * delay_mult), target = victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ gel.use(1)
+ if(user != victim)
+ user.visible_message(span_notice("[user] finishes applying [gel] to [victim]'s [limb.plaintext_zone], emitting a fizzing noise!"), span_notice("You finish applying [gel] to [victim]'s [limb.plaintext_zone]!"), ignored_mobs=victim)
+ to_chat(victim, span_userdanger("[user] finishes applying [gel] to your [limb.plaintext_zone], and you can hear the sizzling of the metal..."))
+ else
+ victim.visible_message(span_notice("[victim] finishes applying [gel] to [victim.p_their()] [limb.plaintext_zone], emitting a funny fizzing sound!"), span_notice("You finish applying [gel] to your [limb.plaintext_zone], and you can hear the sizzling of the metal..."))
+
+ gelled = TRUE
+ set_disabling(TRUE)
+ processes = TRUE
+ return TRUE
+
+/**
+ * The final step of T2/T3, requires a welder/cautery. Guaranteed to work. Cautery is slower.
+ * Once complete, removes the wound entirely.
+ */
+/datum/wound/blunt/robotic/secures_internals/proc/resolder(obj/item/welding_item, mob/user)
+ if (!welding_item.tool_start_check())
+ return TRUE
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ victim.visible_message(span_notice("[user] begins re-soldering [their_or_other] [limb.plaintext_zone]..."), \
+ span_notice("You begin re-soldering [your_or_other] [limb.plaintext_zone]..."))
+
+ var/delay_mult = 1
+ if (welding_item.tool_behaviour == TOOL_CAUTERY)
+ delay_mult *= 3 // less efficient
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.75
+
+ if (!welding_item.use_tool(target = victim, user = user, delay = 7 SECONDS * delay_mult, volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ victim.visible_message(span_green("[user] finishes re-soldering [their_or_other] [limb.plaintext_zone]!"), \
+ span_notice("You finish re-soldering [your_or_other] [limb.plaintext_zone]!"))
+ remove_wound()
+ return TRUE
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/robotic_burns.dm b/modular_skyrat/modules/medical/code/wounds/synth/robotic_burns.dm
new file mode 100644
index 00000000000000..24ac18eeab6ba5
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/robotic_burns.dm
@@ -0,0 +1,441 @@
+#define OVERHEAT_ON_STASIS_HEAT_MULT 0.25
+/// At 100% hercuri composition, a spray of reagents will have its effective chem temp reduced by this. 50%, reduced by half this, etc.
+#define ROBOTIC_BURN_REAGENT_EXPOSURE_HERCURI_MAX_HEAT_DECREMENT 60
+/// At 100% hercuri composition, a spray of reagents will have its heat shock damage reduced by this. 50%, reduced by half this, etc.
+#define ROBOTIC_BURN_REAGENT_EXPOSURE_HERCURI_HEAT_SHOCK_MULT_DECREMENT 0.3
+
+/datum/wound_pregen_data/burnt_metal
+ abstract = TRUE
+ required_limb_biostate = BIO_METAL
+ required_wounding_types = list(WOUND_BURN)
+ wound_series = WOUND_SERIES_METAL_BURN_OVERHEAT
+
+/datum/wound_pregen_data/burnt_metal/generate_scar_priorities()
+ return list("[BIO_METAL]")
+
+/datum/wound/burn/robotic/overheat
+ treat_text = "Introduction of a cold environment or lowering of body temperature."
+
+ simple_desc = "Metals are overheated, increasing damage taken significantly and raising body temperature!"
+ simple_treat_text = "Ideally cryogenics, but any source of low body temperature can work. Spraying with spray bottles/extinguishers/showers \
+ will quickly cool the limb, but cause damage. Hercuri is especially effective in quick cooling. \
+ Clothing reduces the water/hercuri that makes it to the metal, and gauze binds it and reduces the damage taken."
+ homemade_treat_text = "You can also splash any liquid on it for a rather inefficient and damaging coolant!"
+
+ default_scar_file = METAL_SCAR_FILE
+
+ wound_flags = (ACCEPTS_GAUZE|SPLINT_OVERLAY|CAN_BE_GRASPED) // gauze binds the metal and makes it resistant to thermal shock
+
+ processes = TRUE
+
+ /// The virtual temperature of the chassis. Crucial for many things, like our severity, the temp we transfer, our cooling damage, etc.
+ var/chassis_temperature
+
+ /// The lower bound of the chassis_temperature we can start with.
+ var/starting_temperature_min = (BODYTEMP_NORMAL + 200)
+ /// The upper bound of the chassis_temperature we can start with.
+ var/starting_temperature_max = (BODYTEMP_NORMAL + 250)
+
+ /// If [chassis_temperature] goes below this, we reduce in severity.
+ var/cooling_threshold = (BODYTEMP_NORMAL + 3)
+ /// If [chassis_temperature] goes above this, we increase in severity.
+ var/heating_threshold = (BODYTEMP_NORMAL + 300)
+
+ /// The buffer in kelvin we will subtract from the chassis_temperature of a wound we demote to.
+ var/cooling_demote_buffer = 60
+ /// The buffer in kelvin we will add to the chassis_temperature of a wound we promote to.
+ var/heating_promote_buffer = 60
+
+ /// The coefficient of heat transfer we will use when shifting our temp to the victim's.
+ var/bodytemp_coeff = 0.04
+ /// For every degree below normal bodytemp, we will multiply our incoming temperature by 1 + degrees * this. Allows incentivization of freezing yourself instead of just waiting.
+ var/bodytemp_difference_expose_bonus_ratio = 0.035
+ /// The coefficient of heat transfer we will use when shifting our victim's temp to ours.
+ var/outgoing_bodytemp_coeff = 0
+ /// The mult applied to heat output when we are on a important limb, e.g. head/torso.
+ var/important_outgoing_mult = 1.2
+ /// The coefficient of heat transfer we will use when shifting our temp to a turf.
+ var/turf_coeff = 0.02
+
+ /// The maximum temperature we can cause by heating our victim.
+ var/max_outgoing_temperature = BODYTEMP_HEAT_WOUND_LIMIT - 1
+
+ /// If we are hit with burn damage, the damage will be multiplied against this to determine the effective heat we get.
+ var/incoming_damage_heat_coeff = 3
+
+ /// The coefficient of heat transfer we will use when receiving heat from reagent contact.
+ var/base_reagent_temp_coefficient = 0.02
+
+ /// The ratio of temp shift -> brute damage. Careful with this value, it can make stuff really really nasty.
+ var/heat_shock_delta_to_damage_ratio = 0.12
+ /// The minimum heat difference we must have on reagent contact to cause heat shock damage.
+ var/heat_shock_minimum_delta = 5
+
+ /// If we are sprayed with a extinguisher/shower with obscuring clothing on (think clothing that prevents surgery), the effect is multiplied against this.
+ var/sprayed_with_reagent_clothed_mult = 0.15
+
+ /// The wound we demote to when we go below cooling threshold. If null, removes us.
+ var/datum/wound/burn/robotic/demotes_to
+ /// The wound we promote to when we go above heating threshold.
+ var/datum/wound/burn/robotic/promotes_to
+
+ /// The color of the light we will generate.
+ var/light_color
+ /// The power of the light we will generate.
+ var/light_power
+ /// The range of the light we will generate.
+ var/light_range
+
+ /// The glow we have attached to our victim, to simulate our limb glowing.
+ var/obj/effect/dummy/lighting_obj/moblight/mob_glow
+
+/datum/wound/burn/robotic/overheat/New(temperature)
+ chassis_temperature = (isnull(temperature) ? get_random_starting_temperature() : temperature)
+
+ return ..()
+
+/datum/wound/burn/robotic/overheat/Destroy()
+ QDEL_NULL(mob_glow)
+ return ..()
+
+/datum/wound/burn/robotic/overheat/set_victim(mob/living/new_victim)
+ if (victim)
+ QDEL_NULL(mob_glow)
+ UnregisterSignal(victim, COMSIG_MOB_AFTER_APPLY_DAMAGE)
+ UnregisterSignal(victim, COMSIG_ATOM_AFTER_EXPOSE_REAGENTS)
+ if (new_victim)
+ mob_glow = new_victim.mob_light(light_range, light_power, light_color)
+ mob_glow.set_light_on(TRUE)
+ RegisterSignal(new_victim, COMSIG_MOB_AFTER_APPLY_DAMAGE, PROC_REF(victim_attacked))
+ RegisterSignal(new_victim, COMSIG_ATOM_AFTER_EXPOSE_REAGENTS, PROC_REF(victim_exposed_to_reagents))
+
+ return ..()
+
+/datum/wound/burn/robotic/overheat/proc/get_random_starting_temperature()
+ return LERP(starting_temperature_min, starting_temperature_max, rand()) // LERP since we deal with decimals
+
+/datum/wound/burn/robotic/get_limb_examine_description()
+ return span_warning("The metal on this limb is glowing radiantly.")
+
+/datum/wound/burn/robotic/overheat/handle_process(seconds_per_tick, times_fired)
+ if (isnull(victim))
+ var/turf/our_turf = get_turf(limb)
+ if (!isnull(our_turf))
+ expose_temperature(our_turf.GetTemperature(), (turf_coeff * seconds_per_tick))
+ return
+ if (outgoing_bodytemp_coeff <= 0)
+ return
+ var/statis_mult = 1
+ if (IS_IN_STASIS(victim)) // stasis heavily reduces the ingoing and outgoing transfer of heat
+ statis_mult *= OVERHEAT_ON_STASIS_HEAT_MULT
+
+ var/difference_from_average = max((BODYTEMP_NORMAL - victim.bodytemperature), 0)
+ var/difference_mult = 1 + (difference_from_average * bodytemp_difference_expose_bonus_ratio)
+ if (expose_temperature(victim.bodytemperature, (bodytemp_coeff * seconds_per_tick * statis_mult * difference_mult)))
+ return
+ var/mult = outgoing_bodytemp_coeff
+ if (limb_essential())
+ mult *= important_outgoing_mult
+ var/adjustment_allowed = max((max_outgoing_temperature - victim.bodytemperature), 0)
+ var/amount_to_adjust = min((((chassis_temperature - victim.bodytemperature) * mult) * TEMPERATURE_DAMAGE_COEFFICIENT * seconds_per_tick * statis_mult), adjustment_allowed)
+ victim.adjust_bodytemperature(amount_to_adjust)
+
+/// Signal proc for when our victim is externally attacked. Increases chassis temp based on burn damage received.
+/datum/wound/burn/robotic/overheat/proc/victim_attacked(datum/source, damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
+ SIGNAL_HANDLER
+
+ if (def_zone != limb.body_zone) // use this proc since receive damage can also be called for like, chems and shit
+ return
+
+ if (!victim)
+ return
+
+ if (damagetype != BURN)
+ return
+
+ if (wound_bonus == CANT_WOUND)
+ return
+
+ var/effective_damage = (damage - blocked)
+ if (effective_damage <= 0)
+ return
+
+ expose_temperature((chassis_temperature + effective_damage), incoming_damage_heat_coeff)
+
+/**
+ * Signal proc for when our victim is exposed to reagents, obviously.
+ *
+ * Equalizes temp to the reagent temp, but also causes thermal shock. Basically, does damage based on the temp differential.
+ * Clothes reduce the effects massively. Hercuri reduces the thermal shock and gets a special temp buff.
+ */
+/datum/wound/burn/robotic/overheat/proc/victim_exposed_to_reagents(datum/signal_source, list/reagents, datum/reagents/source, methods, volume_modifier, show_message)
+ SIGNAL_HANDLER
+
+ var/reagent_coeff = base_reagent_temp_coefficient
+ if (!get_location_accessible(victim, limb.body_zone))
+ if (ishuman(victim))
+ // hi! its niko! small rant
+ // this proc has no goddamn reason to be on human, it could so easily just have used a proc on carbon that would get the required bodyparts to check
+ // but no. it had to hardcode the list in the proc itself so its impossible to modularly fix this
+ // so instead we just say fuck it and hope to god only human subtypes get this wound
+ // tldr; ryll why
+ var/mob/living/carbon/human/human_victim = victim
+ for (var/obj/item/clothing/iter_clothing as anything in human_victim.get_clothing_on_part(limb))
+ if (iter_clothing.clothing_flags & THICKMATERIAL)
+ return
+
+ reagent_coeff *= sprayed_with_reagent_clothed_mult
+
+ if (istype(source.my_atom, /obj/effect/particle_effect/water/extinguisher)) // this used to be a lot, lot more modular, but sadly reagent temps/volumes and shit are horribly inconsistant
+ expose_temperature(source.chem_temp, (2.55 * reagent_coeff), TRUE)
+ return
+
+ if (istype(source.my_atom, /obj/machinery/shower))
+ expose_temperature(source.chem_temp, (15 * volume_modifier * reagent_coeff), TRUE)
+ return
+
+ var/total_reagent_amount = 0
+ var/hercuri_amount = 0
+ for (var/datum/reagent/iterated_reagent as anything in reagents)
+ total_reagent_amount += reagents[iterated_reagent]
+ if (iterated_reagent.type == /datum/reagent/medicine/c2/hercuri)
+ hercuri_amount = reagents[iterated_reagent]
+
+ var/hercuri_percent = (hercuri_amount / total_reagent_amount)
+
+ var/hercuri_chem_temp_increment = (ROBOTIC_BURN_REAGENT_EXPOSURE_HERCURI_MAX_HEAT_DECREMENT * hercuri_percent)
+ var/local_chem_temp = max(source.chem_temp - hercuri_chem_temp_increment, 0)
+
+ var/heat_shock_damage_mult = 1 - (ROBOTIC_BURN_REAGENT_EXPOSURE_HERCURI_HEAT_SHOCK_MULT_DECREMENT * hercuri_percent)
+
+ expose_temperature(local_chem_temp, (reagent_coeff * volume_modifier * total_reagent_amount), TRUE, heat_shock_damage_mult = heat_shock_damage_mult)
+
+/// Adjusts chassis_temperature by the delta between temperature and itself, multiplied by coeff.
+/// If heat_shock is TRUE, limb will receive brute damage based on the delta.
+/datum/wound/burn/robotic/overheat/proc/expose_temperature(temperature, coeff = 0.02, heat_shock = FALSE, heat_shock_damage_mult = 1)
+ var/temp_delta = (temperature - chassis_temperature) * coeff
+
+ var/unclamped_new_temperature = (chassis_temperature + temp_delta)
+ var/clamped_new_temperature
+ var/heat_adjustment_used
+
+ if(temp_delta > 0)
+ clamped_new_temperature = min(min(chassis_temperature + max(temp_delta, 1), temperature), heating_threshold)
+ heat_adjustment_used = (clamped_new_temperature / unclamped_new_temperature)
+ else
+ clamped_new_temperature = max(max(chassis_temperature + min(temp_delta, -1), temperature), cooling_threshold)
+ heat_adjustment_used = (unclamped_new_temperature / clamped_new_temperature)
+
+ if (heat_shock && abs(temp_delta) > heat_shock_minimum_delta)
+ var/gauze_mult = 1
+ var/obj/item/stack/gauze = limb.current_gauze
+ if (gauze)
+ gauze_mult *= (gauze.splint_factor) * 0.4 // very very effective
+
+ if (limb.grasped_by)
+ gauze_mult *= 0.7 // hold it down yourself
+
+ if (victim)
+ var/gauze_or_not = (!isnull(gauze) ? ", but [gauze] helps to keep it together" : "")
+ var/clothing_text = (!get_location_accessible(victim, limb.body_zone) ? ", [victim.p_their()] clothing absorbing some of the liquid" : "")
+ victim.visible_message(span_warning("[victim]'s [limb.plaintext_zone] strains from the thermal shock[clothing_text][gauze_or_not]!"))
+ playsound(victim, 'sound/items/welder.ogg', 25)
+
+ var/damage = (((abs(temp_delta) * heat_shock_delta_to_damage_ratio) * gauze_mult) * heat_shock_damage_mult) * heat_adjustment_used
+ limb.receive_damage(brute = damage, wound_bonus = CANT_WOUND)
+
+ chassis_temperature = clamped_new_temperature // can only be decimal or 1, so it can only reduce the intensity of the adjustment
+
+ return check_temperature()
+
+/// Removes, demotes, or promotes ourselves to a new wound type if our temperature is past a heating/cooling threshold.
+/datum/wound/burn/robotic/overheat/proc/check_temperature()
+ if (chassis_temperature <= cooling_threshold)
+ if (demotes_to)
+ victim.visible_message(span_green("[victim]'s [limb.plaintext_zone] turns a more pleasant thermal color as it cools down a little..."), span_green("Your [limb.plaintext_zone] seems to cool down a little!"))
+ replace_wound(new demotes_to(cooling_threshold - cooling_demote_buffer))
+ return TRUE
+ else
+ victim.visible_message(span_green("[victim]'s [limb.plaintext_zone] simmers gently as it returns to its usual colors!"), span_green("Your [limb.plaintext_zone] simmers gently as it returns to its usual colors!"))
+ remove_wound()
+ return TRUE
+ else if (promotes_to && chassis_temperature >= heating_threshold)
+ victim.visible_message(span_danger("[victim]'s [limb.plaintext_zone] brightens as it overheats further!"), span_userdanger("Your [limb.plaintext_zone] sizzles and brightens as it overheats further!"))
+ replace_wound(new promotes_to(heating_threshold + heating_promote_buffer))
+ return TRUE
+
+/// Returns a string with our temperature and heating/cooling thresholds, for use in health analyzers.
+/datum/wound/burn/robotic/overheat/proc/get_wound_status_info()
+ var/current_temp_celcius = round(chassis_temperature - T0C, 0.1)
+ var/current_temp_fahrenheit = round(chassis_temperature * 1.8-459.67, 0.1)
+
+ var/cool_celcius = round(cooling_threshold - T0C, 0.1)
+ var/cool_fahrenheit = round(cooling_threshold * 1.8-459.67, 0.1)
+
+ var/heat_celcius = round(heating_threshold - T0C, 0.1)
+ var/heat_fahrenheit = round(heating_threshold * 1.8-459.67, 0.1)
+
+ return "Its current temperature is [span_blue("[current_temp_celcius ] °C ([current_temp_fahrenheit] °F)")], \
+ and needs to cool to [span_nicegreen("[cool_celcius] °C ([cool_fahrenheit] °F)")], but \
+ will worsen if heated to [span_purple("[heat_celcius] °C ([heat_fahrenheit] °F)")]."
+
+/datum/wound/burn/robotic/overheat/get_scanner_description(mob/user)
+ . = ..()
+
+ . += "\nWound status: [get_wound_status_info()]"
+
+/datum/wound/burn/robotic/overheat/get_simple_scanner_description(mob/user)
+ . = ..()
+
+ . += "\nWound status: [get_wound_status_info()]"
+
+// this wound is unaffected by cryoxadone and pyroxadone
+/datum/wound/burn/robotic/overheat/on_xadone(power)
+ return
+
+/datum/wound/burn/robotic/overheat/moderate
+ name = "Transient Overheating"
+ desc = "External metals have exceeded lower-bound thermal limits and have lost some structural integrity, increasing damage taken as well as the chance to \
+ sustain additional wounds."
+ occur_text = "lets out a slight groan as it turns a dull shade of thermal red"
+ examine_desc = "is glowing a dull thermal red and giving off heat"
+ treat_text = "Reduction of body temperature to expedite the passive heat dissipation - or, if thermal shock is to be risked, application of a fire extinguisher/shower."
+ severity = WOUND_SEVERITY_MODERATE
+
+ damage_multiplier_penalty = 1.15 //1.15x damage taken
+
+ starting_temperature_min = (BODYTEMP_NORMAL + 350)
+ starting_temperature_max = (BODYTEMP_NORMAL + 400)
+
+ cooling_threshold = (BODYTEMP_NORMAL + 100)
+ heating_threshold = (BODYTEMP_NORMAL + 500)
+
+ cooling_demote_buffer = 60
+ heating_promote_buffer = 100
+
+ a_or_from = "from"
+
+ // easy to get
+ threshold_penalty = 30
+
+ status_effect_type = /datum/status_effect/wound/burn/robotic/moderate
+
+ sound_volume = 20
+
+ outgoing_bodytemp_coeff = 0.0056
+ bodytemp_coeff = 0.006
+
+ base_reagent_temp_coefficient = 0.03
+ heat_shock_delta_to_damage_ratio = 0.2
+
+ promotes_to = /datum/wound/burn/robotic/overheat/severe
+
+ light_color = COLOR_RED
+ light_power = 0.1
+ light_range = 0.5
+
+ can_scar = FALSE
+
+/datum/wound_pregen_data/burnt_metal/transient_overheat
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/burn/robotic/overheat/moderate
+
+ threshold_minimum = 30
+
+/datum/wound/burn/robotic/overheat/severe
+ name = "Thermal Overload"
+ desc = "Exterior plating has surpassed critical thermal levels, causing significant failure in structural integrity and overheating of internal systems."
+ occur_text = "sizzles, the externals turning a dull shade of orange"
+ examine_desc = "appears discolored and polychromatic, parts of it glowing a dull orange"
+ treat_text = "Isolation from physical hazards, and accommodation of passive heat dissipation - active cooling may be used, but temperature differentials significantly \
+ raise the risk of thermal shock."
+ severity = WOUND_SEVERITY_SEVERE
+
+ a_or_from = "from"
+
+ threshold_penalty = 65
+
+ status_effect_type = /datum/status_effect/wound/burn/robotic/severe
+ damage_multiplier_penalty = 1.25 // 1.25x damage taken
+
+ starting_temperature_min = (BODYTEMP_NORMAL + 550)
+ starting_temperature_max = (BODYTEMP_NORMAL + 600)
+
+ heating_promote_buffer = 150
+
+ cooling_threshold = (BODYTEMP_NORMAL + 375)
+ heating_threshold = (BODYTEMP_NORMAL + 800)
+
+ outgoing_bodytemp_coeff = 0.0053
+ bodytemp_coeff = 0.004
+
+ base_reagent_temp_coefficient = 0.03
+ heat_shock_delta_to_damage_ratio = 0.2
+
+ demotes_to = /datum/wound/burn/robotic/overheat/moderate
+ promotes_to = /datum/wound/burn/robotic/overheat/critical
+
+ light_color = COLOR_BRIGHT_ORANGE
+ light_power = 0.8
+ light_range = 0.5
+
+ scar_keyword = "burnsevere"
+
+/datum/wound_pregen_data/burnt_metal/severe
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/burn/robotic/overheat/severe
+ threshold_minimum = 80
+
+/datum/wound/burn/robotic/overheat/critical
+ name = "Runaway Exothermy"
+ desc = "Carapace is beyond melting point, causing catastrophic structural integrity failure as well as massively heating up the subject."
+ occur_text = "turns a bright shade of radiant white as it sizzles and melts"
+ examine_desc = "is a blinding shade of white, almost melting from the heat"
+ treat_text = "Immediate confinement to cryogenics, as rapid overheating and physical vulnerability may occur. Active cooling is not advised, \
+ since the thermal shock may be lethal with such a temperature differential."
+ severity = WOUND_SEVERITY_CRITICAL
+
+ a_or_from = "from"
+
+ sound_effect = 'sound/effects/wounds/sizzle2.ogg'
+
+ threshold_penalty = 100
+
+ status_effect_type = /datum/status_effect/wound/burn/robotic/critical
+
+ damage_multiplier_penalty = 1.5 //1.5x damage taken
+
+ starting_temperature_min = (BODYTEMP_NORMAL + 1050)
+ starting_temperature_max = (BODYTEMP_NORMAL + 1100)
+
+ cooling_demote_buffer = 100
+
+ cooling_threshold = (BODYTEMP_NORMAL + 775)
+ heating_threshold = INFINITY
+
+ outgoing_bodytemp_coeff = 0.0055 // burn... BURN...
+ bodytemp_coeff = 0.0025
+
+ base_reagent_temp_coefficient = 0.03
+ heat_shock_delta_to_damage_ratio = 0.2
+
+ max_outgoing_temperature = BODYTEMP_HEAT_WOUND_LIMIT // critical CAN cause wounds, but only barely
+
+ demotes_to = /datum/wound/burn/robotic/overheat/severe
+
+ wound_flags = (MANGLES_EXTERIOR|ACCEPTS_GAUZE|SPLINT_OVERLAY|CAN_BE_GRASPED)
+
+ light_color = COLOR_VERY_SOFT_YELLOW
+ light_power = 1.3
+ light_range = 1.5
+
+ scar_keyword = "burncritical"
+
+/datum/wound_pregen_data/burnt_metal/critical
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/burn/robotic/overheat/critical
+ threshold_minimum = 140
+
+#undef OVERHEAT_ON_STASIS_HEAT_MULT
+#undef ROBOTIC_BURN_REAGENT_EXPOSURE_HERCURI_MAX_HEAT_DECREMENT
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/robotic_muscle.dm b/modular_skyrat/modules/medical/code/wounds/synth/robotic_muscle.dm
new file mode 100644
index 00000000000000..4e91c58f7f675b
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/robotic_muscle.dm
@@ -0,0 +1,47 @@
+/datum/wound/muscle/robotic
+ sound_effect = 'sound/effects/wounds/blood1.ogg'
+
+/datum/wound_pregen_data/muscle/robotic
+ required_limb_biostate = (BIO_METAL)
+
+/datum/wound/muscle/robotic/moderate
+ name = "Overworked Servo"
+ desc = "A servo has been overworked, and will operate with reduced efficiency until rested."
+ treat_text = "A tight splint on the affected limb, as well as plenty of rest and sleep."
+ examine_desc = "appears to be moving sluggishly"
+ occur_text = "jitters for a moment before moving sluggishly"
+ severity = WOUND_SEVERITY_MODERATE
+ interaction_efficiency_penalty = 1.5
+ limp_slowdown = 2
+ limp_chance = 30
+ threshold_penalty = 15
+ status_effect_type = /datum/status_effect/wound/muscle/robotic/moderate
+ regen_ticks_needed = 90
+
+/datum/wound_pregen_data/muscle/robotic/servo
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/muscle/robotic/moderate
+ threshold_minimum = 35
+
+/datum/wound/muscle/robotic/severe
+ name = "Exhausted Piston"
+ sound_effect = 'sound/effects/wounds/blood2.ogg'
+ desc = "An important hydraulic piston has been critically overused, resulting in total dysfunction until it recovers."
+ treat_text = "A tight splint on the affected limb, as well as plenty of rest and sleep."
+ examine_desc = "is stiffly limp, the extremities splayed out widely"
+ occur_text = "goes completely stiff, seeming to lock into position"
+ severity = WOUND_SEVERITY_SEVERE
+ interaction_efficiency_penalty = 2
+ limp_slowdown = 5
+ limp_chance = 40
+ threshold_penalty = 35
+ disabling = TRUE
+ status_effect_type = /datum/status_effect/wound/muscle/robotic/severe
+ regen_ticks_needed = 150
+
+/datum/wound_pregen_data/muscle/robotic/hydraulic
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/muscle/robotic/severe
+ threshold_minimum = 80
+
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/robotic_pierce.dm b/modular_skyrat/modules/medical/code/wounds/synth/robotic_pierce.dm
new file mode 100644
index 00000000000000..b7e4f259e5d2af
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/robotic_pierce.dm
@@ -0,0 +1,146 @@
+// Pierce
+// Slow to rise but high damage overall
+// Hard-ish to fix
+/datum/wound/electrical_damage/pierce
+ heat_differential_healing_mult = 0.01
+ simple_desc = "Electrical conduits have been pierced open, resulting in a fault that slowly intensifies, but with extreme maximum voltage!"
+
+/datum/wound_pregen_data/electrical_damage/pierce
+ abstract = TRUE
+ wound_series = WOUND_SERIES_WIRE_PIERCE_ELECTRICAL_DAMAGE
+ required_wounding_types = list(WOUND_PIERCE)
+
+/datum/wound/burn/electrical_damage/pierce/get_limb_examine_description()
+ return span_warning("The metal on this limb is pierced open.")
+
+/datum/wound/electrical_damage/pierce/moderate
+ name = "Punctured Capacitor"
+ desc = "A major capacitor has been broken open, causing slow but noticable electrical damage."
+ occur_text = "shoots out a short stream of sparks"
+ examine_desc = "is shuddering gently, movements a little weak"
+ treat_text = "Replacing of damaged wiring, though repairs via wirecutting instruments or sutures may suffice, albeit at limited efficiency. In case of emergency, \
+ subject may be subjected to high temperatures to allow solder to reset."
+
+ sound_effect = 'modular_skyrat/modules/medical/sound/robotic_slash_T1.ogg'
+
+ severity = WOUND_SEVERITY_MODERATE
+
+ sound_volume = 30
+
+ threshold_penalty = 30
+
+ intensity = 10 SECONDS
+ processing_full_shock_threshold = 7 MINUTES
+
+ processing_shock_power_per_second_max = 1.2
+ processing_shock_power_per_second_min = 1.1
+
+ processing_shock_stun_chance = 0.5
+ processing_shock_spark_chance = 35
+
+ process_shock_spark_count_max = 1
+ process_shock_spark_count_min = 1
+
+ wirecut_repair_percent = 0.065 // not even faster at this point
+ wire_repair_percent = 0.026
+
+ initial_sparks_amount = 1
+
+ status_effect_type = /datum/status_effect/wound/electrical_damage/pierce/moderate
+
+ a_or_from = "a"
+
+ scar_keyword = "piercemoderate"
+
+/datum/wound_pregen_data/electrical_damage/pierce/moderate
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/electrical_damage/pierce/moderate
+ threshold_minimum = 40
+
+/datum/wound/electrical_damage/pierce/severe
+ name = "Penetrated Transformer"
+ desc = "A major transformer has been pierced, causing slow-to-progess but eventually intense electrical damage."
+ occur_text = "sputters and goes limp for a moment as it ejects a stream of sparks"
+ examine_desc = "is shuddering significantly, its servos briefly giving way in a rythmic pattern"
+ treat_text = "Containment of damaged wiring via gauze, then application of fresh wiring/sutures, or resetting of displaced wiring via wirecutter/retractor."
+
+ sound_effect = 'modular_skyrat/modules/medical/sound/robotic_slash_T2.ogg'
+
+ severity = WOUND_SEVERITY_SEVERE
+
+ sound_volume = 15
+
+ threshold_penalty = 40
+
+ intensity = 20 SECONDS
+ processing_full_shock_threshold = 6.5 MINUTES
+
+ processing_shock_power_per_second_max = 1.6
+ processing_shock_power_per_second_min = 1.5
+
+ processing_shock_stun_chance = 2.5
+ processing_shock_spark_chance = 60
+
+ process_shock_spark_count_max = 2
+ process_shock_spark_count_min = 1
+
+ wirecut_repair_percent = 0.068
+ wire_repair_percent = 0.02
+
+ initial_sparks_amount = 3
+
+ status_effect_type = /datum/status_effect/wound/electrical_damage/pierce/moderate
+
+ a_or_from = "a"
+
+ scar_keyword = "piercemoderate"
+
+/datum/wound_pregen_data/electrical_damage/pierce/severe
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/electrical_damage/pierce/severe
+ threshold_minimum = 60
+
+/datum/wound/electrical_damage/pierce/critical
+ name = "Ruptured PSU"
+ desc = "The local PSU of this limb has suffered a core rupture, causing a progressive power failure that will slowly intensify into massive electrical damage."
+ occur_text = "flashes with radiant blue, emitting a noise not unlike a Jacob's Ladder"
+ examine_desc = "'s PSU is visible, with a sizable hole in the center"
+ treat_text = "Immediate securing via gauze, followed by emergency cable replacement and securing via wirecutters or hemostat. \
+ If the fault has become uncontrollable, extreme heat therapy is recommended."
+
+ severity = WOUND_SEVERITY_CRITICAL
+ wound_flags = (ACCEPTS_GAUZE|MANGLES_EXTERIOR|CAN_BE_GRASPED|SPLINT_OVERLAY)
+
+ sound_effect = 'modular_skyrat/modules/medical/sound/robotic_slash_T3.ogg'
+
+ sound_volume = 30
+
+ threshold_penalty = 60
+
+ intensity = 30 SECONDS
+ processing_full_shock_threshold = 5.5 MINUTES
+
+ processing_shock_power_per_second_max = 2.2
+ processing_shock_power_per_second_min = 2.1
+
+ processing_shock_stun_chance = 1
+ processing_shock_spark_chance = 90
+
+ process_shock_spark_count_max = 3
+ process_shock_spark_count_min = 2
+
+ wirecut_repair_percent = 0.067
+ wire_repair_percent = 0.018
+
+ initial_sparks_amount = 8
+
+ status_effect_type = /datum/status_effect/wound/electrical_damage/pierce/moderate
+
+ a_or_from = "a"
+
+ scar_keyword = "piercecritical"
+
+/datum/wound_pregen_data/electrical_damage/pierce/critical
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/electrical_damage/pierce/critical
+ threshold_minimum = 110
diff --git a/modular_skyrat/modules/medical/code/wounds/synth/robotic_slash.dm b/modular_skyrat/modules/medical/code/wounds/synth/robotic_slash.dm
new file mode 100644
index 00000000000000..a28737520b5fdc
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/synth/robotic_slash.dm
@@ -0,0 +1,628 @@
+/// How much damage and progress is reduced when on stasis.
+#define ELECTRICAL_DAMAGE_ON_STASIS_MULT 0.15
+/// How much damage and progress is reduced when limb is grasped.
+#define ELECTRICAL_DAMAGE_GRASPED_MULT 0.7
+/// How much damage and progress is reduced when our victim lies down.
+#define ELECTRICAL_DAMAGE_LYING_DOWN_MULT 0.7
+/// How much progress is reduced when our victim is dead.
+#define ELECTRICAL_DAMAGE_DEAD_PROGRESS_MULT 0.2 // they'll be resting to, so this is more like 0.1
+
+/// Base time for a wirecutter being used.
+#define ELECTRICAL_DAMAGE_WIRECUTTER_BASE_DELAY 8 SECONDS
+/// Base time for a cable coil being used.
+#define ELECTRICAL_DAMAGE_SUTURE_WIRE_BASE_DELAY 0.8 SECONDS
+/// Global damage multiplier for the power a given electrical damage wound will add per tick.
+#define ELECTRICAL_DAMAGE_POWER_PER_TICK_MULT 1
+/// Global damage multiplier for how much repairing wiring will reduce intensity. Higher is more.
+#define ELECTRICAL_DAMAGE_SUTURE_WIRE_HEALING_AMOUNT_MULT 1
+
+/// The minimum shock power we must have available to zap our victim. Must be at least one, since electrocute_act fails if its lower.
+#define ELECTRICAL_DAMAGE_MINIMUM_SHOCK_POWER_PER_ZAP 1
+/// The maximum burn damage our limb can have before we refuse to let people who havent aggrograbbed the limb repair it with wires. This is so people can opt to just fix the burn damage.
+#define ELECTRICAL_DAMAGE_MAX_BURN_DAMAGE_TO_LET_WIRES_REPAIR 5
+
+/datum/wound/electrical_damage
+ name = "Electrical (Wires) Wound"
+
+ simple_treat_text = "Replacing of broken wiring, or repairing via a wirecutter. Bandaging binds the wiring and reduces intensity buildup, \
+ as does firmly grasping the limb - both the victim and someone else can do this. Roboticists/Engineers get a bonus to treatment, as do diagnostic HUDs."
+ homemade_treat_text = "Sutures can repair the wiring at reduced efficiency, as can retractors. In a pinch, high temperatures can repair the wiring!"
+
+ wound_flags = (ACCEPTS_GAUZE|CAN_BE_GRASPED|SPLINT_OVERLAY)
+
+ treatable_tools = list(TOOL_WIRECUTTER, TOOL_RETRACTOR)
+ treatable_by = list(/obj/item/stack/medical/suture)
+ treatable_by_grabbed = list(/obj/item/stack/cable_coil)
+
+ default_scar_file = METAL_SCAR_FILE
+
+ processes = TRUE
+
+ /// How many sparks do we spawn when we're gained?
+ var/initial_sparks_amount = 1
+
+ /// How much of our damage is reduced if the target is shock immune. Percent.
+ var/shock_immunity_self_damage_reduction = 75
+
+ /// Mult for our damage if we are unimportant.
+ var/limb_unimportant_damage_mult = 0.8
+ /// Mult for our progress if we are unimportant.
+ var/limb_unimportant_progress_mult = 0.8
+
+ /// The overall "intensity" of this wound. Goes up to [processing_full_shock_threshold], and is used for determining our effect scaling. Measured in deciseconds.
+ var/intensity
+ /// The time, in deciseconds, it takes to reach 100% power.
+ var/processing_full_shock_threshold = 3 MINUTES
+ /// If [intensity] is at or below this, we remove ourselves.
+ var/minimum_intensity = 0
+
+ /// How much shock power we add to [processing_shock_power_this_tick] per tick. Lower bound
+ var/processing_shock_power_per_second_min = 0.1
+ /// How much shock power we add to [processing_shock_power_this_tick] per tick. Upper bound
+ var/processing_shock_power_per_second_max = 0.2
+
+ /// In the case we get below 1 power, we add the power to this buffer and use it next tick.
+ var/processing_shock_power_this_tick = 0
+ /// The chance for each processed shock to stun the user.
+ var/processing_shock_stun_chance = 0
+ /// The chance for each processed shock to spark.
+ var/processing_shock_spark_chance = 30
+ /// The chance for each processed shock to message the user.
+ var/process_shock_message_chance = 80
+
+ /// Simple mult for how much of real time is added to [intensity].
+ var/seconds_per_intensity_mult = 1
+
+ /// How many sparks we spawn if a shock sparks. Lower bound
+ var/process_shock_spark_count_min = 1
+ /// How many sparks we spawn if a shock sparks. Upper bound
+ var/process_shock_spark_count_max = 1
+
+ // Generally should be less fast than wire, but its effectiveness should increase with severity
+ /// The percent, in decimal, a successful wirecut use will reduce intensity by.
+ var/wirecut_repair_percent
+ // Generally should be lower than wirecut
+ /// The percent, in decimal, a successful wire use will reduce intensity by.
+ var/wire_repair_percent
+
+ /// The basic multiplier to all our effects. Damage, progress, etc.
+ var/overall_effect_mult = 1
+
+ /// The bodyheat our victim must be at or above to start getting passive healing.
+ var/heat_thresh_to_heal = (BODYTEMP_HEAT_DAMAGE_LIMIT + 30)
+ /// The mult that heat differences between normal and bodytemp threshold is multiplied against. Controls passive heat healing.
+ var/heat_differential_healing_mult = 0.08
+
+ /// Percent chance for a heat repair to give the victim a message.
+ var/heat_heal_message_chance = 20
+
+ /// If [get_intensity_mult()] is at or above this, the limb gets disabled. If null, it will never occur.
+ var/disable_at_intensity_mult
+
+/datum/wound_pregen_data/electrical_damage
+ abstract = TRUE
+ required_limb_biostate = (BIO_WIRED)
+ required_wounding_types = list(WOUND_SLASH)
+ wound_series = WOUND_SERIES_WIRE_SLASH_ELECTRICAL_DAMAGE
+
+/datum/wound_pregen_data/electrical_damage/generate_scar_priorities()
+ return list("[BIO_METAL]") // wire scars dont exist so we can just use metal
+
+/datum/wound/burn/electrical_damage/slash/get_limb_examine_description()
+ return span_warning("The wiring on this limb is slashed open.")
+
+/datum/wound/electrical_damage/handle_process(seconds_per_tick, times_fired)
+ . = ..()
+
+ var/base_mult = get_base_mult()
+
+ var/seconds_per_tick_for_intensity = seconds_per_tick * get_progress_mult()
+ seconds_per_tick_for_intensity = modify_progress_after_progress_mult(seconds_per_tick_for_intensity, seconds_per_tick)
+
+ adjust_intensity(seconds_per_tick_for_intensity SECONDS)
+
+ if (!victim || victim.stat == DEAD)
+ return
+
+ var/damage_mult = get_damage_mult(victim)
+ var/intensity_mult = get_intensity_mult()
+
+ damage_mult *= seconds_per_tick
+ damage_mult *= intensity_mult
+
+ var/picked_damage = LERP(processing_shock_power_per_second_min, processing_shock_power_per_second_max, rand())
+ processing_shock_power_this_tick += (picked_damage * damage_mult)
+ if (processing_shock_power_this_tick <= ELECTRICAL_DAMAGE_MINIMUM_SHOCK_POWER_PER_ZAP)
+ return
+
+ var/stun_chance = (processing_shock_stun_chance * intensity_mult) * base_mult
+ var/spark_chance = (processing_shock_spark_chance * intensity_mult) * base_mult
+
+ var/should_stun = SPT_PROB(stun_chance, seconds_per_tick)
+ var/should_message = SPT_PROB(process_shock_message_chance, seconds_per_tick)
+
+ zap(victim,
+ processing_shock_power_this_tick,
+ stun = should_stun,
+ spark = SPT_PROB(spark_chance, seconds_per_tick),
+ animation = should_stun, message = FALSE,
+ message = should_stun,
+ tell_victim_if_no_message = should_message,
+ ignore_immunity = TRUE,
+ jitter_time = seconds_per_tick,
+ stutter_time = 0,
+ delay_stun = TRUE,
+ knockdown = TRUE,
+ ignore_gloves = TRUE
+ )
+ processing_shock_power_this_tick = 0
+
+/// If someone is aggrograbbing us and targetting our limb, intensity progress is multiplied against this.
+#define LIMB_AGGROGRABBED_PROGRESS_MULT 0.5
+
+/// Returns the multiplier used by our intensity progress. Intensity increment is multiplied against this.
+/datum/wound/electrical_damage/proc/get_progress_mult()
+ var/progress_mult = get_base_mult() * seconds_per_intensity_mult
+
+ if (!limb_essential())
+ progress_mult *= limb_unimportant_progress_mult
+
+ if (isliving(victim.pulledby))
+ var/mob/living/living_puller = victim.pulledby
+ if (living_puller.grab_state >= GRAB_AGGRESSIVE && living_puller.zone_selected == limb.body_zone)
+ progress_mult *= LIMB_AGGROGRABBED_PROGRESS_MULT // they're holding it down
+
+ if (victim.stat == DEAD)
+ progress_mult *= ELECTRICAL_DAMAGE_DEAD_PROGRESS_MULT // doesnt totally stop it but slows it down a lot
+
+ return progress_mult
+#undef LIMB_AGGROGRABBED_PROGRESS_MULT
+
+/// Returns the multiplier used by the damage we deal.
+/datum/wound/electrical_damage/proc/get_damage_mult(mob/living/target)
+ SHOULD_BE_PURE(TRUE)
+
+ var/damage_mult = get_base_mult()
+
+ if (!limb_essential())
+ damage_mult *= limb_unimportant_damage_mult
+
+ return damage_mult * ELECTRICAL_DAMAGE_POWER_PER_TICK_MULT
+
+/// Returns the global multiplier used by both progress and damage.
+/datum/wound/electrical_damage/proc/get_base_mult()
+ var/base_mult = 1
+
+ if (victim)
+ if (IS_IN_STASIS(victim))
+ base_mult *= ELECTRICAL_DAMAGE_ON_STASIS_MULT
+ if (victim.body_position == LYING_DOWN)
+ base_mult *= ELECTRICAL_DAMAGE_LYING_DOWN_MULT
+ if (limb.grasped_by)
+ base_mult *= ELECTRICAL_DAMAGE_GRASPED_MULT
+
+ if (victim.has_status_effect(/datum/status_effect/determined))
+ base_mult *= WOUND_DETERMINATION_BLEED_MOD
+
+ if (HAS_TRAIT(victim, TRAIT_SHOCKIMMUNE)) // it'd be a bit cheesy to just become immune to this, so it only makes it a lot lot better
+ base_mult *= shock_immunity_self_damage_reduction
+
+ var/splint_mult = (limb.current_gauze ? limb.current_gauze.splint_factor : 1)
+ base_mult *= splint_mult
+
+ return overall_effect_mult * base_mult
+
+/// Is called after seconds_for_intensity is modified by get_progress_mult().
+/datum/wound/electrical_damage/proc/modify_progress_after_progress_mult(seconds_for_intensity, seconds_per_tick)
+ if (!victim)
+ return seconds_for_intensity
+
+ return seconds_for_intensity - (get_heat_healing() * seconds_per_tick)
+
+/// Returns how many deciseconds progress should be reduced by, based on the current heat of our victim's body.
+/datum/wound/electrical_damage/proc/get_heat_healing(do_message = prob(heat_heal_message_chance))
+ var/healing_amount = max((victim.bodytemperature - heat_thresh_to_heal), 0) * heat_differential_healing_mult
+ if (do_message && healing_amount)
+ to_chat(victim, span_notice("You feel the solder within your [limb.plaintext_zone] reform and repair your [name]..."))
+
+ return healing_amount
+
+/// Changes intensity by the given amount, and then updates our status, removing ourselves if fixed.
+/datum/wound/electrical_damage/proc/adjust_intensity(to_adjust)
+ intensity = clamp((intensity + to_adjust), 0, processing_full_shock_threshold)
+
+ if (disable_at_intensity_mult)
+ set_disabling(get_intensity_mult() >= disable_at_intensity_mult)
+
+ remove_if_fixed()
+
+/datum/wound/electrical_damage/wound_injury(datum/wound/electrical_damage/old_wound, attack_direction)
+ . = ..()
+
+ if (old_wound)
+ intensity = max(intensity, old_wound.intensity)
+ processing_shock_power_this_tick = old_wound.processing_shock_power_this_tick
+
+ do_sparks(initial_sparks_amount, FALSE, victim)
+
+/datum/wound/electrical_damage/modify_desc_before_span(desc, mob/user)
+ . = ..()
+
+ if (limb.current_gauze)
+ return
+
+ var/intensity_mult = get_intensity_mult()
+ if (intensity_mult < 0.2 || (victim.stat == DEAD))
+ return
+
+ . += ", and "
+
+ var/extra
+ switch (intensity_mult)
+ if (0.2 to 0.4)
+ extra += "[span_deadsay("is letting out some sparks")]"
+ if (0.4 to 0.6)
+ extra += "[span_deadsay("is sparking quite a bit")]"
+ if (0.6 to 0.8)
+ extra += "[span_deadsay("is practically hemorrhaging sparks")]"
+ if (0.8 to 1)
+ extra += "[span_deadsay("has golden bolts of electricity constantly striking the surface")]"
+
+ . += extra
+
+/datum/wound/electrical_damage/get_scanner_description(mob/user)
+ . = ..()
+
+ . += "\nWound status: [get_wound_status_info()]"
+
+/datum/wound/electrical_damage/get_simple_scanner_description(mob/user)
+ . = ..()
+
+ . += "\nWound status: [get_wound_status_info()]"
+
+/// Returns a string with our fault intensity and threshold to removal for use in health analyzers.
+/datum/wound/electrical_damage/proc/get_wound_status_info()
+ return "Fault intensity is currently at [span_bold("[get_intensity_mult() * 100]")]%. It must be reduced to [span_blue("[minimum_intensity]")]% to remove the wound."
+
+// this wound is unaffected by cryoxadone and pyroxadone
+/datum/wound/electrical_damage/on_xadone(power)
+ return
+
+/datum/wound/electrical_damage/item_can_treat(obj/item/potential_treater, mob/user)
+ if (istype(potential_treater, /obj/item/stack/cable_coil) && ((user.pulling == victim && user.grab_state >= GRAB_AGGRESSIVE) || (limb.burn_dam <= ELECTRICAL_DAMAGE_MAX_BURN_DAMAGE_TO_LET_WIRES_REPAIR)))
+ return TRUE // if we're aggrograbbed, or relatively undamaged, go ahead. else, we dont want to impede normal treatment
+
+ return ..()
+
+/datum/wound/electrical_damage/treat(obj/item/treating_item, mob/user)
+ if (treating_item.tool_behaviour == TOOL_WIRECUTTER || treating_item.tool_behaviour == TOOL_RETRACTOR)
+ return wirecut(treating_item, user)
+
+ if (istype(treating_item, /obj/item/stack/medical/suture) || istype(treating_item, /obj/item/stack/cable_coil))
+ return suture_wires(treating_item, user)
+
+ return ..()
+
+/**
+ * The "trauma" treatment, done with cables/sutures. Sutures get a debuff.
+ * Low self-tend penalty.
+ * Very fast, but low value. Eats up wires for breakfast.
+ * Has limited wire/HUD bonuses. If you're a robo, use a wirecutter instead.
+ */
+/datum/wound/electrical_damage/proc/suture_wires(obj/item/stack/suturing_item, mob/living/carbon/human/user)
+ if (!suturing_item.tool_start_check())
+ return TRUE
+
+ var/is_suture = (istype(suturing_item, /obj/item/stack/medical/suture))
+
+ var/change = (processing_full_shock_threshold * wire_repair_percent) * ELECTRICAL_DAMAGE_SUTURE_WIRE_HEALING_AMOUNT_MULT
+ var/delay_mult = 1
+ if (user == victim)
+ delay_mult *= 1.5
+ if (is_suture)
+ delay_mult *= 2
+ var/obj/item/stack/medical/suture/suture_item = suturing_item
+ var/obj/item/stack/medical/suture/base_suture = /obj/item/stack/medical/suture
+ change += (suture_item.heal_brute - initial(base_suture.heal_brute))
+
+ // as this is the trauma treatment, there are less bonuses
+ // if youre doing this, youre probably doing this on-the-spot
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ delay_mult *= 0.8
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ delay_mult *= 0.9
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ delay_mult *= 0.8
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ change *= 1.2
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ var/replacing_or_suturing = (is_suture ? "repairing some" : "replacing")
+ while (suturing_item.tool_start_check())
+ user?.visible_message(span_danger("[user] begins [replacing_or_suturing] wiring within [their_or_other] [limb.plaintext_zone] with [suturing_item]..."), \
+ span_notice("You begin [replacing_or_suturing] wiring within [your_or_other] [limb.plaintext_zone] with [suturing_item]..."))
+ if (!suturing_item.use_tool(target = victim, user = user, delay = ELECTRICAL_DAMAGE_SUTURE_WIRE_BASE_DELAY * delay_mult, amount = 1, volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ if (user != victim && user.combat_mode)
+ user?.visible_message(span_danger("[user] mangles some of [their_or_other] [limb.plaintext_zone]'s wiring!"), \
+ span_danger("You mangle some of [your_or_other] [limb.plaintext_zone]'s wiring!"), ignored_mobs = victim)
+ to_chat(victim, span_userdanger("[capitalize(your_or_other)] mangles some of your [limb.plaintext_zone]'s wiring!"))
+ adjust_intensity(change * 2)
+ else
+ var/repairs_or_replaces = (is_suture ? "repairs" : "replaces")
+ var/repair_or_replace = (is_suture ? "repair" : "replace")
+ user?.visible_message(span_notice("[user] [repairs_or_replaces] some of [their_or_other] [limb.plaintext_zone]'s wiring!"), \
+ span_notice("You [repair_or_replace] some of [your_or_other] [limb.plaintext_zone]'s wiring!"))
+ adjust_intensity(-change)
+ victim.balloon_alert(user, "intensity reduced to [get_intensity_mult() * 100]%")
+
+ if (fixed())
+ return TRUE
+ return TRUE
+
+/**
+ * The "proper" treatment, done with wirecutters/retractors. Retractors get a debuff.
+ * High self-tend penalty.
+ * Slow, but high value.
+ * Has high wire/HUD bonuses. The ideal treatment for a robo.
+ */
+/datum/wound/electrical_damage/proc/wirecut(obj/item/wirecutting_tool, mob/living/carbon/human/user)
+ if (!wirecutting_tool.tool_start_check())
+ return TRUE
+
+ var/is_retractor = (wirecutting_tool.tool_behaviour == TOOL_RETRACTOR)
+
+ var/change = (processing_full_shock_threshold * wirecut_repair_percent)
+ var/delay_mult = 1
+ if (user == victim)
+ delay_mult *= 2.5
+ if (is_retractor)
+ delay_mult *= 2
+ change *= 0.8
+ var/knows_wires = FALSE
+ if (HAS_TRAIT(user, TRAIT_KNOW_ROBO_WIRES))
+ delay_mult *= 0.9
+ change *= 1.7
+ knows_wires = TRUE
+ else if (HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ change *= 1.35
+ knows_wires = TRUE
+ if (HAS_TRAIT(user, TRAIT_DIAGNOSTIC_HUD))
+ if (knows_wires)
+ delay_mult *= 0.9
+ else
+ delay_mult *= 0.75
+ if (HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ delay_mult *= 0.8
+
+ var/their_or_other = (user == victim ? "[user.p_their()]" : "[victim]'s")
+ var/your_or_other = (user == victim ? "your" : "[victim]'s")
+ while (wirecutting_tool.tool_start_check())
+ user?.visible_message(span_danger("[user] begins resetting misplaced wiring within [their_or_other] [limb.plaintext_zone]..."), \
+ span_notice("You begin resetting misplaced wiring within [your_or_other] [limb.plaintext_zone]..."))
+ if (!wirecutting_tool.use_tool(target = victim, user = user, delay = ELECTRICAL_DAMAGE_WIRECUTTER_BASE_DELAY * delay_mult, volume = 50, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ if (user != victim && user.combat_mode)
+ user?.visible_message(span_danger("[user] mangles some of [their_or_other] [limb.plaintext_zone]'s wiring!"), \
+ span_danger("You mangle some of [your_or_other] [limb.plaintext_zone]'s wiring!"), ignored_mobs = victim)
+ to_chat(victim, span_userdanger("[capitalize(your_or_other)] mangles some of your [limb.plaintext_zone]'s wiring!"))
+ adjust_intensity(change * 2)
+ else
+ user?.visible_message(span_notice("[user] resets some of [their_or_other] [limb.plaintext_zone]'s wiring!"), \
+ span_notice("You reset some of [your_or_other] [limb.plaintext_zone]'s wiring!"))
+ adjust_intensity(-change)
+ victim.balloon_alert(user, "intensity reduced to [get_intensity_mult() * 100]%")
+
+ if (fixed())
+ return TRUE
+ return TRUE
+
+/// If fixed() is true, we remove ourselves and return TRUE. FALSE otherwise.
+/datum/wound/electrical_damage/proc/remove_if_fixed()
+ if (fixed())
+ to_chat(victim, span_green("Your [limb.plaintext_zone] has recovered from its [name]!"))
+ remove_wound()
+ return TRUE
+ return FALSE
+
+/// Should we remove ourselves?
+/datum/wound/electrical_damage/proc/fixed()
+ return (intensity <= minimum_intensity || isnull(limb))
+
+/// Returns the multiplier we apply to our outgoing damage based off our current intensity. Is always between 0-1.
+/datum/wound/electrical_damage/proc/get_intensity_mult()
+ return (min((intensity / processing_full_shock_threshold), 1))
+
+/// Wrapper for electrocute_act
+/datum/wound/electrical_damage/proc/zap(
+ mob/living/target,
+ damage,
+ coeff = 1,
+ stun,
+ spark = TRUE,
+ animation = TRUE,
+ message = TRUE,
+ ignore_immunity = FALSE,
+ delay_stun = FALSE,
+ knockdown = FALSE,
+ ignore_gloves = FALSE,
+ tell_victim_if_no_message = TRUE,
+ jitter_time = 20 SECONDS,
+ stutter_time = 4 SECONDS,
+)
+
+ var/flags = NONE
+ if (!stun)
+ flags |= SHOCK_NOSTUN
+ if (!animation)
+ flags |= SHOCK_NO_HUMAN_ANIM
+ if (!message)
+ flags |= SHOCK_SUPPRESS_MESSAGE
+ if (tell_victim_if_no_message && target == victim)
+ to_chat(target, span_warning("Your [limb.plaintext_zone] short-circuits and zaps you!"))
+ if (ignore_immunity)
+ flags |= SHOCK_IGNORE_IMMUNITY
+ if (delay_stun)
+ flags |= SHOCK_DELAY_STUN
+ if (knockdown)
+ flags |= SHOCK_KNOCKDOWN
+ if (ignore_gloves)
+ flags |= SHOCK_NOGLOVES
+
+ target.electrocute_act(damage, limb, coeff, flags, jitter_time, stutter_time)
+ if (spark)
+ do_sparks(rand(process_shock_spark_count_min, process_shock_spark_count_max), FALSE, victim)
+
+// Slash
+// Fast to rise, but lower damage overall
+// Also a bit easy to treat
+/datum/wound/electrical_damage/slash
+ simple_desc = "Wiring has been slashed open, resulting in a fault that quickly intensifies!"
+
+/datum/wound/electrical_damage/slash/moderate
+ name = "Frayed Wiring"
+ desc = "Internal wiring has suffered a slight abrasion, causing a slow electrical fault that will intensify over time."
+ occur_text = "lets out a few sparks, as a few frayed wires stick out"
+ examine_desc = "has a few frayed wires sticking out"
+ treat_text = "Replacing of damaged wiring, though repairs via wirecutting instruments or sutures may suffice, albeit at limited efficiency. In case of emergency, \
+ subject may be subjected to high temperatures to allow solder to reset."
+
+ sound_effect = 'modular_skyrat/modules/medical/sound/robotic_slash_T1.ogg'
+
+ severity = WOUND_SEVERITY_MODERATE
+
+ sound_volume = 30
+
+ threshold_penalty = 20
+
+ intensity = 10 SECONDS
+ processing_full_shock_threshold = 3 MINUTES
+
+ processing_shock_power_per_second_max = 0.5
+ processing_shock_power_per_second_min = 0.4
+
+ processing_shock_stun_chance = 0
+ processing_shock_spark_chance = 30
+
+ process_shock_spark_count_max = 1
+ process_shock_spark_count_min = 1
+
+ wirecut_repair_percent = 0.085 // not even faster at this point
+ wire_repair_percent = 0.035
+
+ initial_sparks_amount = 1
+
+ status_effect_type = /datum/status_effect/wound/electrical_damage/slash/moderate
+
+ a_or_from = "from"
+
+ scar_keyword = "slashmoderate"
+
+/datum/wound_pregen_data/electrical_damage/slash/moderate
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/electrical_damage/slash/moderate
+ threshold_minimum = 35
+
+/datum/wound/electrical_damage/slash/severe
+ name = "Severed Conduits"
+ desc = "A number of wires have been completely cut, resulting in electrical faults that will intensify at a worrying rate."
+ occur_text = "sends some electrical fiber in the direction of the blow, beginning to profusely spark"
+ examine_desc = "has multiple severed wires visible to the outside"
+ treat_text = "Containment of damaged wiring via gauze, then application of fresh wiring/sutures, or resetting of displaced wiring via wirecutter/retractor."
+
+ sound_effect = 'modular_skyrat/modules/medical/sound/robotic_slash_T2.ogg'
+
+ severity = WOUND_SEVERITY_SEVERE
+
+ sound_volume = 15
+
+ threshold_penalty = 30
+
+ intensity = 10 SECONDS
+ processing_full_shock_threshold = 2 MINUTES
+
+ processing_shock_power_per_second_max = 0.7
+ processing_shock_power_per_second_min = 0.6
+
+ processing_shock_stun_chance = 0
+ processing_shock_spark_chance = 60
+
+ process_shock_spark_count_max = 2
+ process_shock_spark_count_min = 1
+
+ wirecut_repair_percent = 0.1
+ wire_repair_percent = 0.032
+
+ initial_sparks_amount = 3
+
+ status_effect_type = /datum/status_effect/wound/electrical_damage/slash/severe
+
+ a_or_from = "from"
+
+ scar_keyword = "slashsevere"
+
+/datum/wound_pregen_data/electrical_damage/slash/severe
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/electrical_damage/slash/severe
+ threshold_minimum = 60
+
+/datum/wound/electrical_damage/slash/critical
+ name = "Systemic Fault"
+ desc = "A significant portion of the power distribution network has been cut open, resulting in massive power loss and runaway electrocution."
+ occur_text = "lets out a violent \"zhwarp\" sound as angry electric arcs attack the surrounding air"
+ examine_desc = "has lots of wires mauled wires sticking out"
+ treat_text = "Immediate securing via gauze, followed by emergency cable replacement and securing via wirecutters or retractor. \
+ If the fault has become uncontrollable, extreme heat therapy is recommended."
+
+ severity = WOUND_SEVERITY_CRITICAL
+ wound_flags = (ACCEPTS_GAUZE|MANGLES_INTERIOR|CAN_BE_GRASPED|SPLINT_OVERLAY)
+
+ sound_effect = 'modular_skyrat/modules/medical/sound/robotic_slash_T3.ogg'
+
+ sound_volume = 30
+
+ threshold_penalty = 50
+
+ intensity = 10 SECONDS
+ processing_full_shock_threshold = 1.25 MINUTES
+
+ processing_shock_power_per_second_max = 1.3
+ processing_shock_power_per_second_min = 1.1
+
+ processing_shock_stun_chance = 5
+ processing_shock_spark_chance = 90
+
+ process_shock_spark_count_max = 3
+ process_shock_spark_count_min = 2
+
+ wirecut_repair_percent = 0.12
+ wire_repair_percent = 0.03
+
+ initial_sparks_amount = 8
+
+ status_effect_type = /datum/status_effect/wound/electrical_damage/slash/critical
+
+ a_or_from = "a"
+
+ scar_keyword = "slashcritical"
+
+/datum/wound_pregen_data/electrical_damage/slash/critical
+ abstract = FALSE
+ wound_path_to_generate = /datum/wound/electrical_damage/slash/critical
+ threshold_minimum = 100
+
+#undef ELECTRICAL_DAMAGE_ON_STASIS_MULT
+#undef ELECTRICAL_DAMAGE_GRASPED_MULT
+#undef ELECTRICAL_DAMAGE_LYING_DOWN_MULT
+#undef ELECTRICAL_DAMAGE_DEAD_PROGRESS_MULT
+
+#undef ELECTRICAL_DAMAGE_WIRECUTTER_BASE_DELAY
+#undef ELECTRICAL_DAMAGE_SUTURE_WIRE_BASE_DELAY
+
+#undef ELECTRICAL_DAMAGE_MINIMUM_SHOCK_POWER_PER_ZAP
+#undef ELECTRICAL_DAMAGE_MAX_BURN_DAMAGE_TO_LET_WIRES_REPAIR
+#undef ELECTRICAL_DAMAGE_POWER_PER_TICK_MULT
+#undef ELECTRICAL_DAMAGE_SUTURE_WIRE_HEALING_AMOUNT_MULT
diff --git a/modular_skyrat/modules/medical/code/wounds/wound_effects.dm b/modular_skyrat/modules/medical/code/wounds/wound_effects.dm
new file mode 100644
index 00000000000000..8f9aef16210d3e
--- /dev/null
+++ b/modular_skyrat/modules/medical/code/wounds/wound_effects.dm
@@ -0,0 +1,33 @@
+/datum/status_effect/wound/blunt/robotic/moderate
+ id = "unsecure_moderate"
+
+/datum/status_effect/wound/blunt/robotic/severe
+ id = "unsecure_severe"
+
+/datum/status_effect/wound/blunt/robotic/critical
+ id = "unsecure_critical"
+
+/datum/status_effect/wound/electrical_damage/slash/moderate
+ id = "electric_slash_moderate"
+
+/datum/status_effect/wound/electrical_damage/slash/severe
+ id = "electric_slash_severe"
+
+/datum/status_effect/wound/electrical_damage/slash/critical
+ id = "electric_slash_critical"
+
+/datum/status_effect/wound/electrical_damage/pierce/moderate
+ id = "electric_pierce_moderate"
+
+/datum/status_effect/wound/electrical_damage/pierce/severe
+ id = "electric_pierce_severe"
+
+/datum/status_effect/wound/electrical_damage/pierce/critical
+ id = "electric_pierce_critical"
+
+/datum/status_effect/wound/burn/robotic/moderate
+ id = "overheated"
+/datum/status_effect/wound/burn/robotic/severe
+ id = "warpedmetal"
+/datum/status_effect/wound/burn/robotic/critical
+ id = "demagnetizedmetal"
diff --git a/modular_skyrat/modules/medical/readme.md b/modular_skyrat/modules/medical/readme.md
new file mode 100644
index 00000000000000..6ab0af32ddf7e9
--- /dev/null
+++ b/modular_skyrat/modules/medical/readme.md
@@ -0,0 +1,51 @@
+
+
+https://github.com/Skyrat-SS13/Skyrat-tg/pull/2336
+https://github.com/Skyrat-SS13/Skyrat-tg/pull/23733
+
+## Skyrat Medical Update
+
+Module ID: SKYRAT_MEDICAL_UPDATE
+
+### Description:
+
+Various changes to the medical system, from adding bandage overlays, to new wounds, to modularized procs.
+
+
+
+### TG Proc/File Changes:
+
+- code/_DEFINES/wounds.dm: Added muscle/synth wound series, added them to the global list of wound series
+- cat2_medicine_reagents.dm: /datum/reagent/medicine/c2/hercuri/on_mob_life, Allowed hercuri to affect synthetics, also changed hercuri process flags for this purpose
+- quirks.dm: Commented out the quadruple_amputee/frail blacklist as frail can now apply to prosthetics
+
+
+### Modular Overrides:
+
+- N/A
+
+
+### Defines:
+
+- Many local synthetic wound defines
+
+
+### Included files that are not contained in this module:
+
+- strings/wounds/metal_scar_desc.json -- Required to be here for _string_lists.dm usage
+
+
+### Credits:
+
+Azarak - Original medical update, muscle wounds, bandage overlays
+Niko - Synthetic wounds
+TG coding/Skyrat coding channels and community - Support, ideas, reviews
+
+
diff --git a/modular_skyrat/modules/medical/sound/robotic_slash_T1.ogg b/modular_skyrat/modules/medical/sound/robotic_slash_T1.ogg
new file mode 100644
index 00000000000000..cb7b3d0a79f499
Binary files /dev/null and b/modular_skyrat/modules/medical/sound/robotic_slash_T1.ogg differ
diff --git a/modular_skyrat/modules/medical/sound/robotic_slash_T2.ogg b/modular_skyrat/modules/medical/sound/robotic_slash_T2.ogg
new file mode 100644
index 00000000000000..4fad2583575380
Binary files /dev/null and b/modular_skyrat/modules/medical/sound/robotic_slash_T2.ogg differ
diff --git a/modular_skyrat/modules/medical/sound/robotic_slash_T3.ogg b/modular_skyrat/modules/medical/sound/robotic_slash_T3.ogg
new file mode 100644
index 00000000000000..f0ea8e2217027a
Binary files /dev/null and b/modular_skyrat/modules/medical/sound/robotic_slash_T3.ogg differ
diff --git a/modular_skyrat/modules/medical_designs/medical_designs.dm b/modular_skyrat/modules/medical_designs/medical_designs.dm
index 288adf0643b314..d0360fbfc2bc4a 100644
--- a/modular_skyrat/modules/medical_designs/medical_designs.dm
+++ b/modular_skyrat/modules/medical_designs/medical_designs.dm
@@ -8,3 +8,13 @@
RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_MEDICAL
)
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/surgery/healing/robotic_healing_upgrade
+ name = "Repair robotic limbs upgrade: Advanced"
+ surgery = /datum/surgery/robot_healing/upgraded
+ id = "robotic_heal_surgery_upgrade"
+
+/datum/design/surgery/healing/robotic_healing_upgrade_2
+ name = "Repair robotic limbs upgrade: Experimental"
+ surgery = /datum/surgery/robot_healing/experimental
+ id = "robotic_heal_surgery_upgrade_2"
diff --git a/modular_skyrat/modules/microfusion/code/gun_types.dm b/modular_skyrat/modules/microfusion/code/gun_types.dm
index 24a1eeed05384d..052e3b32db1045 100644
--- a/modular_skyrat/modules/microfusion/code/gun_types.dm
+++ b/modular_skyrat/modules/microfusion/code/gun_types.dm
@@ -49,7 +49,6 @@
*/
/obj/effect/spawner/armory_spawn/microfusion
- icon_state = "random_rifle"
guns = list(
/obj/item/gun/microfusion/mcr01,
/obj/item/gun/microfusion/mcr01,
diff --git a/modular_skyrat/modules/modular_ert/code/odst/odst_outfit.dm b/modular_skyrat/modules/modular_ert/code/odst/odst_outfit.dm
index cb618c41d31623..41f356151946de 100644
--- a/modular_skyrat/modules/modular_ert/code/odst/odst_outfit.dm
+++ b/modular_skyrat/modules/modular_ert/code/odst/odst_outfit.dm
@@ -5,7 +5,7 @@
glasses = /obj/item/clothing/glasses/hud/security/night
ears = /obj/item/radio/headset/headset_cent/alt
gloves = /obj/item/clothing/gloves/combat
- l_hand = /obj/item/gun/ballistic/automatic/pitbull
+ l_hand = /obj/item/gun/ballistic/automatic/sol_rifle/machinegun
belt = /obj/item/storage/belt/military/odst
back = /obj/item/mod/control/pre_equipped/responsory/security
backpack_contents = list(
diff --git a/modular_skyrat/modules/modular_implants/code/nif_persistence.dm b/modular_skyrat/modules/modular_implants/code/nif_persistence.dm
index 1dc272c434e8f9..fd24fd3b5792e7 100644
--- a/modular_skyrat/modules/modular_implants/code/nif_persistence.dm
+++ b/modular_skyrat/modules/modular_implants/code/nif_persistence.dm
@@ -15,6 +15,8 @@
var/nif_is_calibrated
/// How many rewards points does the NIF have stored on it?
var/stored_rewards_points
+ /// A string containing programs that are transfered from one round to the next.
+ var/persistent_nifsofts
/// Saves the NIF data for a individual user.
/mob/living/carbon/human/proc/save_nif_data(datum/modular_persistence/persistence, remove_nif = FALSE)
@@ -51,14 +53,19 @@
persistence.stored_rewards_points = installed_nif.rewards_points
var/datum/component/nif_examine/examine_component = GetComponent(/datum/component/nif_examine)
-
persistence.nif_examine_text = examine_component?.nif_examine_text
+
+ var/persistent_nifsoft_paths = "" // We need to convert all of the paths in the list into a single string
for(var/datum/nifsoft/nifsoft as anything in installed_nif.loaded_nifsofts)
- if(!nifsoft.persistence)
+ if(nifsoft.persistence)
+ nifsoft.save_persistence_data(persistence)
+
+ if(!nifsoft.able_to_keep || !nifsoft.keep_installed)
continue
- nifsoft.save_persistence_data(persistence)
+ persistent_nifsoft_paths += "&[(nifsoft.type)]"
+ persistence.persistent_nifsofts = persistent_nifsoft_paths
/// Loads the NIF data for an individual user.
/mob/living/carbon/human/proc/load_nif_data(datum/modular_persistence/persistence)
@@ -74,6 +81,16 @@
new_nif.current_theme = persistence.nif_theme
new_nif.is_calibrated = persistence.nif_is_calibrated
new_nif.rewards_points = persistence.stored_rewards_points
+
+ var/list/persistent_nifsoft_paths = list()
+ for(var/text as anything in splittext(persistence.persistent_nifsofts, "&"))
+ var/datum/nifsoft/nifsoft_to_add = text2path(text)
+ if(!ispath(nifsoft_to_add, /datum/nifsoft) || !initial(nifsoft_to_add.able_to_keep))
+ continue
+
+ persistent_nifsoft_paths.Add(nifsoft_to_add)
+
+ new_nif.persistent_nifsofts = persistent_nifsoft_paths.Copy()
new_nif.Insert(src)
var/datum/component/nif_examine/examine_component = GetComponent(/datum/component/nif_examine)
diff --git a/modular_skyrat/modules/modular_implants/code/nif_research.dm b/modular_skyrat/modules/modular_implants/code/nif_research.dm
index 7e8e1ca94f8cec..4e88040cddff23 100644
--- a/modular_skyrat/modules/modular_implants/code/nif_research.dm
+++ b/modular_skyrat/modules/modular_implants/code/nif_research.dm
@@ -28,6 +28,16 @@
category = list(RND_CATEGORY_TOOLS + RND_SUBCATEGORY_EQUIPMENT_MEDICAL) // look, the anesthetic machine's there too
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
+/datum/design/mini_soulcatcher
+ name = "Poltergeist-Type RSD"
+ desc = "A miniature version of a Soulcatcher that can be attached to various objects."
+ id = "mini_soulcatcher"
+ build_type = PROTOLATHE | AWAY_LATHE
+ build_path = /obj/item/attachable_soulcatcher
+ materials = list(/datum/material/glass = SMALL_MATERIAL_AMOUNT * 5, /datum/material/iron = SMALL_MATERIAL_AMOUNT * 5)
+ category = list(RND_CATEGORY_AI + RND_SUBCATEGORY_AI_MISC) // look, the anesthetic machine's there too
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_SERVICE | DEPARTMENT_BITFLAG_MEDICAL
+
/datum/design/nifsoft_hud
build_type = PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/silver = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/plastic = SHEET_MATERIAL_AMOUNT)
diff --git a/modular_skyrat/modules/modular_implants/code/nifs.dm b/modular_skyrat/modules/modular_implants/code/nifs.dm
index 69c0a581c3594c..6ce9f780f83966 100644
--- a/modular_skyrat/modules/modular_implants/code/nifs.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifs.dm
@@ -92,6 +92,8 @@
var/list/loaded_nifsofts = list()
///What programs come already installed on the NIF?
var/list/preinstalled_nifsofts = list(/datum/nifsoft/soul_poem)
+ ///What programs do we want to carry between rounds?
+ var/list/persistent_nifsofts = list()
///This shows up in the NIF settings screen as a way to ICly display lore.
var/manufacturer_notes = "There is no data currently avalible for this product."
@@ -147,8 +149,8 @@
linked_mob.AddComponent(/datum/component/nif_examine)
RegisterSignal(linked_mob, COMSIG_LIVING_DEATH, PROC_REF(damage_on_death))
- if(preinstalled_nifsofts)
- send_message("Loading preinstalled NIFSofts, please wait...")
+ if(preinstalled_nifsofts || persistent_nifsofts)
+ send_message("Loading preinstalled and stored NIFSofts, please wait...")
addtimer(CALLBACK(src, PROC_REF(install_preinstalled_nifsofts)), 3 SECONDS)
/obj/item/organ/internal/cyberimp/brain/nif/Remove(mob/living/carbon/organ_owner, special = FALSE)
@@ -174,6 +176,10 @@
for(var/datum/nifsoft/preinstalled_nifsoft as anything in preinstalled_nifsofts)
new preinstalled_nifsoft(src)
+ for(var/stored_nifsoft in persistent_nifsofts)
+ var/datum/nifsoft/new_stored_nifsoft = new stored_nifsoft(src)
+ new_stored_nifsoft.keep_installed = TRUE
+
return TRUE
/obj/item/organ/internal/cyberimp/brain/nif/process(seconds_per_tick)
@@ -501,6 +507,7 @@
new /obj/item/disk/nifsoft_uploader/summoner(src)
new /obj/item/disk/nifsoft_uploader/money_sense(src)
new /obj/item/disk/nifsoft_uploader/dorms(src)
+ new /obj/item/disk/nifsoft_uploader/dorms/hypnosis(src)
new /obj/item/disk/nifsoft_uploader/soulcatcher(src)
/obj/item/storage/box/nif_ghost_box/ghost_role/PopulateContents()
diff --git a/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm b/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm
index 0423e28c830ae0..dfc9916ab83bca 100644
--- a/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm
@@ -45,6 +45,8 @@
"active_cost" = nifsoft.active_cost,
"reference" = REF(nifsoft),
"ui_icon" = nifsoft.ui_icon,
+ "able_to_keep" = nifsoft.able_to_keep,
+ "keep_installed" = nifsoft.keep_installed,
)
data["loaded_nifsofts"] += list(nifsoft_data)
@@ -119,6 +121,8 @@
return FALSE
current_theme = target_theme
+ for(var/datum/nifsoft/installed_nifsoft as anything in loaded_nifsofts)
+ installed_nifsoft.update_theme()
if("activate_nifsoft")
var/datum/nifsoft/activated_nifsoft = locate(params["activated_nifsoft"]) in loaded_nifsofts
@@ -126,3 +130,11 @@
return FALSE
activated_nifsoft.activate()
+
+ if("toggle_keeping_nifsoft")
+ var/datum/nifsoft/nifsoft_to_keep = locate(params["nifsoft_to_keep"]) in loaded_nifsofts
+ if(!nifsoft_to_keep || !nifsoft_to_keep.able_to_keep)
+ return FALSE
+
+ nifsoft_to_keep.keep_installed = !nifsoft_to_keep.keep_installed
+ update_static_data_for_all_viewers()
diff --git a/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm b/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm
index 6319fa60d9fbdf..4cd6beea37955f 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm
@@ -1,11 +1,13 @@
GLOBAL_LIST_INIT(purchasable_nifsofts, list(
/datum/nifsoft/hivemind,
/datum/nifsoft/summoner,
- /datum/nifsoft/shapeshifter,
+ /datum/nifsoft/action_granter/shapeshifter,
/datum/nifsoft/summoner/dorms,
/datum/nifsoft/soul_poem,
/datum/nifsoft/soulcatcher,
+ /datum/nifsoft/scryer,
/datum/nifsoft/summoner/book,
+ /datum/nifsoft/action_granter/hypnosis,
))
/datum/computer_file/program/nifsoft_downloader
@@ -80,6 +82,7 @@ GLOBAL_LIST_INIT(purchasable_nifsofts, list(
"category" = initial(buyable_nifsoft.buying_category),
"ui_icon" = initial(buyable_nifsoft.ui_icon),
"reference" = buyable_nifsoft,
+ "keepable" = initial(buyable_nifsoft.able_to_keep),
)
var/category = nifsoft_details["category"]
if(!(category in product_list))
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts.dm b/modular_skyrat/modules/modular_implants/code/nifsofts.dm
index d7c382cfddbae1..ddbff9cf86e6d6 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts.dm
@@ -18,6 +18,8 @@
var/buying_category = NIFSOFT_CATEGORY_GENERAL
///What font awesome icon is shown next to the name of the nifsoft?
var/ui_icon = "floppy-disk"
+ ///What UI theme do we want to display to users if this NIFSoft has TGUI?
+ var/ui_theme = "default"
///Can the program be installed with other instances of itself?
var/single_install = TRUE
@@ -47,6 +49,10 @@
var/rewards_points_eligible = TRUE
///Does the NIFSoft have anything that is saved cross-round?
var/persistence = FALSE
+ /// Is the NIFSoft something that we want to allow the user to keep?
+ var/able_to_keep = FALSE
+ /// Are we keeping the NIFSoft installed between rounds? This is decided by the user
+ var/keep_installed = FALSE
///Is it a lewd item?
var/lewd_nifsoft = FALSE
@@ -63,6 +69,7 @@
qdel(src)
load_persistence_data()
+ update_theme()
/datum/nifsoft/Destroy()
if(active)
@@ -144,6 +151,15 @@
/datum/nifsoft/ui_state(mob/user)
return GLOB.conscious_state
+/// Updates the theme of the NIFSoft to match the parent NIF
+/datum/nifsoft/proc/update_theme()
+ var/obj/item/organ/internal/cyberimp/brain/nif/target_nif = parent_nif.resolve()
+ if(!target_nif)
+ return FALSE
+
+ ui_theme = target_nif.current_theme
+ return TRUE
+
/// A disk that can upload NIFSofts to a recpient with a NIFSoft installed.
/obj/item/disk/nifsoft_uploader
name = "Generic NIFSoft datadisk"
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/base_types/action_granter.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/base_types/action_granter.dm
new file mode 100644
index 00000000000000..05d1012d629e74
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/base_types/action_granter.dm
@@ -0,0 +1,26 @@
+/// This type of NIFSoft grans the user an action when active.
+/datum/nifsoft/action_granter
+ active_mode = TRUE
+ activation_cost = 10
+ active_cost = 1
+ /// What is the path of the action that we want to grant?
+ var/action_to_grant = /datum/action/innate
+ /// What action are we giving the user of the NIFSoft?
+ var/datum/action/innate/granted_action
+
+/datum/nifsoft/action_granter/activate()
+ . = ..()
+ if(active)
+ granted_action = new action_to_grant
+ granted_action.Grant(linked_mob)
+ return
+
+ if(granted_action)
+ granted_action.Remove(linked_mob)
+
+/datum/nifsoft/action_granter/Destroy()
+ if(granted_action)
+ QDEL_NULL(granted_action)
+ return ..()
+
+
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/dorms.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/dorms.dm
index d6f02e394e4810..97506d8d69d6bc 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/dorms.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/dorms.dm
@@ -1,12 +1,12 @@
/obj/item/disk/nifsoft_uploader/dorms
- name = "Grimoire Purpura"
+ name = "Grimoire Libidine"
loaded_nifsoft = /datum/nifsoft/summoner/dorms
/datum/nifsoft/summoner/dorms
- name = "Grimoire Purpura"
- program_desc = "Grimoire Purpura, a fork of the Grimoire Caeruleam code, allows users to conveniently access an extensive database of various adult toys. "
+ name = "Grimoire Libidine"
+ program_desc = "Grimoire Libidine, a fork of the Grimoire Caeruleam code, allows users to conveniently access an extensive database of various adult toys. "
holographic_filter = FALSE //No RGB toys
- name_tag = "purpura "
+ name_tag = "libidine "
lewd_nifsoft = TRUE
ui_icon = "heart"
@@ -62,7 +62,7 @@
purchase_price = 150
/obj/item/disk/nifsoft_uploader/dorms/contract
- name = "\improper Purpura Contract"
+ name = "\improper Libidine Contract"
loaded_nifsoft = /datum/nifsoft/hypno
reusable = TRUE //This is set to true because of how this handles updating laws
///What laws will be assigned when using the NIFSoft on someone?
@@ -93,8 +93,8 @@
return TRUE
/datum/nifsoft/hypno
- name = "Purpura Contract"
- program_desc = "Once installed, the Purpura Contract compells the user to follow the rules stored in the data of the NIFSoft. \n OOC NOTE: This is strictly here for adult roleplay. None of the laws here actually need to be obeyed and you can uninstall this NIFSoft at any time."
+ name = "Libidine Contract"
+ program_desc = "Once installed, the Libidine Contract compells the user to follow the rules stored in the data of the NIFSoft. \n OOC NOTE: This is strictly here for adult roleplay. None of the laws here actually need to be obeyed and you can uninstall this NIFSoft at any time."
purchase_price = 0
lewd_nifsoft = TRUE
ui_icon = "file-contract"
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/hypnosis.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/hypnosis.dm
new file mode 100644
index 00000000000000..be88ad77cd8345
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/hypnosis.dm
@@ -0,0 +1,66 @@
+/obj/item/disk/nifsoft_uploader/dorms/hypnosis
+ name = "Purpura Eye"
+ loaded_nifsoft = /datum/nifsoft/action_granter/hypnosis
+
+/datum/nifsoft/action_granter/hypnosis
+ name = "Libidine Eye"
+ program_desc = "Based on the hypnotic equipment provided by the LustWish vendor, the Libidine Eye NIFSoft allows the user to ensnare others in a hypnotic trance. ((This is intended as a tool for ERP, don't use this for gameplay reasons.))"
+ buying_category = NIFSOFT_CATEGORY_FUN
+ lewd_nifsoft = TRUE
+ purchase_price = 150
+ able_to_keep = TRUE
+ active_cost = 0.1
+ ui_icon = "eye"
+ action_to_grant = /datum/action/innate/nif_hypnotize
+
+/datum/action/innate/nif_hypnotize
+ name = "Hypnotize"
+ background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
+ background_icon_state = "android"
+ button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
+ button_icon_state = "hypnotize"
+
+/datum/action/innate/nif_hypnotize/Activate()
+ var/mob/living/carbon/human/user = owner
+ if(!istype(user))
+ return FALSE
+
+ var/mob/living/carbon/human/target_human = user.pulling
+ if(!istype(target_human) || user.grab_state < GRAB_AGGRESSIVE)
+ to_chat(user, span_warning("You need to aggressively grab someone to hypnotize them."))
+ return FALSE
+
+ if(!target_human.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
+ to_chat(user, span_warning("[target_human] doesn't want to be hypnotized."))
+ return FALSE
+
+ to_chat(user, span_notice("You begin to place [target_human] into a hypnotic trance."))
+
+ if(!do_after(user, 12 SECONDS, target_human))
+ return FALSE
+
+ var/choice = tgui_alert(target_human, "Do you believe in hypnosis? (This will allow [user] to issue hypnotic suggestions.)", "Hypnosis", list("Yes", "No"))
+ if(choice != "Yes")
+ to_chat(user, span_warning("[target_human]'s attention breaks despite your efforts. They clearly don't seem interested!"))
+ to_chat(target_human, span_warning("Your attention breaks as you realize that you don't want to listen to [user]'s suggestions."))
+ return FALSE
+
+ user.visible_message(span_purple("[target_human] falls into a deep, hypnotic slumber right at the snap of your fingers."), span_purple("You suddenly fall limp at the snap of [user]'s fingers."))
+ user.emote("snap")
+ target_human.SetSleeping(60 SECONDS)
+ target_human.log_message("[target_human] was placed into a hypnotic sleep by [user].", LOG_GAME)
+
+ var/secondary_choice = tgui_alert(user, "Would you like to give [target_human] a hypnotic suggestion or release them?", "Hypnosis", list("Suggestion", "Release"))
+ while(secondary_choice == "Suggestion" && target_human.IsSleeping())
+ if(!in_range(user, target_human))
+ to_chat(user, span_warning("You must be in whisper range to [target_human] in order to give hypnotic suggestions."))
+ target_human.SetSleeping(0)
+ return FALSE
+
+ var/input_text = tgui_input_text(user, "What would you like to suggest?", "Hypnotic Suggestion")
+ to_chat(user, span_purple("You whisper into [target_human]'s ears in a soothing voice."))
+ to_chat(target_human, span_hypnophrase("[input_text]"))
+ secondary_choice = tgui_alert(user, "Would you like to give [target_human] an additional hypnotic suggestion or release them?", "Hypnosis", list("Suggestion", "Release"))
+
+ user.visible_message(span_purple("You wake up from your deep, hypnotic slumber. The suggestions from [user] now settled into your mind."), span_purple("[target_human] wakes up from their slumber."))
+ target_human.SetSleeping(0)
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm
index c7e93412ac2bd4..aab1cc52b7a910 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm
@@ -16,6 +16,7 @@
activation_cost = 100 // Around 1/10th the energy of a standard NIF
buying_category = NIFSOFT_CATEGORY_FUN
ui_icon = "book-open"
+ able_to_keep = TRUE // These NIFSofts are mostly for comsetic/fun reasons anyways.
/// Does the resulting object have a holographic like filter appiled to it?
var/holographic_filter = TRUE
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/scryer.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/scryer.dm
new file mode 100644
index 00000000000000..3cf689624e0951
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/scryer.dm
@@ -0,0 +1,117 @@
+/obj/item/disk/nifsoft_uploader/scryer
+ name = "NIFSoft Scryer Uploader Disk"
+ loaded_nifsoft = /datum/nifsoft/scryer
+
+/datum/nifsoft/scryer
+ name = "NIFLink Holocaller"
+ program_desc = "This ubiquitous NIFSoft adds Scryer functionality similar to MODSuits to the user's NIF; allowing for real-time communication through AR hologlass screens from a hardlight projector sat around the wearer's neck"
+ active_mode = TRUE
+ active_cost = 1
+ activation_cost = 20
+ purchase_price = 200
+ buying_category = NIFSOFT_CATEGORY_UTILITY
+ ui_icon = "video"
+ /// What is the scryer currently associated with the NIFSoft?
+ var/obj/item/clothing/neck/link_scryer/loaded/nifsoft/linked_scryer
+
+/datum/nifsoft/scryer/New()
+ . = ..()
+ var/obj/item/organ/internal/cyberimp/brain/nif/parent_resolved = parent_nif.resolve()
+ if(!istype(parent_resolved))
+ stack_trace("[src] ([REF(src)]) tried to create a linked scryer but it had no parent_nif!")
+ if(!linked_scryer)
+ stack_trace("[src] ([REF(src)]) created with no linked scryer!")
+ linked_scryer = new (parent_resolved)
+ linked_scryer.parent_nifsoft = WEAKREF(src)
+
+/datum/nifsoft/scryer/Destroy()
+ if(!QDELETED(linked_scryer))
+ QDEL_NULL(linked_scryer)
+
+ return ..()
+
+/datum/nifsoft/scryer/activate()
+ . = ..()
+ if(. == FALSE)
+ return FALSE
+
+ if(!active)
+ if(linked_scryer)
+ var/parent_resolved = parent_nif.resolve()
+ if(parent_resolved)
+ return linked_mob.transferItemToLoc(linked_scryer, parent_resolved, TRUE)
+ return FALSE
+
+ if(linked_mob.handcuffed)
+ linked_mob.balloon_alert(linked_mob, "handcuffed")
+ activate()
+ return FALSE
+
+ if(!linked_mob.equip_to_slot_if_possible(linked_scryer, ITEM_SLOT_NECK)) //This sends out a message to the mob if it can't be put on.
+ activate()
+ return FALSE
+
+ return TRUE
+
+/obj/item/clothing/neck/link_scryer
+ /// Do we have custom controls? This is only affects the text shown when examining
+ var/custom_examine_controls = FALSE
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft
+ name = "\improper NIFLink Holocaller"
+ desc = "A nanomachine construct working as a modified version of the MODlink scryer, conjured using a NIF; functionally the same, but able to carry out holocalls in a more portable format."
+ custom_examine_controls = TRUE
+ /// A weakref of the parent NIFSoft that the scryer belongs to.
+ var/datum/weakref/parent_nifsoft
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/Initialize(mapload)
+ . = ..()
+ if(cell)
+ QDEL_NULL(cell)
+
+ cell = new /obj/item/stock_parts/cell/infinite/nif_cell(src)
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/Destroy()
+ if(parent_nifsoft)
+ var/datum/nifsoft/scryer/resolved_nifsoft = parent_nifsoft.resolve()
+ if(!QDELETED(resolved_nifsoft))
+ resolved_nifsoft.linked_scryer = null
+
+ return ..()
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/examine(mob/user)
+ . = ..()
+ . += span_notice("The MODlink ID is [mod_link.id], frequency is [mod_link.frequency || "unset"]. Right-click with a multitool to copy/imprint the frequency.")
+ . += span_notice("Right-click with an empty hand to change the name.")
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot & ITEM_SLOT_NECK)
+ return TRUE
+
+ var/datum/nifsoft/scryer/scryer_nifsoft = parent_nifsoft.resolve()
+ if(!istype(scryer_nifsoft))
+ return FALSE
+
+ scryer_nifsoft.activate() //If it's not on the neck, it shouldn't be active.
+ return TRUE
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/screwdriver_act(mob/living/user, obj/item/tool)
+ balloon_alert(user, "cell non-removable!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/attack_hand_secondary(mob/user, list/modifiers)
+ var/new_label = reject_bad_text(tgui_input_text(user, "Change the visible name", "Set Name", label, MAX_NAME_LEN))
+ if(!new_label)
+ balloon_alert(user, "invalid name!")
+ return
+ label = new_label
+ balloon_alert(user, "name set!")
+ update_name()
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/// This cell is only meant for use in items temporarily created by a NIF. Do not let players extract this from devices.
+/obj/item/stock_parts/cell/infinite/nif_cell
+ name = "Nanite Cell"
+ desc = "If you see this, please make an issue on GitHub."
+
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm
index bbbf7dc2c07b65..dfa70dc78eb7cb 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm
@@ -1,35 +1,15 @@
/obj/item/disk/nifsoft_uploader/shapeshifter
name = "Polymorph"
- loaded_nifsoft = /datum/nifsoft/shapeshifter
+ loaded_nifsoft = /datum/nifsoft/action_granter
-/datum/nifsoft/shapeshifter
+/datum/nifsoft/action_granter/shapeshifter
name = "Polymorph"
program_desc = "This program is a large-scale refitting of the nanomachine channels running over the skin of a NIF user. This allows the nanites to reach under the skin and even into the very bone structure of the host; including incorporation of mimetic materials and femto-level manipulation devices all for the purpose of allowing the user to, essentially, shapeshift on a low level. However, despite the incredible complexity behind these processes, there are still limits on the range of 'forms' a user can take. Mass can neither be created nor destroyed, after all, and you can only distribute and rearrange it in so many ways across a functioning humanoid body; meaning, the user cannot adopt forms too far out of their 'true' one."
- activation_cost = 10
- active_mode = TRUE
- active_cost = 1
compatible_nifs = list(/obj/item/organ/internal/cyberimp/brain/nif/standard)
purchase_price = 350
buying_category = NIFSOFT_CATEGORY_COSMETIC
ui_icon = "paintbrush"
-
- ///The NIF version of the Shapeshifter Ability
- var/datum/action/innate/alter_form/nif/shapeshifter
-
-/datum/nifsoft/shapeshifter/activate()
- . = ..()
- if(active)
- shapeshifter = new
- shapeshifter.Grant(linked_mob)
- return
-
- if(shapeshifter)
- shapeshifter.Remove(linked_mob)
-
-/datum/nifsoft/shapeshifter/Destroy()
- . = ..()
- if(shapeshifter)
- QDEL_NULL(shapeshifter)
+ action_to_grant = /datum/action/innate/alter_form/nif
/// The NIF version of alter form. This lacks the ability to change body color.
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/soul_poem.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/soul_poem.dm
index 4dc4434d59ddd5..093e9e566b73e6 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/soul_poem.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/soul_poem.dm
@@ -166,6 +166,7 @@
var/list/data = list()
data["messages"] = message_list
+ data["theme"] = ui_theme
data["receiving_data"] = receiving_data
data["transmitting_data"] = transmitting_data
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
index 9236a49f963ba5..2ec95a411ef36a 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
@@ -7,6 +7,7 @@
program_desc = "The 'Soulcatcher' coreware is a near-complete upgrade of the nanomachine systems in a NIF, meant for one purpose; supposedly, channeling the dead. This upgrade, in truth, functions as a Resonance Simulation Device; an RSD for short, an instrument capable of hosting someone's consciousness, context or otherwise. 'Resonance', a term for the specific pattern of neural activity that gives way to someone's consciousness, was discovered in the early 2500s by researchers Yun-Seo Jin and Kamakshi Padmanabhan, coining what is now called 'Jin-Padmanabhan Resonance,' or 'JP/Soul Resonance.' This 'Resonance' gives off a sophont's consciousness, their sense of continuation, and their 'I am me.' This Resonance can vary in structure and 'strength' from person to person, and even change over someone's life. When the brain of a sophont undergoes death and stops neural activity, then Resonance dissipates entirely and lingering consciousness becomes essentially an echo, rapidly fading over time.\n\nThe earliest RSDs were massive machines, drawing incredible power and utilizing bleeding-edge, clunky software to 'play' someone's Resonance at 1:1 accuracy with their original brain. However, complications arose that are still being studied. Resonance is replicable and can be re-created artificially; however, like trying to duplicate genetic code, the capture needs to be extremely accurate, and rapidly put into place. Instruments such as RSDs are capable of picking up on lingering consciousness after the end of Resonance, and resuming it through artificial neural activity can give it strength to continue once more. RSDs such as Soulcatchers can only work at such a distance, otherwise running the risk of the Resonance essentially corrupting due to poor signal.\n\nIt is currently impossible to run Resonance in two places at once, because the same Resonance over two places experiences interference; like noise canceling headphones. Slimes and other gestalt consciousnesses can modulate their harmonics to a degree, bearing a partial disconnect and bringing themselves into constructive interference with similar harmonic signatures. A deepscan of the person's brain is necessary to give their consciousness 'context;' running their Resonance and capturing their consciousness alone results in a person with their same original intelligence, but zero memories or identity. These scans rapidly become outdated due to the growth of the brain, and it is prohibitively complex to store them in their entirety.\n\nThe first portable RSD, or Soulcatcher, was developed by the Spider Clan. These were initially designed for the captive interrogation of a person's consciousness without having to worry about the struggling of their body, and for dead or aging members of the mysterious group of orbital shinobi to be able to guide field operatives. These Soulcatchers are the main instrument to play Resonance, but recent advances in medical science have been leading to more. Occasionally, it is known for unusual sources of 'wild' Resonance, called Phantoms, to end up inside of the nearest Soulcatcher, a key finding its own lock; with a wide array of theories as to how these come into existence. Much as how some people intentionally become stable Engrams to achieve digital immortality, such as the witches of the Altspace Coven, it is possible for others to forcibly enter a Soulcatcher and act as a sort of Phantom by hacking their way in."
purchase_price = 150 //RP tool
persistence = TRUE
+ able_to_keep = TRUE
ui_icon = "ghost"
/// What is the linked soulcatcher datum used by this NIFSoft?
@@ -93,6 +94,17 @@
persistence.nif_soulcatcher_rooms = list2params(room_list)
return TRUE
+/datum/nifsoft/soulcatcher/update_theme()
+ . = ..()
+ if(!.)
+ return FALSE // uhoh
+
+ var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
+ if(!istype(current_soulcatcher))
+ stack_trace("[src] ([REF(src)]) tried to update its theme when it was missing a linked_soulcatcher component!")
+ return FALSE
+ current_soulcatcher.ui_theme = ui_theme
+
/datum/modular_persistence
///A param string containing soulcatcher rooms
var/nif_soulcatcher_rooms = ""
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
new file mode 100644
index 00000000000000..cd84179e083898
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
@@ -0,0 +1,111 @@
+/datum/component/soulcatcher/small_device
+ max_souls = 1
+
+/datum/component/soulcatcher/attachable_soulcatcher
+ max_souls = 1
+ communicate_as_parent = TRUE
+ removable = TRUE
+
+/datum/component/soulcatcher/attachable_soulcatcher/New()
+ . = ..()
+ var/obj/item/parent_item = parent
+ if(!istype(parent_item))
+ return COMPONENT_INCOMPATIBLE
+
+ name = parent_item.name
+ var/datum/soulcatcher_room/first_room = soulcatcher_rooms[1]
+ first_room.name = parent_item.name
+ first_room.room_description = parent_item.desc
+
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT, PROC_REF(bring_up_ui))
+ RegisterSignal(parent, COMSIG_PREQDELETED, PROC_REF(remove_self))
+
+/// Adds text to the examine text of the parent item, explaining that the item can be used to enable the use of NIFSoft HUDs
+/datum/component/soulcatcher/attachable_soulcatcher/proc/on_examine(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ examine_text += span_cyan("[source] has a soulcatcher attached to it, Ctrl+Shift+Click to use it.")
+
+/datum/component/soulcatcher/attachable_soulcatcher/proc/bring_up_ui(datum/source, mob/user)
+ SIGNAL_HANDLER
+ INVOKE_ASYNC(src, PROC_REF(ui_interact), user)
+
+/datum/component/soulcatcher/attachable_soulcatcher/Destroy(force)
+ UnregisterSignal(parent, COMSIG_ATOM_EXAMINE)
+ UnregisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT)
+ UnregisterSignal(parent, COMSIG_PREQDELETED)
+ return ..()
+
+/datum/component/soulcatcher/attachable_soulcatcher/remove_self()
+ var/obj/item/parent_item = parent
+ var/turf/drop_turf = get_turf(parent_item)
+ var/obj/item/attachable_soulcatcher/dropped_item = new (drop_turf)
+
+ var/datum/component/soulcatcher/dropped_soulcatcher = dropped_item.GetComponent(/datum/component/soulcatcher)
+ var/datum/soulcatcher_room/target_room = dropped_soulcatcher.soulcatcher_rooms[1]
+ var/list/current_souls = get_current_souls()
+
+ if(current_souls) // If we have souls inside of here, they should be transferred to the new object
+ for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
+ var/datum/soulcatcher_room/current_room = soul.current_room.resolve()
+ if(istype(current_room))
+ current_room.transfer_soul(soul, target_room)
+
+ return ..()
+
+/obj/item/attachable_soulcatcher
+ name = "Poltergeist-Type RSD"
+ desc = "This device, a polymorphic nanomachine net, wraps around objects of most sizes and allows them to function as a container for Resonance. The soul in question within the vessel is imbued much like it would be in a body or a normal Soulcatcher, perceiving the world and even speaking out of their new form. The nanomachine net of the device allows for the consciousness to somewhat manipulate their container, but any large-scale movement is out of the question."
+ icon = 'modular_skyrat/modules/modular_implants/icons/obj/devices.dmi'
+ icon_state = "attachable-soulcatcher"
+ w_class = WEIGHT_CLASS_SMALL
+ /// Do we want to destory the item once it is attached to an item?
+ var/destroy_on_use = TRUE
+ /// What items do we want to prevent the viewer from attaching this to?
+ var/list/blacklisted_items = list(
+ /obj/item/organ,
+ /obj/item/mmi,
+ /obj/item/pai_card,
+ /obj/item/aicard,
+ /obj/item/card,
+ /obj/item/radio,
+ /obj/item/disk/nuclear, // Woah there
+ )
+ /// What soulcathcer component is currnetly linked to this object?
+ var/datum/component/soulcatcher/small_device/linked_soulcatcher
+
+/obj/item/attachable_soulcatcher/Initialize(mapload)
+ . = ..()
+ linked_soulcatcher = AddComponent(/datum/component/soulcatcher/small_device)
+ linked_soulcatcher.name = name
+
+/obj/item/attachable_soulcatcher/attack_self(mob/user, modifiers)
+ linked_soulcatcher.ui_interact(user)
+
+/obj/item/attachable_soulcatcher/afterattack(obj/item/target_item, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!proximity_flag || !istype(target_item))
+ return FALSE
+
+ if(target_item.GetComponent(/datum/component/soulcatcher))
+ balloon_alert(user, "already attached!")
+ return FALSE
+
+ if(is_type_in_list(target_item, blacklisted_items))
+ balloon_alert(user, "incompatible!")
+ return FALSE
+
+ var/datum/component/soulcatcher/new_soulcatcher = target_item.AddComponent(/datum/component/soulcatcher/attachable_soulcatcher)
+ playsound(target_item.loc, 'sound/weapons/circsawhit.ogg', 50, vary = TRUE)
+
+ var/datum/soulcatcher_room/target_room = new_soulcatcher.soulcatcher_rooms[1]
+ var/list/current_souls = linked_soulcatcher.get_current_souls()
+ if(current_souls)
+ for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
+ var/datum/soulcatcher_room/current_room = soul.current_room.resolve()
+ if(istype(current_room))
+ current_room.transfer_soul(soul, target_room)
+ current_room.transfer_soul(soul, target_room)
+
+ if(destroy_on_use)
+ qdel(src)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
index 842aee82d59868..767f396057e6f0 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
@@ -111,4 +111,52 @@
return TRUE
+/obj/item/handheld_soulcatcher/attack_secondary(mob/living/carbon/human/target_mob, mob/living/user, params)
+ if(!istype(target_mob))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/obj/item/organ/internal/brain/target_brain = target_mob.get_organ_slot(ORGAN_SLOT_BRAIN)
+ if(!istype(target_brain))
+ to_chat(user, span_warning("[target_mob] lacks a brain!"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(!HAS_TRAIT(target_brain, TRAIT_RSD_COMPATIBLE))
+ to_chat(user, span_warning("[target_mob]'s brain isn't compatible."))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(target_mob.mind || target_mob.ckey || GetComponent(/datum/component/previous_body))
+ to_chat(user, span_warning("[target_mob] is not able to receive a soul"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/list/soul_list = list()
+ for(var/datum/soulcatcher_room/room as anything in linked_soulcatcher.soulcatcher_rooms)
+ for(var/mob/living/soulcatcher_soul/soul as anything in room.current_souls)
+ if(!soul.round_participant || soul.body_scan_needed)
+ continue
+
+ soul_list += soul
+
+ if(!length(soul_list))
+ to_chat(user, span_warning("There are no souls that can be transferred to [target_mob]."))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/mob/living/soulcatcher_soul/chosen_soul = tgui_input_list(user, "Choose a soul to transfer into the body", name, soul_list)
+ if(!chosen_soul)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(chosen_soul.previous_body)
+ var/mob/living/old_body = chosen_soul.previous_body.resolve()
+ if(!old_body)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ SEND_SIGNAL(old_body, COMSIG_SOULCATCHER_CHECK_SOUL, FALSE)
+
+ chosen_soul.mind.transfer_to(target_mob, TRUE)
+ playsound(src, 'modular_skyrat/modules/modular_implants/sounds/default_good.ogg', 50, FALSE, ignore_walls = FALSE)
+ visible_message(span_notice("[src] beeps: Body transfer complete."))
+ log_admin("[src] was used by [user] to transfer [chosen_soul]'s soulcatcher soul to [target_mob].")
+
+ qdel(chosen_soul)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
#undef RSD_ATTEMPT_COOLDOWN
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
index 97158ba5d9132f..88cca7817a2fe4 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
@@ -18,11 +18,19 @@ GLOBAL_LIST_EMPTY(soulcatchers)
var/list/soulcatcher_rooms = list()
/// What soulcatcher room are verbs sending messages to?
var/datum/soulcatcher_room/targeted_soulcatcher_room
+ /// What theme are we using for our soulcatcher UI?
+ var/ui_theme = "default"
/// Are ghosts currently able to join this soulcatcher?
var/ghost_joinable = TRUE
/// Do we want to ask the user permission before the ghost joins?
var/require_approval = TRUE
+ /// What is the max number of people we can keep in this soulcatcher? If this is set to `FALSE` we don't have a limit
+ var/max_souls = FALSE
+ /// Are are the souls inside able to emote/speak as the parent?
+ var/communicate_as_parent = FALSE
+ /// Is the soulcatcher removable from the parent object?
+ var/removable = FALSE
/datum/component/soulcatcher/New()
. = ..()
@@ -136,6 +144,32 @@ GLOBAL_LIST_EMPTY(soulcatchers)
return TRUE
+/// Returns a list containing all of the souls currently present within a soulcatcher.
+/datum/component/soulcatcher/proc/get_current_souls()
+ var/list/current_souls = list()
+ for(var/datum/soulcatcher_room/room as anything in soulcatcher_rooms)
+ for(var/mob/living/soulcatcher_soul as anything in room.current_souls)
+ current_souls += soulcatcher_soul
+
+ return current_souls
+
+/// Checks the total number of souls present and compares it with `max_souls` returns `TRUE` if there is room (or no limit), otherwise returns `FALSE`
+/datum/component/soulcatcher/proc/check_for_vacancy()
+ if(!max_souls)
+ return TRUE
+
+ if(length(get_current_souls()) >= max_souls)
+ return FALSE
+
+ return TRUE
+
+/// Attempts to remove the soulcatcher from the attached object
+/datum/component/soulcatcher/proc/remove_self()
+ if(!removable)
+ return FALSE
+
+ qdel(src)
+
/**
* Soulcatcher Room
*
@@ -266,10 +300,6 @@ GLOBAL_LIST_EMPTY(soulcatchers)
if(!message_to_send) //Why say nothing?
return FALSE
- var/sender_name = ""
- if(message_sender)
- sender_name = "[message_sender] "
-
var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
var/tag = sheet.icon_tag("nif-soulcatcher")
var/soulcatcher_icon = ""
@@ -277,6 +307,33 @@ GLOBAL_LIST_EMPTY(soulcatchers)
if(tag)
soulcatcher_icon = tag
+ var/mob/living/soulcatcher_soul/soul_sender = message_sender
+ if(istype(soul_sender) && soul_sender.communicating_externally)
+ var/master_resolved = master_soulcatcher.resolve()
+ if(!master_resolved)
+ return FALSE
+ var/datum/component/soulcatcher/parent_soulcatcher = master_resolved
+ var/obj/item/parent_object = parent_soulcatcher.parent
+ if(!istype(parent_object))
+ return FALSE
+
+ var/temp_name = parent_object.name
+ parent_object.name = "[parent_object.name] [soulcatcher_icon]"
+
+ if(emote)
+ parent_object.manual_emote(html_decode(message_to_send))
+ log_emote("[soul_sender] in [name] soulcatcher room emoted: [message_to_send], as an external object")
+ else
+ parent_object.say(html_decode(message_to_send))
+ log_say("[soul_sender] in [name] soulcatcher room said: [message_to_send], as an external object")
+
+ parent_object.name = temp_name
+ return TRUE
+
+ var/sender_name = ""
+ if(message_sender)
+ sender_name = "[message_sender] "
+
var/first_room_name_word = splittext(name, " ")
var/message = ""
var/owner_message = ""
@@ -334,7 +391,7 @@ GLOBAL_LIST_EMPTY(soulcatchers)
var/list/joinable_soulcatchers = list()
for(var/datum/component/soulcatcher/soulcatcher in GLOB.soulcatchers)
- if(!soulcatcher.ghost_joinable || !isobj(soulcatcher.parent))
+ if(!soulcatcher.ghost_joinable || !isobj(soulcatcher.parent) || !soulcatcher.check_for_vacancy())
continue
var/obj/item/soulcatcher_parent = soulcatcher.parent
@@ -360,6 +417,7 @@ GLOBAL_LIST_EMPTY(soulcatchers)
var/datum/soulcatcher_room/room_to_join
if(length(rooms_to_join) < 1)
+ to_chat(src, span_warning("There no rooms that you can join."))
return FALSE
if(length(rooms_to_join) == 1)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
index fefaf69c25e730..8ae004b9667706 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
@@ -23,6 +23,12 @@
var/able_to_speak = TRUE
/// Is the soul able to change their own name?
var/able_to_rename = TRUE
+ /// Is the soul able to speak as the object it is inside?
+ var/able_to_speak_as_container = TRUE
+ /// Is the soul able to emote as the object it is inside?
+ var/able_to_emote_as_container = TRUE
+ /// Are emote's and Say's done through the container the mob is in?
+ var/communicating_externally = FALSE
/// Is the soul able to leave the soulcatcher?
var/able_to_leave = TRUE
@@ -110,7 +116,7 @@
if(!message || message == "")
return
- if(!able_to_speak)
+ if((!able_to_speak && !communicating_externally) || (!able_to_speak_as_container && communicating_externally))
to_chat(src, span_warning("You are unable to speak!"))
return FALSE
@@ -126,7 +132,7 @@
if(!message)
return FALSE
- if(!able_to_emote)
+ if((!able_to_emote && !communicating_externally) || (!able_to_emote_as_container && communicating_externally))
to_chat(src, span_warning("You are unable to emote!"))
return FALSE
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
index b59cee1f7ad316..5dbd31631ec311 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
@@ -13,6 +13,11 @@
data["ghost_joinable"] = ghost_joinable
data["require_approval"] = require_approval
+ data["theme"] = ui_theme
+ data["communicate_as_parent"] = communicate_as_parent
+ data["current_soul_count"] = length(get_current_souls())
+ data["max_souls"] = max_souls
+ data["removable"] = removable
data["current_rooms"] = list()
for(var/datum/soulcatcher_room/room in soulcatcher_rooms)
@@ -41,6 +46,8 @@
"able_to_rename" = soul.able_to_rename,
"ooc_notes" = soul.ooc_notes,
"scan_needed" = soul.body_scan_needed,
+ "able_to_speak_as_container" = soul.able_to_speak_as_container,
+ "able_to_emote_as_container" = soul.able_to_emote_as_container,
)
room_data["souls"] += list(soul_list)
@@ -147,7 +154,7 @@
continue
var/datum/component/soulcatcher/soulcatcher_component = held_item.GetComponent(/datum/component/soulcatcher)
- if(!soulcatcher_component)
+ if(!soulcatcher_component || !soulcatcher_component.check_for_vacancy())
continue
for(var/datum/soulcatcher_room/room in soulcatcher_component.soulcatcher_rooms)
@@ -191,6 +198,14 @@
return TRUE
+ if("toggle_soul_external_communication")
+ if(params["communication_type"] == "emote")
+ target_soul.able_to_emote_as_container = !target_soul.able_to_emote_as_container
+ else
+ target_soul.able_to_speak_as_container = !target_soul.able_to_speak_as_container
+
+ return TRUE
+
if("toggle_soul_renaming")
target_soul.able_to_rename = !target_soul.able_to_rename
return TRUE
@@ -224,6 +239,13 @@
target_room.send_message(message_to_send, message_sender, emote)
return TRUE
+ if("delete_self")
+ if(tgui_alert(usr, "Are you sure you want to detach the soulcatcher?", parent, list("Yes", "No")) != "Yes")
+ return FALSE
+
+ remove_self()
+ return TRUE
+
/datum/component/soulcatcher_user/New()
. = ..()
var/mob/living/soulcatcher_soul/parent_soul = parent
@@ -259,6 +281,9 @@
"able_to_emote" = user_soul.able_to_emote,
"able_to_speak" = user_soul.able_to_speak,
"able_to_rename" = user_soul.able_to_rename,
+ "able_to_speak_as_container" = user_soul.able_to_speak_as_container,
+ "able_to_emote_as_container" = user_soul.able_to_emote_as_container,
+ "communicating_externally" = user_soul.communicating_externally,
"ooc_notes" = user_soul.ooc_notes,
"scan_needed" = user_soul.body_scan_needed,
)
@@ -272,6 +297,9 @@
"owner" = current_room.outside_voice,
)
+ var/datum/component/soulcatcher/master_soulcatcher = current_room.master_soulcatcher.resolve()
+ data["communicate_as_parent"] = master_soulcatcher.communicate_as_parent
+
for(var/mob/living/soulcatcher_soul/soul in current_room.current_souls)
if(soul == user_soul)
continue
@@ -310,3 +338,6 @@
user_soul.reset_name()
+ if("toggle_external_communication")
+ user_soul.communicating_externally = !user_soul.communicating_externally
+ return TRUE
diff --git a/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi b/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi
index d0a3f736fa60a8..401441f95de1f5 100644
Binary files a/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi and b/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi differ
diff --git a/modular_skyrat/modules/modular_items/code/bags.dm b/modular_skyrat/modules/modular_items/code/bags.dm
index 3e5e779268f7b1..f49954277e0b7d 100644
--- a/modular_skyrat/modules/modular_items/code/bags.dm
+++ b/modular_skyrat/modules/modular_items/code/bags.dm
@@ -4,6 +4,7 @@
desc = "It's a nondescript pouch made with dark fabric. It has a clip, for fitting in pockets."
icon = 'modular_skyrat/modules/modular_items/icons/storage.dmi'
icon_state = "survival"
+ w_class = WEIGHT_CLASS_NORMAL
resistance_flags = FLAMMABLE
slot_flags = ITEM_SLOT_POCKETS
@@ -19,14 +20,34 @@
icon_state = "ammopouch"
w_class = WEIGHT_CLASS_BULKY
custom_price = PAYCHECK_CREW * 4
+ // this is just to have post_reskin called later
+ uses_advanced_reskins = TRUE
+ unique_reskin = list(
+ "Ammo Pouch" = list(
+ RESKIN_ICON_STATE = "ammopouch"
+ ),
+ "Casing Pouch" = list(
+ RESKIN_ICON_STATE = "casingpouch"
+ ),
+ )
/obj/item/storage/pouch/ammo/Initialize(mapload)
. = ..()
atom_storage.max_specific_storage = WEIGHT_CLASS_NORMAL
- atom_storage.max_total_storage = 30
+ atom_storage.max_total_storage = 12
atom_storage.max_slots = 3
atom_storage.numerical_stacking = FALSE
- atom_storage.can_hold = typecacheof(list(/obj/item/ammo_box/magazine, /obj/item/ammo_casing, /obj/item/ammo_box/revolver, /obj/item/stock_parts/cell/microfusion))
+ atom_storage.can_hold = typecacheof(list(/obj/item/ammo_box/magazine, /obj/item/ammo_casing, /obj/item/stock_parts/cell/microfusion))
+
+/obj/item/storage/pouch/ammo/post_reskin(mob/our_mob)
+ if(icon_state == "casingpouch")
+ name = "casing pouch"
+ desc = "A pouch for your ammo that goes in your pocket, carefully segmented for holding shell casings and nothing else."
+ atom_storage.can_hold = typecacheof(list(/obj/item/ammo_casing))
+ atom_storage.max_specific_storage = WEIGHT_CLASS_TINY
+ atom_storage.numerical_stacking = TRUE
+ atom_storage.max_slots = 10
+ atom_storage.max_total_storage = WEIGHT_CLASS_TINY * 10
/obj/item/storage/pouch/material
name = "material pouch"
@@ -47,8 +68,7 @@
/// It's a pocket medkit. Use sparingly?
/obj/item/storage/pouch/medical
name = "medkit pouch"
- desc = "A standard medkit pouch compartmentalized for the bare essentials of field medical care, made with fireproof kevlar \
- (but hopefully you never have to test that). Cannot, itself, contain a medkit. Comes with a pocket clip."
+ desc = "A standard medkit pouch compartmentalized for field medical care. Comes with a set of pocket clips."
resistance_flags = FIRE_PROOF
icon_state = "medkit"
/// The list of things that medical pouches can hold. Stolen from what medkits can hold, but modified for things you would probably want at pocket-access.
@@ -65,6 +85,7 @@
/obj/item/reagent_containers/spray,
/obj/item/reagent_containers/hypospray,
/obj/item/storage/pill_bottle,
+ /obj/item/storage/box/bandages,
/obj/item/stack/medical,
/obj/item/flashlight/pen,
/obj/item/bonesetter,
@@ -96,16 +117,23 @@
/// It's... not as egregious as a full pocket medkit.
/obj/item/storage/pouch/medical/firstaid
name = "first aid pouch"
- desc = "A standard nondescript first-aid pouch, compartmentalized for the bare essentials of field medical care. Made with fireproof kevlar \
- (but hopefully you never have to test that). Slightly smaller than a full-on medkit, \
- but has better weight distribution, making it more comfortable to wear. Comes with a pocket-clip."
+ desc = "A standard nondescript first-aid pouch, compartmentalized for the bare essentials of field medical care. Comes with a pocket clip."
icon_state = "firstaid"
/obj/item/storage/pouch/medical/firstaid/Initialize(mapload)
. = ..()
atom_storage.max_specific_storage = WEIGHT_CLASS_SMALL
- atom_storage.max_slots = 4
- atom_storage.max_total_storage = 8
+ /*
+ hi. you might think this is egregious. five slots? that's a lot!
+ here's a thought: the pocket first aid kit from the colonial replicator [modular_skyrat\modules\food_replicator\code\storage.dm] has
+ mostly unrestricted storage, limited by having 4 max total storage, so at best you're only fitting 4 tiny items. but that's 4 of *any* tiny item.
+ or 2 small items (that aren't guns/mags). so it's basically just turning 1 pocket slot into 2, if you think about it hard enough.
+ this is a thing you have to buy from cargo's goodies tab. not even an import. and it only fits medical supplies.
+ i think it can have a lil extra storage as a treat.
+ */
+ atom_storage.max_slots = 5
+ atom_storage.max_total_storage = 10
+ atom_storage.numerical_stacking = TRUE
/obj/item/storage/pouch/medical/firstaid/loaded/Initialize(mapload)
. = ..()
@@ -113,6 +141,7 @@
var/static/items_inside = list(
/obj/item/stack/medical/suture = 1,
/obj/item/stack/medical/mesh = 1,
+ /obj/item/storage/box/bandages = 1,
/obj/item/stack/medical/gauze/twelve = 1,
/obj/item/reagent_containers/hypospray/medipen/ekit = 1,
)
@@ -125,6 +154,6 @@
/obj/item/cautery = 1,
/obj/item/bonesetter = 1,
/obj/item/stack/medical/gauze/twelve = 1,
- /obj/item/reagent_containers/hypospray/medipen/ekit = 1,
+ /obj/item/reagent_containers/hypospray/medipen/ekit = 2,
)
generate_items_inside(items_inside, src)
diff --git a/modular_skyrat/modules/modular_items/code/tailoring.dm b/modular_skyrat/modules/modular_items/code/tailoring.dm
index 22bce1ce3df7f4..8d7cf31f58fbbd 100644
--- a/modular_skyrat/modules/modular_items/code/tailoring.dm
+++ b/modular_skyrat/modules/modular_items/code/tailoring.dm
@@ -6,6 +6,14 @@
reqs = list(/obj/item/clothing/glasses/blindfold = 1)
category = CAT_CLOTHING
+/datum/crafting_recipe/paper_mask
+ name = "Paper Mask"
+ result = /obj/item/clothing/mask/paper
+ time = 30
+ tool_behaviors = list(TOOL_WIRECUTTER)
+ reqs = list(/obj/item/paper = 5)
+ category = CAT_CLOTHING
+
/datum/crafting_recipe/crusader_belt
name = "Crusader Belt and Sheath"
result = /obj/item/storage/belt/crusader
@@ -13,7 +21,7 @@
tool_behaviors = list(TOOL_WIRECUTTER, TOOL_SCREWDRIVER, TOOL_WELDER) //To cut the leather and fasten/weld the sheath detailing
time = 30
category = CAT_CLOTHING
-
+
/datum/crafting_recipe/crusader_belt/on_craft_completion(mob/user, atom/result)
var/obj/item/storage/belt/crusader/crusader_belt = result
crusader_belt.PopulateContents()
@@ -54,7 +62,7 @@
reqs = list(/obj/item/clothing/glasses/hud/eyepatch/med = 1)
tool_behaviors = list(TOOL_SCREWDRIVER, TOOL_WIRECUTTER)
category = CAT_CLOTHING
-
+
/datum/crafting_recipe/mesonpatch
name = "Meson Eyepatch HUD"
result = /obj/item/clothing/glasses/hud/eyepatch/meson
diff --git a/modular_skyrat/modules/modular_items/icons/storage.dmi b/modular_skyrat/modules/modular_items/icons/storage.dmi
index 8da1deba6dc97c..6587afcfc67ef6 100644
Binary files a/modular_skyrat/modules/modular_items/icons/storage.dmi and b/modular_skyrat/modules/modular_items/icons/storage.dmi differ
diff --git a/modular_skyrat/modules/modular_vending/code/clothesmate.dm b/modular_skyrat/modules/modular_vending/code/clothesmate.dm
index 73dc53a6465a42..19dd353f1fe819 100644
--- a/modular_skyrat/modules/modular_vending/code/clothesmate.dm
+++ b/modular_skyrat/modules/modular_vending/code/clothesmate.dm
@@ -36,6 +36,7 @@
/obj/item/clothing/glasses/betterunshit = 5,
/obj/item/clothing/glasses/thin = 5,
/obj/item/clothing/glasses/hud/ar/projector = 5,
+ /obj/item/clothing/gloves/bracer/wraps,
),
),
@@ -125,6 +126,7 @@
/obj/item/clothing/shoes/colorable_sandals = 5,
/obj/item/clothing/shoes/sports = 5,
/obj/item/clothing/shoes/wraps/colourable = 5,
+ /obj/item/clothing/shoes/wraps/cloth = 5,
/obj/item/clothing/shoes/jungleboots = 5,
/obj/item/clothing/shoes/jackboots/knee = 5,
/obj/item/clothing/shoes/jackboots/recolorable = 5,
diff --git a/modular_skyrat/modules/modular_vending/code/games.dm b/modular_skyrat/modules/modular_vending/code/games.dm
index cbc169a99ff737..99edcdb796c9cf 100644
--- a/modular_skyrat/modules/modular_vending/code/games.dm
+++ b/modular_skyrat/modules/modular_vending/code/games.dm
@@ -13,6 +13,7 @@
"products" = list(
/obj/item/hairbrush = 3,
/obj/item/clothing/mask/holocigarette = 5,
+ /obj/item/attachable_soulcatcher = 5,
),
)
)
diff --git a/modular_skyrat/modules/modular_vending/code/wardrobes.dm b/modular_skyrat/modules/modular_vending/code/wardrobes.dm
index e5f3e0210e7c9d..103bb2ca9754e0 100644
--- a/modular_skyrat/modules/modular_vending/code/wardrobes.dm
+++ b/modular_skyrat/modules/modular_vending/code/wardrobes.dm
@@ -61,9 +61,12 @@
/obj/item/clothing/mask/breath = 2,
/obj/item/reagent_containers/cup/bottle/morphine = 2,
/obj/item/reagent_containers/syringe = 2,
+ /obj/item/reagent_containers/spray/hercuri/chilled = 2,
+ /obj/item/clothing/gloves/color/black = 2, // fire resistant, allows the robo to painlessly mold metal. also its down here because its a treatment item
/obj/item/bonesetter = 2, // for dislocations
/obj/item/stack/medical/gauze = 4, // for ALL wounds
/obj/item/healthanalyzer/no_medibot = 2, // disallows medibot use so its not wasted immediately on medibots
+ /obj/item/healthanalyzer/simple = 2,
/obj/item/storage/backpack/science/robo = 2,
/obj/item/storage/backpack/satchel/science/robo = 2,
/obj/item/storage/backpack/duffelbag/science/robo = 2,
diff --git a/modular_skyrat/modules/modular_weapons/code/autolathe_designs.dm b/modular_skyrat/modules/modular_weapons/code/autolathe_designs.dm
index f51d6883d64318..89695235d2b8e1 100644
--- a/modular_skyrat/modules/modular_weapons/code/autolathe_designs.dm
+++ b/modular_skyrat/modules/modular_weapons/code/autolathe_designs.dm
@@ -46,16 +46,6 @@
build_path = /obj/item/ammo_casing/c45/rubber
category = list(RND_CATEGORY_INITIAL, RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_AMMO)
-// .460 Rowland magnum, for the M45A5
-
-/datum/design/b460
- name = ".460 Rowland magnum"
- id = "b460"
- build_type = AUTOLATHE
- materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 8)
- build_path = /obj/item/ammo_casing/b460
- category = list(RND_CATEGORY_HACKED, RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_AMMO)
-
// 10mm
/datum/design/c10mm_lethal
name = "10mm Bullet"
diff --git a/modular_skyrat/modules/modular_weapons/code/automatic.dm b/modular_skyrat/modules/modular_weapons/code/automatic.dm
deleted file mode 100644
index e9ccae147c91b7..00000000000000
--- a/modular_skyrat/modules/modular_weapons/code/automatic.dm
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
-* MAGAZINES
-*/
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat
- name = "CFA Wildcat magazine (.34)"
- desc = "Magazines taking .34 ammunition; it fits in the CFA Wildcat. Alt+click to reskin it."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/ammo.dmi'
- icon_state = "smg34"
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_AP, AMMO_TYPE_RUBBER, AMMO_TYPE_INCENDIARY)
- ammo_type = /obj/item/ammo_casing/c34
- caliber = "c34acp"
- max_ammo = 30
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/ap
- ammo_type = /obj/item/ammo_casing/c34/ap
- round_type = AMMO_TYPE_AP
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/rubber
- ammo_type = /obj/item/ammo_casing/c34/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/incendiary
- ammo_type = /obj/item/ammo_casing/c34_incendiary
- round_type = AMMO_TYPE_INCENDIARY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/empty
- start_empty = 1
-
-/*
-* WILDCAT
-* 3rnd burst .32 calibre, 15 damage.
-* Fills the role of a low damage, high magazine capacity magdump gun.
-*/
-
-/obj/item/gun/ballistic/automatic/cfa_wildcat
- name = "\improper CFA Wildcat"
- desc = "A robust roller-delayed SMG chambered for .34 ammunition."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile40x32.dmi'
- icon_state = "mp5"
- inhand_icon_state = "arg"
- selector_switch_icon = TRUE
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat
- can_suppress = FALSE
- fire_delay = 1.25
- spread = 5
- mag_display = TRUE
- empty_indicator = FALSE
- fire_sound = 'sound/weapons/gun/smg/shot_alt.ogg'
- weapon_weight = WEAPON_MEDIUM
- w_class = WEIGHT_CLASS_BULKY
-
-/obj/item/gun/ballistic/automatic/cfa_wildcat/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/cfa_wildcat/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/cfa_wildcat/no_mag
- spawnwithmagazine = FALSE
-
-/*
-* CFA LYNX
-*/
-
-/obj/item/gun/ballistic/automatic/cfa_lynx
- name = "\improper CFA Lynx"
- desc = "A carbine with a high magazine capacity. Chambered in 4.2x30mm."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi'
- icon_state = "cfa-lynx"
- inhand_icon_state = "arg"
- selector_switch_icon = FALSE
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx
- can_suppress = FALSE
- fire_delay = 1.90 //Previously 0.5. Changed due to it being the Blueshield's default firearm.
- spread = 2
- mag_display = TRUE
- empty_indicator = FALSE
- fire_sound = 'sound/weapons/gun/smg/shot_alt.ogg'
- weapon_weight = WEAPON_MEDIUM
- w_class = WEIGHT_CLASS_BULKY
-
-/obj/item/gun/ballistic/automatic/cfa_lynx/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/cfa_lynx/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/cfa_lynx/no_mag
- spawnwithmagazine = FALSE
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_lynx
- name = "CFA Lynx Magazine (4.2x30mm)"
- desc = "A magazine for the CFA Lynx. It has a small inscription on the base, '4.2x30mm'. Alt+click to reskin it."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/ammo.dmi'
- icon_state = "lynx"
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_AP, AMMO_TYPE_RUBBER, AMMO_TYPE_INCENDIARY)
- ammo_type = /obj/item/ammo_casing/c42x30mm
- caliber = CALIBER_42X30MM
- max_ammo = 40
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_lynx/ap
- ammo_type = /obj/item/ammo_casing/c42x30mm/ap
- round_type = AMMO_TYPE_AP
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_lynx/rubber
- ammo_type = /obj/item/ammo_casing/c42x30mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_lynx/incendiary
- ammo_type = /obj/item/ammo_casing/c42x30mm/inc
- round_type = AMMO_TYPE_INCENDIARY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_lynx/empty
- start_empty = TRUE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/advert.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/advert.dm
new file mode 100644
index 00000000000000..3f0dedf288903c
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/advert.dm
@@ -0,0 +1,36 @@
+/obj/structure/sign/poster/official/carwo_grenade
+ name = "Tydhouer - Precision Timing"
+ desc = "This poster depicts, alongside the prominent logo of Carwo Defense Systems, a variety of specialist .980 Tydhouer grenades for the Kiboko launcher."
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/propaganda.dmi'
+ icon_state = "grenadier"
+
+/obj/structure/sign/poster/official/carwo_grenade/examine_more(mob/user)
+ . = ..()
+
+ . += "Small text details that certain types of grenades may not be available in your \
+ region depending on local weapons regulations. Suspiciously, however, if you squint at \
+ it a bit, the background colors of the image come together vaguely in the shape of \
+ a computer board and a multitool. What did they mean by this?"
+
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/carwo_grenade, 32)
+
+/obj/structure/sign/poster/official/carwo_magazine
+ name = "Standardisation - Magazines of the Future"
+ desc = "This poster depicts, alongside the prominent logo of Carwo Defense Systems, the variety of magazine types the company has on offer for rifles. \
+ It also goes into great deal to say, more or less, that any rifle can take any rifle magazine. Now this is technology like never seen before."
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/propaganda.dmi'
+ icon_state = "mag_size"
+
+/obj/structure/sign/poster/official/carwo_magazine/examine_more(mob/user)
+ . = ..()
+
+ . += "Small text details that certain types of magazines may not be available in your \
+ region depending on local weapons regulations. Suspiciously, however, if you squint at \
+ it a bit, the background colors of the image come together vaguely in the shape of \
+ a computer board and a multitool. What did they mean by this?"
+
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/carwo_magazine, 32)
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/grenade.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/grenade.dm
new file mode 100644
index 00000000000000..37c9fc0f83598e
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/grenade.dm
@@ -0,0 +1,260 @@
+#define AMMO_MATS_GRENADE list( \
+ /datum/material/iron = SMALL_MATERIAL_AMOUNT * 4, \
+)
+
+#define AMMO_MATS_GRENADE_SHRAPNEL list( \
+ /datum/material/iron = SMALL_MATERIAL_AMOUNT * 2,\
+ /datum/material/titanium = SMALL_MATERIAL_AMOUNT * 2, \
+)
+
+#define AMMO_MATS_GRENADE_INCENDIARY list( \
+ /datum/material/iron = SMALL_MATERIAL_AMOUNT * 2,\
+ /datum/material/plasma = SMALL_MATERIAL_AMOUNT * 2, \
+)
+
+#define GRENADE_SMOKE_RANGE 0.75
+
+// .980 grenades
+// Grenades that can be given a range to detonate at by their firing gun
+
+/obj/item/ammo_casing/c980grenade
+ name = ".980 Tydhouer practice grenade"
+ desc = "A large grenade shell that will detonate at a range given to it by the gun that fires it. Practice shells disintegrate into harmless sparks."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "980_solid"
+
+ caliber = CALIBER_980TYDHOUER
+ projectile_type = /obj/projectile/bullet/c980grenade
+
+ custom_materials = AMMO_MATS_GRENADE
+
+ harmful = FALSE //Erm, technically
+
+
+/obj/item/ammo_casing/c980grenade/fire_casing(atom/target, mob/living/user, params, distro, quiet, zone_override, spread, atom/fired_from)
+ var/obj/item/gun/ballistic/automatic/sol_grenade_launcher/firing_launcher = fired_from
+ if(istype(firing_launcher))
+ loaded_projectile.range = firing_launcher.target_range
+
+ . = ..()
+
+
+/obj/projectile/bullet/c980grenade
+ name = ".980 Tydhouer practice grenade"
+ damage = 20
+ stamina = 30
+
+ range = 14
+
+ speed = 2 // Higher means slower, y'all
+
+ sharpness = NONE
+
+
+/obj/projectile/bullet/c980grenade/on_hit(atom/target, blocked = FALSE)
+ ..()
+ fuse_activation(target)
+ return BULLET_ACT_HIT
+
+
+/obj/projectile/bullet/c980grenade/on_range()
+ fuse_activation(get_turf(src))
+ return ..()
+
+
+/// Generic proc that is called when the projectile should 'detonate', being either on impact or when the range runs out
+/obj/projectile/bullet/c980grenade/proc/fuse_activation(atom/target)
+ playsound(src, 'modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg', 50, TRUE, -3)
+ do_sparks(3, FALSE, src)
+
+
+/obj/item/ammo_box/c980grenade
+ name = "ammo box (.980 Tydhouer practice)"
+ desc = "A box of four .980 Tydhouer practice grenades. Instructions on the box indicate these are dummy practice rounds that will disintegrate into sparks on detonation. Neat!"
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "980box_solid"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ caliber = CALIBER_980TYDHOUER
+ ammo_type = /obj/item/ammo_casing/c980grenade
+ max_ammo = 4
+
+
+// .980 smoke grenade
+
+/obj/item/ammo_casing/c980grenade/smoke
+ name = ".980 Tydhouer smoke grenade"
+ desc = "A large grenade shell that will detonate at a range given to it by the gun that fires it. Bursts into a laser-weakening smoke cloud."
+
+ icon_state = "980_smoke"
+
+ projectile_type = /obj/projectile/bullet/c980grenade/smoke
+
+
+/obj/projectile/bullet/c980grenade/smoke
+ name = ".980 Tydhouer smoke grenade"
+
+
+/obj/projectile/bullet/c980grenade/smoke/fuse_activation(atom/target)
+ playsound(src, 'modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg', 50, TRUE, -3)
+ playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
+ var/datum/effect_system/fluid_spread/smoke/bad/smoke = new
+ smoke.set_up(GRENADE_SMOKE_RANGE, holder = src, location = src)
+ smoke.start()
+
+
+/obj/item/ammo_box/c980grenade/smoke
+ name = "ammo box (.980 Tydhouer smoke)"
+ desc = "A box of four .980 Tydhouer smoke grenades. Instructions on the box indicate these are smoke rounds that will make a small cloud of laser-dampening smoke on detonation."
+
+ icon_state = "980box_smoke"
+
+ ammo_type = /obj/item/ammo_casing/c980grenade/smoke
+
+
+// .980 shrapnel grenade
+
+/obj/item/ammo_casing/c980grenade/shrapnel
+ name = ".980 Tydhouer shrapnel grenade"
+ desc = "A large grenade shell that will detonate at a range given to it by the gun that fires it. Explodes into shrapnel on detonation."
+
+ icon_state = "980_explosive"
+
+ projectile_type = /obj/projectile/bullet/c980grenade/shrapnel
+
+ custom_materials = AMMO_MATS_GRENADE_SHRAPNEL
+ advanced_print_req = TRUE
+
+ harmful = TRUE
+
+
+/obj/projectile/bullet/c980grenade/shrapnel
+ name = ".980 Tydhouer shrapnel grenade"
+
+ /// What type of casing should we put inside the bullet to act as shrapnel later
+ var/casing_to_spawn = /obj/item/grenade/c980payload
+
+
+/obj/projectile/bullet/c980grenade/shrapnel/fuse_activation(atom/target)
+ var/obj/item/grenade/shrapnel_maker = new casing_to_spawn(get_turf(src))
+
+ shrapnel_maker.detonate()
+ qdel(shrapnel_maker)
+
+ playsound(src, 'modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg', 50, TRUE, -3)
+
+
+/obj/item/ammo_box/c980grenade/shrapnel
+ name = "ammo box (.980 Tydhouer shrapnel)"
+ desc = "A box of four .980 Tydhouer shrapnel grenades. Instructions on the box indicate these are shrapnel rounds. Its also covered in hazard signs, odd."
+
+ icon_state = "980box_explosive"
+
+ ammo_type = /obj/item/ammo_casing/c980grenade/shrapnel
+
+
+/obj/item/grenade/c980payload
+ shrapnel_type = /obj/projectile/bullet/shrapnel/short_range
+ shrapnel_radius = 2
+ ex_dev = 0
+ ex_heavy = 0
+ ex_light = 0
+ ex_flame = 0
+
+
+/obj/projectile/bullet/shrapnel/short_range
+ range = 2
+
+
+// .980 phosphor grenade
+
+/obj/item/ammo_casing/c980grenade/shrapnel/phosphor
+ name = ".980 Tydhouer phosphor grenade"
+ desc = "A large grenade shell that will detonate at a range given to it by the gun that fires it. Explodes into smoke and flames on detonation."
+
+ icon_state = "980_gas_alternate"
+
+ projectile_type = /obj/projectile/bullet/c980grenade/shrapnel/phosphor
+
+ custom_materials = AMMO_MATS_GRENADE_INCENDIARY
+
+
+/obj/projectile/bullet/c980grenade/shrapnel/phosphor
+ name = ".980 Tydhouer phosphor grenade"
+
+ casing_to_spawn = /obj/item/grenade/c980payload/phosphor
+
+
+/obj/projectile/bullet/c980grenade/shrapnel/phosphor/fuse_activation(atom/target)
+ . = ..()
+
+ playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
+ var/datum/effect_system/fluid_spread/smoke/quick/smoke = new
+ smoke.set_up(GRENADE_SMOKE_RANGE, holder = src, location = src)
+ smoke.start()
+
+
+/obj/item/ammo_box/c980grenade/shrapnel/phosphor
+ name = "ammo box (.980 Tydhouer phosphor)"
+ desc = "A box of four .980 Tydhouer phosphor grenades. Instructions on the box indicate these are incendiary explosive rounds. Its also covered in hazard signs, odd."
+
+ icon_state = "980box_gas_alternate"
+
+ ammo_type = /obj/item/ammo_casing/c980grenade/shrapnel/phosphor
+
+
+/obj/item/ammo_casing/shrapnel_exploder/phosphor
+ pellets = 8
+
+ projectile_type = /obj/projectile/bullet/incendiary/fire/backblast/short_range
+
+
+/obj/item/grenade/c980payload/phosphor
+ shrapnel_type = /obj/projectile/bullet/incendiary/fire/backblast/short_range
+
+
+/obj/projectile/bullet/incendiary/fire/backblast/short_range
+ range = 2
+
+
+// .980 tear gas grenade
+
+/obj/item/ammo_casing/c980grenade/riot
+ name = ".980 Tydhouer tear gas grenade"
+ desc = "A large grenade shell that will detonate at a range given to it by the gun that fires it. Bursts into a tear gas cloud."
+
+ icon_state = "980_gas"
+
+ projectile_type = /obj/projectile/bullet/c980grenade/riot
+
+
+/obj/projectile/bullet/c980grenade/riot
+ name = ".980 Tydhouer tear gas grenade"
+
+/obj/projectile/bullet/c980grenade/riot/fuse_activation(atom/target)
+ playsound(src, 'modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg', 50, TRUE, -3)
+ playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new()
+ smoke.chemholder.add_reagent(/datum/reagent/consumable/condensedcapsaicin, 10)
+ smoke.set_up(GRENADE_SMOKE_RANGE, holder = src, location = src)
+ smoke.start()
+
+
+/obj/item/ammo_box/c980grenade/riot
+ name = "ammo box (.980 Tydhouer tear gas)"
+ desc = "A box of four .980 Tydhouer tear gas grenades. Instructions on the box indicate these are smoke rounds that will make a small cloud of laser-dampening smoke on detonation."
+
+ icon_state = "980box_gas"
+
+ ammo_type = /obj/item/ammo_casing/c980grenade/riot
+
+#undef AMMO_MATS_GRENADE
+#undef AMMO_MATS_GRENADE_SHRAPNEL
+#undef AMMO_MATS_GRENADE_INCENDIARY
+
+#undef GRENADE_SMOKE_RANGE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/pistol.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/pistol.dm
new file mode 100644
index 00000000000000..e20070dcef50af
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/pistol.dm
@@ -0,0 +1,134 @@
+// .35 Sol Short
+// Pistol caliber caseless round used almost exclusively by SolFed weapons
+
+/obj/item/ammo_casing/c35sol
+ name = ".35 Sol Short lethal bullet casing"
+ desc = "A SolFed standard caseless lethal pistol round."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "35sol"
+
+ caliber = CALIBER_SOL35SHORT
+ projectile_type = /obj/projectile/bullet/c35sol
+
+
+/obj/item/ammo_casing/c35sol/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/caseless)
+
+
+/obj/projectile/bullet/c35sol
+ name = ".35 Sol Short bullet"
+ damage = 25
+
+ wound_bonus = 10 // Normal bullets are 20
+ bare_wound_bonus = 20
+
+
+/obj/item/ammo_box/c35sol
+ name = "ammo box (.35 Sol Short lethal)"
+ desc = "A box of .35 Sol Short pistol rounds, holds twenty-four rounds."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "35box"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ caliber = CALIBER_SOL35SHORT
+ ammo_type = /obj/item/ammo_casing/c35sol
+ max_ammo = 24
+
+
+// .35 Sol's equivalent to a rubber bullet
+
+/obj/item/ammo_casing/c35sol/incapacitator
+ name = ".35 Sol Short incapacitator bullet casing"
+ desc = "A SolFed standard caseless less-lethal pistol round. Exhausts targets on hit, has a tendency to bounce off walls at shallow angles."
+
+ icon_state = "35sol_disabler"
+
+ projectile_type = /obj/projectile/bullet/c35sol/incapacitator
+ harmful = FALSE
+
+
+/obj/projectile/bullet/c35sol/incapacitator
+ name = ".35 Sol Short incapacitator bullet"
+ damage = 5
+ stamina = 30
+
+ wound_bonus = -40
+ bare_wound_bonus = -20
+
+ weak_against_armour = TRUE
+
+ // The stats of the ricochet are a nerfed version of detective revolver rubber ammo
+ // This is due to the fact that there's a lot more rounds fired quickly from weapons that use this, over a revolver
+ ricochet_auto_aim_angle = 30
+ ricochet_auto_aim_range = 5
+ ricochets_max = 4
+ ricochet_incidence_leeway = 50
+ ricochet_chance = 130
+ ricochet_decay_damage = 0.8
+
+ shrapnel_type = null
+ sharpness = NONE
+ embedding = null
+
+
+/obj/item/ammo_box/c35sol/incapacitator
+ name = "ammo box (.35 Sol Short incapacitator)"
+ desc = "A box of .35 Sol Short pistol rounds, holds twenty-four rounds. The blue stripe indicates this should hold less-lethal ammunition."
+
+ icon_state = "35box_disabler"
+
+ ammo_type = /obj/item/ammo_casing/c35sol/incapacitator
+
+
+// .35 Sol ripper, similar to the detective revolver's dumdum rounds, causes slash wounds and is weak to armor
+
+/obj/item/ammo_casing/c35sol/ripper
+ name = ".35 Sol Short ripper bullet casing"
+ desc = "A SolFed standard caseless ripper pistol round. Causes slashing wounds on targets, but is weak to armor."
+
+ icon_state = "35sol_shrapnel"
+ projectile_type = /obj/projectile/bullet/c35sol/ripper
+
+ custom_materials = AMMO_MATS_RIPPER
+ advanced_print_req = TRUE
+
+
+/obj/projectile/bullet/c35sol/ripper
+ name = ".35 Sol ripper bullet"
+ damage = 15
+
+ weak_against_armour = TRUE
+
+ sharpness = SHARP_EDGED
+
+ wound_bonus = 20
+ bare_wound_bonus = 20
+
+ embedding = list(
+ embed_chance = 75,
+ fall_chance = 3,
+ jostle_chance = 4,
+ ignore_throwspeed_threshold = TRUE,
+ pain_stam_pct = 0.4,
+ pain_mult = 5,
+ jostle_pain_mult = 6,
+ rip_time = 1 SECONDS,
+ )
+
+ embed_falloff_tile = -15
+
+
+/obj/item/ammo_box/c35sol/ripper
+ name = "ammo box (.35 Sol Short ripper)"
+ desc = "A box of .35 Sol Short pistol rounds, holds twenty-four rounds. The purple stripe indicates this should hold hollowpoint-like ammunition."
+
+ icon_state = "35box_shrapnel"
+
+ ammo_type = /obj/item/ammo_casing/c35sol/ripper
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/rifle.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/rifle.dm
new file mode 100644
index 00000000000000..a012b594c29a67
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/ammo/rifle.dm
@@ -0,0 +1,195 @@
+// .40 Sol Long
+// Rifle caliber caseless ammo that kills people good
+
+/obj/item/ammo_casing/c40sol
+ name = ".40 Sol Long lethal bullet casing"
+ desc = "A SolFed standard caseless lethal rifle round."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "40sol"
+
+ caliber = CALIBER_SOL40LONG
+ projectile_type = /obj/projectile/bullet/c40sol
+
+
+/obj/item/ammo_casing/c40sol/Initialize(mapload)
+ . = ..()
+
+ AddElement(/datum/element/caseless)
+
+
+/obj/projectile/bullet/c40sol
+ name = ".40 Sol Long bullet"
+ damage = 35
+
+ wound_bonus = 10
+ bare_wound_bonus = 20
+
+
+/obj/item/ammo_box/c40sol
+ name = "ammo box (.40 Sol Long lethal)"
+ desc = "A box of .40 Sol Long rifle rounds, holds thirty bullets."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "40box"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ caliber = CALIBER_SOL40LONG
+ ammo_type = /obj/item/ammo_casing/c40sol
+ max_ammo = 30
+
+
+// .40 Sol fragmentation rounds, embeds shrapnel in the target almost every time at close to medium range. Teeeechnically less lethals.
+
+/obj/item/ammo_casing/c40sol/fragmentation
+ name = ".40 Sol Long fragmentation bullet casing"
+ desc = "A SolFed standard caseless fragmentation rifle round. Shatters upon impact, ejecting sharp shrapnel that can potentially incapacitate targets."
+
+ icon_state = "40sol_disabler"
+
+ projectile_type = /obj/projectile/bullet/c40sol/fragmentation
+
+ advanced_print_req = TRUE
+
+ harmful = FALSE
+
+
+/obj/projectile/bullet/c40sol/fragmentation
+ name = ".40 Sol Long fragmentation bullet"
+ damage = 15
+ stamina = 30
+
+ weak_against_armour = TRUE
+
+ sharpness = SHARP_EDGED
+ wound_bonus = 0
+ bare_wound_bonus = 10
+
+ shrapnel_type = /obj/item/shrapnel/stingball
+ embedding = list(
+ embed_chance = 50,
+ fall_chance = 5,
+ jostle_chance = 5,
+ ignore_throwspeed_threshold = TRUE,
+ pain_stam_pct = 0.4,
+ pain_mult = 2,
+ jostle_pain_mult = 3,
+ rip_time = 0.5 SECONDS,
+ )
+
+ embed_falloff_tile = -5
+
+
+/obj/item/ammo_box/c40sol/fragmentation
+ name = "ammo box (.40 Sol Long fragmentation)"
+ desc = "A box of .40 Sol Long rifle rounds, holds thirty bullets. The blue stripe indicates this should hold less lethal ammunition."
+
+ icon_state = "40box_disabler"
+
+ ammo_type = /obj/item/ammo_casing/c40sol/fragmentation
+
+
+// .40 Sol match grade, bounces a lot, and if there's less than 20 bullet armor on wherever these hit, it'll go completely through the target and out the other side
+
+/obj/item/ammo_casing/c40sol/pierce
+ name = ".40 Sol Long match bullet casing"
+ desc = "A SolFed standard caseless match grade rifle round. Fires at a higher pressure and thus fires slightly faster projectiles. \
+ Rumors say you can do sick ass wall bounce trick shots with these, though the official suggestion is to just shoot your target and \
+ not the wall next to them."
+
+ icon_state = "40sol_pierce"
+
+ projectile_type = /obj/projectile/bullet/c40sol/pierce
+
+ custom_materials = AMMO_MATS_AP
+ advanced_print_req = TRUE
+
+
+/obj/projectile/bullet/c40sol/pierce
+ name = ".40 Sol match bullet"
+
+ icon_state = "gaussphase"
+
+ speed = 0.5
+
+ damage = 25
+ armour_penetration = 20
+
+ wound_bonus = -30
+ bare_wound_bonus = -10
+
+ ricochets_max = 2
+ ricochet_chance = 80
+ ricochet_auto_aim_range = 4
+ ricochet_incidence_leeway = 65
+
+ projectile_piercing = PASSMOB
+
+
+/obj/projectile/bullet/c40sol/pierce/on_hit(atom/target, blocked = FALSE)
+ if(isliving(target))
+ var/mob/living/poor_sap = target
+
+ // If the target mob has enough armor to stop the bullet, or the bullet has already gone through two people, stop it on this hit
+ if((poor_sap.run_armor_check(def_zone, BULLET, "", "", silent = TRUE) > 20) || (pierces > 2))
+ projectile_piercing = NONE
+
+ if(damage > 10) // Lets just be safe with this one
+ damage -= 5
+ armour_penetration -= 10
+
+ return ..()
+
+
+/obj/item/ammo_box/c40sol/pierce
+ name = "ammo box (.40 Sol Long match)"
+ desc = "A box of .40 Sol Long rifle rounds, holds thirty bullets. The yellow stripe indicates this should hold high performance ammuniton."
+
+ icon_state = "40box_pierce"
+
+ ammo_type = /obj/item/ammo_casing/c40sol/pierce
+
+
+// .40 Sol incendiary
+
+/obj/item/ammo_casing/c40sol/incendiary
+ name = ".40 Sol Long incendiary bullet casing"
+ desc = "A SolFed standard caseless incendiary rifle round. Leaves no flaming trail, only igniting targets on impact."
+
+ icon_state = "40sol_flame"
+
+ projectile_type = /obj/projectile/bullet/c40sol/incendiary
+
+ custom_materials = AMMO_MATS_TEMP
+ advanced_print_req = TRUE
+
+
+/obj/projectile/bullet/c40sol/incendiary
+ name = ".40 Sol Long incendiary bullet"
+ icon_state = "redtrac"
+
+ damage = 25
+
+ /// How many firestacks the bullet should impart upon a target when impacting
+ var/firestacks_to_give = 1
+
+
+/obj/projectile/bullet/c40sol/incendiary/on_hit(atom/target, blocked = FALSE)
+ . = ..()
+
+ if(iscarbon(target))
+ var/mob/living/carbon/gaslighter = target
+ gaslighter.adjust_fire_stacks(firestacks_to_give)
+ gaslighter.ignite_mob()
+
+
+/obj/item/ammo_box/c40sol/incendiary
+ name = "ammo box (.40 Sol Long incendiary)"
+ desc = "A box of .40 Sol Long rifle rounds, holds thirty bullets. The orange stripe indicates this should hold incendiary ammunition."
+
+ icon_state = "40box_flame"
+
+ ammo_type = /obj/item/ammo_casing/c40sol/incendiary
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/grenade_launcher.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/grenade_launcher.dm
new file mode 100644
index 00000000000000..b927e1cd38d987
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/grenade_launcher.dm
@@ -0,0 +1,93 @@
+// Low caliber grenade launcher (fun & games)
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher
+ name = "\improper Carwo 'Kiboko' Grenade Launcher"
+ desc = "A unique grenade launcher firing .980 grenades. A laser sight system allows its user to specify a range for the grenades it fires to detonate at."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns48x.dmi'
+ icon_state = "kiboko"
+
+ worn_icon = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_worn.dmi'
+ worn_icon_state = "kiboko"
+
+ lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_lefthand.dmi'
+ righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_righthand.dmi'
+ inhand_icon_state = "kiboko"
+
+ SET_BASE_PIXEL(-8, 0)
+
+ special_mags = TRUE
+
+ bolt_type = BOLT_TYPE_LOCKING
+
+ w_class = WEIGHT_CLASS_BULKY
+ weapon_weight = WEAPON_HEAVY
+ slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/c980_grenade
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/grenade_launcher.ogg'
+
+ can_suppress = FALSE
+ can_bayonet = FALSE
+
+ burst_size = 1
+ fire_delay = 5
+ actions_types = list()
+
+ /// The currently stored range to detonate shells at
+ var/target_range = 14
+ /// The maximum range we can set grenades to detonate at, just to be safe
+ var/maximum_target_range = 14
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_CARWO)
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/examine_more(mob/user)
+ . = ..()
+
+ . += "The Kiboko is one of the strangest weapons Carwo offers. A grenade launcher, \
+ though not in the standard grenade size. The much lighter .980 Tydhouer grenades \
+ developed for the weapon offered many advantages over standard grenade launching \
+ ammunition. For a start, it was significantly lighter, and easier to carry large \
+ amounts of. What it also offered, however, and the reason SolFed funded the \
+ project: Variable time fuze. Using the large and expensive ranging sight on the \
+ launcher, its user can set an exact distance for the grenade to self detonate at. \
+ The dream of militaries for decades, finally realized. The smaller shells do not, \
+ however, make the weapon any more enjoyable to fire. The kick is only barely \
+ manageable thanks to the massive muzzle brake at the front."
+
+ return .
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/examine(mob/user)
+ . = ..()
+
+ . += span_notice("With Right Click you can set the range that shells will detonate at.")
+ . += span_notice("A small indicator in the sight notes the current detonation range is: [target_range].")
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/afterattack_secondary(atom/target, mob/living/user, proximity_flag, click_parameters)
+ if(!target || !user)
+ return
+
+ var/distance_ranged = get_dist(user, target)
+ if(distance_ranged > maximum_target_range)
+ user.balloon_alert(user, "out of range")
+ return
+
+ target_range = distance_ranged
+ user.balloon_alert(user, "range set: [target_range]")
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/no_mag
+ spawnwithmagazine = FALSE
+
+// fun & games but evil this time
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/evil
+ icon_state = "kiboko_evil"
+ worn_icon_state = "kiboko_evil"
+ inhand_icon_state = "kiboko_evil"
+
+ spawn_magazine_type = /obj/item/ammo_box/magazine/c980_grenade/drum
+
+/obj/item/gun/ballistic/automatic/sol_grenade_launcher/evil/no_mag
+ spawnwithmagazine = FALSE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/gunsets.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/gunsets.dm
new file mode 100644
index 00000000000000..030d17d0c43604
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/gunsets.dm
@@ -0,0 +1,85 @@
+// Base yellow carwo case
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case
+ desc = "A thick yellow gun case with foam inserts laid out to fit a weapon, magazines, and gear securely."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi'
+ icon_state = "case_carwo"
+
+ worn_icon_state = "yellowcase"
+
+ lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_lefthand.dmi'
+ righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_righthand.dmi'
+ inhand_icon_state = "yellowcase"
+
+// Empty version of the case
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/empty
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/empty/PopulateContents()
+ return
+
+// Sindano in a box, how innovative!
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano
+ name = "\improper Carwo 'Sindano' gunset"
+
+ weapon_to_spawn = /obj/item/gun/ballistic/automatic/sol_smg/no_mag
+ extra_to_spawn = /obj/item/ammo_box/magazine/c35sol_pistol/stendo
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano/PopulateContents()
+ new weapon_to_spawn (src)
+
+ generate_items_inside(list(
+ /obj/item/ammo_box/c35sol/incapacitator = 1,
+ /obj/item/ammo_box/c35sol = 1,
+ /obj/item/ammo_box/magazine/c35sol_pistol/stendo/starts_empty = 1,
+ /obj/item/ammo_box/magazine/c35sol_pistol/starts_empty = 2,
+ ), src)
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano/evil
+ weapon_to_spawn = /obj/item/gun/ballistic/automatic/sol_smg/evil/no_mag
+
+// Boxed grenade launcher, grenades sold seperately on this one
+
+/obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/kiboko_magless
+ name = "\improper Carwo 'Kiboko' gunset"
+
+ weapon_to_spawn = /obj/item/gun/ballistic/automatic/sol_grenade_launcher/no_mag
+ extra_to_spawn = /obj/item/ammo_box/magazine/c980_grenade/starts_empty
+
+
+/obj/structure/closet/secure_closet/armory_kiboko
+ name = "heavy equipment locker"
+ req_access = list(ACCESS_ARMORY)
+ icon_state = "shotguncase"
+
+/obj/structure/closet/secure_closet/armory_kiboko/PopulateContents()
+ . = ..()
+
+ generate_items_inside(list(
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/kiboko_magless = 1,
+ /obj/item/ammo_box/c980grenade = 2,
+ /obj/item/ammo_box/c980grenade/smoke = 1,
+ /obj/item/ammo_box/c980grenade/riot = 1,
+ ), src)
+
+/obj/structure/closet/secure_closet/armory_kiboko_but_evil
+ name = "heavy equipment locker"
+ icon = 'modular_skyrat/master_files/icons/obj/closet.dmi'
+ icon_door = "riot"
+ icon_state = "riot"
+ req_access = list(ACCESS_SYNDICATE)
+ anchored = 1
+
+/obj/structure/closet/secure_closet/armory_kiboko_but_evil/PopulateContents()
+ . = ..()
+
+ generate_items_inside(list(
+ /obj/item/gun/ballistic/automatic/sol_grenade_launcher/evil/no_mag = 1,
+ /obj/item/ammo_box/magazine/c980_grenade/drum/starts_empty = 2,
+ /obj/item/ammo_box/c980grenade/shrapnel = 1,
+ /obj/item/ammo_box/c980grenade/shrapnel/phosphor = 1,
+ /obj/item/ammo_box/c980grenade/smoke = 1,
+ /obj/item/ammo_box/c980grenade/riot = 1,
+ ), src)
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/magazines.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/magazines.dm
new file mode 100644
index 00000000000000..0734cb89258f53
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/magazines.dm
@@ -0,0 +1,113 @@
+// .35 Sol pistol magazines
+
+/obj/item/ammo_box/magazine/c35sol_pistol
+ name = "\improper Sol pistol magazine"
+ desc = "A standard size magazine for SolFed pistols, holds twelve rounds."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "pistol_35_standard"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_TINY
+
+ ammo_type = /obj/item/ammo_casing/c35sol
+ caliber = CALIBER_SOL35SHORT
+ max_ammo = 12
+
+/obj/item/ammo_box/magazine/c35sol_pistol/starts_empty
+ start_empty = TRUE
+
+/obj/item/ammo_box/magazine/c35sol_pistol/stendo
+ name = "\improper Sol extended pistol magazine"
+ desc = "An extended magazine for SolFed pistols, holds twenty-four rounds."
+
+ icon_state = "pistol_35_stended"
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ max_ammo = 24
+
+/obj/item/ammo_box/magazine/c35sol_pistol/stendo/starts_empty
+ start_empty = TRUE
+
+// .40 Sol rifle magazines
+
+/obj/item/ammo_box/magazine/c40sol_rifle
+ name = "\improper Sol rifle short magazine"
+ desc = "A shortened magazine for SolFed rifles, holds fifteen rounds."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "rifle_short"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_TINY
+
+ ammo_type = /obj/item/ammo_casing/c40sol
+ caliber = CALIBER_SOL40LONG
+ max_ammo = 15
+
+/obj/item/ammo_box/magazine/c40sol_rifle/starts_empty
+
+ start_empty = TRUE
+
+/obj/item/ammo_box/magazine/c40sol_rifle/standard
+ name = "\improper Sol rifle magazine"
+ desc = "A standard size magazine for SolFed rifles, holds thirty rounds."
+
+ icon_state = "rifle_standard"
+
+ w_class = WEIGHT_CLASS_SMALL
+
+ max_ammo = 30
+
+/obj/item/ammo_box/magazine/c40sol_rifle/standard/starts_empty
+ start_empty = TRUE
+
+
+/obj/item/ammo_box/magazine/c40sol_rifle/drum
+ name = "\improper Sol rifle drum magazine"
+ desc = "A massive drum magazine for SolFed rifles, holds sixty rounds."
+
+ icon_state = "rifle_drum"
+
+ w_class = WEIGHT_CLASS_BULKY
+
+ max_ammo = 60
+
+/obj/item/ammo_box/magazine/c40sol_rifle/drum/starts_empty
+ start_empty = TRUE
+
+// .980 grenade magazines
+
+/obj/item/ammo_box/magazine/c980_grenade
+ name = "\improper Kiboko grenade box"
+ desc = "A standard size box for .980 grenades, holds four shells."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi'
+ icon_state = "granata_standard"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_SMALL
+
+ ammo_type = /obj/item/ammo_casing/c980grenade
+ caliber = CALIBER_980TYDHOUER
+ max_ammo = 4
+
+/obj/item/ammo_box/magazine/c980_grenade/starts_empty
+ start_empty = TRUE
+
+/obj/item/ammo_box/magazine/c980_grenade/drum
+ name = "\improper Kiboko grenade drum"
+ desc = "A drum for .980 grenades, holds six shells."
+
+ icon_state = "granata_drum"
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ max_ammo = 6
+
+/obj/item/ammo_box/magazine/c980_grenade/drum/starts_empty
+ start_empty = TRUE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/rifle.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/rifle.dm
new file mode 100644
index 00000000000000..5b298fec87c7ef
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/rifle.dm
@@ -0,0 +1,160 @@
+// Base Sol rifle
+
+/obj/item/gun/ballistic/automatic/sol_rifle
+ name = "\improper Carwo 'd'Infanteria' Rifle"
+ desc = "A heavy battle rifle commonly seen in the hands of SolFed military types. Accepts any standard SolFed rifle magazine."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns48x.dmi'
+ icon_state = "infanterie"
+
+ worn_icon = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_worn.dmi'
+ worn_icon_state = "infanterie"
+
+ lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_lefthand.dmi'
+ righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_righthand.dmi'
+ inhand_icon_state = "infanterie"
+
+ SET_BASE_PIXEL(-8, 0)
+
+ special_mags = TRUE
+
+ bolt_type = BOLT_TYPE_LOCKING
+
+ w_class = WEIGHT_CLASS_BULKY
+ weapon_weight = WEAPON_HEAVY
+ slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/c40sol_rifle
+ spawn_magazine_type = /obj/item/ammo_box/magazine/c40sol_rifle/standard
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/rifle_heavy.ogg'
+ suppressed_sound = 'modular_skyrat/modules/modular_weapons/sounds/suppressed_rifle.ogg'
+ can_suppress = TRUE
+
+ can_bayonet = FALSE
+
+ suppressor_x_offset = 12
+
+ burst_size = 1
+ fire_delay = 0.45 SECONDS
+ actions_types = list()
+
+ spread = 7.5
+ projectile_wound_bonus = -10
+
+/obj/item/gun/ballistic/automatic/sol_rifle/Initialize(mapload)
+ . = ..()
+
+ give_autofire()
+
+/// Separate proc for handling auto fire just because one of these subtypes isn't otomatica
+/obj/item/gun/ballistic/automatic/sol_rifle/proc/give_autofire()
+ AddComponent(/datum/component/automatic_fire, fire_delay)
+
+/obj/item/gun/ballistic/automatic/sol_rifle/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_CARWO)
+
+/obj/item/gun/ballistic/automatic/sol_rifle/examine_more(mob/user)
+ . = ..()
+
+ . += "The d'Infanterie rifles are, as the name may imply, built by Carwo for \
+ use by SolFed's various infantry branches. Following the rather reasonable \
+ military requirements of using the same few cartridges and magazines, \
+ the lifespans of logistics coordinators and quartermasters everywhere \
+ were lengthened by several years. While typically only for military sale \
+ in the past, the recent collapse of certain unnamed weapons manufacturers \
+ has caused Carwo to open many of its military weapons to civilian sale, \
+ which includes this one."
+
+ return .
+
+/obj/item/gun/ballistic/automatic/sol_rifle/no_mag
+ spawnwithmagazine = FALSE
+
+// Sol marksman rifle
+
+/obj/item/gun/ballistic/automatic/sol_rifle/marksman
+ name = "\improper Carwo 'd'Elite' Marksman Rifle"
+ desc = "A heavy marksman rifle commonly seen in the hands of SolFed military types. Accepts any standard SolFed rifle magazine."
+
+ icon_state = "elite"
+ worn_icon_state = "elite"
+ inhand_icon_state = "elite"
+
+ spawn_magazine_type = /obj/item/ammo_box/magazine/c40sol_rifle
+
+ fire_delay = 0.75 SECONDS
+
+ spread = 0
+ projectile_damage_multiplier = 1.2
+ projectile_wound_bonus = 10
+
+/obj/item/gun/ballistic/automatic/sol_rifle/marksman/Initialize(mapload)
+ . = ..()
+
+ AddComponent(/datum/component/scope, range_modifier = 2)
+
+/obj/item/gun/ballistic/automatic/sol_rifle/marksman/give_autofire()
+ return
+
+/obj/item/gun/ballistic/automatic/sol_rifle/marksman/examine_more(mob/user)
+ . = ..()
+
+ . += "This particlar variant, often called 'd'Elite', is a marksman rifle. \
+ Automatic fire was forsaken for a semi-automatic setup, a more fitting \
+ stock, and more often than not a scope. Typically also seen with smaller \
+ magazines for convenience for the shooter, but as with any other Sol \
+ rifle, all standard magazine types will work."
+
+ return .
+
+/obj/item/gun/ballistic/automatic/sol_rifle/marksman/no_mag
+ spawnwithmagazine = FALSE
+
+// Machinegun based on the base Sol rifle
+
+/obj/item/gun/ballistic/automatic/sol_rifle/machinegun
+ name = "\improper Carwo 'd'Outomaties' Machinegun"
+ desc = "A hefty machinegun commonly seen in the hands of SolFed military types. Accepts any standard SolFed rifle magazine."
+
+ icon_state = "outomaties"
+ worn_icon_state = "outomaties"
+ inhand_icon_state = "outomaties"
+
+ bolt_type = BOLT_TYPE_OPEN
+
+ spawn_magazine_type = /obj/item/ammo_box/magazine/c40sol_rifle/drum
+
+ fire_delay = 0.1 SECONDS
+
+ recoil = 1
+ spread = 12.5
+ projectile_wound_bonus = -20
+
+/obj/item/gun/ballistic/automatic/sol_rifle/machinegun/examine_more(mob/user)
+ . = ..()
+
+ . += "The d'Outomaties variant of the rifle, what you are looking at now, \
+ is a modification to turn the weapon into a passable, if sub-optimal \
+ light machinegun. To support the machinegun role, the internals were \
+ converted to make the gun into an open bolt, faster firing machine. These \
+ additions, combined with a battle rifle not meant to be used fully auto \
+ much to begin with, made for a relatively unwieldy weapon. A machinegun, \
+ however, is still a machinegun, no matter how hard it is to keep on target."
+
+ return .
+
+/obj/item/gun/ballistic/automatic/sol_rifle/machinegun/no_mag
+ spawnwithmagazine = FALSE
+
+// Evil version of the rifle (nothing different its just black)
+
+/obj/item/gun/ballistic/automatic/sol_rifle/evil
+ desc = "A heavy battle rifle, this one seems to be painted tacticool black. Accepts any standard SolFed rifle magazine."
+
+ icon_state = "infanterie_evil"
+ worn_icon_state = "infanterie_evil"
+ inhand_icon_state = "infanterie_evil"
+
+/obj/item/gun/ballistic/automatic/sol_rifle/evil/no_mag
+ spawnwithmagazine = FALSE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/shotgun.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/shotgun.dm
new file mode 100644
index 00000000000000..af5f6131d887eb
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/shotgun.dm
@@ -0,0 +1,62 @@
+// SolFed shotgun (this was gonna be in a proprietary shotgun shell type outside of 12ga at some point, wild right?)
+
+/obj/item/gun/ballistic/shotgun/riot/sol
+ name = "\improper Carwo 'Renoster' Shotgun"
+ desc = "A twelve gauge shotgun with a six shell capacity underneath. Made for and used by SolFed's various military branches."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns48x.dmi'
+ icon_state = "renoster"
+
+ worn_icon = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_worn.dmi'
+ worn_icon_state = "renoster"
+
+ lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_lefthand.dmi'
+ righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_righthand.dmi'
+ inhand_icon_state = "renoster"
+
+ inhand_x_dimension = 32
+ inhand_y_dimension = 32
+
+ SET_BASE_PIXEL(-8, 0)
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/shotgun_heavy.ogg'
+ rack_sound = 'modular_skyrat/modules/modular_weapons/sounds/shotgun_rack.ogg'
+ suppressed_sound = 'modular_skyrat/modules/modular_weapons/sounds/suppressed_heavy.ogg'
+ can_suppress = TRUE
+
+ suppressor_x_offset = 9
+
+ w_class = WEIGHT_CLASS_BULKY
+ slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_OCLOTHING
+
+/obj/item/gun/ballistic/shotgun/riot/sol/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_CARWO)
+
+/obj/item/gun/ballistic/shotgun/riot/sol/examine_more(mob/user)
+ . = ..()
+
+ . += "The Renoster was designed at its core as a police shotgun. \
+ As consequence, it holds all the qualities a police force would want \
+ in one. Large shell capacity, sturdy frame, while holding enough \
+ capacity for modification to satiate even the most overfunded of \
+ peacekeeper forces. Inevitably, the weapon made its way into civilian \
+ markets alongside its sale to several military branches that also \
+ saw value in having a heavy shotgun."
+
+ return .
+
+/obj/item/gun/ballistic/shotgun/riot/sol/update_appearance(updates)
+ if(sawn_off)
+ suppressor_x_offset = 0
+ SET_BASE_PIXEL(0, 0)
+
+ . = ..()
+
+// Shotgun but EVIL!
+
+/obj/item/gun/ballistic/shotgun/riot/sol/evil
+ desc = "A twleve guage shotgun with an eight shell capacity underneath. This one is painted in a tacticool black."
+
+ icon_state = "renoster_evil"
+ worn_icon_state = "renoster_evil"
+ inhand_icon_state = "renoster_evil"
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/submachinegun.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/submachinegun.dm
new file mode 100644
index 00000000000000..61b1ed902fabd9
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/carwo_defense_systems/submachinegun.dm
@@ -0,0 +1,62 @@
+// Base Sol SMG
+
+/obj/item/gun/ballistic/automatic/sol_smg
+ name = "\improper Carwo 'Sindano' Submachinegun"
+ desc = "A small submachinegun commonly seen in the hands of PMCs and other unsavory corpos. Accepts any standard Sol pistol magazine."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns32x.dmi'
+ icon_state = "sindano"
+
+ inhand_icon_state = "c20r"
+
+ special_mags = TRUE
+
+ bolt_type = BOLT_TYPE_OPEN
+
+ w_class = WEIGHT_CLASS_NORMAL
+ weapon_weight = WEAPON_MEDIUM
+ slot_flags = ITEM_SLOT_OCLOTHING | ITEM_SLOT_BELT
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/c35sol_pistol
+ spawn_magazine_type = /obj/item/ammo_box/magazine/c35sol_pistol/stendo
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/smg_light.ogg'
+ can_suppress = TRUE
+
+ can_bayonet = FALSE
+
+ suppressor_x_offset = 11
+
+ burst_size = 3
+ fire_delay = 0.2 SECONDS
+
+ spread = 7.5
+
+/obj/item/gun/ballistic/automatic/sol_smg/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_CARWO)
+
+/obj/item/gun/ballistic/automatic/sol_smg/examine_more(mob/user)
+ . = ..()
+
+ . += "The Sindano submachinegun was originally produced for military contract. \
+ These guns were seen in the hands of anyone from medics, ship techs, logistics officers, \
+ and shuttle pilots often had several just to show off. Due to SolFed's quest to \
+ extend the lifespans of their logistics officers and quartermasters, the weapon \
+ uses the same standard pistol cartridge that most other miltiary weapons of \
+ small caliber use. This results in interchangeable magazines between pistols \
+ and submachineguns, neat!"
+
+ return .
+
+/obj/item/gun/ballistic/automatic/sol_smg/no_mag
+ spawnwithmagazine = FALSE
+
+// Sindano (evil)
+
+/obj/item/gun/ballistic/automatic/sol_smg/evil
+ desc = "A small submachinegun, this one is painted in tacticool black. Accepts any standard Sol pistol magazine."
+
+ icon_state = "sindano_evil"
+
+/obj/item/gun/ballistic/automatic/sol_smg/evil/no_mag
+ spawnwithmagazine = FALSE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/advert.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/advert.dm
new file mode 100644
index 00000000000000..f076b0d73b3452
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/advert.dm
@@ -0,0 +1,41 @@
+/obj/structure/sign/poster/official/trappiste_suppressor
+ name = "Keep It Quiet - Ear Protection Unneeded"
+ desc = "This poster depicts, alongside the prominent logo of Trappiste Fabriek, a \
+ diagram of the average suppressor, and how on most* Trappiste weapons \
+ the sound of firing will be low enough to eradicate the need for ear protection. \
+ How safety minded, they even have a non-liability statement too."
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/propaganda.dmi'
+ icon_state = "keep_it_quiet"
+
+/obj/structure/sign/poster/official/trappiste_suppressor/examine_more(mob/user)
+ . = ..()
+
+ . += "It was hard to notice before, but now that you really look at it... \
+ This thing is completely covered in micro scale text telling you in just about \
+ every human language and then some that Trappiste isn't liable for ear damage \
+ caused by their weapons, suppressed or not."
+
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/trappiste_suppressor, 32)
+
+/obj/structure/sign/poster/official/trappiste_ammunition
+ name = "Know Your Ammuniton Colors"
+ desc = "This poster depicts, alongside the prominent logo of Trappiste Fabriek, \
+ a variety of colors that one may find on .585 Trappiste rounds. \
+ A plain white case usually means lethal, while a blue stripe is less-lethal \
+ and a purple stripe is more lethal. How informative."
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/propaganda.dmi'
+ icon_state = "know_the_difference"
+
+/obj/structure/sign/poster/official/trappiste_ammunition/examine_more(mob/user)
+ . = ..()
+
+ . += "Small text details that this information may also be transferrable \
+ to other types of SolFed ammunition, but that you should check the box \
+ the bullets come in just to be sure. Trappiste is, of course,\
+ not liable for excess harm caused by misreading color identification systems."
+
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/trappiste_ammunition, 32)
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/ammo.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/ammo.dm
new file mode 100644
index 00000000000000..498f55a5627b6d
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/ammo.dm
@@ -0,0 +1,91 @@
+// .585 Trappiste
+// High caliber round used in large pistols and revolvers
+
+/obj/item/ammo_casing/c585trappiste
+ name = ".585 Trappiste lethal bullet casing"
+ desc = "A white polymer cased high caliber round commonly used in handguns."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/ammo.dmi'
+ icon_state = "585trappiste"
+
+ caliber = CALIBER_585TRAPPISTE
+ projectile_type = /obj/projectile/bullet/c585trappiste
+
+/obj/projectile/bullet/c585trappiste
+ name = ".585 Trappiste bullet"
+ damage = 45
+ wound_bonus = 0 // Normal bullets are 20
+
+/obj/item/ammo_box/c585trappiste
+ name = "ammo box (.585 Trappiste lethal)"
+ desc = "A box of .585 Trappiste pistol rounds, holds twelve cartridges."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/ammo.dmi'
+ icon_state = "585box"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ caliber = CALIBER_585TRAPPISTE
+ ammo_type = /obj/item/ammo_casing/c585trappiste
+ max_ammo = 12
+
+// .585 Trappiste equivalent to a rubber bullet
+
+/obj/item/ammo_casing/c585trappiste/incapacitator
+ name = ".585 Trappiste flathead bullet casing"
+ desc = "A white polymer cased high caliber round with a relatively soft, flat tip. Designed to flatten against targets and usually not penetrate on impact."
+
+ icon_state = "585trappiste_disabler"
+
+ projectile_type = /obj/projectile/bullet/c585trappiste/incapacitator
+ harmful = FALSE
+
+/obj/projectile/bullet/c585trappiste/incapacitator
+ name = ".585 Trappiste flathead bullet"
+ damage = 20
+ stamina = 40
+ wound_bonus = 10
+
+ weak_against_armour = TRUE
+
+ shrapnel_type = null
+ sharpness = NONE
+ embedding = null
+
+/obj/item/ammo_box/c585trappiste/incapacitator
+ name = "ammo box (.585 Trappiste flathead)"
+ desc = "A box of .585 Trappiste pistol rounds, holds twelve cartridges. The blue stripe indicates that it should hold less lethal rounds."
+
+ icon_state = "585box_disabler"
+
+ ammo_type = /obj/item/ammo_casing/c585trappiste/incapacitator
+
+// .585 hollowpoint, made to cause nasty wounds
+
+/obj/item/ammo_casing/c585trappiste/hollowpoint
+ name = ".585 Trappiste hollowhead bullet casing"
+ desc = "A white polymer cased high caliber round with a hollowed tip. Designed to cause as much damage on impact to fleshy targets as possible."
+
+ icon_state = "585trappiste_shrapnel"
+ projectile_type = /obj/projectile/bullet/c585trappiste/hollowpoint
+
+ advanced_print_req = TRUE
+
+/obj/projectile/bullet/c585trappiste/hollowpoint
+ name = ".585 Trappiste hollowhead bullet"
+ damage = 35
+
+ weak_against_armour = TRUE
+
+ wound_bonus = 30
+ bare_wound_bonus = 40
+
+/obj/item/ammo_box/c585trappiste/hollowpoint
+ name = "ammo box (.585 Trappiste hollowhead)"
+ desc = "A box of .585 Trappiste pistol rounds, holds twelve cartridges. The purple stripe indicates that it should hold hollowpoint-like rounds."
+
+ icon_state = "585box_shrapnel"
+
+ ammo_type = /obj/item/ammo_casing/c585trappiste/hollowpoint
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/gunsets.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/gunsets.dm
new file mode 100644
index 00000000000000..41607663a508ef
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/gunsets.dm
@@ -0,0 +1,34 @@
+// Base yellow with symbol trappiste case
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case
+ desc = "A thick yellow gun case with foam inserts laid out to fit a weapon, magazines, and gear securely. The five square grid of Trappiste Fabriek is displayed prominently on the top."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi'
+ icon_state = "case_trappiste"
+
+ lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_lefthand.dmi'
+ righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_righthand.dmi'
+ inhand_icon_state = "yellowcase"
+
+// Empty version of the case
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/empty
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/empty/PopulateContents()
+ return
+
+// Gunset for the Wespe pistol
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/wespe
+ name = "Trappiste 'Wespe' gunset"
+
+ weapon_to_spawn = /obj/item/gun/ballistic/automatic/pistol/sol/no_mag
+ extra_to_spawn = /obj/item/ammo_box/magazine/c35sol_pistol
+
+// Gunset for the Skild heavy pistol
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild
+ name = "Trappiste 'Skild' gunset"
+
+ weapon_to_spawn = /obj/item/gun/ballistic/automatic/pistol/trappiste/no_mag
+ extra_to_spawn = /obj/item/ammo_box/magazine/c585trappiste_pistol
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/magazines.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/magazines.dm
new file mode 100644
index 00000000000000..77506ccd29f0f2
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/magazines.dm
@@ -0,0 +1,19 @@
+// .585 pistol magazines
+
+/obj/item/ammo_box/magazine/c585trappiste_pistol
+ name = "\improper Trappiste pistol magazine"
+ desc = "A standard size magazine for Trappiste pistols, holds six rounds."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/ammo.dmi'
+ icon_state = "pistol_585_standard"
+
+ multiple_sprites = AMMO_BOX_FULL_EMPTY
+
+ w_class = WEIGHT_CLASS_SMALL
+
+ ammo_type = /obj/item/ammo_casing/c585trappiste
+ caliber = CALIBER_585TRAPPISTE
+ max_ammo = 6
+
+/obj/item/ammo_box/magazine/c585trappiste_pistol/spawns_empty
+ start_empty = TRUE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/pistol.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/pistol.dm
new file mode 100644
index 00000000000000..1ae391325d0a0b
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/pistol.dm
@@ -0,0 +1,100 @@
+// .35 Sol pistol
+
+/obj/item/gun/ballistic/automatic/pistol/sol
+ name = "\improper Trappiste 'Wespe' Pistol"
+ desc = "The standard issue service pistol of SolFed's various military branches. Comes with attached light."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi'
+ icon_state = "wespe"
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/pistol_light.ogg'
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/c35sol_pistol
+ special_mags = TRUE
+
+ suppressor_x_offset = 7
+ suppressor_y_offset = 0
+
+ fire_delay = 0.3 SECONDS
+
+/obj/item/gun/ballistic/automatic/pistol/sol/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_TRAPPISTE)
+
+/obj/item/gun/ballistic/automatic/pistol/sol/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ starting_light = new /obj/item/flashlight/seclite(src), \
+ is_light_removable = FALSE, \
+ )
+
+/obj/item/gun/ballistic/automatic/pistol/sol/examine_more(mob/user)
+ . = ..()
+
+ . += "The Wespe is a pistol that was made entirely for military use. \
+ Required to use a standard round, standard magazines, and be able \
+ to function in all of the environments that SolFed operated in \
+ commonly. These qualities just so happened to make the weapon \
+ popular in frontier space and is likely why you are looking at \
+ one now."
+
+ return .
+
+/obj/item/gun/ballistic/automatic/pistol/sol/no_mag
+ spawnwithmagazine = FALSE
+
+// Sol pistol evil gun
+
+/obj/item/gun/ballistic/automatic/pistol/sol/evil
+ desc = "The standard issue service pistol of SolFed's various military branches. Comes with attached light. This one is painted tacticool black."
+
+ icon_state = "wespe_evil"
+
+/obj/item/gun/ballistic/automatic/pistol/sol/evil/no_mag
+ spawnwithmagazine = FALSE
+
+// Trappiste high caliber pistol in .585
+
+/obj/item/gun/ballistic/automatic/pistol/trappiste
+ name = "\improper Trappiste 'Skild' Pistol"
+ desc = "A somewhat rare to see Trappiste pistol firing the high caliber .585 developed by the same company. Sees rare use mainly due to its tendency to cause severe wrist discomfort."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi'
+ icon_state = "skild"
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/pistol_heavy.ogg'
+ suppressed_sound = 'modular_skyrat/modules/modular_weapons/sounds/suppressed_heavy.ogg'
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/c585trappiste_pistol
+
+ suppressor_x_offset = 8
+ suppressor_y_offset = 0
+
+ fire_delay = 1 SECONDS
+
+ recoil = 3
+
+/obj/item/gun/ballistic/automatic/pistol/trappiste/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_TRAPPISTE)
+
+/obj/item/gun/ballistic/automatic/pistol/trappiste/examine_more(mob/user)
+ . = ..()
+
+ . += "The Skild only exists due to a widely known event that SolFed's military \
+ would prefer wasn't anywhere near as popular. A general, name unknown as of now, \
+ was recorded complaining about the lack of capability the Wespe provided to the \
+ military, alongside several statements comparing the Wespe's lack of masculinity \
+ to the, quote, 'unique lack of testosterone those NRI mongrels field'. While the \
+ identities of both the general and people responsible for the leaking of the recording \
+ are still classified, many high ranking SolFed military staff suspiciously have stopped \
+ appearing in public, unlike the Skild. A lot of several thousand pistols, the first \
+ of the weapons to ever exist, were not so silently shipped to SolFed's Plutonian \
+ shipping hub from TRAPPIST. SolFed military command refuses to answer any \
+ further questions about the incident to this day."
+
+ return .
+
+/obj/item/gun/ballistic/automatic/pistol/trappiste/no_mag
+ spawnwithmagazine = FALSE
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/revolver.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/revolver.dm
new file mode 100644
index 00000000000000..22cbb471449abc
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/trappiste_fabriek/revolver.dm
@@ -0,0 +1,81 @@
+// .35 Sol mini revolver
+
+/obj/item/gun/ballistic/revolver/sol
+ name = "\improper Trappiste 'Eland' Revolver"
+ desc = "A small revolver with a comically short barrel and cylinder space for eight .35 Sol Short rounds."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi'
+ icon_state = "eland"
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/c35sol
+
+ suppressor_x_offset = 3
+
+ w_class = WEIGHT_CLASS_SMALL
+
+ can_suppress = TRUE
+
+/obj/item/gun/ballistic/revolver/sol/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_TRAPPISTE)
+
+/obj/item/gun/ballistic/revolver/sol/examine_more(mob/user)
+ . = ..()
+
+ . += "The Eland is one of the few Trappiste weapons not made for military contract. \
+ Instead, the Eland started life as a police weapon, offered as a gun to finally \
+ outmatch all others in the cheap police weapons market. Unfortunately, this \
+ coincided with nearly every SolFed police force realising they are actually \
+ comically overfunded. With military weapons bought for police forces taking \
+ over the market, the Eland instead found home in the civilian personal defense \
+ market. That is likely the reason you are looking at this one now."
+
+ return .
+
+/obj/item/ammo_box/magazine/internal/cylinder/c35sol
+ ammo_type = /obj/item/ammo_casing/c35sol
+ caliber = CALIBER_SOL35SHORT
+ max_ammo = 8
+
+// .585 super revolver
+
+/obj/item/gun/ballistic/revolver/takbok
+ name = "\improper Trappiste 'Takbok' Revolver"
+ desc = "A hefty revolver with an equally large cylinder capable of holding five .585 Trappiste rounds."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi'
+ icon_state = "takbok"
+
+ fire_sound = 'modular_skyrat/modules/modular_weapons/sounds/revolver_heavy.ogg'
+ suppressed_sound = 'modular_skyrat/modules/modular_weapons/sounds/suppressed_heavy.ogg'
+
+ accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/c585trappiste
+
+ suppressor_x_offset = 5
+
+ can_suppress = TRUE
+
+ fire_delay = 1 SECONDS
+ recoil = 3
+
+/obj/item/gun/ballistic/revolver/takbok/give_manufacturer_examine()
+ AddElement(/datum/element/manufacturer_examine, COMPANY_TRAPPISTE)
+
+/obj/item/gun/ballistic/revolver/takbok/examine_more(mob/user)
+ . = ..()
+
+ . += "The Takbok is a unique design for Trappiste for the sole reason that it \
+ was made at first to be a one-off. Founder of partner company Carwo Defense, \
+ Darmaan Khaali Carwo herself, requested a sporting revolver from Trappiste. \
+ What was delivered wasn't a target revolver, it was a target crusher. The \
+ weapon became popular as Carwo crushed many shooting competitions using \
+ the Takbok, with the design going on several production runs up until \
+ 2523 when the popularity of the gun fell off. Due to the number of revolvers \
+ made, they are still easy enough to find if you look despite production \
+ having already ceased many years ago."
+
+ return .
+
+/obj/item/ammo_box/magazine/internal/cylinder/c585trappiste
+ ammo_type = /obj/item/ammo_casing/c585trappiste
+ caliber = CALIBER_585TRAPPISTE
+ max_ammo = 5
diff --git a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/xhihao_light_arms/guns.dm b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/xhihao_light_arms/guns.dm
index 4207eb85eef483..ec586d0d35f68d 100644
--- a/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/xhihao_light_arms/guns.dm
+++ b/modular_skyrat/modules/modular_weapons/code/company_and_or_faction_based/xhihao_light_arms/guns.dm
@@ -15,6 +15,7 @@
inhand_icon_state = "enchanted_rifle"
accepted_magazine_type = /obj/item/ammo_box/magazine/internal/boltaction/bubba
can_be_sawn_off = FALSE
+ knife_x_offset = 35
/obj/item/gun/ballistic/rifle/boltaction/sporterized/Initialize(mapload)
. = ..()
diff --git a/modular_skyrat/modules/modular_weapons/code/energy.dm b/modular_skyrat/modules/modular_weapons/code/energy.dm
deleted file mode 100644
index 550af2c130eff8..00000000000000
--- a/modular_skyrat/modules/modular_weapons/code/energy.dm
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
-* BOLT RESPONDER
-* A mini disabler
-* 12 shot capacity VS normal disabler's 20.
-*/
-
-
-/obj/item/gun/energy/disabler/bolt_disabler
- name = "Bolt Responder"
- desc = "A pocket-sized non-lethal energy gun with low ammo capacity."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi'
- icon_state = "cfa-disabler"
- righthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi'
- ammo_type = list(/obj/item/ammo_casing/energy/disabler)
- ammo_x_offset = 2
- w_class = WEIGHT_CLASS_SMALL
- cell_type = /obj/item/stock_parts/cell/mini_egun
- ammo_x_offset = 2
- charge_sections = 3
-
-/obj/item/gun/energy/disabler/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_BOLT)
-
-/obj/item/gun/energy/disabler/bolt_disabler/give_gun_safeties()
- return
-
-/obj/item/gun/energy/disabler/bolt_disabler/add_seclight_point()
- return
-/*
-* CFA PHALANX
-* Similar to the HoS's laser
-* Fires a bouncing non-lethal, lethal and knockdown projectile.
-*/
-
-/obj/item/gun/energy/e_gun/cfa_phalanx
- name = "\improper Mk.II Phalanx plasma blaster"
- desc = "Fires a disabling and lethal bouncing projectile, as well as a special muscle-seizing projectile that knocks targets down."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi'
- icon_state = "phalanx1"
- righthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi'
- w_class = WEIGHT_CLASS_NORMAL
- force = 10
- ammo_type = list(/obj/item/ammo_casing/energy/disabler/bounce, /obj/item/ammo_casing/energy/laser/bounce, /obj/item/ammo_casing/energy/electrode/knockdown)
- ammo_x_offset = 1
- charge_sections = 5
- cell_type = /obj/item/stock_parts/cell/hos_gun
-
-/obj/item/gun/energy/e_gun/cfa_phalanx/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/energy/e_gun/cfa_phalanx/give_gun_safeties()
- return
-
-/*
-* CFA PALADIN
-* Identical to a heavy laser.
-*/
-
-/obj/item/gun/energy/laser/cfa_paladin
- name = "\improper Mk.IV Paladin plasma carbine"
- desc = "Essentially a handheld laser cannon. This is solely for killing, and it's dual-laser system reflects that."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi'
- icon_state = "paladin"
- force = 10
- ammo_type = list(/obj/item/ammo_casing/energy/laser/double)
- charge_sections = 5
-
-/obj/item/gun/energy/laser/cfa_paladin/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/energy/laser/cfa_paladin/give_gun_safeties()
- return
-
-/*
-* BOUNCE DISABLER
-* A disabler that will always ricochet.
-*/
-
-/obj/item/ammo_casing/energy/disabler/bounce
- projectile_type = /obj/projectile/beam/disabler/bounce
- select_name = "disable"
- e_cost = 60
- fire_sound = 'sound/weapons/taser2.ogg'
- harmful = FALSE
-
-/obj/effect/projectile/tracer/disabler/bounce
- name = "disabler"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/projectiles.dmi'
- icon_state = "bouncebeam"
-
-/obj/projectile/beam/disabler/bounce
- name = "disabler arc"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/projectiles.dmi'
- icon_state = "bouncebeam"
- damage = 30
- damage_type = STAMINA
- armor_flag = ENERGY
- eyeblur = 1
- tracer_type = /obj/effect/projectile/tracer/disabler/bounce
- light_range = 5
- light_power = 0.75
- speed = 1.4
- ricochets_max = 6
- ricochet_incidence_leeway = 170
- ricochet_chance = 130
- ricochet_decay_damage = 0.9
-
-// Allows the projectile to reflect on walls like how bullets ricochet.
-/obj/projectile/beam/disabler/bounce/check_ricochet_flag(atom/A)
- return TRUE
-
-/*
-* BOUNCE LASER
-* A laser that will always ricochet.
-*/
-
-/obj/item/ammo_casing/energy/laser/bounce
- projectile_type = /obj/projectile/beam/laser/bounce
- select_name = "lethal"
- e_cost = 100
-
-/obj/projectile/beam/laser/bounce
- name = "energy arc"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/projectiles.dmi'
- icon_state = "bouncebeam_red"
- damage = 20
- damage_type = BURN
- armor_flag = LASER
- light_range = 5
- light_power = 0.75
- speed = 1.4
- ricochets_max = 6
- ricochet_incidence_leeway = 170
- ricochet_chance = 130
- ricochet_decay_damage = 0.9
-
-// Allows the projectile to reflect on walls like how bullets ricochet.
-/obj/projectile/beam/laser/bounce/check_ricochet_flag(atom/A)
- return TRUE
-
-/*
-* KNOCKDOWN BOLT
-* A taser that had the same stamina impact as a disabler, but a five-second knockdown and taser hitter effects.
-*/
-
-/obj/item/ammo_casing/energy/electrode/knockdown
- projectile_type = /obj/projectile/energy/electrode/knockdown
- select_name = "knockdown"
- fire_sound = 'sound/weapons/taser.ogg'
- e_cost = 200
- harmful = FALSE
-
-/obj/projectile/energy/electrode/knockdown
- name = "electrobolt"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/projectiles.dmi'
- icon_state = "electro_bolt"
- knockdown = 50
- stamina = 30
- range = 6
-
-/*
-* SINGLE LASER
-* Has an unique sprite
-* Low-powered laser for rapid fire
-* Pea-shooter tier.
-*/
-
-
-/obj/item/ammo_casing/energy/laser/single
- projectile_type = /obj/projectile/beam/laser/single
- e_cost = 50
- select_name = "lethal"
-
-/obj/projectile/beam/laser/single
- name = "laser bolt"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/projectiles.dmi'
- icon_state = "single_laser"
- damage = 15
- eyeblur = 1
- light_range = 5
- light_power = 0.75
- speed = 0.5
- armour_penetration = 10
-
-/*
-* DOUBLE LASER
-* Visually, this fires two lasers. In code, it's just one.
-* It's fast and great for turrets.
-*/
-
-/obj/item/ammo_casing/energy/laser/double
- projectile_type = /obj/projectile/beam/laser/double
- e_cost = 100
- select_name = "lethal"
- fire_sound = 'sound/weapons/lasercannonfire.ogg'
-
-/obj/projectile/beam/laser/double
- name = "laser bolt"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/projectiles.dmi'
- icon_state = "double_laser"
- damage = 40
- eyeblur = 1
- light_range = 5
- light_power = 0.75
- speed = 0.5
- armour_penetration = 10
-
-/*
-* ENERGY BULLETS
-* Ballistic gunplay but it allows us to target a different part of the armour block.
-* Also allows the benefits of lasers (blobs strains, xenos) over bullets to be used with ballistic gunplay.
-*/
-
-/obj/item/ammo_casing/laser
- name = "type I plasma projectile"
- desc = "A chemical mixture that once triggered, creates a deadly projectile, melting it's own casing in the process."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/ammo.dmi'
- icon_state = "plasma_shell"
- worn_icon_state = "shell"
- caliber = CALIBER_LASER
- custom_materials = list(/datum/material/iron=SHEET_MATERIAL_AMOUNT * 2,/datum/material/plasma=HALF_SHEET_MATERIAL_AMOUNT)
- projectile_type = /obj/projectile/beam/laser/single
-
-/obj/item/ammo_casing/laser/double
- name = "type II plasma projectile"
- desc = "A chemical mixture that once triggered, creates a deadly projectile, melting it's own casing in the process."
- icon_state = "plasma_shell2"
- worn_icon_state = "shell"
- custom_materials = list(/datum/material/iron=SHEET_MATERIAL_AMOUNT * 2,/datum/material/plasma=HALF_SHEET_MATERIAL_AMOUNT)
- projectile_type = /obj/projectile/beam/laser/double
-
-/obj/item/ammo_casing/laser/bounce
- name = "type III reflective projectile (lethal)"
- desc = "A chemical mixture that once triggered, creates a deadly bouncing projectile, melting it's own casing in the process."
- icon_state = "bounce_shell"
- worn_icon_state = "shell"
- custom_materials = list(/datum/material/iron=SHEET_MATERIAL_AMOUNT * 2,/datum/material/plasma=HALF_SHEET_MATERIAL_AMOUNT)
- projectile_type = /obj/projectile/beam/laser/bounce
-
-/obj/item/ammo_casing/laser/bounce/disabler
- name = "type III reflective projectile (disabler)"
- desc = "A chemical mixture that once triggered, creates bouncing disabler projectile, melting it's own casing in the process."
- icon_state = "disabler_shell"
- projectile_type = /obj/projectile/beam/disabler/bounce
-
-
diff --git a/modular_skyrat/modules/modular_weapons/code/gunsets.dm b/modular_skyrat/modules/modular_weapons/code/gunsets.dm
new file mode 100644
index 00000000000000..7373e0fa729322
--- /dev/null
+++ b/modular_skyrat/modules/modular_weapons/code/gunsets.dm
@@ -0,0 +1,70 @@
+/*
+* GUNSET BOXES
+*/
+
+/obj/item/storage/toolbox/guncase/skyrat
+ desc = "A thick gun case with foam inserts laid out to fit a weapon, magazines, and gear securely."
+
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi'
+ icon_state = "guncase"
+
+ worn_icon = 'modular_skyrat/modules/modular_weapons/icons/mob/worn/cases.dmi'
+ worn_icon_state = "darkcase"
+
+ slot_flags = ITEM_SLOT_BACK
+
+ material_flags = NONE
+
+ /// Is the case visually opened or not
+ var/opened = FALSE
+
+/obj/item/storage/toolbox/guncase/skyrat/Initialize(mapload)
+ . = ..()
+ atom_storage.max_total_storage = 14 // Technically means you could fit multiple large guns in here but its a case you cant backpack anyways so what it do
+ atom_storage.max_slots = 6 // We store some extra items in these so lets make a little extra room
+
+/obj/item/storage/toolbox/guncase/skyrat/update_icon()
+ . = ..()
+ if(opened)
+ icon_state = "[initial(icon_state)]-open"
+ else
+ icon_state = initial(icon_state)
+
+/obj/item/storage/toolbox/guncase/skyrat/AltClick(mob/user)
+ . = ..()
+ opened = !opened
+ update_icon()
+
+/obj/item/storage/toolbox/guncase/skyrat/attack_self(mob/user)
+ . = ..()
+ opened = !opened
+ update_icon()
+
+// Empty guncase
+
+/obj/item/storage/toolbox/guncase/skyrat/empty
+
+/obj/item/storage/toolbox/guncase/skyrat/empty/PopulateContents()
+ return
+
+// Small case for pistols and whatnot
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol
+ name = "small gun case"
+
+ icon_state = "guncase_s"
+
+ slot_flags = NONE
+
+ w_class = WEIGHT_CLASS_NORMAL
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/Initialize(mapload)
+ . = ..()
+ atom_storage.max_specific_storage = WEIGHT_CLASS_NORMAL
+
+// Empty pistol case
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/empty
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/empty/PopulateContents()
+ return
diff --git a/modular_skyrat/modules/modular_weapons/code/modular_projectiles.dm b/modular_skyrat/modules/modular_weapons/code/modular_projectiles.dm
index 16a9ba35f7f129..4a0625b491fe7a 100644
--- a/modular_skyrat/modules/modular_weapons/code/modular_projectiles.dm
+++ b/modular_skyrat/modules/modular_weapons/code/modular_projectiles.dm
@@ -167,149 +167,3 @@
damage = 8
fire_stacks = 1
wound_bonus = -90
-
-/*
-* 4.2x30mm
-*/
-
-/obj/item/ammo_casing/c42x30mm
- name = "4.2x30mm bullet casing"
- desc = "A 4.2x30mm bullet casing."
- caliber = CALIBER_42X30MM
- projectile_type = /obj/projectile/bullet/c42x30mm
-
-/obj/item/ammo_casing/c42x30mm/ap
- name = "4.2x30mm armor-piercing bullet casing"
- desc = "A 4.2x30mm armor-piercing bullet casing."
- projectile_type = /obj/projectile/bullet/c42x30mm/ap
- custom_materials = AMMO_MATS_AP
- advanced_print_req = TRUE
-
-/obj/item/ammo_casing/c42x30mm/inc
- name = "4.2x30mm incendiary bullet casing"
- desc = "A 4.2x30mm incendiary bullet casing."
- projectile_type = /obj/projectile/bullet/incendiary/c42x30mm
- custom_materials = AMMO_MATS_TEMP
- advanced_print_req = TRUE
-
-/obj/projectile/bullet/c42x30mm
- name = "4.2x30mm bullet"
- damage = 20
- wound_bonus = -5
- bare_wound_bonus = 5
- embed_falloff_tile = -4
-
-/obj/projectile/bullet/c42x30mm/ap
- name = "4.2x30mm armor-piercing bullet"
- damage = 15
- armour_penetration = 40
- embedding = null
-
-/obj/projectile/bullet/incendiary/c42x30mm
- name = "4.2x30mm incendiary bullet"
- damage = 10
- fire_stacks = 1
-
-/obj/projectile/bullet/c42x30mm_rubber
- name = "4.2x30mm rubber bullet"
- damage = 3
- stamina = 17
- ricochets_max = 6
- ricochet_incidence_leeway = 0
- ricochet_chance = 130
- ricochet_decay_damage = 0.7
- shrapnel_type = null
- sharpness = NONE
- embedding = null
- wound_bonus = -50
-
-/obj/item/ammo_casing/c42x30mm/rubber
- name = "4.2x30mm rubber bullet casing"
- desc = "A 4.2x30mm rubber bullet casing."
- projectile_type = /obj/projectile/bullet/c42x30mm_rubber
- harmful = FALSE
-
-/*
-* 12mm Magnum
-*/
-
-/obj/item/ammo_casing/c12mm
- name = "12mm Magnum bullet casing"
- desc = "A 12mm Magnum bullet casing."
- caliber = CALIBER_12MM
- projectile_type = /obj/projectile/bullet/c12mm
-
-/obj/item/ammo_casing/c12mm/ap
- name = "12mm Magnum armor-piercing bullet casing"
- desc = "A 12mm Magnum bullet casing with a titanium core."
- custom_materials = AMMO_MATS_AP
- projectile_type = /obj/projectile/bullet/c12mm/ap
- advanced_print_req = TRUE
-
-/obj/item/ammo_casing/c12mm/hp
- name = "12mm Magnum hollow-point bullet casing"
- desc = "A 12mm Magnum bullet casing with a hollow tip that fragments on contact."
- projectile_type = /obj/projectile/bullet/c12mm/hp
- advanced_print_req = TRUE
-
-/obj/item/ammo_casing/c12mm/fire
- name = "12mm Magnum incendiary bullet casing"
- desc = "A 12mm Magnum bullet casing with a magnesium coated tip meant for setting things on fire."
- custom_materials = AMMO_MATS_TEMP
- projectile_type = /obj/projectile/bullet/incendiary/c12mm
- advanced_print_req = TRUE
-
-/obj/item/ammo_casing/c12mm/rubber
- name = "12mm Magnum rubber bullet casing"
- desc = "A low powder load 12mm Magnum bullet casing with a flat rubber tip. Headshots heavily discouraged."
- projectile_type = /obj/projectile/bullet/c12mm/rubber
- harmful = FALSE
-
-/obj/projectile/bullet/c12mm
- name = "12mm bullet"
- damage = 40
-
-/obj/projectile/bullet/c12mm/ap
- name = "12mm armor-piercing bullet"
- damage = 37
- armour_penetration = 40
-
-/obj/projectile/bullet/c12mm/hp
- name = "12mm hollow-point bullet"
- damage = 60
- weak_against_armour = TRUE
-
-/obj/projectile/bullet/incendiary/c12mm
- name = "12mm incendiary bullet"
- damage = 20
- fire_stacks = 2
-
-/obj/projectile/bullet/c12mm/rubber
- name = "12mm Magnum rubber ball"
- damage = 10
- stamina = 40
- ricochets_max = 6
- ricochet_incidence_leeway = 0
- ricochet_chance = 130
- ricochet_decay_damage = 0.7
- shrapnel_type = null
- sharpness = NONE
- embedding = null
-
-/*
-* 6.8x43mm
-*/
-
-/obj/item/ammo_casing/a68
- name = "6.8mm bullet casing"
- desc = "A 6.8mm bullet casing."
- icon_state = "762-casing"
- caliber = CALIBER_A68
- projectile_type = /obj/projectile/bullet/a68
-
-/obj/projectile/bullet/a68
- name = "6.8 bullet"
- damage = 55
- armour_penetration = 10
- wound_bonus = -45
- wound_falloff_tile = 0
diff --git a/modular_skyrat/modules/sec_haul/code/guns/pepperball_gun.dm b/modular_skyrat/modules/modular_weapons/code/pepperball_gun.dm
similarity index 81%
rename from modular_skyrat/modules/sec_haul/code/guns/pepperball_gun.dm
rename to modular_skyrat/modules/modular_weapons/code/pepperball_gun.dm
index ffca51d4e676e3..e507cfe1a6aacd 100644
--- a/modular_skyrat/modules/sec_haul/code/guns/pepperball_gun.dm
+++ b/modular_skyrat/modules/modular_weapons/code/pepperball_gun.dm
@@ -1,7 +1,7 @@
/obj/item/gun/ballistic/automatic/pistol/pepperball
name = "\improper Bolt Pepperball AHG"
desc = "An incredibly mediocre 'firearm' designed to fire soft pepper balls meant to easily subdue targets."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/pepperball.dmi'
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/pepperball/pepperball.dmi'
icon_state = "peppergun"
w_class = WEIGHT_CLASS_NORMAL
accepted_magazine_type = /obj/item/ammo_box/magazine/pepperball
@@ -21,7 +21,7 @@
/obj/item/ammo_box/magazine/pepperball
name = "pistol magazine (pepperball)"
desc = "A gun magazine filled with balls."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/pepperball.dmi'
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/pepperball/pepperball.dmi'
icon_state = "pepperball"
ammo_type = /obj/item/ammo_casing/pepperball
caliber = CALIBER_PEPPERBALL
@@ -37,7 +37,7 @@
/obj/projectile/bullet/pepperball
name = "pepperball orb"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi'
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/pepperball/projectiles.dmi'
icon_state = "pepperball"
damage = 0
stamina = 5
@@ -67,8 +67,16 @@
/obj/item/ammo_box/advanced/pepperballs
name = "pepperball ammo box"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
+ icon = 'modular_skyrat/modules/modular_weapons/icons/obj/pepperball/ammoboxes.dmi'
icon_state = "box10x24"
ammo_type = /obj/item/ammo_casing/pepperball
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 3)
max_ammo = 15
+
+// Gunset for the pepperball pistol
+
+/obj/item/storage/toolbox/guncase/skyrat/pistol/pepperball
+ name = "Pepperball AHG gunset"
+
+ weapon_to_spawn = /obj/item/gun/ballistic/automatic/pistol/pepperball
+ extra_to_spawn = /obj/item/ammo_box/magazine/pepperball
diff --git a/modular_skyrat/modules/modular_weapons/code/pistol.dm b/modular_skyrat/modules/modular_weapons/code/pistol.dm
deleted file mode 100644
index 7333fa60324539..00000000000000
--- a/modular_skyrat/modules/modular_weapons/code/pistol.dm
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
-* PISTOLS
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_snub
- name = "CFA Snub"
- desc = "An easily-concealable pistol chambered for 4.2x30mm."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi'
- icon_state = "cfa-snub"
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_snub
- can_suppress = TRUE
- fire_sound_volume = 30
- w_class = WEIGHT_CLASS_SMALL
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_snub/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_snub/give_gun_safeties()
- return
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_snub/empty
- spawnwithmagazine = FALSE
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_ruby
- name = "CFA Ruby"
- desc = "A heavy-duty sidearm chambered in 12x27mm."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi'
- icon_state = "cfa_ruby"
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby
- can_suppress = FALSE
- fire_sound_volume = 120
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_ruby/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_ruby/give_gun_safeties()
- return
-
-/obj/item/gun/ballistic/automatic/pistol/cfa_ruby/empty
- spawnwithmagazine = FALSE
-
-/*
-* AMMO
-*/
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_snub
- name = "CFA Snub magazine (4.2x30mm)"
- desc = "An advanced magazine with smart type displays. Alt+click to reskin it."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/ammo.dmi'
- icon_state = "m42x30"
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_AP, AMMO_TYPE_RUBBER, AMMO_TYPE_INCENDIARY)
- ammo_type = /obj/item/ammo_casing/c42x30mm
- caliber = CALIBER_42X30MM
- max_ammo = 16
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_snub/ap
- ammo_type = /obj/item/ammo_casing/c42x30mm/ap
- round_type = AMMO_TYPE_AP
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_snub/rubber
- ammo_type = /obj/item/ammo_casing/c42x30mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_snub/incendiary
- ammo_type = /obj/item/ammo_casing/c42x30mm/inc
- round_type = AMMO_TYPE_INCENDIARY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_snub/empty
- start_empty = TRUE
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_ruby
- name = "CFA Ruby magazine (12mm Magnum)"
- desc = "An advanced magazine with smart type displays. Alt+click to reskin it."
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/ammo.dmi'
- icon_state = "m12mm"
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_AP, AMMO_TYPE_RUBBER, AMMO_TYPE_HOLLOWPOINT, AMMO_TYPE_INCENDIARY)
- ammo_type = /obj/item/ammo_casing/c12mm
- caliber = CALIBER_12MM
- max_ammo = 8
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/empty
- start_empty = TRUE
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/ap
- ammo_type = /obj/item/ammo_casing/c12mm/ap
- round_type = AMMO_TYPE_AP
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/rubber
- ammo_type = /obj/item/ammo_casing/c12mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/hp
- ammo_type = /obj/item/ammo_casing/c12mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/incendiary
- ammo_type = /obj/item/ammo_casing/c12mm/fire
- round_type = AMMO_TYPE_INCENDIARY
diff --git a/modular_skyrat/modules/modular_weapons/code/revolver.dm b/modular_skyrat/modules/modular_weapons/code/revolver.dm
deleted file mode 100644
index 669f874026f9c8..00000000000000
--- a/modular_skyrat/modules/modular_weapons/code/revolver.dm
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
-* REVOLVERS
-* Revolving rifles! We have three versions. An improvised slower firing one, a normal one, and a golden premium one.
-* The gold rifle uses .45, it's only 5 more points of damage unfortunately.
-* Fun hint: A box of .45 bullets functions the same as a speedloader.
-*/
-
-/obj/item/gun/ballistic/revolver/rifle
- name = "\improper .38 revolving rifle"
- desc = "A revolving rifle chambered in .38. "
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile40x32.dmi'
- icon_state = "revolving-rifle"
- accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/rev38 //This is just a detective's revolver but it's too big for bags..
- pixel_x = -4 // It's centred on a 40x32 pixel spritesheet.
- w_class = WEIGHT_CLASS_BULKY
- weapon_weight = WEAPON_HEAVY // The entire purpose of this is that it's a bulky rifle instead of a revolver.
- slot_flags = ITEM_SLOT_BELT
- inhand_x_dimension = 64
- inhand_y_dimension = 64
- lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/64x_guns_left.dmi'
- righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/64x_guns_right.dmi'
- inhand_icon_state = "revolving"
-
-/obj/item/gun/ballistic/revolver/rifle/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_IZHEVSK)
-
-/obj/item/gun/ballistic/revolver/rifle/gold
- name = "\improper .45 revolving rifle"
- desc = "A gold trimmed revolving rifle! It fires .45 bullets."
- icon_state = "revolving-rifle-gold"
- accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/rev45 //Gold! We're using .45 because TG's 10mm does 40 damage, this does 30.
- w_class = WEIGHT_CLASS_BULKY
- inhand_icon_state = "revolving_gold"
-
-// .45 Cylinder
-
-/obj/item/ammo_box/magazine/internal/cylinder/rev45
- name = "revolver .45 cylinder"
- ammo_type = /obj/item/ammo_casing/c45
- caliber = list(".45")
- max_ammo = 6
diff --git a/modular_skyrat/modules/modular_weapons/code/rifle.dm b/modular_skyrat/modules/modular_weapons/code/rifle.dm
deleted file mode 100644
index eb40dc2033037b..00000000000000
--- a/modular_skyrat/modules/modular_weapons/code/rifle.dm
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
-* CFA RIFLE
-*/
-
-/obj/item/gun/ballistic/automatic/cfa_rifle
- name = "Cantanheim 6.8mm rifle"
- desc = "A simple semi-automatic rifle chambered in 6.8mm. The letters 'XJP' are crossed out in the receiver." //Different 6.8mm than the FTU's propietary pulse ballistics
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile40x32.dmi'
- icon_state = "cfa_rifle"
- inhand_icon_state = "irifle"
- inhand_x_dimension = 64
- inhand_y_dimension = 64
- lefthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/64x_guns_left.dmi'
- righthand_file = 'modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/64x_guns_right.dmi'
- worn_icon_state = "gun"
- accepted_magazine_type = /obj/item/ammo_box/magazine/cm68
- fire_delay = 5
- can_suppress = FALSE
- burst_size = 1
- actions_types = list()
- mag_display = FALSE
- mag_display_ammo = FALSE
- empty_indicator = FALSE
- recoil = 1
- weapon_weight = WEAPON_HEAVY
- pixel_x = -8
- w_class = WEIGHT_CLASS_BULKY
-
-/obj/item/gun/ballistic/automatic/cfa_rifle/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/scope, range_modifier = 1.5)
-
-/obj/item/gun/ballistic/automatic/cfa_rifle/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/cfa_rifle/give_gun_safeties()
- return
-
-/obj/item/gun/ballistic/automatic/cfa_rifle/empty
- spawnwithmagazine = FALSE
-
-/obj/item/ammo_box/magazine/cm68
- name = "rifle magazine (6.8mm)"
- icon = 'modular_skyrat/modules/modular_weapons/icons/obj/ammo.dmi'
- icon_state = "6.8"
- ammo_type = /obj/item/ammo_casing/a68
- caliber = CALIBER_A68
- max_ammo = 10
- multiple_sprites = 2
-
-/obj/item/ammo_box/magazine/cm68/empty
- start_empty = 1
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_lefthand.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_lefthand.dmi
new file mode 100644
index 00000000000000..b5f9ad7b01f280
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_lefthand.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_righthand.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_righthand.dmi
new file mode 100644
index 00000000000000..ae79418dfffbd9
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_righthand.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_worn.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_worn.dmi
new file mode 100644
index 00000000000000..d0a854cf3030e2
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/mob/company_and_or_faction_based/carwo_defense_systems/guns_worn.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_lefthand.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_lefthand.dmi
new file mode 100644
index 00000000000000..c9fac088741be1
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_lefthand.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_righthand.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_righthand.dmi
new file mode 100644
index 00000000000000..56a5d535c0d355
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/mob/inhands/cases_righthand.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/swords_righthand.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/swords_righthand.dmi
index 8cd99c88bcee5e..96e2c1f5d978b3 100644
Binary files a/modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/swords_righthand.dmi and b/modular_skyrat/modules/modular_weapons/icons/mob/inhands/weapons/swords_righthand.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/mob/worn/cases.dmi b/modular_skyrat/modules/modular_weapons/icons/mob/worn/cases.dmi
new file mode 100644
index 00000000000000..f029c2956292f4
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/mob/worn/cases.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi
new file mode 100644
index 00000000000000..cb733806546f30
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/ammo.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns32x.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns32x.dmi
new file mode 100644
index 00000000000000..c74c79b6e8cee0
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns32x.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns48x.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns48x.dmi
new file mode 100644
index 00000000000000..9131e6d19bb32c
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/guns48x.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/propaganda.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/propaganda.dmi
new file mode 100644
index 00000000000000..9438b887628ccc
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/carwo_defense_systems/propaganda.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/cases.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/cases.dmi
index 238153cd32bd1c..d639b8b4d861fc 100644
Binary files a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/cases.dmi and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/cases.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/ammo.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/ammo.dmi
new file mode 100644
index 00000000000000..89e7d90f56ddd3
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/ammo.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi
new file mode 100644
index 00000000000000..067d79a810b5c4
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/guns32x.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/propaganda.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/propaganda.dmi
new file mode 100644
index 00000000000000..4ddf2216e7c186
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/icons/obj/company_and_or_faction_based/trappiste_fabriek/propaganda.dmi differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi
deleted file mode 100644
index 410bb55110911d..00000000000000
Binary files a/modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile40x32.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile40x32.dmi
deleted file mode 100644
index 2e3c0b4e5a31d4..00000000000000
Binary files a/modular_skyrat/modules/modular_weapons/icons/obj/guns/projectile40x32.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi
index bc5b963bd5c67f..098a8f304816c0 100644
Binary files a/modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi and b/modular_skyrat/modules/modular_weapons/icons/obj/gunsets.dmi differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/pepperball/ammoboxes.dmi
similarity index 100%
rename from modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi
rename to modular_skyrat/modules/modular_weapons/icons/obj/pepperball/ammoboxes.dmi
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/pepperball.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/pepperball/pepperball.dmi
similarity index 100%
rename from modular_skyrat/modules/sec_haul/icons/guns/pepperball.dmi
rename to modular_skyrat/modules/modular_weapons/icons/obj/pepperball/pepperball.dmi
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi b/modular_skyrat/modules/modular_weapons/icons/obj/pepperball/projectiles.dmi
similarity index 100%
rename from modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi
rename to modular_skyrat/modules/modular_weapons/icons/obj/pepperball/projectiles.dmi
diff --git a/modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg b/modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg
new file mode 100644
index 00000000000000..d5a8562f45f51f
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/grenade_burst.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/grenade_launcher.ogg b/modular_skyrat/modules/modular_weapons/sounds/grenade_launcher.ogg
new file mode 100644
index 00000000000000..5ddb76de7d06b7
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/grenade_launcher.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/laser_fire.ogg b/modular_skyrat/modules/modular_weapons/sounds/laser_fire.ogg
deleted file mode 100644
index 7b003b8cc8b63f..00000000000000
Binary files a/modular_skyrat/modules/modular_weapons/sounds/laser_fire.ogg and /dev/null differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/pistol_heavy.ogg b/modular_skyrat/modules/modular_weapons/sounds/pistol_heavy.ogg
new file mode 100644
index 00000000000000..34e0412f823d03
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/pistol_heavy.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/pistol_light.ogg b/modular_skyrat/modules/modular_weapons/sounds/pistol_light.ogg
new file mode 100644
index 00000000000000..cd014f5eda8a0d
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/pistol_light.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/revolver_heavy.ogg b/modular_skyrat/modules/modular_weapons/sounds/revolver_heavy.ogg
new file mode 100644
index 00000000000000..d02d1c750c24d8
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/revolver_heavy.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/rifle_heavy.ogg b/modular_skyrat/modules/modular_weapons/sounds/rifle_heavy.ogg
new file mode 100644
index 00000000000000..3ad9d60c9013c0
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/rifle_heavy.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/shotgun_heavy.ogg b/modular_skyrat/modules/modular_weapons/sounds/shotgun_heavy.ogg
new file mode 100644
index 00000000000000..2de9e77bf08589
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/shotgun_heavy.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/shotgun_rack.ogg b/modular_skyrat/modules/modular_weapons/sounds/shotgun_rack.ogg
new file mode 100644
index 00000000000000..2d68a21e5df1fa
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/shotgun_rack.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/smg_light.ogg b/modular_skyrat/modules/modular_weapons/sounds/smg_light.ogg
new file mode 100644
index 00000000000000..d34cb9440ca457
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/smg_light.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/suppressed_heavy.ogg b/modular_skyrat/modules/modular_weapons/sounds/suppressed_heavy.ogg
new file mode 100644
index 00000000000000..b7b721b76665f6
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/suppressed_heavy.ogg differ
diff --git a/modular_skyrat/modules/modular_weapons/sounds/suppressed_rifle.ogg b/modular_skyrat/modules/modular_weapons/sounds/suppressed_rifle.ogg
new file mode 100644
index 00000000000000..06aba901c0642b
Binary files /dev/null and b/modular_skyrat/modules/modular_weapons/sounds/suppressed_rifle.ogg differ
diff --git a/modular_skyrat/modules/mold/code/mold_mobs.dm b/modular_skyrat/modules/mold/code/mold_mobs.dm
index 59b1db554628ef..c7ba1226435e34 100644
--- a/modular_skyrat/modules/mold/code/mold_mobs.dm
+++ b/modular_skyrat/modules/mold/code/mold_mobs.dm
@@ -15,6 +15,7 @@
icon = 'modular_skyrat/modules/mold/icons/blob_mobs.dmi'
gold_core_spawnable = NO_SPAWN
faction = list(FACTION_MOLD)
+ basic_mob_flags = DEL_ON_DEATH
/**
* OIL SHAMBLERS
@@ -69,6 +70,17 @@
SSvis_overlays.add_vis_overlay(src, icon, OIL_SHAMBLER_OVERLAY, layer, plane, dir, alpha)
SSvis_overlays.add_vis_overlay(src, icon, OIL_SHAMBLER_OVERLAY, OIL_SHAMBLER_OVERLAY_LAYER, EMISSIVE_PLANE, dir, alpha)
+/mob/living/basic/mold/oil_shambler/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if(!isliving(target))
+ return
+
+ var/mob/living/ignite_target = target
+ if(prob(ignite_chance))
+ ignite_target.adjust_fire_stacks(additional_fire_stacks)
+
+ ignite_target.ignite_mob()
+
/datum/ai_controller/basic_controller/oil_shambler
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
@@ -79,7 +91,7 @@
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/oil_shambler,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/oil_shambler,
)
@@ -88,23 +100,6 @@
emote_hear = list("bubbles.", "crackles.", "groans.")
emote_see = list("bubbles.")
-/datum/ai_planning_subtree/basic_melee_attack_subtree/oil_shambler
- melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/oil_shambler
-
-/datum/ai_behavior/basic_melee_attack/oil_shambler/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
- . = ..()
- var/atom/target = controller.blackboard[target_key]
- var/mob/living/basic/mold/oil_shambler/oil_shambler = controller.pawn
-
- if(!isliving(target))
- return
-
- var/mob/living/ignite_target = target
- if(prob(oil_shambler.ignite_chance))
- ignite_target.adjust_fire_stacks(oil_shambler.additional_fire_stacks)
-
- if(ignite_target.fire_stacks)
- ignite_target.ignite_mob()
/**
* DISEASE MOLD
@@ -139,6 +134,17 @@
/// The disease given on melee attacks
var/datum/disease/given_disease = /datum/disease/cryptococcus
+/mob/living/basic/mold/diseased_rat/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ . = ..()
+
+ if(!isliving(target))
+ return
+
+ var/mob/living/carbon/disease_target = target
+ if(can_inject(disease_target))
+ to_chat(disease_target, span_danger("[src] manages to penetrate your clothing with its teeth!"))
+ disease_target.ForceContractDisease(new given_disease(), FALSE, TRUE)
+
/datum/ai_controller/basic_controller/diseased_rat
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
@@ -149,7 +155,7 @@
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/diseased_rat,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/diseased_rat,
)
@@ -158,21 +164,6 @@
emote_hear = list("squeaks.", "gnashes.", "hisses.")
emote_see = list("drools.")
-/datum/ai_planning_subtree/basic_melee_attack_subtree/diseased_rat
- melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/diseased_rat
-
-/datum/ai_behavior/basic_melee_attack/diseased_rat/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
- . = ..()
- var/atom/target = controller.blackboard[target_key]
- var/mob/living/basic/mold/diseased_rat/diseased_rat = controller.pawn
-
- if(!isliving(target))
- return
-
- var/mob/living/carbon/disease_target = target
- if(diseased_rat.can_inject(disease_target))
- to_chat(disease_target, span_danger("[diseased_rat] manages to penetrate your clothing with its teeth!"))
- disease_target.ForceContractDisease(new diseased_rat.given_disease(), FALSE, TRUE)
/**
* ELECTRIC MOLD
@@ -202,6 +193,15 @@
pass_flags = PASSTABLE
+ /// What the mob injects per bite
+ var/inject_reagent = /datum/reagent/teslium
+ /// How many units to inject per bite
+ var/inject_amount = 2
+
+/mob/living/basic/mold/electric_mosquito/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/venomous, inject_reagent, inject_amount)
+
/datum/ai_controller/basic_controller/electric_mosquito
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
@@ -212,7 +212,7 @@
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/electric_mosquito,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/electric_mosquito,
)
@@ -221,24 +221,11 @@
emote_hear = list("zaps.", "buzzes.", "crackles.")
emote_see = list("arcs.")
-/datum/ai_planning_subtree/basic_melee_attack_subtree/electric_mosquito
- melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/electric_mosquito
-
-/datum/ai_behavior/basic_melee_attack/electric_mosquito/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
- . = ..()
- var/atom/target = controller.blackboard[target_key]
-
- if(!iscarbon(target))
- return
-
- var/mob/living/carbon/shock_target = target
- shock_target.reagents.add_reagent(/datum/reagent/teslium, 2)
-
/**
* RADIATION MOLD
*
* Weird centipede things that spawn with a rad mold
- * They have a chance to irradiate their target on hit, as well as splashing mutagen on death
+ * They have a chance to irradiate their target on hit
*/
/mob/living/basic/mold/centaur
name = "centaur"
@@ -271,21 +258,21 @@
/// The chance to irradiate on hit
var/irradiate_chance = 20
- /// The chem to splash on death
- var/death_chem = /datum/reagent/toxin/mutagen
/mob/living/basic/mold/centaur/Initialize(mapload)
. = ..()
update_overlays()
-/mob/living/basic/mold/centaur/death(gibbed)
- visible_message(span_warning("[src] ruptures!"))
- var/datum/reagents/reagent_spawn = new /datum/reagents(300)
- reagent_spawn.my_atom = src
- reagent_spawn.add_reagent(death_chem, 20)
- chem_splash(loc, null, CENTAUR_DEATH_SPLASH_RANGE, list(reagent_spawn))
- playsound(src, 'sound/effects/splat.ogg', CENTAUR_DEATH_SPLAT_VOLUME, TRUE)
- return ..()
+/mob/living/basic/mold/centaur/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ . = ..()
+
+ if(!isliving(target))
+ return
+
+ var/mob/living/radiation_target = target
+ if(prob(irradiate_chance))
+ radiation_pulse(radiation_target, CENTAUR_RAD_PULSE_RANGE, CENTAUR_RAD_PULSE_THRESHOLD, FALSE, TRUE)
+ playsound(src, 'modular_skyrat/modules/horrorform/sound/horror_scream.ogg', CENTAUR_ATTACK_SCREAM_VOLUME, TRUE)
/datum/ai_controller/basic_controller/centaur
blackboard = list(
@@ -297,7 +284,7 @@
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/centaur,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/centaur,
)
@@ -306,21 +293,6 @@
emote_hear = list("chitters.", "groans.", "wails.")
emote_see = list("writhes.")
-/datum/ai_planning_subtree/basic_melee_attack_subtree/centaur
- melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/centaur
-
-/datum/ai_behavior/basic_melee_attack/centaur/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
- . = ..()
- var/atom/target = controller.blackboard[target_key]
- var/mob/living/basic/mold/centaur/centaur = controller.pawn
-
- if(!isliving(target))
- return
-
- var/mob/living/radiation_target = target
- if(prob(centaur.irradiate_chance))
- radiation_pulse(radiation_target, CENTAUR_RAD_PULSE_RANGE, CENTAUR_RAD_PULSE_THRESHOLD, FALSE, TRUE)
- playsound(src, 'modular_skyrat/modules/horrorform/sound/horror_scream.ogg', CENTAUR_ATTACK_SCREAM_VOLUME, TRUE)
#undef OIL_SHAMBLER_OVERLAY
diff --git a/modular_skyrat/modules/nanotrasen_naval_command/code/outfits.dm b/modular_skyrat/modules/nanotrasen_naval_command/code/outfits.dm
index ff6662d210d9b7..bca8833da461f9 100644
--- a/modular_skyrat/modules/nanotrasen_naval_command/code/outfits.dm
+++ b/modular_skyrat/modules/nanotrasen_naval_command/code/outfits.dm
@@ -29,7 +29,10 @@
gloves = /obj/item/clothing/gloves/tackler/combat/insulated
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/lieutenant
name = "Nanotrasen Naval Command - Lieutenant"
@@ -44,7 +47,10 @@
gloves = /obj/item/clothing/gloves/combat/naval
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/lieutenant_commander
name = "Nanotrasen Naval Command - Lieutenant Commander"
@@ -61,7 +67,10 @@
gloves = /obj/item/clothing/gloves/combat/naval
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/commander
name = "Nanotrasen Naval Command - Commander"
@@ -78,7 +87,10 @@
gloves = /obj/item/clothing/gloves/combat/naval
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/captain
name = "Nanotrasen Naval Command - Captain"
@@ -95,7 +107,10 @@
gloves = /obj/item/clothing/gloves/combat/naval
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/rear_admiral
name = "Nanotrasen Naval Command - Rear Admiral"
@@ -108,7 +123,10 @@
gloves = /obj/item/clothing/gloves/combat/naval
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh_captain)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/admiral
name = "Nanotrasen Naval Command - Admiral"
@@ -123,7 +141,10 @@
gloves = /obj/item/clothing/gloves/combat/naval
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh_captain)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
/datum/outfit/centcom/naval/fleet_admiral
name = "Nanotrasen Naval Command - Fleet Admiral"
@@ -138,4 +159,7 @@
gloves = /obj/item/clothing/gloves/combat/naval/fleet_admiral
- backpack_contents = list(/obj/item/storage/box/survival/security, /obj/item/storage/box/gunset/pdh_corpo)
+ backpack_contents = list(
+ /obj/item/storage/box/survival/security,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
+ )
diff --git a/modular_skyrat/modules/nanotrasen_rep/code/m45a5.dm b/modular_skyrat/modules/nanotrasen_rep/code/m45a5.dm
deleted file mode 100644
index 8d14eab2b2fc97..00000000000000
--- a/modular_skyrat/modules/nanotrasen_rep/code/m45a5.dm
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-/obj/item/gun/ballistic/automatic/pistol/m45a5
- name = "\improper M45A5 Elite"
- desc = "A hand-assembled custom sporting handgun by Alpha Centauri Armories, chambered in .460 Rowland magnum. This model has a highly modular structure, to acommodate for ammo costs."
- icon = 'modular_skyrat/modules/blueshield/icons/M45A5.dmi'
- icon_state = "m45a5"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/m45a5
- can_suppress = FALSE
- fire_delay = 4.25 //Originally 1.75 which was unintentionally extremely fast.
- fire_sound_volume = 60
- spread = 2
- force = 8 //There's heavier guns that dealt less damage on melee than this so we're reducing it from the original 12
- recoil = 0
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/dp_fire.ogg'
-
-/obj/item/gun/ballistic/automatic/pistol/m45a5/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/ammo_box/magazine/m45a5
- name = "ACA modular magazine"
- desc = "A magazine able to chamber .460 Rowland Magnun. Made for the M45A5, as it's the only available sidearm with a smart multi-caliber mechanism."
- icon = 'modular_skyrat/modules/blueshield/icons/M45A5.dmi'
- icon_state = "rowlandmodular"
- ammo_type = /obj/item/ammo_casing/b460
- caliber = CALIBER_460
- max_ammo = 8 //Previously 15, then previously 10. Locked to 8, as you can now restock .460 Magnum which was unintended due to its strong nature.
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_casing/b460
- name = ".460 Rowland Magnum bullet casing"
- desc = "A .460 Rowland magnum casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sl-casing"
- caliber = CALIBER_460
- projectile_type = /obj/projectile/bullet/b460
-
-/obj/projectile/bullet/b460
- name = ".460 RM JHP bullet"
- damage = 30
- wound_bonus = 30
- weak_against_armour = TRUE
- speed = 2 //Previously 2.25. Now compensates for ammo count.
-
diff --git a/modular_skyrat/modules/nanotrasen_rep/code/nanotrasen_consultant.dm b/modular_skyrat/modules/nanotrasen_rep/code/nanotrasen_consultant.dm
index b9126f3a3a5940..02524eafb6fa67 100644
--- a/modular_skyrat/modules/nanotrasen_rep/code/nanotrasen_consultant.dm
+++ b/modular_skyrat/modules/nanotrasen_rep/code/nanotrasen_consultant.dm
@@ -54,7 +54,7 @@
head = /obj/item/clothing/head/nanotrasen_consultant
backpack_contents = list(
/obj/item/melee/baton/telescopic = 1,
- /obj/item/storage/box/gunset/nanotrasen_consultant = 1,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild = 1,
)
skillchips = list(/obj/item/skillchip/disk_verifier)
@@ -111,21 +111,6 @@
inserted_item = /obj/item/pen/fountain/captain
greyscale_colors = "#017941#0060b8"
-/obj/item/storage/box/gunset/nanotrasen_consultant
- name = "M45A5 gunset"
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/m45a5/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/nanotrasen_consultant/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/m45a5/nomag(src)
- new /obj/item/ammo_box/magazine/m45a5(src)
- new /obj/item/ammo_box/magazine/m45a5(src)
- new /obj/item/ammo_box/magazine/m45a5(src)
- new /obj/item/ammo_box/magazine/m45a5(src)
-
/obj/item/storage/bag/garment/nanotrasen_consultant
name = "Nanotrasen consultant's garment bag"
desc = "A bag for storing extra clothes and shoes. This one belongs to the Nanotrasen consultant."
diff --git a/modular_skyrat/modules/novaya_ert/code/advanced_choice_beacon.dm b/modular_skyrat/modules/novaya_ert/code/advanced_choice_beacon.dm
index 094970ccd55181..0003bed02e2a36 100644
--- a/modular_skyrat/modules/novaya_ert/code/advanced_choice_beacon.dm
+++ b/modular_skyrat/modules/novaya_ert/code/advanced_choice_beacon.dm
@@ -121,7 +121,7 @@
icon = 'modular_skyrat/modules/novaya_ert/icons/turret_deployable.dmi'
icon_state = "living"
base_icon_state = "living"
- stun_projectile = /obj/projectile/bullet/b12mm/rubber
+ stun_projectile = /obj/projectile/bullet/a762x39/rubber
lethal_projectile = /obj/projectile/bullet/a762x39
max_integrity = 150
req_access = list(ACCESS_CENT_GENERAL)
diff --git a/modular_skyrat/modules/novaya_ert/code/automatic.dm b/modular_skyrat/modules/novaya_ert/code/automatic.dm
index a3ed276527932a..64e2637edd58b8 100644
--- a/modular_skyrat/modules/novaya_ert/code/automatic.dm
+++ b/modular_skyrat/modules/novaya_ert/code/automatic.dm
@@ -12,7 +12,7 @@
icon_state = "nri_smg"
inhand_icon_state = "nri_smg"
w_class = WEIGHT_CLASS_BULKY
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx
+ accepted_magazine_type = /obj/item/ammo_box/magazine/uzim9mm
fire_delay = 1
burst_size = 5
dual_wield_spread = 5
@@ -26,15 +26,16 @@
/obj/item/gun/ballistic/automatic/nri_smg/give_manufacturer_examine()
AddElement(/datum/element/manufacturer_examine, COMPANY_IZHEVSK)
-/obj/item/gun/ballistic/automatic/pistol/ladon/nri
+/obj/item/gun/ballistic/automatic/pistol/nri
name = "\improper Szabo-Ivanek service pistol"
- desc = "A mass produced NRI-made modified reproduction of the PDH-6 line of handguns rechambered in 9×25mm.\
+ desc = "A mass produced NRI-made modified reproduction of the Wespe line of handguns rechambered in 9×25mm.\
'PATRIOT DEFENSE SYSTEMS' is inscribed on the receiver, indicating it's been made with a plasteel printer."
icon = 'modular_skyrat/modules/novaya_ert/icons/pistol.dmi'
+ icon_state = "ladon"
w_class = WEIGHT_CLASS_SMALL
accepted_magazine_type = /obj/item/ammo_box/magazine/m9mm_aps
burst_size = 3
fire_delay = 3
-/obj/item/gun/ballistic/automatic/pistol/ladon/nri/give_manufacturer_examine()
+/obj/item/gun/ballistic/automatic/pistol/nri/give_manufacturer_examine()
AddElement(/datum/element/manufacturer_examine, COMPANY_IZHEVSK)
diff --git a/modular_skyrat/modules/novaya_ert/code/belt.dm b/modular_skyrat/modules/novaya_ert/code/belt.dm
index 02cedded45f8db..cb34dbe63fdd2d 100644
--- a/modular_skyrat/modules/novaya_ert/code/belt.dm
+++ b/modular_skyrat/modules/novaya_ert/code/belt.dm
@@ -52,7 +52,7 @@
/obj/item/storage/belt/military/nri/medic/full/PopulateContents()
generate_items_inside(list(
- /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx = 4,
+ /obj/item/ammo_box/magazine/uzim9mm = 4,
/obj/item/knife/combat = 1,
/obj/item/grenade/smokebomb = 1,
/obj/item/grenade/frag = 1,
@@ -60,7 +60,7 @@
/obj/item/storage/belt/military/nri/engineer/full/PopulateContents()
generate_items_inside(list(
- /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx = 4,
+ /obj/item/ammo_box/magazine/uzim9mm = 4,
/obj/item/knife/combat = 1,
/obj/item/grenade/smokebomb = 1,
/obj/item/grenade/frag = 1,
diff --git a/modular_skyrat/modules/novaya_ert/code/outfit.dm b/modular_skyrat/modules/novaya_ert/code/outfit.dm
index a88805933afb9c..35b90414bd0ee4 100644
--- a/modular_skyrat/modules/novaya_ert/code/outfit.dm
+++ b/modular_skyrat/modules/novaya_ert/code/outfit.dm
@@ -17,7 +17,7 @@
/obj/item/beamout_tool,
/obj/item/crucifix,
/obj/item/reagent_containers/cup/glass/waterbottle/large/cryptobiolin)
- l_pocket = /obj/item/gun/ballistic/automatic/pistol/ladon/nri
+ l_pocket = /obj/item/gun/ballistic/automatic/pistol/nri
r_pocket = /obj/item/ammo_box/magazine/m9mm_aps
shoes = /obj/item/clothing/shoes/combat
@@ -157,7 +157,7 @@
belt = /obj/item/clipboard
back = /obj/item/storage/backpack/satchel/leather
backpack_contents = list(/obj/item/storage/box/nri_survival_pack,
- /obj/item/gun/ballistic/automatic/pistol/ladon/nri,
+ /obj/item/gun/ballistic/automatic/pistol/nri,
/obj/item/ammo_box/magazine/m9mm_aps,
/obj/item/ammo_box/magazine/m9mm_aps,
/obj/item/storage/medkit/expeditionary,
diff --git a/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm b/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm
index 5e40743aa8e62d..bcc8b49c26d503 100644
--- a/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm
+++ b/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm
@@ -59,7 +59,7 @@
weak_against_armour = TRUE
/obj/projectile/beam/laser/plasma_glob/on_hit(atom/target, blocked)
- if(istype(target, /obj/structure/blob) || istype(target, /mob/living/simple_animal/hostile/blob))
+ if(istype(target, /obj/structure/blob) || HAS_TRAIT(target, TRAIT_BLOB_ALLY))
damage = damage * 0.75
return ..()
diff --git a/modular_skyrat/modules/novaya_ert/code/toolbox.dm b/modular_skyrat/modules/novaya_ert/code/toolbox.dm
index 19dde9e7ef2253..77ad7051a52e2a 100644
--- a/modular_skyrat/modules/novaya_ert/code/toolbox.dm
+++ b/modular_skyrat/modules/novaya_ert/code/toolbox.dm
@@ -48,15 +48,9 @@
desc = "It contains a few magazines."
ammo_type = /obj/item/ammo_box/magazine/akm/ap
-/obj/item/storage/toolbox/ammobox/full/bison
- name = "ammo box (PP-95)"
- desc = "It contains a few magazines."
- ammo_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx
- amount = 4
-
/obj/item/storage/toolbox/ammobox/full/nri_smg
name = "ammo box (QLP/04)"
- ammo_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx
+ ammo_type = /obj/item/ammo_box/magazine/uzim9mm
amount = 7
/obj/item/storage/toolbox/ammobox/full/l6_saw
@@ -65,12 +59,6 @@
ammo_type = /obj/item/ammo_box/magazine/m7mm
amount = 7
-/obj/item/storage/toolbox/ammobox/full/makarov
- name = "ammo box (R-C Makarov)"
- desc = "It contains a few magazines."
- ammo_type = /obj/item/ammo_box/magazine/multi_sprite/makarov
- amount = 7
-
/obj/item/storage/toolbox/ammobox/full/aps
name = "ammo box (Szabo-Ivanek/APS)"
desc = "It contains a few magazines."
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/ammo.dm b/modular_skyrat/modules/opposing_force/code/equipment/ammo.dm
index 687e74fe581db0..cbdbc89bc1923c 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/ammo.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/ammo.dm
@@ -33,13 +33,13 @@
item_type = /obj/item/ammo_box/magazine/uzim9mm
description = "A thirty-two round magazine for the mini uzi. Uses 9x19mm ammunition."
-/datum/opposing_force_equipment/ammo/lynx
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_lynx
- description = "A forty round magazine for CFA Lynx and QLP/04 submachine guns."
+/datum/opposing_force_equipment/ammo/sol40standard
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/standard
+ description = "A thirty round magazine for any SolFed rifle."
-/datum/opposing_force_equipment/ammo/cm68
- item_type = /obj/item/ammo_box/magazine/cm68
- description = "6.8mm bullets in a ten round magazine for a Cantanheim 6.8 rifle."
+/datum/opposing_force_equipment/ammo/sol40drum
+ item_type = /obj/item/ammo_box/magazine/c40sol_rifle/drum
+ description = "A sixty round drum for any SolFed rifle."
/datum/opposing_force_equipment/ammo/makarov
item_type = /obj/item/ammo_box/magazine/m9mm
@@ -81,144 +81,14 @@
item_type = /obj/item/ammo_box/a357/match
description = "A seven-round .357 magnum speedloader for a revolver, loaded with match-grade ammunition that bounces off walls several times."
-/datum/opposing_force_equipment/ammo/cfa_snub
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_snub
+/datum/opposing_force_equipment/ammo/sol35_standard
+ item_type = /obj/item/ammo_box/magazine/c35sol_pistol
-/datum/opposing_force_equipment/ammo/cfa_snubap
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_snub/ap
+/datum/opposing_force_equipment/ammo/sol35_extended
+ item_type = /obj/item/ammo_box/magazine/c35sol_pistol/stendo
-/datum/opposing_force_equipment/ammo/cfa_snubrubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_snub/rubber
-
-/datum/opposing_force_equipment/ammo/cfa_snubincendiary
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_snub/incendiary
-
-/datum/opposing_force_equipment/ammo/cfa_ruby
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby
-
-/datum/opposing_force_equipment/ammo/cfa_rubyap
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/ap
-
-/datum/opposing_force_equipment/ammo/cfa_rubyrubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/rubber
-
-/datum/opposing_force_equipment/ammo/cfa_rubyhp
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/hp
-
-/datum/opposing_force_equipment/ammo/cfa_rubyincendiary
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_ruby/incendiary
-
-/datum/opposing_force_equipment/ammo/wildcat
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat
-
-/datum/opposing_force_equipment/ammo/wildcatap
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/ap
-
-/datum/opposing_force_equipment/ammo/wildcatrubber
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/rubber
-
-/datum/opposing_force_equipment/ammo/wildcatincendiary
- item_type = /obj/item/ammo_box/magazine/multi_sprite/cfa_wildcat/incendiary
-
-/datum/opposing_force_equipment/ammo/glock17
- name = "Glock 17 Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g17
-
-/datum/opposing_force_equipment/ammo/glock17hp
- name = "Glock 17 Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g17/hp
-
-/datum/opposing_force_equipment/ammo/glock18
- name = "Glock 18 Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g18
-
-/datum/opposing_force_equipment/ammo/glock18hp
- name = "Glock 18 Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g18/hp
-
-/datum/opposing_force_equipment/ammo/ladon
- name = "P-3 'Ladon' Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/ladon
-
-/datum/opposing_force_equipment/ammo/ladonhp
- name = "P-3 'Ladon' Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/ladon/hp
-
-/datum/opposing_force_equipment/ammo/dozer
- name = "DZR-9 'Dozer' Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/dozer
-
-/datum/opposing_force_equipment/ammo/dozerhp
- name = "DZR-9 'Dozer' Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/dozer/hp
-
-/datum/opposing_force_equipment/ammo/pdh
- name = "PDH-6H 'Peacekeeper' Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/pdh
-
-/datum/opposing_force_equipment/ammo/pdhhp
- name = "PDH-6H 'Peacekeeper' Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/pdh/hp
-
-/datum/opposing_force_equipment/ammo/mk58
- name = "MK-58 Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/mk58
-
-/datum/opposing_force_equipment/ammo/mk58hp
- name = "MK-58 Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/mk58/hp
-
-/datum/opposing_force_equipment/ammo/croon
- name = "DT-4 'Croon' Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/croon
-
-/datum/opposing_force_equipment/ammo/zeta
- name = "Zeta-6 'Spurchamber' Speedloader"
- item_type = /obj/item/ammo_box/revolver/zeta/full
-
-/datum/opposing_force_equipment/ammo/revolution
- name = "Revolution-8 'Spurmaster' Speedloader"
- item_type = /obj/item/ammo_box/revolver/revolution/full
-
-/datum/opposing_force_equipment/ammo/g11
- name = "G11 K-490 Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g11
-
-/datum/opposing_force_equipment/ammo/g11hp
- name = "G11 K-490 Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/g11/hp
-
-/datum/opposing_force_equipment/ammo/vintorez
- name = "PCR Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/vintorez
-
-/datum/opposing_force_equipment/ammo/vintorezhp
- name = "PCR Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/vintorez/hp
-
-/datum/opposing_force_equipment/ammo/pcr
- name = "PCR Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/pcr
-
-/datum/opposing_force_equipment/ammo/pcrhp
- name = "PCR Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/pcr/hp
-
-/datum/opposing_force_equipment/ammo/pitbull
- name = "Pitbull PDW Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/pitbull
-
-/datum/opposing_force_equipment/ammo/pitbullhp
- name = "Pitbull PDW Magazine (HP)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/pitbull/hp
-
-/datum/opposing_force_equipment/ammo/ostwind
- name = "DTR-6 Rifle Magazine"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/ostwind
-
-/datum/opposing_force_equipment/ammo/ostwindfrag
- name = "DTR-6 Rifle Magazine (Frag)"
- item_type = /obj/item/ammo_box/magazine/multi_sprite/ostwind/ihdf
+/datum/opposing_force_equipment/ammo/trappiste585_standard
+ item_type = /obj/item/ammo_box/magazine/c585trappiste_pistol
/datum/opposing_force_equipment/ammo/bulldog
name = "Bulldog Magazine"
@@ -252,7 +122,3 @@
/datum/opposing_force_equipment/ammo/c20rfire
name = "C-20r Magazine (Incendiary)"
item_type = /obj/item/ammo_box/magazine/smgm45/incen
-
-/datum/opposing_force_equipment/ammo/pepperball
- name = "Pepperball Magazine"
- item_type = /obj/item/ammo_box/magazine/pepperball
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/clothing.dm b/modular_skyrat/modules/opposing_force/code/equipment/clothing.dm
index 1756407a2b97bd..d397c756778d1c 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/clothing.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/clothing.dm
@@ -86,7 +86,7 @@
/datum/opposing_force_equipment/clothing/nukiemod
name = "Blood-Red MODsuit"
- item_type = /obj/item/mod/control/pre_equipped/nuclear
+ item_type = /obj/item/mod/control/pre_equipped/nuclear/unrestricted
description = "A suit designed by Gorlex Marauders, offering armor ruled illegal in most of Spinward Stellar."
/datum/opposing_force_equipment/clothing/elitemod
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/pistols.dm b/modular_skyrat/modules/opposing_force/code/equipment/pistols.dm
index 88abe0e68b14ce..8d77b5c37a3626 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/pistols.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/pistols.dm
@@ -17,7 +17,7 @@
admin_note = "WARNING: Roughly on-par with the .357, can use AP rounds."
/datum/opposing_force_equipment/pistol/nri_pistol
- item_type = /obj/item/gun/ballistic/automatic/pistol/ladon/nri
+ item_type = /obj/item/gun/ballistic/automatic/pistol/nri
admin_note = "WARNING: Roughly on-par with the .357, can use AP rounds. Essentially a reflavored APS."
/datum/opposing_force_equipment/pistol/g357
@@ -33,36 +33,5 @@
item_type = /obj/item/gun/ballistic/revolver/nagant
admin_note = "WARNING: This weapon is very powerful, firing semi-auto 60 damage bullets."
-/datum/opposing_force_equipment/pistol/snub
- item_type = /obj/item/gun/ballistic/automatic/pistol/cfa_snub
- description = "A small easily-concealable modern pistol chambered in the more widely-used 4.6x30mm. It's specifically designed to be compact."
- admin_note = "Fires 20 damage bullets."
-
-/datum/opposing_force_equipment/pistol/ruby
- item_type = /obj/item/gun/ballistic/automatic/pistol/cfa_ruby
- description = "A large and loud modern handgun made to fit more universally used cartridges. It's chambered in .45, or 11.43x23mm."
- admin_note = "Fires 40 damage bullets."
-
-/datum/opposing_force_equipment/pistol/glock17
- item_type = /obj/item/gun/ballistic/automatic/pistol/g17
-
-/datum/opposing_force_equipment/pistol/glock18
- item_type = /obj/item/gun/ballistic/automatic/pistol/g18
-
-/datum/opposing_force_equipment/pistol/ladon
- item_type = /obj/item/gun/ballistic/automatic/pistol/ladon
-
-/datum/opposing_force_equipment/pistol/pdh
- item_type = /obj/item/gun/ballistic/automatic/pistol/pdh
-
-/datum/opposing_force_equipment/pistol/mk58
- item_type = /obj/item/gun/ballistic/automatic/pistol/mk58
-
-/datum/opposing_force_equipment/pistol/zeta
- item_type = /obj/item/gun/ballistic/revolver/zeta
-
-/datum/opposing_force_equipment/pistol/revolution
- item_type = /obj/item/gun/ballistic/revolver/revolution
-
-/datum/opposing_force_equipment/pistol/pepperball
- item_type = /obj/item/gun/ballistic/automatic/pistol/pepperball
+/datum/opposing_force_equipment/pistol/wespe
+ item_type = /obj/item/gun/ballistic/automatic/pistol/sol/evil
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/rifles.dm b/modular_skyrat/modules/opposing_force/code/equipment/rifles.dm
index cc9e853eb7cee2..05739759a805d6 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/rifles.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/rifles.dm
@@ -9,17 +9,5 @@
item_type = /obj/item/gun/ballistic/automatic/akm/nri
admin_note = "WARNING: This weapon is extremely powerful, firing a 3 round burst of 38 damage bullets."
-/datum/opposing_force_equipment/rifle/cfa
- item_type = /obj/item/gun/ballistic/automatic/cfa_rifle
- admin_note = "WARNING: This weapon is very powerful, firing single-shot 60 damage bullets."
-
-/datum/opposing_force_equipment/rifle/norwind
- item_type = /obj/item/gun/ballistic/automatic/norwind
- description = "A rare M112 DMR rechambered to 12.7x30mm for peacekeeping work, it comes with a scope for medium-long range engagements. A bayonet lug is visible."
-
-/datum/opposing_force_equipment/rifle/ostwind
- item_type = /obj/item/gun/ballistic/automatic/ostwind
- description = "A 6.3mm special-purpose rifle designed for specific situations."
-
-/datum/opposing_force_equipment/rifle/g11
- item_type = /obj/item/gun/ballistic/automatic/g11
+/datum/opposing_force_equipment/rifle/infanterie
+ item_type = /obj/item/gun/ballistic/automatic/sol_rifle/evil
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/shotguns.dm b/modular_skyrat/modules/opposing_force/code/equipment/shotguns.dm
index 4a33b9bb162050..74e5d53263ce30 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/shotguns.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/shotguns.dm
@@ -1,9 +1,8 @@
/datum/opposing_force_equipment/shotgun
category = OPFOR_EQUIPMENT_CATEGORY_SHOTGUNS
-/datum/opposing_force_equipment/shotgun/m23
- item_type = /obj/item/gun/ballistic/shotgun/m23
- description = "An eight-round pump-action shotgun found in an old station. Comes loaded with beanbag shells but can take any 12 gauge load."
+/datum/opposing_force_equipment/shotgun/renoster
+ item_type = /obj/item/gun/ballistic/shotgun/riot/sol/evil
/datum/opposing_force_equipment/shotgun/as2
item_type = /obj/item/gun/ballistic/shotgun/automatic/as2
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/submachineguns.dm b/modular_skyrat/modules/opposing_force/code/equipment/submachineguns.dm
index 15b7b82940b0a9..7b687ae7eca8cc 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/submachineguns.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/submachineguns.dm
@@ -1,6 +1,9 @@
/datum/opposing_force_equipment/submachine_gun
category = OPFOR_EQUIPMENT_CATEGORY_SUBMACHINE_GUNS
+/datum/opposing_force_equipment/submachine_gun/sindano
+ item_type = /obj/item/gun/ballistic/automatic/sol_smg/evil
+
/datum/opposing_force_equipment/submachine_gun/mp40
item_type = /obj/item/gun/ballistic/automatic/mp40
admin_note = "WARNING: This weapon is extremely powerful, firing a 3 round burst of 30 damage bullets."
@@ -23,27 +26,9 @@
description = "The uzi nine millimeter, a timeless submachinegun for a warrior out of time."
admin_note = "WARNING: This weapon is decently powerful, firing a 2 round burst of 30 damage bullets."
-/datum/opposing_force_equipment/submachine_gun/lynx
- item_type = /obj/item/gun/ballistic/automatic/cfa_lynx
- admin_note = "Capable of high rate of fire full auto of 20 damage bullets."
-
/datum/opposing_force_equipment/submachine_gun/nri_smg
item_type = /obj/item/gun/ballistic/automatic/nri_smg
admin_note = "Capable of high rate of fire bursts of 20 damage bullets."
-/datum/opposing_force_equipment/submachine_gun/dozer
- item_type = /obj/item/gun/ballistic/automatic/dozer
-
-/datum/opposing_force_equipment/submachine_gun/croon
- item_type = /obj/item/gun/ballistic/automatic/croon
-
-/datum/opposing_force_equipment/submachine_gun/pcr
- item_type = /obj/item/gun/ballistic/automatic/pcr
- description = "An accurate, fast-firing SMG chambered in 9x19mm."
-
-/datum/opposing_force_equipment/submachine_gun/pitbull
- item_type = /obj/item/gun/ballistic/automatic/pitbull
- description = "A sturdy personal defense weapon designed to fire 10mm Auto rounds."
-
/datum/opposing_force_equipment/submachine_gun/c20r
item_type = /obj/item/gun/ballistic/automatic/c20r/unrestricted
diff --git a/modular_skyrat/modules/opposing_force/code/items.dm b/modular_skyrat/modules/opposing_force/code/items.dm
index 394ec009bee572..d185e4e5c91a5d 100644
--- a/modular_skyrat/modules/opposing_force/code/items.dm
+++ b/modular_skyrat/modules/opposing_force/code/items.dm
@@ -53,10 +53,10 @@
new /obj/item/clothing/gloves/tackler/combat(src)
new /obj/item/clothing/shoes/combat(src)
new /obj/item/clothing/glasses/sunglasses(src)
- new /obj/item/clothing/mask/gas/sechailer/swat(src)
+ new /obj/item/clothing/mask/gas/syndicate(src)
new /obj/item/storage/belt/military(src)
new /obj/item/card/id/advanced/chameleon(src)
- new /obj/item/mod/control/pre_equipped/nuclear(src)
+ new /obj/item/mod/control/pre_equipped/nuclear/unrestricted(src)
/obj/item/guardiancreator/tech/choose/traitor/opfor
allowling = TRUE
diff --git a/modular_skyrat/modules/opposing_force/code/roundend.dm b/modular_skyrat/modules/opposing_force/code/roundend.dm
index 07b81ff4e8ac92..1725eeaf363b26 100644
--- a/modular_skyrat/modules/opposing_force/code/roundend.dm
+++ b/modular_skyrat/modules/opposing_force/code/roundend.dm
@@ -1,7 +1,7 @@
/datum/controller/subsystem/ticker/proc/opfor_report()
var/list/result = list()
- result += "
Opposing Force Report: "
+ result += "Opposing Force Report: "
if(!SSopposing_force.approved_applications.len)
result += span_red("No applications were approved.")
@@ -9,6 +9,4 @@
for(var/datum/opposing_force/opfor in SSopposing_force.approved_applications)
result += opfor.roundend_report()
- result += "
"
-
- return result.Join()
+ return "
[result.Join()]
"
diff --git a/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm b/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm
index 264d9f8bedce84..6e724e44dbccec 100644
--- a/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm
+++ b/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm
@@ -254,21 +254,37 @@ SUBSYSTEM_DEF(player_ranks)
* or in the legacy system.
*
* Arguments:
- * * admin - The admin making the rank change.
+ * * admin - The admin making the rank change. Can be a /client or a /datum/admins.
* * ckey - The ckey of the player you want to now possess that player rank.
* * rank_title - The title of the group you want to add the ckey to.
*/
-/datum/controller/subsystem/player_ranks/proc/add_player_to_group(client/admin, ckey, rank_title)
+/datum/controller/subsystem/player_ranks/proc/add_player_to_group(admin, ckey, rank_title)
if(IsAdminAdvancedProcCall())
return FALSE
if(!ckey || !admin || !rank_title)
+ stack_trace("Missing either ckey ([ckey || "*NULL*"]), admin ([admin || "*NULL*"]) or rank_title ([rank_title || "*NULL*"]) in add_player_to_group()! Fix this ASAP!")
return FALSE
- if(!check_rights_for(admin, R_PERMISSIONS))
- to_chat(admin, span_warning("You do not possess the permissions to do this."))
+ var/is_admin_client = istype(admin, /client)
+ var/client/admin_client = is_admin_client ? admin : null
+ // If it's not a client, then it should be an admins datum.
+ var/datum/admins/admin_holder = null
+ if(is_admin_client)
+ admin_holder = admin_client?.holder
+ else if(istype(admin, /datum/admins))
+ admin_holder = admin
+
+ if(!admin_holder)
+ return FALSE
+
+ if(!admin_holder.check_for_rights(R_PERMISSIONS))
+ if(is_admin_client)
+ to_chat(admin, span_warning("You do not possess the permissions to do this."))
+
return FALSE
+
rank_title = lowertext(rank_title)
var/datum/player_rank_controller/controller = get_controller_for_group(rank_title)
@@ -282,14 +298,16 @@ SUBSYSTEM_DEF(player_ranks)
var/already_in_config = controller.get_ckeys_for_legacy_save()
if(already_in_config[ckey])
- to_chat(admin, span_warning("\"[ckey]\" is already a [rank_title]!"))
+ if(is_admin_client)
+ to_chat(admin, span_warning("\"[ckey]\" is already a [rank_title]!"))
+
return FALSE
if(controller.should_use_legacy_system())
controller.add_player_legacy(ckey)
return TRUE
- return add_player_rank_sql(controller, ckey, admin.ckey)
+ return add_player_rank_sql(controller, ckey, admin_holder.target)
/**
@@ -325,19 +343,34 @@ SUBSYSTEM_DEF(player_ranks)
* or in the legacy system.
*
* Arguments:
- * * admin - The admin making the rank change.
+ * * admin - The admin making the rank change. Can be a /client or a /datum/admins.
* * ckey - The ckey of the player you want to no longer possess that player rank.
* * rank_title - The title of the group you want to remove the ckey from.
*/
-/datum/controller/subsystem/player_ranks/proc/remove_player_from_group(client/admin, ckey, rank_title)
+/datum/controller/subsystem/player_ranks/proc/remove_player_from_group(admin, ckey, rank_title)
if(IsAdminAdvancedProcCall())
return FALSE
if(!ckey || !admin || !rank_title)
+ stack_trace("Missing either ckey ([ckey || "*NULL*"]), admin ([admin || "*NULL*"]) or rank_title ([rank_title || "*NULL*"]) in remove_player_from_group()! Fix this ASAP!")
return FALSE
- if(!check_rights_for(admin, R_PERMISSIONS))
- to_chat(admin, span_warning("You do not possess the permissions to do this."))
+ var/is_admin_client = istype(admin, /client)
+ var/client/admin_client = is_admin_client ? admin : null
+ // If it's not a client, then it should be an admins datum.
+ var/datum/admins/admin_holder = null
+ if(is_admin_client)
+ admin_holder = admin_client?.holder
+ else if(istype(admin, /datum/admins))
+ admin_holder = admin
+
+ if(!admin_holder)
+ return FALSE
+
+ if(!admin_holder.check_for_rights(R_PERMISSIONS))
+ if(is_admin_client)
+ to_chat(admin, span_warning("You do not possess the permissions to do this."))
+
return FALSE
rank_title = lowertext(rank_title)
@@ -345,22 +378,16 @@ SUBSYSTEM_DEF(player_ranks)
var/datum/player_rank_controller/controller = get_controller_for_group(rank_title)
if(!controller)
- stack_trace("Invalid player rank \"[rank_title]\" supplied in add_player_to_group()!")
+ stack_trace("Invalid player rank \"[rank_title]\" supplied in remove_player_from_group()!")
return FALSE
ckey = ckey(ckey)
- var/already_in_config = controller.get_ckeys_for_legacy_save()
-
- if(!already_in_config[ckey])
- to_chat(admin, span_warning("\"[ckey]\" is already not a [rank_title]!"))
- return FALSE
-
if(controller.should_use_legacy_system())
controller.remove_player_legacy(ckey)
return TRUE
- return remove_player_rank_sql(controller, ckey, admin.ckey)
+ return remove_player_rank_sql(controller, ckey, admin_holder.target)
/**
diff --git a/modular_skyrat/modules/player_ranks/code/world_topic.dm b/modular_skyrat/modules/player_ranks/code/world_topic.dm
new file mode 100644
index 00000000000000..086373fd33fd63
--- /dev/null
+++ b/modular_skyrat/modules/player_ranks/code/world_topic.dm
@@ -0,0 +1,67 @@
+
+/datum/world_topic/set_player_rank
+ keyword = "set_player_rank"
+ require_comms_key = TRUE
+
+/datum/world_topic/set_player_rank/Run(list/input)
+ . = list()
+
+ var/sender_discord_id = input["sender_discord_id"]
+
+ if(!sender_discord_id)
+ .["success"] = FALSE
+ .["message"] = "Invalid sender Discord ID, this should not be happening! Report this immediately!"
+ return
+
+ var/target_ckey = ckey(input["target_ckey"])
+
+ if(!target_ckey)
+ .["success"] = FALSE
+ .["message"] = "Invalid target ckey provided."
+ return
+
+ var/sender_ckey = ckey(SSdiscord.lookup_ckey(sender_discord_id))
+
+ if(!sender_ckey)
+ .["success"] = FALSE
+ .["message"] = "No ckey was found to be attached to the provided Discord account ID, **[sender_discord_id]**. Please verify your Discord account following the instructions of the in-game verb before trying this command again."
+ return
+
+ var/datum/admins/linked_admin_holder = GLOB.admin_datums[sender_ckey] || GLOB.deadmins[sender_ckey]
+
+ if(!linked_admin_holder)
+ .["success"] = FALSE
+ .["message"] = "No valid admin datum was found associated with the ckey associated to your Discord account."
+ return
+
+ if(!linked_admin_holder.check_for_rights(R_PERMISSIONS))
+ .["success"] = FALSE
+ .["message"] = "You do not possess the permissions to execute this command."
+ return
+
+ var/target_rank = input["target_rank"]
+
+ if(!target_rank)
+ .["success"] = FALSE
+ .["message"] = "Invalid target rank provided."
+ return
+
+ target_rank = capitalize(target_rank)
+
+ var/desired_rank_status = !!text2num(input["desired_rank_status"])
+
+ if(desired_rank_status)
+ var/result = SSplayer_ranks.add_player_to_group(linked_admin_holder, target_ckey, target_rank)
+
+ .["success"] = !!result
+ .["message"] = result ? "**[linked_admin_holder.target]** successfully added **[target_rank]** status to **[target_ckey]**." : "**[linked_admin_holder.target]** was unable to add **[target_rank]** status to **[target_ckey]**. Please verify that you entered their ckey correctly and that they did not already possess that status before trying again. Use the in-game verb to get more information if you keep on receiving this error."
+ message_admins(replacetext(.["message"], "*", ""))
+ return
+
+ else
+ var/result = SSplayer_ranks.remove_player_from_group(linked_admin_holder, target_ckey, target_rank)
+
+ .["success"] = !!result
+ .["message"] = result ? "**[linked_admin_holder.target]** successfully removed **[target_rank]** status from **[target_ckey]**." : "**[linked_admin_holder.target]** was unable to remove **[target_rank]** status from **[target_ckey]**. Please verify that you entered their ckey correctly and that they did possess that status before trying again. Use the in-game verb to get more information if you keep on receiving this error."
+ message_admins(replacetext(.["message"], "*", ""))
+ return
diff --git a/modular_skyrat/modules/primitive_catgirls/code/spawner.dm b/modular_skyrat/modules/primitive_catgirls/code/spawner.dm
index 8cb3f3234feb3c..5cbe1d682c0a3a 100644
--- a/modular_skyrat/modules/primitive_catgirls/code/spawner.dm
+++ b/modular_skyrat/modules/primitive_catgirls/code/spawner.dm
@@ -66,8 +66,23 @@
/datum/team/primitive_catgirls
name = "Icewalkers"
+ member_name = "Icewalker"
show_roundend_report = FALSE
+/datum/team/primitive_catgirls/roundend_report()
+ var/list/report = list()
+
+ report += span_header("An Ice Walker Tribe inhabited the wastes... ")
+ if(length(members))
+ report += "The [member_name]s were:"
+ report += printplayerlist(members)
+ else
+ report += "But none of its members woke up!"
+
+ return "
[report.Join(" ")]
"
+
+// Antagonist datum
+
/datum/antagonist/primitive_catgirl
name = "\improper Icewalker"
job_rank = ROLE_LAVALAND // If you're ashwalker banned you should also not be playing this, other way around as well
diff --git a/modular_skyrat/modules/primitive_catgirls/code/species.dm b/modular_skyrat/modules/primitive_catgirls/code/species.dm
index 7bcaec865dd942..586c7b22900524 100644
--- a/modular_skyrat/modules/primitive_catgirls/code/species.dm
+++ b/modular_skyrat/modules/primitive_catgirls/code/species.dm
@@ -21,6 +21,7 @@
mutanttongue = /obj/item/organ/internal/tongue/cat/primitive
species_language_holder = /datum/language_holder/primitive_felinid
+ language_prefs_whitelist = list(/datum/language/primitive_catgirl)
bodytemp_normal = 270 // If a normal human gets hugged by one its gonna feel cold
bodytemp_heat_damage_limit = 283 // To them normal station atmos would be sweltering
diff --git a/modular_skyrat/modules/projectiles/code/guns/misc/m6pdw.dm b/modular_skyrat/modules/projectiles/code/guns/misc/m6pdw.dm
index df95f6072e933e..d4c367bafdfbc1 100644
--- a/modular_skyrat/modules/projectiles/code/guns/misc/m6pdw.dm
+++ b/modular_skyrat/modules/projectiles/code/guns/misc/m6pdw.dm
@@ -10,7 +10,7 @@
lefthand_file = 'modular_skyrat/modules/projectiles/icons/inhands/lefthand.dmi'
w_class = WEIGHT_CLASS_NORMAL
spawnwithmagazine = FALSE
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/g17
+ accepted_magazine_type = /obj/item/ammo_box/magazine/c35sol_pistol
can_suppress = FALSE
fire_sound = 'sound/weapons/gun/pistol/shot_alt.ogg'
rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
diff --git a/modular_skyrat/modules/resleeving/code/research/resleeving_research.dm b/modular_skyrat/modules/resleeving/code/research/resleeving_research.dm
new file mode 100644
index 00000000000000..4dcd00cc73e9e6
--- /dev/null
+++ b/modular_skyrat/modules/resleeving/code/research/resleeving_research.dm
@@ -0,0 +1,14 @@
+/datum/design/rsd_interface
+ name = "RSD Phylactery"
+ desc = "A brain interface that allows for transfer of Resonance from a handheld RSD, such as the Evoker model."
+ id = "rsd_interface"
+ build_type = PROTOLATHE | AWAY_LATHE
+ departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
+ category = list(RND_CATEGORY_EQUIPMENT)
+ materials = list(
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.5,
+ /datum/material/gold = SHEET_MATERIAL_AMOUNT,
+ /datum/material/silver = SHEET_MATERIAL_AMOUNT,
+ )
+ build_path = /obj/item/rsd_interface
+
diff --git a/modular_skyrat/modules/resleeving/code/rsd_interface.dm b/modular_skyrat/modules/resleeving/code/rsd_interface.dm
new file mode 100644
index 00000000000000..106bb11ea38bc4
--- /dev/null
+++ b/modular_skyrat/modules/resleeving/code/rsd_interface.dm
@@ -0,0 +1,44 @@
+/obj/item/rsd_interface
+ name = "RSD Phylactery"
+ desc = "A small device inserted, typically, into inert brains. As Resonance cannot persist in what's referred to as a 'vacuum', RSDs--much like the brains and CPUs they emulate--employ cerebral white noise as a foundation for Resonance to persist in otherwise dead-quiet containers.."
+ icon = 'modular_skyrat/modules/aesthetics/implanter/implanter.dmi'
+ icon_state = "implanter1"
+ inhand_icon_state = "syringe_0"
+ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
+
+/// Attempts to use the item on the target brain.
+/obj/item/rsd_interface/afterattack(obj/item/organ/internal/brain/target_brain, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!proximity_flag || !istype(target_brain))
+ return FALSE
+
+ if(HAS_TRAIT(target_brain, TRAIT_NIFSOFT_HUD_GRANTER))
+ balloon_alert("already upgraded!")
+ return FALSE
+
+ user.visible_message(span_notice("[user] upgrades [target_brain] with [src]."), span_notice("You upgrade [target_brain] to be RSD compatible."))
+ target_brain.AddElement(/datum/element/rsd_interface)
+ playsound(target_brain.loc, 'sound/weapons/circsawhit.ogg', 50, vary = TRUE)
+
+ qdel(src)
+
+/datum/element/rsd_interface/Attach(datum/target)
+ . = ..()
+ if(!istype(target, /obj/item/organ/internal/brain))
+ return ELEMENT_INCOMPATIBLE
+
+ RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ ADD_TRAIT(target, TRAIT_RSD_COMPATIBLE, INNATE_TRAIT)
+
+/// Adds text to the examine text of the parent item, explaining that the item can be used to enable the use of NIFSoft HUDs
+/datum/element/rsd_interface/proc/on_examine(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ examine_text += span_cyan("Souls can be transferred to [source], assuming it is inert.")
+
+/datum/element/rsd_interface/Detach(datum/target)
+ UnregisterSignal(target, COMSIG_ATOM_EXAMINE)
+ REMOVE_TRAIT(target, TRAIT_RSD_COMPATIBLE, INNATE_TRAIT)
+
+ return ..()
+
diff --git a/modular_skyrat/modules/science_tools/medical_tool_designs.dm b/modular_skyrat/modules/science_tools/medical_tool_designs.dm
new file mode 100644
index 00000000000000..de93e46d1420a4
--- /dev/null
+++ b/modular_skyrat/modules/science_tools/medical_tool_designs.dm
@@ -0,0 +1,165 @@
+// Standard tool overrides
+/datum/design/scalpel
+ construction_time = 1 SECONDS
+
+/datum/design/scalpel/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/hemostat
+ construction_time = 1 SECONDS
+
+/datum/design/hemostat/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/retractor
+ construction_time = 1 SECONDS
+
+/datum/design/retractor/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/circular_saw
+ construction_time = 1 SECONDS
+
+/datum/design/circular_saw/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/cautery
+ construction_time = 1 SECONDS
+
+/datum/design/cautery/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/surgicaldrill
+ construction_time = 1 SECONDS
+
+/datum/design/surgicaldrill/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/bonesetter
+ construction_time = 1 SECONDS
+
+/datum/design/bonesetter/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/healthanalyzer
+ construction_time = 5 SECONDS
+
+// it's fine to give them health analyzers, the choke for medibot production is the medkits
+/datum/design/healthanalyzer/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+// Advanced tool overrides
+/datum/design/laserscalpel
+ construction_time = 1 SECONDS
+
+/datum/design/laserscalpel/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/mechanicalpinches
+ construction_time = 1 SECONDS
+
+/datum/design/mechanicalpinches/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/searingtool
+ construction_time = 1 SECONDS
+
+/datum/design/searingtool/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/healthanalyzer_advanced
+ construction_time = 5 SECONDS
+
+/datum/design/healthanalyzer_advanced/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+// Alien tool overrides
+/datum/design/alienscalpel
+ construction_time = 1 SECONDS
+
+/datum/design/alienscalpel/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/alienhemostat
+ construction_time = 1 SECONDS
+
+/datum/design/alienhemostat/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/alienretractor
+ construction_time = 1 SECONDS
+
+/datum/design/alienretractor/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/aliensaw
+ construction_time = 1 SECONDS
+
+/datum/design/aliensaw/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/aliencautery
+ construction_time = 1 SECONDS
+
+/datum/design/aliencautery/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
+
+/datum/design/aliendrill
+ construction_time = 1 SECONDS
+
+/datum/design/aliendrill/New()
+ build_type |= MECHFAB
+ category += list(RND_CATEGORY_MECHFAB_EQUIPMENT + RND_SUBCATEGORY_MECHFAB_EQUIPMENT_MEDICAL)
+
+ return ..()
diff --git a/modular_skyrat/modules/science_tools/readme.md b/modular_skyrat/modules/science_tools/readme.md
new file mode 100644
index 00000000000000..c07b765a2261d3
--- /dev/null
+++ b/modular_skyrat/modules/science_tools/readme.md
@@ -0,0 +1,44 @@
+
+
+https://github.com/Skyrat-SS13/Skyrat-tg/pull/24162
+
+## Science tools
+
+Module ID: SCIENCE_TOOLS
+
+### Description: Lets sci print watered down engi tools, and robotics, medical tools.
+
+
+
+### TG Proc/File Changes:
+
+- N/A
+
+
+### Modular Overrides:
+
+- N/A
+
+
+### Defines:
+
+- 2 defines in research.dm
+
+
+### Included files that are not contained in this module:
+
+- ~skyrat_defines/research.dm
+
+
+### Credits:
+
+- Niko: Author
+
+
diff --git a/modular_skyrat/modules/science_tools/research.dm b/modular_skyrat/modules/science_tools/research.dm
new file mode 100644
index 00000000000000..4992a07e30277d
--- /dev/null
+++ b/modular_skyrat/modules/science_tools/research.dm
@@ -0,0 +1,9 @@
+/datum/techweb_node/exp_tools/New()
+ . = ..()
+ // if this datum is ever instantiated twice, somehow, this is more efficient. i feel like an idiot writing this
+ var/static/list/science_tools = list(
+ SCIENCE_JAWS_OF_LIFE_DESIGN_ID,
+ SCIENCE_DRILL_DESIGN_ID,
+ )
+ design_ids += science_tools
+
diff --git a/modular_skyrat/modules/science_tools/tool_designs.dm b/modular_skyrat/modules/science_tools/tool_designs.dm
new file mode 100644
index 00000000000000..43a38743aceb41
--- /dev/null
+++ b/modular_skyrat/modules/science_tools/tool_designs.dm
@@ -0,0 +1,18 @@
+/datum/design/jawsoflife/science
+ name = "Hybrid cutters"
+ desc = "An off-shoot of the jaws of life that lacks the door-opening power"
+ id = SCIENCE_JAWS_OF_LIFE_DESIGN_ID // added one more requirement since the Jaws of Life are a bit OP
+ build_path = /obj/item/crowbar/power/science
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/handdrill/science
+ id = SCIENCE_DRILL_DESIGN_ID
+ build_type = PROTOLATHE | AWAY_LATHE
+ build_path = /obj/item/screwdriver/power/science
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/handdrill/science/New()
+ name = ("Science " + name)
+ desc += " with a science paintjob"
+
+ return ..()
diff --git a/modular_skyrat/modules/science_tools/tools.dm b/modular_skyrat/modules/science_tools/tools.dm
new file mode 100644
index 00000000000000..b9c836587dfa85
--- /dev/null
+++ b/modular_skyrat/modules/science_tools/tools.dm
@@ -0,0 +1,14 @@
+/obj/item/crowbar/power/science
+ name = "hybrid cutters" // hybrid between crowbar and wirecutters
+ desc = "Quite similar to the jaws of life, this tool combines the utility of a crowbar and a set of wirecutters without the hydraulic force required to pry open doors."
+ icon_state = "jaws_sci"
+ inhand_icon_state = "jaws_sci"
+ force_opens = FALSE
+
+/obj/item/screwdriver/power/science
+ icon_state = "drill_sci"
+
+/obj/item/screwdriver/power/science/Initialize(mapload)
+ . = ..()
+
+ desc += " This one sports a nifty science paintjob, but is otherwise normal."
diff --git a/modular_skyrat/modules/sec_haul/code/guns/ammo.dm b/modular_skyrat/modules/sec_haul/code/guns/ammo.dm
index 06213fe61758ef..a0e90388d659d9 100644
--- a/modular_skyrat/modules/sec_haul/code/guns/ammo.dm
+++ b/modular_skyrat/modules/sec_haul/code/guns/ammo.dm
@@ -1,35 +1,6 @@
/obj/item/ammo_box/advanced
multiple_sprites = AMMO_BOX_FULL_EMPTY
-/datum/techweb_node/smartgun_rails
- id = "smartgun_rails"
- display_name = "Experimental SMARTGUN Ammunition"
- description = "Standard ammo for a non-standard SMARTGUN."
- prereq_ids = list("electronic_weapons", "exotic_ammo")
- design_ids = list("smartgun")
- research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 3000)
-
-/*
-* 6mm
-*/
-
-/obj/item/ammo_box/advanced/b6mm
- name = "6.3mm ammo box"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
- icon_state = "box10mm"
- ammo_type = /obj/item/ammo_casing/b6mm
- max_ammo = 30
-
-/obj/item/ammo_box/advanced/b6mm/rubber
- name = "6.3mm dissuasive pellet box"
- icon_state = "box10mm-rubber"
- ammo_type = /obj/item/ammo_casing/b6mm/rubber
-
-/obj/item/ammo_box/advanced/b6mm/ihdf
- name = "6.3mm fragmentation pellet box"
- icon_state = "box10mm-hv"
- ammo_type = /obj/item/ammo_casing/b6mm/ihdf
-
/*
* 9mm
*/
@@ -53,114 +24,3 @@
/obj/item/ammo_box/c10mm/ihdf
name = "peacekeeper ammo box (10mm ihdf)"
ammo_type = /obj/item/ammo_casing/c10mm/ihdf
-
-/*
-* 12mm
-*/
-
-/obj/item/ammo_box/advanced/b12mm
- name = "12.7x30mm FMJ box"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
- icon_state = "magnum_l"
- ammo_type = /obj/item/ammo_casing/b12mm
- max_ammo = 15
-
-/obj/item/ammo_box/advanced/b12mm/rubber
- name = "12.7x30mm beanbag box"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
- icon_state = "magnum_r"
- ammo_type = /obj/item/ammo_casing/b12mm/rubber
-
-/obj/item/ammo_box/advanced/b12mm/hp
- name = "12.7x30mm JHP box"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
- icon_state = "magnum_p"
- ammo_type = /obj/item/ammo_casing/b12mm/hp
-
-/*
-* S.M.A.R.T. RIFLE
-*/
-
-/datum/design/smartgun
- name = "\improper S.M.A.R.T. Rifle Shock-Rails"
- id = "smartgun"
- build_type = PROTOLATHE
- materials = list(/datum/material/silver = SHEET_MATERIAL_AMOUNT * 5, /datum/material/gold = SHEET_MATERIAL_AMOUNT * 5, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 5)
- build_path = /obj/item/ammo_box/advanced/smartgun
- category = list(RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_AMMO)
- departmental_flags = DEPARTMENT_BITFLAG_SECURITY
-
-/obj/item/ammo_box/advanced/smartgun
- name = "5mm shock-rail box"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
- icon_state = "smartgun_chain"
- ammo_type = /obj/item/ammo_casing/smartgun
- multiple_sprites = AMMO_BOX_PER_BULLET
- max_ammo = 4
-
-/*
-* MULTI-SPRITE MAGS
-*/
-
-/obj/item/ammo_box/magazine/multi_sprite
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- desc = "An advanced magazine with smart type displays. Alt+click to reskin it."
- w_class = WEIGHT_CLASS_SMALL
- item_flags = NO_MAT_REDEMPTION
- multitype = TRUE
- var/round_type = AMMO_TYPE_LETHAL
- var/base_name = ""
- var/list/possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_HOLLOWPOINT, AMMO_TYPE_RUBBER, AMMO_TYPE_IHDF)
-
-/obj/item/ammo_box/magazine/multi_sprite/Initialize(mapload)
- . = ..()
- base_name = name
- name = "[base_name] [round_type]"
- update_icon()
-
-/obj/item/ammo_box/magazine/multi_sprite/AltClick(mob/user)
- . = ..()
- if(possible_types.len <= 1)
- return
- var/new_type = input("Please select a magazine type to reskin to:", "Reskin", null, null) as null|anything in sort_list(possible_types)
- if(!new_type)
- new_type = AMMO_TYPE_LETHAL
- round_type = new_type
- name = "[base_name] [round_type]"
- update_icon()
-
-/obj/item/ammo_box/magazine/multi_sprite/update_icon()
- . = ..()
- var/shells_left = stored_ammo.len
- switch(multiple_sprites)
- if(AMMO_BOX_PER_BULLET)
- icon_state = "[initial(icon_state)]_[round_type]-[shells_left]"
- if(AMMO_BOX_FULL_EMPTY)
- icon_state = "[initial(icon_state)]_[round_type]-[shells_left ? "full" : "empty"]"
- desc = "[initial(desc)] There [(shells_left == 1) ? "is" : "are"] [shells_left] shell\s left!"
-
-/obj/item/ammo_box/revolver
- name = "speed loader"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- desc = "Designed to quickly reload revolvers."
- icon_state = "speedloader"
- max_ammo = 8
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- var/round_type = AMMO_TYPE_LETHAL
- var/list/possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_HOLLOWPOINT, AMMO_TYPE_RUBBER, AMMO_TYPE_IHDF)
- start_empty = TRUE //SOmething strange going on with refills.
-
-/obj/item/ammo_box/revolver/AltClick(mob/user)
- . = ..()
- var/new_type = input("Please select a magazine type to reskin to:", "Reskin", null, null) as null|anything in sort_list(possible_types)
- if(!new_type)
- new_type = AMMO_TYPE_LETHAL
- round_type = new_type
- name = "[initial(name)] [round_type]"
- update_appearance()
-
-/obj/item/ammo_box/revolver/update_overlays()
- . = ..()
- if(stored_ammo.len)
- . += "[initial(icon_state)]_[round_type]"
-
diff --git a/modular_skyrat/modules/sec_haul/code/guns/armory_spawns.dm b/modular_skyrat/modules/sec_haul/code/guns/armory_spawns.dm
index 7d338480d8d7e1..517aeded62dfc5 100644
--- a/modular_skyrat/modules/sec_haul/code/guns/armory_spawns.dm
+++ b/modular_skyrat/modules/sec_haul/code/guns/armory_spawns.dm
@@ -1,6 +1,7 @@
/obj/effect/spawner/armory_spawn
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/spawner.dmi'
- icon_state = "random_gun"
+ icon_state = "loot"
+ icon = 'icons/effects/random_spawners.dmi'
+
layer = OBJ_LAYER
/// A list of possible guns to spawn.
var/list/guns
@@ -35,12 +36,10 @@
new spawned_ballistic_gun.spawn_magazine_type (spawned_box)
/obj/effect/spawner/armory_spawn/shotguns
- icon_state = "random_shotgun"
guns = list(
- /obj/item/gun/ballistic/shotgun/riot,
- /obj/item/gun/ballistic/shotgun/riot,
- /obj/item/gun/ballistic/shotgun/riot,
- /obj/item/gun/ballistic/shotgun/riot,
+ /obj/item/gun/ballistic/shotgun/riot/sol,
+ /obj/item/gun/ballistic/shotgun/riot/sol,
+ /obj/item/gun/ballistic/shotgun/riot/sol,
)
/obj/structure/closet/ammunitionlocker/useful/PopulateContents()
@@ -56,16 +55,14 @@
/obj/item/storage/box/ammo_box
name = "ammo box"
desc = "A box filled with ammunition."
- icon_state = "boxhrifle"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammoboxes.dmi'
+ icon = 'modular_skyrat/modules/microfusion/icons/microfusion_cells.dmi'
+ icon_state = "microfusion_box"
illustration = null
layer = 2.9
/obj/item/storage/box/ammo_box/microfusion
name = "microfusion cell container"
desc = "A box filled with microfusion cells."
- icon = 'modular_skyrat/modules/microfusion/icons/microfusion_cells.dmi'
- icon_state = "microfusion_box"
/obj/item/storage/box/ammo_box/microfusion/PopulateContents()
new /obj/item/stock_parts/cell/microfusion(src)
@@ -73,25 +70,23 @@
new /obj/item/stock_parts/cell/microfusion(src)
/obj/effect/spawner/armory_spawn/centcom_rifles
- icon_state = "random_rifle"
guns = list(
- /obj/item/gun/ballistic/automatic/ar,
- /obj/item/gun/ballistic/automatic/m16,
- /obj/item/gun/ballistic/automatic/cfa_rifle,
+ /obj/item/gun/ballistic/automatic/sol_rifle,
+ /obj/item/gun/ballistic/automatic/sol_rifle,
+ /obj/item/gun/ballistic/automatic/sol_rifle/machinegun,
)
/obj/effect/spawner/armory_spawn/centcom_lasers
guns = list(
/obj/item/gun/energy/laser,
- /obj/item/gun/energy/laser/cfa_paladin,
+ /obj/item/gun/energy/laser,
/obj/item/gun/energy/e_gun,
)
-/obj/effect/spawner/armory_spawn/cmg
- icon_state = "random_rifle"
+/obj/effect/spawner/armory_spawn/smg
vertical_guns = FALSE
guns = list(
- /obj/item/storage/box/gunset/cmg,
- /obj/item/storage/box/gunset/cmg,
- /obj/item/storage/box/gunset/cmg,
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano,
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano,
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano,
)
diff --git a/modular_skyrat/modules/sec_haul/code/guns/bullets.dm b/modular_skyrat/modules/sec_haul/code/guns/bullets.dm
index e6f23736b4e107..c7bc249b351e61 100644
--- a/modular_skyrat/modules/sec_haul/code/guns/bullets.dm
+++ b/modular_skyrat/modules/sec_haul/code/guns/bullets.dm
@@ -1,63 +1,3 @@
-/*
-* 6.3mm
-* FLECHETTE | FRAGMENTING | DISSUASIVE
-*/
-
-/obj/item/ammo_casing/b6mm
- name = "6.3mm flechette casing"
- desc = "A spent flechette."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sl-casing"
- caliber = CALIBER_6MM
- projectile_type = /obj/projectile/bullet/b6mm
-
-/obj/projectile/bullet/b6mm
- name = "6.3mm flechette"
- damage = 10
- speed = 1
- embedding = list(embed_chance=10, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
- armour_penetration = 50
-
-/obj/item/ammo_casing/b6mm/rubber
- name = "6.3mm dissuasive pellet casing"
- desc = "A 6.3mm dissuasive pellet casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sr-casing"
- caliber = CALIBER_6MM
- projectile_type = /obj/projectile/bullet/b6mm/rubber
- harmful = FALSE
-
-/obj/projectile/bullet/b6mm/rubber
- name = "6mm dissuasive pellet"
- icon_state = "pellet"
- damage = 2
- stamina = 15
- ricochets_max = 2
- ricochet_incidence_leeway = 0
- ricochet_chance = 130
- ricochet_decay_damage = 0.8
- shrapnel_type = null
- sharpness = NONE
- embedding = null
-
-/obj/item/ammo_casing/b6mm/ihdf
- name = "6.3mm frag casing"
- desc = "A 6.3mm fragmentation round casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "si-casing"
- caliber = CALIBER_6MM
- projectile_type = /obj/projectile/bullet/b6mm/ihdf
- harmful = TRUE
-
-/obj/projectile/bullet/b6mm/ihdf
- name = "6.3mm fragmentation pellet"
- icon_state = "ihdf"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi'
- damage = 15
- bare_wound_bonus = 50
- embedding = list(embed_chance=60, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
- weak_against_armour = TRUE
-
/*
* 9x25mm Mk.12
*/
@@ -86,15 +26,11 @@
/obj/item/ammo_casing/c9mm/ihdf
name = "9x25mm Mk.12 IHDF casing"
desc = "A modern 9x25mm Mk.12 bullet casing. This one fires a bullet of 'Intelligent High-Impact Dispersal Foam', which is best compared to a riot-grade foam dart."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "si-casing"
projectile_type = /obj/projectile/bullet/c9mm/ihdf
harmful = FALSE
/obj/projectile/bullet/c9mm/ihdf
name = "9x25mm IHDF bullet"
- icon_state = "ihdf"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi'
damage = 30
damage_type = STAMINA
embedding = list(embed_chance=0, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
@@ -102,8 +38,6 @@
/obj/item/ammo_casing/c9mm/rubber
name = "9x25mm Mk.12 rubber casing"
desc = "A modern 9x25mm Mk.12 bullet casing. This less than lethal round sure hurts to get shot by, but causes little physical harm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sr-casing"
projectile_type = /obj/projectile/bullet/c9mm/rubber
harmful = FALSE
@@ -135,11 +69,14 @@
custom_materials = AMMO_MATS_TEMP
advanced_print_req = TRUE
+/obj/item/ammo_casing/c10mm/reaper
+ can_be_printed = FALSE
+ // it's a hitscan 50 damage 40 AP bullet designed to be fired out of a gun with a 2rnd burst and 1.25x damage multiplier
+ // Let's Not
+
/obj/item/ammo_casing/c10mm/rubber
name = "10mm rubber bullet casing"
desc = "A 10mm rubber bullet casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sr-casing"
projectile_type = /obj/projectile/bullet/c10mm/rubber
harmful = FALSE
@@ -158,204 +95,11 @@
/obj/item/ammo_casing/c10mm/ihdf
name = "10mm IHDF bullet casing"
desc = "A 10mm intelligent high-impact dispersal foam bullet casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "si-casing"
projectile_type = /obj/projectile/bullet/c10mm/ihdf
harmful = FALSE
/obj/projectile/bullet/c10mm/ihdf
name = "10mm IHDF bullet"
- icon_state = "ihdf"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi'
damage = 40
damage_type = STAMINA
embedding = list(embed_chance=0, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
-
-/*
-* 12.7x30mm
-* FMJ | JHP | BEANBAG
-*/
-
-/obj/item/ammo_casing/b12mm
- name = "12.7x30mm FMJ casing"
- desc = "A 12.7x30mm FMJ casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sl-casing"
- caliber = CALIBER_12MM
- projectile_type = /obj/projectile/bullet/b12mm
-
-/obj/projectile/bullet/b12mm
- name = "12.7x30mm bullet"
- damage = 35
- wound_bonus = 30
- speed = 1
-
-/obj/item/ammo_casing/b12mm/rubber
- name = "12.7x30mm beanbag slug casing"
- desc = "A 12.7x30mm beanbag slug casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sr-casing"
- caliber = CALIBER_12MM
- projectile_type = /obj/projectile/bullet/b12mm/rubber
- harmful = FALSE
-
-/obj/projectile/bullet/b12mm/rubber
- name = "12.7x30mm beanbag slug"
- damage = 10
- stamina = 35
- ricochets_max = 6
- ricochet_incidence_leeway = 0
- ricochet_chance = 130
- ricochet_decay_damage = 0.8
- shrapnel_type = null
- sharpness = NONE
- embedding = null
-
-
-/obj/item/ammo_casing/b12mm/hp
- name = "12.7x30mm JHP casing"
- desc = "A 12.7x30mm JHP bullet casing."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sh-casing"
- caliber = CALIBER_12MM
- projectile_type = /obj/projectile/bullet/b12mm/hp
- advanced_print_req = TRUE
-
-/obj/projectile/bullet/b12mm/hp
- name = "12mm hollowpoint bullet"
- damage = 35
- wound_bonus = 40
- embedding = list(embed_chance=75, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
- weak_against_armour = TRUE
-
-/*
-* .577S
-* FMJ
-*/
-
-/obj/item/ammo_casing/b577
- name = ".577 Snider bullet casing"
- desc = "A .577 Snider bullet casing."
- caliber = CALIBER_B577
- projectile_type = /obj/projectile/bullet/b577
-
-/obj/projectile/bullet/b577
- name = ".577S FMJ bullet"
- damage = 40
- wound_bonus = 15
- bare_wound_bonus = 30
- dismemberment = 15
-
-//SMARTGUN
-/obj/item/ammo_casing/smartgun
- name = "smartgun rail frame"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "smartgun"
- desc = "A smartgun rail."
- caliber = "smartgun"
- projectile_type = /obj/projectile/bullet/smartgun
- can_be_printed = FALSE
-
-/obj/projectile/bullet/smartgun
- name = "smartgun rail"
- icon_state = "gaussphase"
- embedding = list(embed_chance=100, fall_chance=2, jostle_chance=0, ignore_throwspeed_threshold=TRUE, pain_stam_pct=20, pain_mult=4, rip_time=40)
- damage = 10
- stamina = 70
- ricochets_max = 6
- ricochet_incidence_leeway = 0
- ricochet_chance = 130
- ricochet_decay_damage = 0.8
- shrapnel_type = /obj/item/shrapnel/bullet/smartgun
- hitsound = 'modular_skyrat/modules/sec_haul/sound/rail.ogg'
-
-/obj/projectile/bullet/smartgun/on_hit(atom/target, blocked, pierce_hit)
- . = ..()
- if(ishuman(target))
- var/mob/living/carbon/human/H = target
- H.emote("scream")
- H.add_mood_event("tased", /datum/mood_event/tased)
- if((H.status_flags & CANKNOCKDOWN) && !HAS_TRAIT(H, TRAIT_STUNIMMUNE))
- addtimer(CALLBACK(H, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), jitter), 5)
-
-/obj/item/shrapnel/bullet/smartgun
- name = "smartgun shredder"
- icon = 'icons/obj/weapons/guns/projectiles.dmi'
- icon_state = "gaussphase"
- embedding = null
-
-/*
-* 4.73x33mm CASELESS
-* FMJ | JHP | IHDF | RUBBER
-*/
-
-/obj/item/ammo_casing/b473
- name = "4.73x33mm FMJ bullet"
- desc = "A 4.73x33mm FMJ bullet."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sl-casing"
- caliber = CALIBER_473MM
- projectile_type = /obj/projectile/bullet/b473
-
-/obj/item/ammo_casing/b473/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/caseless)
-
-/obj/projectile/bullet/b473
- name = "4.73x33mm FMJ bullet"
- damage = 20
- speed = 0.7
-
-/obj/item/ammo_casing/b473/hp
- name = "4.73x33mm JHP bullet"
- desc = "A 4.73x33mm JHP bullet."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sh-casing"
- caliber = CALIBER_473MM
- projectile_type = /obj/projectile/bullet/b473/hp
- advanced_print_req = TRUE
-
-/obj/projectile/bullet/b473/hp
- name = "4.73x33mm JHP bullet"
- damage = 20
- wound_bonus = 30
- embedding = list(embed_chance=75, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
- weak_against_armour = TRUE
-
-/obj/item/ammo_casing/b473/rubber
- name = "4.73x33mm rubber bullet"
- desc = "A 4.73x33mm rubber bullet."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "sr-casing"
- caliber = CALIBER_473MM
- projectile_type = /obj/projectile/bullet/b473/rubber
- harmful = FALSE
-
-/obj/projectile/bullet/b473/rubber
- name = "4.73x33mm rubber bullet"
- damage = 5
- stamina = 20
- ricochets_max = 6
- ricochet_incidence_leeway = 0
- ricochet_chance = 130
- ricochet_decay_damage = 0.8
- shrapnel_type = null
- sharpness = NONE
- embedding = null
-
-/obj/item/ammo_casing/b473/ihdf
- name = "4.73x33mm IHDF bullet"
- desc = "A 4.73x33mm intelligent high-impact dispersal foam bullet."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi'
- icon_state = "si-casing"
- caliber = CALIBER_473MM
- projectile_type = /obj/projectile/bullet/b473/ihdf
- harmful = FALSE
-
-/obj/projectile/bullet/b473/ihdf
- name = "4.73x33mm ihdf bullet"
- icon_state = "ihdf"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/projectiles.dmi'
- damage = 25
- damage_type = STAMINA
- embedding = list(embed_chance=0, fall_chance=3, jostle_chance=4, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10)
diff --git a/modular_skyrat/modules/sec_haul/code/guns/cargo_stuff.dm b/modular_skyrat/modules/sec_haul/code/guns/cargo_stuff.dm
index e32557bd318a16..97c006240a5495 100644
--- a/modular_skyrat/modules/sec_haul/code/guns/cargo_stuff.dm
+++ b/modular_skyrat/modules/sec_haul/code/guns/cargo_stuff.dm
@@ -1,10 +1,10 @@
/datum/supply_pack/security/armory/cmg
- name = "NT CMG-2 PDW Crate"
- desc = "Three entirely proprietary CMG-2 kits, chambered in 9x25mm. Each kit contains an ammo pouch, one less-lethal rubber magazine, and two lethal magazines."
+ name = "Carwo 'Sindano' Submachinegun Crate"
+ desc = "Three entirely proprietary Sindano kits, chambered in .35 Sol Short. Each kit contains three empty magazines and a box each of incapacitator and lethal rounds."
cost = CARGO_CRATE_VALUE * 20
contains = list(
- /obj/item/storage/box/gunset/cmg,
- /obj/item/storage/box/gunset/cmg,
- /obj/item/storage/box/gunset/cmg,
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano,
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano,
+ /obj/item/storage/toolbox/guncase/skyrat/carwo_large_case/sindano,
)
- crate_name = "NT CMG-2 PDW Crate"
+ crate_name = "Carwo 'Sindano' Submachinegun Crate"
diff --git a/modular_skyrat/modules/sec_haul/code/guns/cmg.dm b/modular_skyrat/modules/sec_haul/code/guns/cmg.dm
deleted file mode 100644
index 99f8c2586bfeed..00000000000000
--- a/modular_skyrat/modules/sec_haul/code/guns/cmg.dm
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * The CMG-2.
- *
- * It sure does exist. Comes with a projectile damage malus for some sense of parity with the old 9mm Peacekeeper round.
- * Supposed to fill the niche of being a sidearm, apparently? But it's a longarm, but it also fires 9mm and has a damage malus
- * (slightly buffed from the usual 9mm PK -> 9x25mm + 0.5x damage multiplier).
- */
-
-/obj/item/gun/ballistic/automatic/cmg
- name = "\improper NT CMG-2 PDW"
- desc = "A bullpup, two-round burst PDW chambered in 9x25mm, developed by Nanotrasen R&D and based on a licensed Scarborough Arms design. \
- It features a folding stock and comes pre-attached with a dot sight. Unfortunately, the recoil management system reduces the \
- stopping power of individual rounds, but the manufacturer insists that quirk can be mitigated by not missing."
- icon_state = "cmg1"
- icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns.dmi'
- inhand_icon_state = "c20r"
- selector_switch_icon = TRUE
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/cmg
- fire_delay = 2 //Slightly buffed firespeed over the last cmg because the bullets are a bit weaker
- burst_size = 1
- actions_types = list()
- can_bayonet = TRUE
- knife_x_offset = 26
- knife_y_offset = 10
- mag_display = TRUE
- mag_display_ammo = TRUE
- empty_indicator = TRUE
- w_class = WEIGHT_CLASS_BULKY
- projectile_damage_multiplier = 0.7
- // raw outputs:
- // lethal: 30 * 0.7 = 21
- // AP: 27 * 0.7 = 18.9
- // HP: 40 * 0.7 = 28
- // INC: 15 * 0.7 = 10.5
- // rubber: 5 * 0.7 = 3.5 brute, 25 * 0.7 = 17.5 stam
- // IHDF (does this even get used): 30 * 0.7 = 21 stam
-
- /// what sound do we play when finished adjusting the stock?
- var/folding_sound = 'sound/weapons/batonextend.ogg'
- /// is our stock collapsed?
- var/folded = FALSE
- /// how long does it take to extend/collapse the stock
- var/toggle_time = 1 SECONDS
- /// what's our spread with our extended stock (mild varedit compatibility I Guess)?
- var/unfolded_spread = 0
- /// what's our spread with a folded stock (see above comment)?
- var/folded_spread = 20
-
-/obj/item/gun/ballistic/automatic/cmg/examine(mob/user)
- . = ..()
- . += span_notice("Ctrl-click to [folded ? "extend" : "collapse"] the stock.")
-
-/obj/item/gun/ballistic/automatic/cmg/CtrlClick(mob/user)
- if(!user.is_holding(src))
- return // fuckin around w/ a collapsible stock without hands is Suboptimal
- if(item_flags & IN_STORAGE)
- return // if you could unfold it while it's stowed away that'd defeat the purpose
- toggle_stock(user)
- . = ..()
-
-/obj/item/gun/ballistic/automatic/cmg/proc/toggle_stock(mob/user, var/forced)
- if(!user && forced) // for the possible case of having every shipped CMG be pre-folded
- folded = !folded
- update_fold_stats()
- return
- balloon_alert(user, "[folded ? "extending" : "collapsing"] stock...")
- if(!do_after(user, toggle_time))
- balloon_alert(user, "interrupted!")
- return
- folded = !folded
- update_fold_stats()
- balloon_alert(user, "stock [folded ? "collapsed" : "extended"]")
- playsound(src.loc, folding_sound, 30, 1)
-
-/obj/item/gun/ballistic/automatic/cmg/proc/update_fold_stats()
- if(folded)
- spread = folded_spread
- if(suppressed)
- w_class = WEIGHT_CLASS_BULKY
- else
- w_class = WEIGHT_CLASS_NORMAL
- else
- spread = unfolded_spread
- if(suppressed)
- w_class = WEIGHT_CLASS_HUGE
- else
- w_class = WEIGHT_CLASS_BULKY
- update_icon()
-
-/obj/item/gun/ballistic/automatic/cmg/update_overlays()
- . = ..()
- . += "[icon_state]-stock[folded ? "_in" : "_out"]"
-
-/obj/item/gun/ballistic/automatic/cmg/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/automatic_fire, 0.3 SECONDS)
-
-/obj/item/gun/ballistic/automatic/cmg/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, \
- light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', \
- light_overlay = "flight", \
- overlay_x = 24, \
- overlay_y = 10)
-
-/obj/item/ammo_box/magazine/multi_sprite/cmg
- name = "9x25mm PDW magazine"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "g11"
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- caliber = CALIBER_9MM
- max_ammo = 24
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/cmg/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/cmg/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/cmg/lethal
- ammo_type = /obj/item/ammo_casing/c9mm
- round_type = AMMO_TYPE_LETHAL
-
-/obj/item/storage/box/gunset/cmg
- name = "cmg supply box"
-
-/obj/item/gun/ballistic/automatic/cmg/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/cmg/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/cmg/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg/lethal(src)
- new /obj/item/ammo_box/magazine/multi_sprite/cmg/lethal(src)
diff --git a/modular_skyrat/modules/sec_haul/code/guns/guns.dm b/modular_skyrat/modules/sec_haul/code/guns/guns.dm
deleted file mode 100644
index 04a85fa0c87e14..00000000000000
--- a/modular_skyrat/modules/sec_haul/code/guns/guns.dm
+++ /dev/null
@@ -1,1162 +0,0 @@
-/*
-* GLOCK
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/g17
- name = "\improper GK-17"
- desc = "A weapon from bygone times, this has been made to look like an old, blocky firearm from the 21st century. Let's hope it's more reliable. Chambered in 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/glock.dmi'
- icon_state = "glock"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/g17
- can_suppress = FALSE
- fire_sound = 'sound/weapons/gun/pistol/shot_alt.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- fire_delay = 1.90
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/automatic/pistol/g17/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/pistol/g17/add_seclight_point()
- return
-
-/obj/item/ammo_box/magazine/multi_sprite/g17
- name = "\improper GK-17 magazine"
- desc = "A magazine for the GK-17 handgun, chambered for 9x25mm ammo."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "g17"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 17
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/g17/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/g17/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/g17/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/gun/ballistic/automatic/pistol/g18
- name = "\improper GK-18"
- desc = "A CFA-made burst firing cheap polymer pistol chambered in 9x25mm. Its heavy duty barrel affects firerate."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/glock.dmi'
- icon_state = "glock_spec"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/g18
- can_suppress = FALSE
- fire_sound = 'sound/weapons/gun/pistol/shot_alt.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- burst_size = 3
- fire_delay = 2.10
- spread = 8
- actions_types = list(/datum/action/item_action/toggle_firemode)
- mag_display = FALSE
- mag_display_ammo = FALSE
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/automatic/pistol/g18/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_CANTALAN)
-
-/obj/item/gun/ballistic/automatic/pistol/g18/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/ammo_box/magazine/multi_sprite/g18
- name = "\improper GK-18 magazine"
- desc = "A magazine for the GK-18 machine pistol, chambered for 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "g18"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 33
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/g18/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/g18/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/g18/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/gun/ballistic/automatic/pistol/g17/mesa
- name = "\improper Glock 20"
- desc = "A weapon from bygone times, and this is the exact 21st century version. In fact, even more reliable. Chambered in 10mm Auto."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/glock.dmi'
- icon_state = "glock_mesa"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/ladon // C o m p a t i b i l i t y .
- fire_sound = 'modular_skyrat/master_files/sound/weapons/glock17_fire.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- fire_delay = 0.9
-
-/obj/item/gun/ballistic/automatic/pistol/g17/mesa/give_manufacturer_examine()
- return
-
-/obj/item/gun/ballistic/automatic/pistol/g17/mesa/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/*
-* PDH 40x32
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/pdh
- name = "\improper PDH-6H 'Osprey'"
- desc = "A modern ballistics sidearm, used primarily by the military, however this one has had a paintjob to match command. It's chambered in 12.7x30mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/pdh.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi'
- icon_state = "pdh"
- inhand_icon_state = "pdh"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pdh
- can_suppress = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/hpistol_fire.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/alt
- name = "\improper PDH-6C 'SOCOM'"
- desc = "A prestigious 12mm sidearm normally seen in the hands of SolFed special operation units due to its reliable and time-tested design. Now's one of those times that pays to be the strong, silent type."
- icon_state = "pdh_alt"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pdh
- can_suppress = FALSE
- fire_sound = 'sound/weapons/gun/pistol/shot_suppressed.ogg'
- fire_delay = 8
- fire_sound_volume = 30
- spread = 1
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh
- name = "12mm PDH-6 magazine"
- desc = "A heavy 12mm magazine made for the PDH-6H and PDH-6C handguns."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/b12mm
- caliber = CALIBER_12MM
- max_ammo = 8
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(
- AMMO_TYPE_LETHAL,
- AMMO_TYPE_HOLLOWPOINT,
- AMMO_TYPE_RUBBER,
- )
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh/hp
- ammo_type = /obj/item/ammo_casing/b12mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh/rubber
- ammo_type = /obj/item/ammo_casing/b12mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/corpo
- name = "\improper PDH-6M 'Corpo'"
- desc = "A prestigious ballistic sidearm, from Armadyne's military division, normally given to high-ranking corporate agents. It has a 3 round burst mode and uses .357 Magnum ammunition."
- icon_state = "pdh_corpo"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pdh_corpo
- can_suppress = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/hpistol_fire.ogg'
- burst_size = 3
- fire_delay = 2
- spread = 5
- actions_types = list(/datum/action/item_action/toggle_firemode)
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh_corpo
- name = "\improper PDH-6M magazine"
- desc = "A magazine for Armadyne's exclusive corporate handgun. Chambered for .357, to your disgrace."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/a357
- caliber = CALIBER_357
- max_ammo = 14
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(
- AMMO_TYPE_LETHAL,
- )
-
-/*
-* PDH STRIKER
-*/
-
-// A temporary home for this gun until the Corporate Diplomat PR goes through.
-/obj/item/gun/ballistic/automatic/pistol/pdh/striker
- name = "\improper PDH-6 'Striker'"
- desc = "A sidearm used by Armadyne corporate agents who didn't make the cut for the Corpo model. Chambered in .38 special."
- icon_state = "pdh_striker"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pdh_striker
- can_suppress = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/hpistol_fire.ogg'
- burst_size = 3
- fire_delay = 2
- spread = 9
- actions_types = list(/datum/action/item_action/toggle_firemode)
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh_striker
- name = "\improper PDH-6M magazine"
- desc = "A magazine for the PDH-6 'Striker'. Chambered in the strange choice of .38 special."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/c38
- caliber = CALIBER_38
- max_ammo = 10
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(
- AMMO_TYPE_LETHAL,
- )
-
-/*
-* PDH PEACEKEEPER
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/peacekeeper
- name = "\improper PDH-6B"
- desc = "A modern ballistic sidearm, used primarily by law enforcement, chambered in 9x25mm."
- fire_delay = 1.95
- icon_state = "pdh_peacekeeper"
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/pistol_fire.ogg'
- projectile_damage_multiplier = 0.5
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper
- name = "\improper PDH-6B magazine"
- desc = "A magazine for the PDG-6B law enforcement pistol, chambered for 9x25mm ammo."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 16
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* LADON 40x32
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/ladon
- name = "\improper Ladon pistol"
- desc = "Modern handgun based off the PDH series, chambered in 10mm Auto."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ladon.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi'
- icon_state = "ladon"
- inhand_icon_state = "ladon"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/ladon
- can_suppress = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/pistol_fire.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- fire_delay = 4.20
- projectile_damage_multiplier = 0.7
-
-/obj/item/gun/ballistic/automatic/pistol/ladon/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/gun/ballistic/automatic/pistol/ladon/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/ammo_box/magazine/multi_sprite/ladon
- name = "\improper Ladon magazine"
- desc = "A magazine for the Ladon pistol, chambered for 10mm Auto."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/c10mm
- caliber = CALIBER_10MM
- max_ammo = 12
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/ladon/hp
- ammo_type = /obj/item/ammo_casing/c10mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/ladon/ihdf
- ammo_type = /obj/item/ammo_casing/c10mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/ladon/rubber
- ammo_type = /obj/item/ammo_casing/c10mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* MAKAROV
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/makarov
- name = "\improper R-C 'Makarov'"
- desc = "A mediocre pocket-sized handgun of NRI origin, chambered in 10mm Auto."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/makarov.dmi'
- icon_state = "makarov"
- w_class = WEIGHT_CLASS_SMALL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/makarov
- can_suppress = TRUE
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- projectile_damage_multiplier = 0.6
-
-/obj/item/gun/ballistic/automatic/pistol/makarov/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_IZHEVSK)
-
-/obj/item/ammo_box/magazine/multi_sprite/makarov
- name = "\improper R-C Makarov magazine"
- desc = "A tiny magazine for the R-C Makarov pocket pistol, chambered in 10mm Auto."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/c10mm
- caliber = CALIBER_10MM
- max_ammo = 6
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/makarov/hp
- ammo_type = /obj/item/ammo_casing/c10mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/makarov/ihdf
- ammo_type = /obj/item/ammo_casing/c10mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/makarov/rubber
- ammo_type = /obj/item/ammo_casing/c10mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* MK-58
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/mk58
- name = "\improper MK-58"
- desc = "A modern 9x25mm handgun with an olive polymer lower frame. Looks like a generic 21st century military sidearm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mk58.dmi'
- icon_state = "mk58"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/mk58
- can_suppress = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/pistol_fire.ogg'
- rack_sound = 'sound/weapons/gun/pistol/rack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/automatic/pistol/mk58/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/ammo_box/magazine/multi_sprite/mk58
- name = "\improper MK-58 magazine"
- desc = "A flimsy double-stack polymer magazine for the MK-58 handgun, chambered for 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "g17"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 12
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/mk58/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/mk58/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/mk58/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* FIREFLY
-*/
-
-/obj/item/gun/ballistic/automatic/pistol/firefly
- name = "\improper P-92 pistol"
- desc = "A simple sidearm made by Armadyne's Medical Directive, with a heavy front for weak wrists. A small warning label on the back says it's not fit for surgical work, and chambered for 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/firefly.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- icon_state = "firefly"
- inhand_icon_state = "firefly"
- fire_delay = 1.95
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/firefly
- can_suppress = FALSE
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/automatic/pistol/firefly/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/gun/ballistic/automatic/pistol/firefly/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-
-/obj/item/ammo_box/magazine/multi_sprite/firefly
- name = "\improper P-92 magazine"
- desc = "A twelve-round magazine for the P-92 pistol, chambered in 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pdh"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 12
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/firefly/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/firefly/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/firefly/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-/*
-* CROON 40x32
-*/
-
-/obj/item/gun/ballistic/automatic/croon
- name = "\improper Croon submachine gun"
- desc = "A low-quality 6.3mm reproduction of a popular SMG model, jams like a bitch. Although crude and unofficial, it gets the job done."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/croon.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi'
- icon_state = "croon"
- inhand_icon_state = "croon"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/croon
- can_suppress = FALSE
- fire_sound = 'sound/weapons/gun/smg/shot.ogg'
- rack_sound = 'sound/weapons/gun/smg/smgrack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
- burst_size = 3
- fire_delay = 2.10
- spread = 25
- actions_types = list(/datum/action/item_action/toggle_firemode)
- mag_display = FALSE
- mag_display_ammo = FALSE
-
-/obj/item/gun/ballistic/automatic/croon/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_IZHEVSK)
-
-/obj/item/ammo_box/magazine/multi_sprite/croon
- name = "\improper Croon magazine"
- desc = "A straight 6.3mm magazine for the Croon SMG."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "croon"
- ammo_type = /obj/item/ammo_casing/b6mm
- caliber = CALIBER_6MM
- max_ammo = 15
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_IHDF)
-
-/obj/item/ammo_box/magazine/multi_sprite/croon/rubber
- ammo_type = /obj/item/ammo_casing/b6mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/croon/ihdf
- ammo_type = /obj/item/ammo_casing/b6mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/*
-* DOZER
-*/
-
-/obj/item/gun/ballistic/automatic/dozer
- name = "\improper Dozer PDW"
- desc = "The DZR-9, a notorious 9x25mm PDW that lives up to its nickname."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/dozer.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- icon_state = "dozer"
- inhand_icon_state = "dozer"
- w_class = WEIGHT_CLASS_NORMAL
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/dozer
- can_suppress = TRUE
- mag_display = FALSE
- mag_display_ammo = FALSE
- burst_size = 2
- fire_delay = 1.90
- actions_types = list(/datum/action/item_action/toggle_firemode)
- fire_sound = 'sound/weapons/gun/rifle/shot.ogg'
- rack_sound = 'sound/weapons/gun/smg/smgrack.ogg'
- lock_back_sound = 'sound/weapons/gun/pistol/slide_lock.ogg'
- bolt_drop_sound = 'sound/weapons/gun/pistol/slide_drop.ogg'
-
-/obj/item/gun/ballistic/automatic/dozer/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/ammo_box/magazine/multi_sprite/dozer
- name = "\improper Dozer magazine"
- desc = "A magazine for the Dozer PDW, chambered for 9x25mm Mark 12."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "croon"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 8
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_HOLLOWPOINT, AMMO_TYPE_INCENDIARY, AMMO_TYPE_AP)
-
-/obj/item/ammo_box/magazine/multi_sprite/dozer/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/dozer/ap
- ammo_type = /obj/item/ammo_casing/c9mm/ap
- round_type = AMMO_TYPE_AP
-
-/obj/item/ammo_box/magazine/multi_sprite/dozer/inc
- ammo_type = /obj/item/ammo_casing/c9mm/fire
- round_type = AMMO_TYPE_INCENDIARY
-
-/*
-* DMR 40x32
-*/
-
-/obj/item/gun/ballistic/automatic/dmr
- name = "\improper Gen-2 Ripper rifle"
- desc = "An incredibly powerful marksman rifle with an internal stabilization gymbal. It's chambered in .577 Snider."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/dmr.dmi'
- icon_state = "dmr"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/dmr.dmi'
- worn_icon_state = "dmr_worn"
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi'
- inhand_icon_state = "dmr"
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_SUITSTORE
- accepted_magazine_type = /obj/item/ammo_box/magazine/dmr
- fire_delay = 1.7
- can_suppress = FALSE
- can_bayonet = FALSE
- mag_display = TRUE
- fire_sound_volume = 60
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/sniper_fire.ogg'
-
-/obj/item/gun/ballistic/automatic/dmr/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/dmr/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/ammo_box/magazine/dmr
- name = "\improper Gen-2 Ripper magazine"
- desc = "A magazine for the Ripper DMR, chambered for .577 Snider."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "dmr"
- ammo_type = /obj/item/ammo_casing/b577
- caliber = CALIBER_B577
- max_ammo = 25
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/*
-* ZETA
-*/
-
-/obj/item/gun/ballistic/revolver/zeta
- name = "\improper Zeta-6 revolver"
- desc = "A fairly common double-action six-shooter chambered for 10mm Auto, 'Spurchamber' is engraved on the cylinder."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/zeta.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- icon_state = "zeta"
- inhand_icon_state = "zeta"
- accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/zeta
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/revolver_fire.ogg'
- fire_delay = 3
-
-/obj/item/gun/ballistic/revolver/zeta/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_BOLT)
-
-/obj/item/ammo_box/magazine/internal/cylinder/zeta
- name = "\improper Zeta-6 cylinder"
- desc = "If you see this, you should call a Bluespace Technician. Unless you're that Bluespace Technician."
- ammo_type = /obj/item/ammo_casing/c10mm
- caliber = CALIBER_10MM
- max_ammo = 6
-
-/obj/item/ammo_box/revolver/zeta
- name = "\improper Zeta-6 speedloader"
- desc = "A speedloader for the Spurchamber revolver, chambered for 10mm Auto ammo."
- icon_state = "speedloader"
- ammo_type = /obj/item/ammo_casing/c10mm
- max_ammo = 6
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- caliber = CALIBER_10MM
- start_empty = TRUE
-
-/obj/item/ammo_box/revolver/zeta/full
- start_empty = FALSE
-
-/*
-* REVOLUTION
-*/
-
-/obj/item/gun/ballistic/revolver/revolution
- name = "\improper Revolution-8 revolver"
- desc = "The Zeta 6's distant cousin, sporting an eight-round competition grade cylinder chambered for 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/revolution.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- icon_state = "revolution"
- inhand_icon_state = "revolution"
- accepted_magazine_type = /obj/item/ammo_box/magazine/internal/cylinder/revolution
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/revolver_fire.ogg'
- fire_delay = 1.90
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/revolver/revolution/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_BOLT)
-
-/obj/item/ammo_box/magazine/internal/cylinder/revolution
- name = "\improper Revolution-8 cylinder"
- desc = "If you see this, you should call a Bluespace Technician. Unless you're that Bluespace Technician."
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 8
-
-/obj/item/ammo_box/revolver/revolution
- name = "\improper Revolution-8 speedloader"
- desc = "A speedloader for the Revolution-8 revolver, chambered in 9x25mm."
- icon_state = "speedloader"
- ammo_type = /obj/item/ammo_casing/c9mm
- max_ammo = 8
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- caliber = CALIBER_9MM
- start_empty = TRUE
-
-/obj/item/ammo_box/revolver/revolution/full
- start_empty = FALSE
-
-/*
-* S.M.A.R.T. RIFLE
-*/
-
-/obj/item/gun/ballistic/automatic/smartgun
- name = "\improper OP-15 'S.M.A.R.T.' rifle"
- desc = "Suppressive Manual Action Reciprocating Taser rifle. A modified version of an Armadyne heavy machine gun fitted to fire miniature shock-bolts."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/smartgun.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi'
- icon_state = "smartgun"
- w_class = WEIGHT_CLASS_HUGE
- weapon_weight = WEAPON_HEAVY
- slot_flags = ITEM_SLOT_BACK
- inhand_icon_state = "smartgun_worn"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/smartgun.dmi'
- worn_icon_state = "smartgun_worn"
- accepted_magazine_type = /obj/item/ammo_box/magazine/smartgun
- actions_types = null
- can_suppress = FALSE
- can_bayonet = FALSE
- mag_display = TRUE
- mag_display_ammo = TRUE
- empty_alarm = TRUE
- tac_reloads = FALSE
- bolt_type = BOLT_TYPE_STANDARD
- semi_auto = FALSE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_fire.ogg'
- rack_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_cock.ogg'
- lock_back_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_open.ogg'
- bolt_drop_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_cock.ogg'
- load_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_magin.ogg'
- load_empty_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_magin.ogg'
- eject_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_magout.ogg'
- load_empty_sound = 'modular_skyrat/modules/sec_haul/sound/chaingun_magout.ogg'
- var/recharge_time = 5 SECONDS
- var/recharging = FALSE
-
-/obj/item/gun/ballistic/automatic/smartgun/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/gun/ballistic/automatic/smartgun/process_chamber()
- . = ..()
- recharging = TRUE
- addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time)
-
-/obj/item/gun/ballistic/automatic/smartgun/proc/recharge()
- recharging = FALSE
- playsound(src, 'sound/weapons/kenetic_reload.ogg', 60, 1)
-
-/obj/item/gun/ballistic/automatic/smartgun/can_shoot()
- . = ..()
- if(recharging)
- return FALSE
-
-/obj/item/gun/ballistic/automatic/smartgun/update_icon()
- . = ..()
- if(!magazine)
- icon_state = "smartgun_open"
- else
- icon_state = "smartgun_closed"
-
-/obj/item/ammo_box/magazine/smartgun
- name = "\improper SMART-Rifle magazine"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "smartgun"
- ammo_type = /obj/item/ammo_casing/smartgun
- caliber = "smartgun"
- max_ammo = 5
- multiple_sprites = AMMO_BOX_PER_BULLET
-
-/obj/item/gun/ballistic/automatic/smartgun/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/gun/ballistic/automatic/smartgun/scoped
- name = "\improper OP-10 'S.M.A.R.T.' Rifle";
- desc = "Suppressive Manual Action Reciprocating Taser rifle. A gauss rifle fitted to fire miniature shock-bolts. Looks like this one is prety heavy, but it has a scope on it.";
- recharge_time = 6 SECONDS;
- recoil = 3;
- slowdown = 0.25;
-
-/obj/item/gun/ballistic/automatic/smartgun/scoped/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/scope, range_modifier = 1.5)
-
-
-/obj/structure/closet/secure_closet/smartgun
- name = "smartgun locker"
- req_access = list(ACCESS_ARMORY)
- icon_state = "shotguncase"
-
-/obj/structure/closet/secure_closet/smartgun/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/smartgun/nomag(src)
- new /obj/item/ammo_box/magazine/smartgun(src)
- new /obj/item/ammo_box/magazine/smartgun(src)
- new /obj/item/ammo_box/magazine/smartgun(src)
-
-/*
-* G11
-*/
-
-/obj/item/gun/ballistic/automatic/g11
- name = "\improper G11 K-490"
- desc = "An outdated german caseless battle rifle that has been revised countless times during the late 2400s. Takes 4.73x33mm toploaded magazines."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/g11.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- icon_state = "g11"
- inhand_icon_state = "g11"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/g11.dmi'
- worn_icon_state = "g11_worn"
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/g11
- can_suppress = FALSE
- fire_delay = 0.5
- spread = 10
- mag_display = TRUE
- mag_display_ammo = TRUE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/ltrifle_fire.ogg'
- can_bayonet = TRUE
-
-/obj/item/gun/ballistic/automatic/g11/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/g11/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/ammo_box/magazine/multi_sprite/g11
- name = "\improper G-11 magazine"
- desc = "A magazine for the G-11 rifle, meant to be filled with angry propellant cubes. Chambered for 4.73mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "g11"
- ammo_type = /obj/item/ammo_casing/b473
- caliber = CALIBER_473MM
- max_ammo = 50
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/g11/hp
- ammo_type = /obj/item/ammo_casing/b473/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/g11/ihdf
- ammo_type = /obj/item/ammo_casing/b473/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/*
-* SHOTGUNS
-*/
-
-/obj/item/gun/ballistic/shotgun/m23
- name = "\improper Model 23-37"
- desc = "An outdated police shotgun sporting an eight-round tube, chambered in twelve-gauge."
- icon_state = "riotshotgun"
- worn_icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi'
- inhand_icon_state = "shotgun"
- accepted_magazine_type = /obj/item/ammo_box/magazine/internal/shot/m23
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
-
-/obj/item/gun/ballistic/shotgun/m23/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_BOLT)
-
-/obj/item/ammo_box/magazine/internal/shot/m23
- name = "m23 shotgun internal magazine"
- caliber = CALIBER_SHOTGUN
- ammo_type = /obj/item/ammo_casing/shotgun/rubbershot
- max_ammo = 8
-
-/obj/item/gun/ballistic/shotgun/automatic/as2
- name = "\improper M2 auto-shotgun"
- desc = "A semi-automatic twelve-gauge shotgun with a four-round internal tube."
- icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns.dmi'
- icon_state = "as2"
- worn_icon_state = "riotshotgun"
- worn_icon = 'modular_skyrat/modules/aesthetics/guns/icons/guns_back.dmi'
- lefthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_lefthand.dmi'
- righthand_file = 'modular_skyrat/modules/aesthetics/guns/icons/guns_righthand.dmi'
- inhand_icon_state = "riot_shotgun"
- inhand_x_dimension = 32
- inhand_y_dimension = 32
- can_suppress = TRUE
- suppressed_sound = 'modular_skyrat/modules/aesthetics/guns/sound/suppressed_shotgun.ogg'
- suppressed_volume = 100
- vary_fire_sound = TRUE
- fire_sound = 'modular_skyrat/modules/aesthetics/guns/sound/shotgun_light.ogg'
- fire_delay = 5
- accepted_magazine_type = /obj/item/ammo_box/magazine/internal/shot/as2
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
-
-/obj/item/gun/ballistic/shotgun/automatic/as2/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/ammo_box/magazine/internal/shot/as2
- name = "shotgun internal magazine"
- caliber = CALIBER_SHOTGUN
- ammo_type = /obj/item/ammo_casing/shotgun
- max_ammo = 4
-
-/*
-* NORWIND
-*/
-/obj/item/gun/ballistic/automatic/norwind
- name = "\improper Norwind rifle"
- desc = "A rare M112 DMR rechambered to 12.7x30mm for peacekeeping work, it comes with a scope for medium-long range engagements. A bayonet lug is visible."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi'
- worn_icon_state = "norwind_worn"
- icon_state = "norwind"
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
- inhand_icon_state = "norwind"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi'
- worn_icon_state = "norwind_worn"
- alt_icons = TRUE
- alt_icon_state = "norwind_worn"
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/norwind
- can_suppress = FALSE
- can_bayonet = TRUE
- mag_display = TRUE
- mag_display_ammo = TRUE
- actions_types = null
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/ltrifle_fire.ogg'
- burst_size = 1
- fire_delay = 10
- actions_types = list()
-
-/obj/item/gun/ballistic/automatic/norwind/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/scope, range_modifier = 1.75)
-
-/obj/item/gun/ballistic/automatic/norwind/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/gun/ballistic/automatic/norwind/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/ammo_box/magazine/multi_sprite/norwind
- name = "\improper Norwind magazine"
- desc = "An eight-round magazine for the Norwind DMR, chambered for 12mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "norwind"
- ammo_type = /obj/item/ammo_casing/b12mm
- caliber = CALIBER_12MM
- max_ammo = 8
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_HOLLOWPOINT, AMMO_TYPE_RUBBER)
-
-/obj/item/ammo_box/magazine/multi_sprite/norwind/hp
- ammo_type = /obj/item/ammo_casing/b12mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/norwind/rubber
- ammo_type = /obj/item/ammo_casing/b12mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* VINTOREZ
-*/
-
-/obj/item/gun/ballistic/automatic/vintorez
- name = "\improper VKC 'Vintorez'"
- desc = "The VKC Vintorez is a lightweight integrally-suppressed scoped carbine usually employed in stealth operations. It was rechambered to 9x19mm for peacekeeping work."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/vintorez.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- icon_state = "vintorez"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi'
- worn_icon_state = "norwind_worn"
- alt_icons = TRUE
- alt_icon_state = "vintorez_worn"
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
- inhand_icon_state = "vintorez"
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/vintorez
- suppressed = TRUE
- can_unsuppress = FALSE
- can_bayonet = FALSE
- mag_display = FALSE
- mag_display_ammo = FALSE
- fire_delay = 4
- spread = 10
- fire_sound = 'sound/weapons/gun/smg/shot_suppressed.ogg'
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/automatic/vintorez/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/scope, range_modifier = 1.5)
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/vintorez/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_IZHEVSK)
-
-/obj/item/ammo_box/magazine/multi_sprite/vintorez
- name = "\improper VKC magazine"
- desc = "A twenty-round magazine for the VKC marksman rifle, chambered in 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "norwind"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 20
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/vintorez/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/vintorez/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/vintorez/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* PCR-9
-*/
-
-/obj/item/gun/ballistic/automatic/pcr
- name = "\improper PCR-9 SMG"
- desc = "An accurate, fast-firing SMG chambered in 9x19mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/pcr.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi'
- worn_icon_state = "ostwind_worn"
- inhand_icon_state = "pcr"
- icon_state = "pcr"
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
- w_class = WEIGHT_CLASS_BULKY
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pcr
- fire_delay = 1.80
- can_suppress = FALSE
- spread = 10
- can_bayonet = FALSE
- mag_display = TRUE
- mag_display_ammo = TRUE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/smg_fire.ogg'
- projectile_damage_multiplier = 0.5
-
-/obj/item/gun/ballistic/automatic/pcr/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/pcr/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_BOLT)
-
-/obj/item/gun/ballistic/automatic/pcr/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/ammo_box/magazine/multi_sprite/pcr
- name = "\improper PCR-9 magazine"
- desc = "A thirty-two round magazine for the PCR-9 submachine gun, chambered for 9x25mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pcr"
- ammo_type = /obj/item/ammo_casing/c9mm
- caliber = CALIBER_9MM
- max_ammo = 32
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/pcr/hp
- ammo_type = /obj/item/ammo_casing/c9mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/pcr/ihdf
- ammo_type = /obj/item/ammo_casing/c9mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/pcr/rubber
- ammo_type = /obj/item/ammo_casing/c9mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/gun/ballistic/automatic/pitbull
- name = "\improper Pitbull PDW"
- desc = "A sturdy personal defense weapon designed to fire 10mm Auto rounds."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/pitbull.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- inhand_icon_state = "pitbull"
- icon_state = "pitbull"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi'
- worn_icon_state = "ostwind_worn"
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/pitbull
- fire_delay = 4.20
- can_suppress = FALSE
- burst_size = 3
- spread = 15
- actions_types = list(/datum/action/item_action/toggle_firemode)
- mag_display = TRUE
- mag_display_ammo = TRUE
- fire_sound = 'modular_skyrat/modules/sec_haul/sound/sfrifle_fire.ogg'
- can_bayonet = TRUE
- projectile_damage_multiplier = 0.7
-
-/obj/item/gun/ballistic/automatic/pitbull/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/pitbull/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_BOLT)
-
-/obj/item/gun/ballistic/automatic/pitbull/add_seclight_point()
- AddComponent(/datum/component/seclite_attachable, light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', light_overlay = "flight")
-
-/obj/item/ammo_box/magazine/multi_sprite/pitbull
- name = "\improper Pitbull magazine"
- desc = "A twenty-four round magazine for the Pitbull PDW, chambered in 10mm Auto."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pcr"
- ammo_type = /obj/item/ammo_casing/c10mm
- caliber = CALIBER_10MM
- max_ammo = 24
- multiple_sprites = AMMO_BOX_FULL_EMPTY
-
-/obj/item/ammo_box/magazine/multi_sprite/pitbull/hp
- ammo_type = /obj/item/ammo_casing/c10mm/hp
- round_type = AMMO_TYPE_HOLLOWPOINT
-
-/obj/item/ammo_box/magazine/multi_sprite/pitbull/ihdf
- ammo_type = /obj/item/ammo_casing/c10mm/ihdf
- round_type = AMMO_TYPE_IHDF
-
-/obj/item/ammo_box/magazine/multi_sprite/pitbull/rubber
- ammo_type = /obj/item/ammo_casing/c10mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/*
-* DTR-6
-*/
-
-/obj/item/gun/ballistic/automatic/ostwind
- name = "\improper DTR-6 Rifle"
- desc = "A 6.3mm special-purpose rifle designed to deal with threats uniquely. You feel like this is a support type firearm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi'
- righthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi'
- lefthand_file = 'modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi'
- inhand_icon_state = "ostwind"
- icon_state = "ostwind"
- worn_icon = 'modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi'
- worn_icon_state = "ostwind_worn"
- alt_icons = TRUE
- alt_icon_state = "ostwind_worn"
- w_class = WEIGHT_CLASS_BULKY
- slot_flags = ITEM_SLOT_BACK | ITEM_SLOT_BELT | ITEM_SLOT_OCLOTHING
- accepted_magazine_type = /obj/item/ammo_box/magazine/multi_sprite/ostwind
- spread = 10
- fire_delay = 2
- can_suppress = FALSE
- burst_size = 2
- actions_types = list(/datum/action/item_action/toggle_firemode)
- mag_display = TRUE
- mag_display_ammo = TRUE
- fire_sound = 'sound/weapons/gun/smg/shot.ogg'
- can_bayonet = TRUE
-
-/obj/item/gun/ballistic/automatic/ostwind/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/automatic_fire, fire_delay)
-
-/obj/item/gun/ballistic/automatic/ostwind/give_manufacturer_examine()
- AddElement(/datum/element/manufacturer_examine, COMPANY_ARMADYNE)
-
-/obj/item/ammo_box/magazine/multi_sprite/ostwind
- name = "\improper DTR-6 magazine"
- desc = "A thirty round double-stack magazine for the DTR-6 rifle, capable of loading flechettes, fragmentation ammo or dissuasive pellets. Chambered for 6.3mm."
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/mags.dmi'
- icon_state = "pcr"
- ammo_type = /obj/item/ammo_casing/b6mm
- caliber = CALIBER_6MM
- max_ammo = 30
- multiple_sprites = AMMO_BOX_FULL_EMPTY
- possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_IHDF)
-
-/obj/item/ammo_box/magazine/multi_sprite/ostwind/rubber
- ammo_type = /obj/item/ammo_casing/b6mm/rubber
- round_type = AMMO_TYPE_RUBBER
-
-/obj/item/ammo_box/magazine/multi_sprite/ostwind/ihdf
- ammo_type = /obj/item/ammo_casing/b6mm/ihdf
- round_type = AMMO_TYPE_IHDF
diff --git a/modular_skyrat/modules/sec_haul/code/guns/gunsets.dm b/modular_skyrat/modules/sec_haul/code/guns/gunsets.dm
deleted file mode 100644
index f89300df920cc1..00000000000000
--- a/modular_skyrat/modules/sec_haul/code/guns/gunsets.dm
+++ /dev/null
@@ -1,483 +0,0 @@
-/*
-* GUNSET BOXES
-*/
-
-/obj/item/storage/box/gunset
- name = "gun case"
- desc = "A gun case with foam inserts laid out to fit a weapon, magazines, and gear securely."
- icon_state = "guncase" //Currently only comes as a generic gray, though there's sprites for Armadyne branded ones in the icon file. There's also sprites for smaller ones!
- inhand_icon_state = "sec-case"
- icon = 'modular_skyrat/modules/sec_haul/icons/guns/gunsets.dmi'
- lefthand_file = 'icons/mob/inhands/equipment/briefcase_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/equipment/briefcase_righthand.dmi'
- w_class = WEIGHT_CLASS_BULKY
- resistance_flags = FLAMMABLE
- drop_sound = 'sound/items/handling/ammobox_drop.ogg'
- pickup_sound = 'sound/items/handling/ammobox_pickup.ogg'
- illustration = null
- var/opened = FALSE
-
-//Add this extra line to examine() if you make an armadyne variant: "It has a textured carbon grip, and the [span_red("Armadyne Corporation")] logo etched into the top."
-
-/obj/item/storage/box/gunset/PopulateContents()
- . = ..()
- new /obj/item/storage/pouch/ammo(src)
-
-/obj/item/storage/box/gunset/update_icon()
- . = ..()
- if(opened)
- icon_state = "[initial(icon_state)]-open"
- else
- icon_state = initial(icon_state)
-
-/obj/item/storage/box/gunset/AltClick(mob/user)
- . = ..()
- opened = !opened
- update_icon()
-
-/obj/item/storage/box/gunset/attack_self(mob/user)
- . = ..()
- opened = !opened
- update_icon()
-
-/*
-* GUN SETS
-*/
-
-/*
-* SIDEARMS
-*/
-
-/*
-* G-17
-*/
-
-/obj/item/storage/box/gunset/glock17
- name = "GK-17 supply box"
-
-/obj/item/gun/ballistic/automatic/pistol/g17/nomag
- spawnwithmagazine = FALSE
-
-
-/obj/item/storage/box/gunset/glock17/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/g17/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g17(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g17(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g17(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g17(src)
-
-/*
-* LADON
-*/
-
-/obj/item/storage/box/gunset/ladon
- name = "p-3 ladon supply box"
-
-/obj/item/gun/ballistic/automatic/pistol/ladon/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/ladon/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/ladon/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ladon(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ladon(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ladon(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ladon(src)
-
-/*
-* DOZER
-*/
-
-/obj/item/storage/box/gunset/dozer
- name = "dozer supply box"
-
-/obj/item/gun/ballistic/automatic/dozer/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/dozer/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/dozer/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/dozer(src)
- new /obj/item/ammo_box/magazine/multi_sprite/dozer(src)
- new /obj/item/ammo_box/magazine/multi_sprite/dozer(src)
-
-/*
-* PDH
-*/
-
-/obj/item/storage/box/gunset/pdh_peacekeeper
- name = "9x19mm handgun supply box"
- desc = "Ideally contains a fast-firing 9x19mm Pistol."
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/peacekeeper/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pdh_peacekeeper/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/pdh/peacekeeper/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_peacekeeper(src)
-
-/*
-* MK-58
-*/
-
-/obj/item/storage/box/gunset/mk58
- name = "mk-58 supply box"
-
-/obj/item/gun/ballistic/automatic/pistol/mk58/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/mk58/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/mk58/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/mk58(src)
- new /obj/item/ammo_box/magazine/multi_sprite/mk58(src)
- new /obj/item/ammo_box/magazine/multi_sprite/mk58(src)
- new /obj/item/ammo_box/magazine/multi_sprite/mk58(src)
-
-/*
-* CROON
-*/
-
-/obj/item/storage/box/gunset/croon
- name = "weathered supply box"
- desc = "Ideally contains a cheap 6mm SMG."
-
-/obj/item/gun/ballistic/automatic/croon/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/croon/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/croon/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/croon(src)
- new /obj/item/ammo_box/magazine/multi_sprite/croon(src)
- new /obj/item/ammo_box/magazine/multi_sprite/croon(src)
- new /obj/item/ammo_box/magazine/multi_sprite/croon(src)
-
-/*
-* MAKAROV
-*/
-
-/obj/item/storage/box/gunset/makarov
- name = "makarov supply box"
-
-
-/obj/item/ammo_box/magazine/multi_sprite/makarov/empty
- start_empty = TRUE
-
-/obj/item/storage/box/gunset/makarov/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/makarov(src)
- new /obj/item/ammo_box/magazine/multi_sprite/makarov(src)
- new /obj/item/ammo_box/magazine/multi_sprite/makarov(src)
- new /obj/item/ammo_box/magazine/multi_sprite/makarov(src)
- new /obj/item/ammo_box/magazine/multi_sprite/makarov(src)
-
-/*
-* ZETA
-*/
-
-/obj/item/storage/box/gunset/zeta
- name = "10mm revolver supply box"
- desc = "Ideally contains a slow-firing revolver that packs a punch."
-
-/obj/item/storage/box/gunset/zeta/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/revolver/zeta(src)
- new /obj/item/ammo_box/revolver/zeta(src) //These start empty.
- new /obj/item/ammo_box/revolver/zeta(src)
- new /obj/item/ammo_box/revolver/zeta(src)
- new /obj/item/ammo_box/c10mm(src)
-
-/*
-* REVOLUTION
-*/
-
-/obj/item/storage/box/gunset/revolution
- name = "revolution supply box"
-
-/obj/item/storage/box/gunset/revolution/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/revolver/revolution(src)
- new /obj/item/ammo_box/revolver/revolution(src)
- new /obj/item/ammo_box/revolver/revolution(src)
- new /obj/item/ammo_box/revolver/revolution(src)
- new /obj/item/ammo_box/c9mm(src)
-
-/*
-* PRIMARIES
-*/
-
-/*
-* PCR-9
-*/
-
-/obj/item/storage/box/gunset/pcr
- name = "9mm SMG supply box"
- desc = "Ideally contains a 9x19mm SMG with decent firerate."
-
-/obj/item/gun/ballistic/automatic/pcr/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pcr/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pcr/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pcr(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pcr(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pcr(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pcr(src)
-
-/*
-* NORWIND
-*/
-
-/obj/item/storage/box/gunset/norwind
- name = "12.7x30mm DMR supply box."
- desc = "Ideally contains an unwieldy rifle that hits like a truck."
-
-/obj/item/gun/ballistic/automatic/norwind/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/norwind/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/norwind/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/norwind(src)
- new /obj/item/ammo_box/magazine/multi_sprite/norwind(src)
- new /obj/item/ammo_box/magazine/multi_sprite/norwind(src)
- new /obj/item/ammo_box/magazine/multi_sprite/norwind(src)
-
-/*
-* OSTWIND
-*/
-
-/obj/item/storage/box/gunset/ostwind
- name = "6mm SPR box."
- desc = "Ideally contains an all-around balanced special purpose rifle."
-
-/obj/item/gun/ballistic/automatic/ostwind/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/ostwind/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/ostwind/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ostwind(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ostwind(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ostwind(src)
- new /obj/item/ammo_box/magazine/multi_sprite/ostwind(src)
-
-/*
-* PITBULL
-*/
-
-/obj/item/storage/box/gunset/pitbull
- name = "10mm PDW supply box"
- desc = "Ideally contains a slow-firing 10mm PDW that packs a punch."
-
-/obj/item/gun/ballistic/automatic/pitbull/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pitbull/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pitbull/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pitbull(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pitbull(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pitbull(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pitbull(src)
-
-/*
-* JOB-SPECIFIC
-*/
-
-/*
-* CAPTAIN
-*/
-
-/obj/item/storage/box/gunset/pdh_captain
- name = "pdh 'socom' supply box"
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/alt/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pdh_captain/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/pdh/alt/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
-
-/*
-* HOS
-*/
-
-/obj/item/storage/box/gunset/glock18_hos
- name = "GK-18 supply box"
- desc = "Ideally contains a fast-firing 9x19mm pistol made out of cheap plastic."
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/g18/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/glock18_hos/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/g18/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g18(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g18(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g18(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g18/ihdf(src)
-
-/*
-* HOP
-*/
-
-/obj/item/storage/box/gunset/pdh
- name = "pdh 'osprey' supply box"
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pdh/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/pdh/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh(src)
-
-/*
-* CORPO
-*/
-
-/obj/item/storage/box/gunset/pdh_corpo
- name = "pdh 'corporate' supply box"
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/corpo/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pdh_corpo/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/pdh/corpo/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_corpo(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_corpo(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_corpo(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_corpo(src)
-
-/*
-* STRIKER
-*/
-
-/obj/item/storage/box/gunset/pdh_striker
- name = "pdh 'striker' supply box"
- w_class = WEIGHT_CLASS_NORMAL
-
-/obj/item/gun/ballistic/automatic/pistol/pdh/striker/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pdh_striker/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/pdh/striker/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_striker(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_striker(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_striker(src)
- new /obj/item/ammo_box/magazine/multi_sprite/pdh_striker(src)
-
-// KRAUT SPACE MAGIC!
-/obj/item/storage/box/gunset/g11
- name = "g11 supply box"
-
-/obj/item/gun/ballistic/automatic/g11/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/g11/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/g11/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g11(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g11(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g11(src)
- new /obj/item/ammo_box/magazine/multi_sprite/g11(src)
-
-/*
-* OLD SECMED SIDEARM
-*/
-
-/obj/item/storage/box/gunset/firefly
- name = "9x19mm special pistol supply box"
- desc = "Ideally contains a special 9x19mm Pistol."
- w_class = WEIGHT_CLASS_NORMAL
-/obj/item/gun/ballistic/automatic/pistol/firefly/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/firefly/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/firefly/nomag(src)
- new /obj/item/ammo_box/magazine/multi_sprite/firefly(src)
- new /obj/item/ammo_box/magazine/multi_sprite/firefly(src)
- new /obj/item/ammo_box/magazine/multi_sprite/firefly/rubber(src)
- new /obj/item/ammo_box/magazine/multi_sprite/firefly/rubber(src)
-
-/*
-* LASER
-*/
-
-/obj/item/storage/box/gunset/laser
- name = "laser gun supply box"
-
-/obj/item/storage/box/gunset/laser/PopulateContents()
- . = ..()
- new /obj/item/gun/energy/laser(src)
-
-/obj/item/storage/box/gunset/e_gun
-
-/obj/item/storage/box/gunset/e_gun/PopulateContents()
- . = ..()
- new /obj/item/gun/energy/e_gun(src)
-
-/*
-* PEPPERBALLS
-*/
-
-/obj/item/storage/box/gunset/pepperball
- name = "pepperball supply box"
- w_class = WEIGHT_CLASS_NORMAL
-/obj/item/gun/ballistic/automatic/pistol/pepperball/nomag
- spawnwithmagazine = FALSE
-
-/obj/item/storage/box/gunset/pepperball/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/automatic/pistol/pepperball/nomag(src)
- new /obj/item/ammo_box/magazine/pepperball(src)
- new /obj/item/ammo_box/magazine/pepperball(src)
- new /obj/item/ammo_box/magazine/pepperball(src)
- new /obj/item/ammo_box/magazine/pepperball(src)
-
-
-/*
-* SHOTGUNS
-*/
-
-/obj/item/storage/box/gunset/m23
- name = "m23 supply box"
-
-/obj/item/storage/box/gunset/m23/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/shotgun/m23(src)
- new /obj/item/storage/box/beanbag(src)
- new /obj/item/storage/box/beanbag(src)
- new /obj/item/storage/box/beanbag(src)
-
-/obj/item/storage/box/gunset/as2
- name = "as2 supply box"
-
-/obj/item/storage/box/gunset/as2/PopulateContents()
- . = ..()
- new /obj/item/gun/ballistic/shotgun/automatic/as2(src)
- new /obj/item/storage/box/beanbag(src)
- new /obj/item/storage/box/beanbag(src)
- new /obj/item/storage/box/beanbag(src)
diff --git a/modular_skyrat/modules/sec_haul/code/peacekeeper/armadyne_clothing.dm b/modular_skyrat/modules/sec_haul/code/peacekeeper/armadyne_clothing.dm
index 89f0108f4dc22f..40110bc3820fb5 100644
--- a/modular_skyrat/modules/sec_haul/code/peacekeeper/armadyne_clothing.dm
+++ b/modular_skyrat/modules/sec_haul/code/peacekeeper/armadyne_clothing.dm
@@ -106,7 +106,7 @@
r_pocket = /obj/item/assembly/flash/handheld
backpack_contents = list(
/obj/item/melee/baton/telescopic,
- /obj/item/storage/box/gunset/pdh_corpo,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
)
back = /obj/item/storage/backpack/satchel/leather
box = /obj/item/storage/box/survival/security
@@ -124,12 +124,12 @@
glasses = /obj/item/clothing/glasses/hud/security/sunglasses/peacekeeper/armadyne
mask = /obj/item/clothing/mask/gas/sechailer
suit = /obj/item/clothing/suit/armor/vest/peacekeeper/armadyne/armor
- suit_store = /obj/item/gun/ballistic/automatic/pitbull
+ suit_store = /obj/item/gun/ballistic/automatic/sol_smg
shoes = /obj/item/clothing/shoes/jackboots/peacekeeper/armadyne
backpack_contents = list(
- /obj/item/storage/box/gunset/pdh_striker,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/wespe,
/obj/item/storage/box/handcuffs,
- /obj/item/ammo_box/magazine/multi_sprite/pitbull,
+ /obj/item/ammo_box/magazine/c35sol_pistol/stendo,
/obj/item/modular_computer/pda/security,
)
back = /obj/item/storage/backpack/security
@@ -147,13 +147,13 @@
glasses = /obj/item/clothing/glasses/hud/security/sunglasses/peacekeeper/armadyne
mask = /obj/item/clothing/mask/gas/sechailer/swat
suit = /obj/item/clothing/suit/armor/vest/peacekeeper/armadyne/armor
- suit_store = /obj/item/gun/ballistic/automatic/norwind
+ suit_store = /obj/item/gun/ballistic/automatic/sol_rifle
shoes = /obj/item/clothing/shoes/jackboots/peacekeeper/armadyne
belt = /obj/item/storage/belt/security/webbing/peacekeeper/armadyne
backpack_contents = list(
- /obj/item/storage/box/gunset/pdh_striker,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/wespe,
/obj/item/storage/box/handcuffs,
- /obj/item/ammo_box/magazine/multi_sprite/norwind,
+ /obj/item/ammo_box/magazine/c40sol_rifle/standard,
/obj/item/modular_computer/pda/security,
)
back = /obj/item/storage/backpack/security
@@ -165,23 +165,23 @@
/datum/outfit/armadyne_security/high_alert
name = "Armadyne Corporate Security (High Alert)"
belt = /obj/item/storage/belt/security/webbing/peacekeeper/armadyne
- suit_store = /obj/item/gun/ballistic/automatic/dmr
+ suit_store = /obj/item/gun/ballistic/automatic/sol_rifle
backpack_contents = list(
/obj/item/melee/baton/telescopic,
- /obj/item/storage/box/gunset/pdh_corpo,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/wespe,
/obj/item/storage/box/handcuffs,
- /obj/item/ammo_box/magazine/dmr = 2,
+ /obj/item/ammo_box/magazine/c40sol_rifle/standard = 2,
)
/datum/outfit/armadyne_security/commander/high_alert
name = "Armadyne Corporate Security Commander (High Alert)"
- suit_store = /obj/item/gun/ballistic/automatic/dmr
+ suit_store = /obj/item/gun/ballistic/automatic/sol_rifle
backpack_contents = list(
/obj/item/melee/baton/telescopic,
- /obj/item/storage/box/gunset/pdh_corpo,
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild,
/obj/item/storage/box/handcuffs,
- /obj/item/ammo_box/magazine/dmr = 2,
+ /obj/item/ammo_box/magazine/c40sol_rifle/standard = 2,
)
/obj/item/card/id/advanced/armadyne
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi b/modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi
deleted file mode 100644
index cbcea85d3e11c0..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/ammo_cartridges.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/automag.dmi b/modular_skyrat/modules/sec_haul/icons/guns/automag.dmi
deleted file mode 100644
index b8345858e783a4..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/automag.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/croon.dmi b/modular_skyrat/modules/sec_haul/icons/guns/croon.dmi
deleted file mode 100644
index c52529979875f9..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/croon.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/dmr.dmi b/modular_skyrat/modules/sec_haul/icons/guns/dmr.dmi
deleted file mode 100644
index 742b839fd381aa..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/dmr.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/dozer.dmi b/modular_skyrat/modules/sec_haul/icons/guns/dozer.dmi
deleted file mode 100644
index 13f45e80fbf7d8..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/dozer.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/energy/allstar.dmi b/modular_skyrat/modules/sec_haul/icons/guns/energy/allstar.dmi
deleted file mode 100644
index 5251c860046e0d..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/energy/allstar.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/firefly.dmi b/modular_skyrat/modules/sec_haul/icons/guns/firefly.dmi
deleted file mode 100644
index 8ef8d9e74728cd..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/firefly.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/g11.dmi b/modular_skyrat/modules/sec_haul/icons/guns/g11.dmi
deleted file mode 100644
index 7d17f7274047a9..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/g11.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/glock.dmi b/modular_skyrat/modules/sec_haul/icons/guns/glock.dmi
deleted file mode 100644
index ee12d3df9c6d72..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/glock.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/gunsets.dmi b/modular_skyrat/modules/sec_haul/icons/guns/gunsets.dmi
deleted file mode 100644
index daf0244d3d259a..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/gunsets.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi b/modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi
deleted file mode 100644
index 43b271bdec2ac4..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi b/modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi
deleted file mode 100644
index e77ceeeb272690..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/inhands/lefthand40x32.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi b/modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi
deleted file mode 100644
index 8055cd1aaa7e5e..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi b/modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi
deleted file mode 100644
index c4e1ebd38e2f34..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/inhands/righthand40x32.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/ladon.dmi b/modular_skyrat/modules/sec_haul/icons/guns/ladon.dmi
deleted file mode 100644
index 312d06a1584206..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/ladon.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/mags.dmi b/modular_skyrat/modules/sec_haul/icons/guns/mags.dmi
deleted file mode 100644
index 68c1d20756e941..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/mags.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/makarov.dmi b/modular_skyrat/modules/sec_haul/icons/guns/makarov.dmi
deleted file mode 100644
index 4f0ed19dbf2f67..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/makarov.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/mk58.dmi b/modular_skyrat/modules/sec_haul/icons/guns/mk58.dmi
deleted file mode 100644
index ab7496ad13b691..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/mk58.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi b/modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi
deleted file mode 100644
index c05509da30ea55..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/norwind.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi b/modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi
deleted file mode 100644
index 3f2d26a819acc2..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/ostwind.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/pcr.dmi b/modular_skyrat/modules/sec_haul/icons/guns/pcr.dmi
deleted file mode 100644
index 8e8fb126853042..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/pcr.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/pcr2.dmi b/modular_skyrat/modules/sec_haul/icons/guns/pcr2.dmi
deleted file mode 100644
index 87bb9f4fdd25d3..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/pcr2.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/pdh.dmi b/modular_skyrat/modules/sec_haul/icons/guns/pdh.dmi
deleted file mode 100644
index e4e2a606b8e292..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/pdh.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/pitbull.dmi b/modular_skyrat/modules/sec_haul/icons/guns/pitbull.dmi
deleted file mode 100644
index d429b62a3a768a..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/pitbull.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/revolution.dmi b/modular_skyrat/modules/sec_haul/icons/guns/revolution.dmi
deleted file mode 100644
index da6ab4703d6eb8..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/revolution.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/sas14.dmi b/modular_skyrat/modules/sec_haul/icons/guns/sas14.dmi
deleted file mode 100644
index 77452eaf7c1624..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/sas14.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/smartgun.dmi b/modular_skyrat/modules/sec_haul/icons/guns/smartgun.dmi
deleted file mode 100644
index ef61aa8667f01e..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/smartgun.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/spawner.dmi b/modular_skyrat/modules/sec_haul/icons/guns/spawner.dmi
deleted file mode 100644
index a5a06c490a7424..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/spawner.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/vintorez.dmi b/modular_skyrat/modules/sec_haul/icons/guns/vintorez.dmi
deleted file mode 100644
index 413f0fd8fdb1c6..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/vintorez.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/sec_haul/icons/guns/zeta.dmi b/modular_skyrat/modules/sec_haul/icons/guns/zeta.dmi
deleted file mode 100644
index ae0d76adccb2b8..00000000000000
Binary files a/modular_skyrat/modules/sec_haul/icons/guns/zeta.dmi and /dev/null differ
diff --git a/modular_skyrat/modules/self_actualization_device/code/self_actualization_device.dm b/modular_skyrat/modules/self_actualization_device/code/self_actualization_device.dm
index 1af51a6f4da300..d50759e484f973 100644
--- a/modular_skyrat/modules/self_actualization_device/code/self_actualization_device.dm
+++ b/modular_skyrat/modules/self_actualization_device/code/self_actualization_device.dm
@@ -124,7 +124,7 @@
use_power(500)
-/// Ejects the occupant as either their preference character, or as a monke based on emag status.
+/// Ejects the occupant after asking them if they want to accept the rejuvenation. If yes, they exit as their preferences character.
/obj/machinery/self_actualization_device/proc/eject_new_you()
if(state_open || !occupant || !powered())
return
@@ -132,21 +132,36 @@
if(!ishuman(occupant))
return FALSE
+ var/mob/living/carbon/human/human_occupant = occupant
- var/mob/living/carbon/human/patient = occupant
- var/original_name = patient.dna.real_name
+ var/failure = FALSE
+ var/failure_text
- patient.client?.prefs?.safe_transfer_prefs_to_with_damage(patient)
- patient.dna.update_dna_identity()
- log_game("[key_name(patient)] used a Self-Actualization Device at [loc_name(src)].")
+ if (!isnull(human_occupant.ckey) && isnull(human_occupant.client)) // player mob, currently disconnected
+ failure = TRUE
+ failure_text = "ERROR: Treatment elicited no response from occupant genes. Subject may be suffering from Sudden Sleep Disorder."
+ else if (tgui_alert(occupant, "The SAD you are within is about to rejuvenate you, resetting your body to its default state (in character preferences). Do you consent?", "Rejuvenate", list("Yes", "No"), timeout = 10 SECONDS) != "Yes")
+ failure = TRUE // defaults to rejecting it unless specified otherwise
+ failure_text = "ERROR: Occupant genes have willfully rejected the procedure. You may try again if you think this was an error."
- if(patient.dna.real_name != original_name)
- message_admins("[key_name_admin(patient)] has used the Self-Actualization Device, and changed the name of their character. \
- Original Name: [original_name], New Name: [patient.dna.real_name]. \
- This may be a false positive from changing from a humanized monkey into a character, so be careful.")
+ if (failure)
+ say(failure_text)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ else
+ var/mob/living/carbon/human/patient = occupant
+ var/original_name = patient.dna.real_name
+
+ patient.client?.prefs?.safe_transfer_prefs_to_with_damage(patient)
+ patient.dna.update_dna_identity()
+ log_game("[key_name(patient)] used a Self-Actualization Device at [loc_name(src)].")
+
+ if(patient.dna.real_name != original_name)
+ message_admins("[key_name_admin(patient)] has used the Self-Actualization Device, and changed the name of their character. \
+ Original Name: [original_name], New Name: [patient.dna.real_name]. \
+ This may be a false positive from changing from a humanized monkey into a character, so be careful.")
+ playsound(src, 'sound/machines/microwave/microwave-end.ogg', 100, FALSE)
open_machine()
- playsound(src, 'sound/machines/microwave/microwave-end.ogg', 100, FALSE)
/obj/machinery/self_actualization_device/screwdriver_act(mob/living/user, obj/item/used_item)
. = TRUE
diff --git a/modular_skyrat/modules/space_vines/scythes.dm b/modular_skyrat/modules/space_vines/scythes.dm
index 084d33385501a4..910753489e3350 100644
--- a/modular_skyrat/modules/space_vines/scythes.dm
+++ b/modular_skyrat/modules/space_vines/scythes.dm
@@ -24,7 +24,7 @@
var/swiping = FALSE
/obj/item/scythe/pre_attack(atom/A, mob/living/user, params)
- if(!istype(A, /obj/structure/spacevine) && !istype(A, /mob/living/simple_animal/hostile/venus_human_trap))
+ if(!istype(A, /obj/structure/spacevine) && !istype(A, /mob/living/basic/venus_human_trap))
return ..()
if(swiping)
return ..()
diff --git a/modular_skyrat/modules/space_vines/venus.dm b/modular_skyrat/modules/space_vines/venus.dm
index 7dafe18a755f05..abd912b8e9041e 100644
--- a/modular_skyrat/modules/space_vines/venus.dm
+++ b/modular_skyrat/modules/space_vines/venus.dm
@@ -1,21 +1,5 @@
-/mob/living/simple_animal/hostile/venus_human_trap/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- . = ..()
- check_vines()
-
-/mob/living/simple_animal/hostile/venus_human_trap/death(gibbed)
- for(var/i in vines)
- qdel(i)
- return ..()
-
-/mob/living/simple_animal/hostile/venus_human_trap/start_pulling(atom/movable/movable_target, state, force, supress_message)
+/mob/living/basic/venus_human_trap/start_pulling(atom/movable/movable_target, state, force, supress_message)
if(isliving(movable_target))
to_chat(src, span_boldwarning("You cannot drag living things!"))
return
return ..()
-
-/mob/living/simple_animal/hostile/venus_human_trap/proc/check_vines()
- var/obj/structure/spacevine/find_vine = locate() in get_turf(src)
- if(!find_vine)
- adjustHealth(maxHealth * 0.05)
- else
- adjustHealth(maxHealth * -0.05)
diff --git a/modular_skyrat/modules/synths/code/README.md b/modular_skyrat/modules/synths/code/README.md
index 21ce4e629dabd8..8f37a1cec49409 100644
--- a/modular_skyrat/modules/synths/code/README.md
+++ b/modular_skyrat/modules/synths/code/README.md
@@ -10,6 +10,7 @@ Adds in a roundstart robotic race. Currently in a very sad state, and is being w
### TG Proc/File Changes:
+- defib.dm: /obj/item/shockpaddles/proc/do_help() modified
- Will fill out as I discover what edits were made to acommodate these.
### Modular Overrides:
@@ -18,12 +19,14 @@ Adds in a roundstart robotic race. Currently in a very sad state, and is being w
### Defines:
-- Will fill out as I discover what defines were made to acommodate these.
+- ~skyrat_defines/medical_defines.dm: SYNTH_DEFIBBED_TRAUMA_DURATION
+- ~skyrat_defines/medical_defines.dm: SYNTH_DEFIBBED_TRAUMA_SEVERITY
### Included files that are not contained in this module:
- N/A
### Credits:
+Niko - Making defibs fuck synths up
Nerevar - Initial code, I think. Correct this file if wrong.
RimiNosha - Updating the code and adding various QoL features.
diff --git a/modular_skyrat/modules/synths/code/bodyparts/limbs.dm b/modular_skyrat/modules/synths/code/bodyparts/limbs.dm
index aba679efaef2dd..1126295f1fe690 100644
--- a/modular_skyrat/modules/synths/code/bodyparts/limbs.dm
+++ b/modular_skyrat/modules/synths/code/bodyparts/limbs.dm
@@ -1,5 +1,5 @@
-#define SYNTH_BRUTE_MODIFIER 1.3
-#define SYNTH_BURN_MODIFIER 1.3
+#define SYNTH_BRUTE_MODIFIER 1.0
+#define SYNTH_BURN_MODIFIER 1.0
// Synth bois!
/obj/item/bodypart/head/robot/synth
diff --git a/modular_skyrat/modules/synths/code/defib.dm b/modular_skyrat/modules/synths/code/defib.dm
new file mode 100644
index 00000000000000..ecb6e2e952eb4c
--- /dev/null
+++ b/modular_skyrat/modules/synths/code/defib.dm
@@ -0,0 +1,12 @@
+/**
+ * Global timer proc used in defib.dm. Removes the temporary trauma caused by being defibbed as a synth.
+ *
+ * Args:
+ * * obj/item/organ/internal/brain/synth_brain: The brain with the trauma on it. Non-nullable.
+ * * datum/brain_trauma/trauma: The trauma itself. Non-nullable.
+ */
+/proc/remove_synth_defib_trauma(obj/item/organ/internal/brain/synth_brain, datum/brain_trauma/trauma)
+ if (QDELETED(synth_brain) || QDELETED(trauma))
+ return
+
+ QDEL_NULL(trauma)
diff --git a/modular_skyrat/modules/synths/code/reagents/pill.dm b/modular_skyrat/modules/synths/code/reagents/pill.dm
index 15b1fd95085882..acf54dbd69ec60 100644
--- a/modular_skyrat/modules/synths/code/reagents/pill.dm
+++ b/modular_skyrat/modules/synths/code/reagents/pill.dm
@@ -2,7 +2,7 @@
name = "liquid solder pill"
desc = "Used to treat synthetic brain damage."
icon_state = "pill21"
- list_reagents = list(/datum/reagent/medicine/liquid_solder = 50)
+ list_reagents = list(/datum/reagent/medicine/liquid_solder = 10)
rename_with_volume = TRUE
// Lower quantity solder pill.
@@ -15,12 +15,12 @@
name = "nanite slurry pill"
desc = "Used to repair robotic bodyparts."
icon_state = "pill18"
- list_reagents = list(/datum/reagent/medicine/nanite_slurry = 19)
+ list_reagents = list(/datum/reagent/medicine/nanite_slurry = 15) // 20 is OD
rename_with_volume = TRUE
/obj/item/reagent_containers/pill/system_cleaner
name = "system cleaner pill"
desc = "Used to detoxify synthetic bodies."
icon_state = "pill7"
- list_reagents = list(/datum/reagent/medicine/system_cleaner = 50)
+ list_reagents = list(/datum/reagent/medicine/system_cleaner = 10)
rename_with_volume = TRUE
diff --git a/modular_skyrat/modules/synths/code/research_nodes.dm b/modular_skyrat/modules/synths/code/research_nodes.dm
new file mode 100644
index 00000000000000..c75d134db45f0c
--- /dev/null
+++ b/modular_skyrat/modules/synths/code/research_nodes.dm
@@ -0,0 +1,19 @@
+/datum/techweb_node/improved_robotic_tend_wounds
+ id = "improved_robotic_surgery"
+ display_name = "Improved Robotic Repair Surgeries"
+ description = "As it turns out, you don't actually need to cut out entire support rods if it's just scratched!"
+ prereq_ids = list("engineering")
+ design_ids = list(
+ "robotic_heal_surgery_upgrade"
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 900)
+
+/datum/techweb_node/advanced_robotic_tend_wounds
+ id = "advanced_robotic_surgery"
+ display_name = "Advanced Robotic Surgeries"
+ description = "Did you know Hephaestus actually has a free online tutorial for synthetic trauma repairs? It's true!"
+ prereq_ids = list("improved_robotic_surgery")
+ design_ids = list(
+ "robotic_heal_surgery_upgrade_2"
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1300) // less expensive than the organic surgery research equivalent since its JUST tend wounds
diff --git a/modular_skyrat/modules/synths/code/species/synthetic.dm b/modular_skyrat/modules/synths/code/species/synthetic.dm
index 0809111890958b..1630d14e1f27eb 100644
--- a/modular_skyrat/modules/synths/code/species/synthetic.dm
+++ b/modular_skyrat/modules/synths/code/species/synthetic.dm
@@ -187,7 +187,7 @@
SPECIES_PERK_ICON = "robot",
SPECIES_PERK_NAME = "Synthetic Benefits",
SPECIES_PERK_DESC = "Unlike organics, you DON'T explode when faced with a vacuum! Additionally, your chassis is built with such strength as to \
- grant you immunity to OVERpressure! Just make sure that the extreme cold or heat doesn't fry your circuitry. On top of this, synthetics are unable to be wounded!"
+ grant you immunity to OVERpressure! Just make sure that the extreme cold or heat doesn't fry your circuitry."
))
perk_descriptions += list(list(
@@ -203,7 +203,7 @@
SPECIES_PERK_NAME = "Synthetic Oddities",
SPECIES_PERK_DESC = "[plural_form] are unable to gain nutrition from traditional foods. Instead, you must either consume welding fuel or extend a \
wire from your arm to draw power from an APC. In addition to this, welders and wires are your sutures and mesh and only specific chemicals even metabolize inside \
- of you. This ranges from whiskey, to synthanol, to various obscure medicines."
+ of you. This ranges from whiskey, to synthanol, to various obscure medicines. Finally, you suffer from a set of wounds exclusive to synthetics."
))
return perk_descriptions
diff --git a/modular_skyrat/modules/synths/code/surgery/robot_chassis_restoration.dm b/modular_skyrat/modules/synths/code/surgery/robot_chassis_restoration.dm
index ab100c0f7515e2..42e3ff853bbd73 100644
--- a/modular_skyrat/modules/synths/code/surgery/robot_chassis_restoration.dm
+++ b/modular_skyrat/modules/synths/code/surgery/robot_chassis_restoration.dm
@@ -1,14 +1,15 @@
+#define SYNTH_REVIVE_WELD_INTERNALS_DAMAGE 30
+
+// Should be a very quick surgery, it's meant to replace defibs (mostly!)
/datum/surgery/positronic_restoration
name = "Posibrain Reboot (Revival)"
steps = list(
/datum/surgery_step/mechanic_unwrench,
/datum/surgery_step/pry_off_plating/fullbody,
- /datum/surgery_step/cut_wires/fullbody,
- /datum/surgery_step/replace_wires/fullbody,
- /datum/surgery_step/prepare_electronics,
- /datum/surgery_step/add_plating/fullbody,
/datum/surgery_step/weld_plating/fullbody,
+ /datum/surgery_step/prepare_electronics,
/datum/surgery_step/finalize_positronic_restoration,
+ /datum/surgery_step/add_plating/fullbody,
/datum/surgery_step/mechanic_close,
)
@@ -24,52 +25,34 @@
return TRUE
/datum/surgery_step/pry_off_plating/fullbody
- time = 12 SECONDS
+ time = 1.4 SECONDS
/datum/surgery_step/pry_off_plating/fullbody/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
display_results(
user,
target,
- span_notice("You begin to pry open compromised panels on [target]'s braincase..."),
- span_notice("[user] begins to pry open compromised panels on [target]'s braincase."),
- )
-
-/datum/surgery_step/cut_wires/fullbody
- time = 12 SECONDS
-
-/datum/surgery_step/cut_wires/fullbody/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
- display_results(
- user,
- target,
- span_notice("You begin to trim [target]'s nonfunctional wires..."),
- span_notice("[user] begins to cut [target]'s loose wires."),
+ span_notice("You begin to pry open the outer protective panels on [target]'s braincase..."),
+ span_notice("[user] begins to pry open the outer protective panels on [target]'s braincase."),
)
/datum/surgery_step/weld_plating/fullbody
- time = 12 SECONDS
+ time = 2 SECONDS
/datum/surgery_step/weld_plating/fullbody/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
display_results(
user,
target,
- span_notice("You begin to slice compromised panels from [target]'s braincase..."),
- span_notice("[user] begins to slice compromised panels from [target]'s braincase."),
+ span_notice("You begin to slice the inner protective panels from [target]'s braincase..."),
+ span_notice("[user] begins to slice the inner protective panels from [target]'s braincase."),
)
-/datum/surgery_step/replace_wires/fullbody
- time = 7 SECONDS
- cableamount = 15
+/datum/surgery_step/weld_plating/fullbody/success(mob/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results)
+ . = ..()
-/datum/surgery_step/replace_wires/fullbody/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
- display_results(
- user,
- target,
- span_notice("You begin to replace [target]'s wiring..."),
- span_notice("[user] begins to replace [target]'s wiring."),
- )
+ target.apply_damage(SYNTH_REVIVE_WELD_INTERNALS_DAMAGE, BRUTE, "[target_zone]", wound_bonus = CANT_WOUND)
/datum/surgery_step/add_plating/fullbody
- time = 12 SECONDS
+ time = 3 SECONDS
ironamount = 15
/datum/surgery_step/add_plating/fullbody/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
@@ -80,13 +63,21 @@
span_notice("[user] begins to add new panels to [target]'s braincase."),
)
+/datum/surgery_step/add_plating/fullbody/success(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
+ . = ..()
+
+ target.heal_bodypart_damage(brute = SYNTH_REVIVE_WELD_INTERNALS_DAMAGE, target_zone = "[target_zone]")
+
/datum/surgery_step/finalize_positronic_restoration
- name = "finalize positronic restoration (multitool)"
+ name = "finalize positronic restoration (multitool/shocking implement)"
implements = list(
TOOL_MULTITOOL = 100,
+ /obj/item/shockpaddles = 70,
+ /obj/item/melee/touch_attack/shock = 70,
+ /obj/item/melee/baton/security = 35,
+ /obj/item/gun/energy = 10
)
- repeatable = TRUE
- time = 12 SECONDS
+ time = 5 SECONDS
/datum/surgery_step/finalize_positronic_restoration/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
display_results(
@@ -99,10 +90,13 @@
target.notify_ghost_cloning("Someone is trying to reboot your posibrain.", source = target)
/datum/surgery_step/finalize_positronic_restoration/success(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
+ if (target.stat < DEAD)
+ target.visible_message(span_notice("...[target] is completely unaffected! Seems like they're already active!"))
+ return FALSE
+
target.cure_husk()
target.grab_ghost()
target.updatehealth()
- target.setOrganLoss(ORGAN_SLOT_BRAIN, NONE)
if(target.revive())
target.emote("chime")
@@ -114,3 +108,9 @@
target.visible_message(span_warning("...[target.p_they()] convulses, then goes offline."))
return TRUE
+/datum/surgery_step/finalize_positronic_restoration/failure(mob/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, fail_prob)
+ . = ..()
+
+ target.adjustOrganLoss(ORGAN_SLOT_BRAIN, 5, 130)
+
+#undef SYNTH_REVIVE_WELD_INTERNALS_DAMAGE
diff --git a/modular_skyrat/modules/synths/code/surgery/robot_healing.dm b/modular_skyrat/modules/synths/code/surgery/robot_healing.dm
index 645409a5c98f41..742cf7281ebd1e 100644
--- a/modular_skyrat/modules/synths/code/surgery/robot_healing.dm
+++ b/modular_skyrat/modules/synths/code/surgery/robot_healing.dm
@@ -122,6 +122,9 @@
other_message += " as best as they can while [target] has clothing on"
target.heal_bodypart_damage(healed_brute, healed_burn, 0, BODYTYPE_ROBOTIC)
+
+ self_message += get_progress(user, target, healed_brute, healed_burn)
+
display_results(user, target, span_notice("[self_message]."), "[other_message].", "[other_message].")
if(istype(surgery, /datum/surgery/robot_healing))
@@ -157,19 +160,89 @@
/***************************TYPES***************************/
/datum/surgery/robot_healing/basic
- name = "Repair robotic limbs (basic)"
- healing_step_type = /datum/surgery_step/robot_heal/basic
+ name = "Repair robotic limbs (Basic)"
desc = "A surgical procedure that provides repairs and maintenance to robotic limbs. Is slightly more efficient when the patient is severely damaged."
- replaced_by = null
+ healing_step_type = /datum/surgery_step/robot_heal/basic
+ replaced_by = /datum/surgery/robot_healing/upgraded
+
+/datum/surgery/robot_healing/upgraded
+ name = "Repair robotic limbs (Adv.)"
+ desc = "A surgical procedure that provides highly effective repairs and maintenance to robotic limbs. Is somewhat more efficient when the patient is severely damaged."
+ healing_step_type = /datum/surgery_step/robot_heal/upgraded
+ replaced_by = /datum/surgery/robot_healing/experimental
+ requires_tech = TRUE
+
+/datum/surgery/robot_healing/experimental
+ name = "Repair robotic limbs (Exp.)"
+ desc = "A surgical procedure that quickly provides highly effective repairs and maintenance to robotic limbs. Is moderately more efficient when the patient is severely damaged."
+ healing_step_type = /datum/surgery_step/robot_heal/experimental
+ replaced_by = /datum/surgery/robot_healing/experimental
+ requires_tech = TRUE
/***************************STEPS***************************/
/datum/surgery_step/robot_heal/basic
- name = "repair damage"
brute_heal_amount = 10
burn_heal_amount = 10
missing_health_bonus = 15
- time = 10
+ time = 2.5 SECONDS
+
+/datum/surgery_step/robot_heal/upgraded
+ brute_heal_amount = 12
+ burn_heal_amount = 12
+ missing_health_bonus = 11
+ time = 2.3 SECONDS
+
+/datum/surgery_step/robot_heal/experimental
+ brute_heal_amount = 14
+ burn_heal_amount = 14
+ missing_health_bonus = 8
+ time = 2 SECONDS
+
+// Mostly a copypaste of standard tend wounds get_progress(). In order to abstract this, I'd have to rework the hierarchy of surgery upstream, so I'll just do this. Pain.
+/**
+ * Args:
+ * * mob/user: The user performing this surgery.
+ * * mob/living/carbon/target: The target of the surgery.
+ * * brute_healed: The amount of brute we just healed.
+ * * burn_healed: The amount of burn we just healed.
+ *
+ * Returns:
+ * * A string containing either an estimation of how much longer the surgery will take, or exact numbers of the remaining damages, depending on if a health analyzer
+ * is held or not.
+ */
+/datum/surgery_step/robot_heal/proc/get_progress(mob/user, mob/living/carbon/target, brute_healed, burn_healed)
+ var/estimated_remaining_steps = 0
+ if(brute_healed > 0)
+ estimated_remaining_steps = max(0, (target.getBruteLoss() / brute_healed))
+ if(burn_healed > 0)
+ estimated_remaining_steps = max(estimated_remaining_steps, (target.getFireLoss() / burn_healed)) // whichever is higher between brute or burn steps
+
+ var/progress_text
+
+ if(locate(/obj/item/healthanalyzer) in user.held_items)
+ if(target.getBruteLoss())
+ progress_text = ". Remaining brute: [target.getBruteLoss()]"
+ if(target.getFireLoss())
+ progress_text += ". Remaining burn: [target.getFireLoss()]"
+ else
+ switch(estimated_remaining_steps)
+ if(-INFINITY to 1)
+ return
+ if(1 to 3)
+ progress_text = ", finishing up the last few signs of damage"
+ if(3 to 6)
+ progress_text = ", counting down the last few patches of trauma"
+ if(6 to 9)
+ progress_text = ", continuing to plug away at [target.p_their()] extensive damages"
+ if(9 to 12)
+ progress_text = ", steadying yourself for the long surgery ahead"
+ if(12 to 15)
+ progress_text = ", though [target.p_they()] still look[target.p_s()] heavily battered"
+ if(15 to INFINITY)
+ progress_text = ", though you feel like you're barely making a dent in treating [target.p_their()] broken body"
+
+ return progress_text
#undef DAMAGE_ROUNDING
#undef FAIL_DAMAGE_MULTIPLIER
diff --git a/modular_skyrat/modules/tableflip/code/flipped_table.dm b/modular_skyrat/modules/tableflip/code/flipped_table.dm
index 22b8f71d060d1f..80776abf09aa22 100644
--- a/modular_skyrat/modules/tableflip/code/flipped_table.dm
+++ b/modular_skyrat/modules/tableflip/code/flipped_table.dm
@@ -18,9 +18,8 @@
AddElement(/datum/element/connect_loc, loc_connections)
-/obj/structure/flippedtable/CanAllowThrough(atom/movable/mover, turf/target)
+/obj/structure/flippedtable/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
- var/attempted_dir = get_dir(loc, target)
if(table_type == /obj/structure/table/glass) //Glass table, jolly ranchers pass
if(istype(mover) && (mover.pass_flags & PASSGLASS))
return TRUE
@@ -30,12 +29,11 @@
if(projectile.trajectory && angle2dir_cardinal(projectile.trajectory.angle) == dir)
return TRUE
return FALSE
- if(attempted_dir == dir)
+ if(border_dir == dir)
return FALSE
- else if(attempted_dir != dir)
- return TRUE
+ return TRUE
-/obj/structure/flippedtable/proc/on_exit(datum/source, atom/movable/leaving, atom/new_location)
+/obj/structure/flippedtable/proc/on_exit(datum/source, atom/movable/leaving, direction)
SIGNAL_HANDLER
if(table_type == /obj/structure/table/glass) //Glass table, jolly ranchers pass
@@ -45,20 +43,20 @@
if(istype(leaving, /obj/projectile))
return
- if(get_dir(leaving.loc, new_location) == dir)
+ if(direction == dir)
return COMPONENT_ATOM_BLOCK_EXIT
/obj/structure/flippedtable/CtrlShiftClick(mob/user)
. = ..()
- if(!istype(user) || !user.can_interact_with(src) || iscorticalborer(user)) //skyrat edit: no borer flipping
+ if(!istype(user) || !user.can_interact_with(src) || iscorticalborer(user))
return FALSE
- user.visible_message(span_danger("[user] starts flipping [src]!"), span_notice("You start flipping over the [src]!"))
+ user.balloon_alert_to_viewers("flipping table upright...")
if(do_after(user, max_integrity * 0.25))
var/obj/structure/table/new_table = new table_type(src.loc)
new_table.update_integrity(src.get_integrity())
if(custom_materials)
new_table.set_custom_materials(custom_materials)
- user.visible_message(span_danger("[user] flips over the [src]!"), span_notice("You flip over the [src]!"))
+ user.balloon_alert_to_viewers("table flipped upright")
playsound(src, 'sound/items/trayhit2.ogg', 100)
qdel(src)
@@ -70,7 +68,7 @@
return
if(!can_flip)
return
- user.visible_message(span_danger("[user] starts flipping [src]!"), span_notice("You start flipping over the [src]!"))
+ user.balloon_alert_to_viewers("flipping table...")
if(!do_after(user, max_integrity * 0.25))
return
@@ -90,10 +88,33 @@
//Finally, add the custom materials, so the flags still apply to it
flipped_table.set_custom_materials(custom_materials)
- user.visible_message(span_danger("[user] flips over the [src]!"), span_notice("You flip over the [src]!"))
- playsound(src, 'sound/items/trayhit2.ogg', 100)
+ var/sound_volume = 100
+ var/balloon_message = "table flipped"
+ var/user_pacifist = HAS_TRAIT(user, TRAIT_PACIFISM)
+
+ if (user_pacifist)
+ balloon_message = "table gently flipped"
+ sound_volume = 40
+
+ user.balloon_alert_to_viewers(balloon_message)
+ playsound(src, 'sound/items/trayhit2.ogg', sound_volume)
qdel(src)
+ var/turf/throw_target = get_step(flipped_table, flipped_table.dir)
+ if (!isnull(throw_target) && !user_pacifist)
+ for (var/atom/movable/movable_entity in flipped_table.loc)
+ if (movable_entity == flipped_table)
+ continue
+ if (movable_entity.anchored)
+ continue
+ if (movable_entity.invisibility > SEE_INVISIBLE_LIVING)
+ continue
+ if(!ismob(movable_entity) && !isobj(movable_entity))
+ continue
+ if(movable_entity.throwing || (movable_entity.movement_type & (FLOATING|FLYING)))
+ continue
+ movable_entity.safe_throw_at(throw_target, range = 1, speed = 1, force = MOVE_FORCE_NORMAL, gentle = TRUE)
+
/obj/structure/table
var/flipped_table_type = /obj/structure/flippedtable
var/can_flip = TRUE
diff --git a/modular_skyrat/modules/time_clock/code/console_tgui.dm b/modular_skyrat/modules/time_clock/code/console_tgui.dm
index 9f1f557d40a2f3..306a2d9c49cb11 100644
--- a/modular_skyrat/modules/time_clock/code/console_tgui.dm
+++ b/modular_skyrat/modules/time_clock/code/console_tgui.dm
@@ -29,7 +29,7 @@
/obj/item/gun/energy/e_gun/hos, \
/obj/item/pinpointer/nuke, \
/obj/item/gun/energy/e_gun, \
- /obj/item/storage/box/gunset/pdh, \
+ /obj/item/storage/toolbox/guncase/skyrat/pistol/trappiste_small_case/skild, \
/obj/item/storage/belt/sabre, \
/obj/item/mod/control/pre_equipped/magnate, \
/obj/item/clothing/suit/armor/vest/warden, \
@@ -51,7 +51,6 @@
/obj/item/card/id/departmental_budget/car, \
/obj/item/clothing/suit/armor/reactive/teleport, \
/obj/item/mod/control/pre_equipped/research, \
- /obj/item/gun/ballistic/automatic/pistol/g18/nomag, \
)
diff --git a/sound/ambience/antag/bloodcult/bloodcult_eyes.ogg b/sound/ambience/antag/bloodcult/bloodcult_eyes.ogg
new file mode 100644
index 00000000000000..38c223b1ad8580
Binary files /dev/null and b/sound/ambience/antag/bloodcult/bloodcult_eyes.ogg differ
diff --git a/sound/ambience/antag/bloodcult.ogg b/sound/ambience/antag/bloodcult/bloodcult_gain.ogg
similarity index 100%
rename from sound/ambience/antag/bloodcult.ogg
rename to sound/ambience/antag/bloodcult/bloodcult_gain.ogg
diff --git a/sound/ambience/antag/bloodcult/bloodcult_halos.ogg b/sound/ambience/antag/bloodcult/bloodcult_halos.ogg
new file mode 100644
index 00000000000000..bd22934fd301b2
Binary files /dev/null and b/sound/ambience/antag/bloodcult/bloodcult_halos.ogg differ
diff --git a/sound/ambience/antag/bloodcult/bloodcult_scribe.ogg b/sound/ambience/antag/bloodcult/bloodcult_scribe.ogg
new file mode 100644
index 00000000000000..a01ef30a1d487f
Binary files /dev/null and b/sound/ambience/antag/bloodcult/bloodcult_scribe.ogg differ
diff --git a/sound/attributions.txt b/sound/attributions.txt
index 09ac2bd56429db..3ae6c797dd33bd 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -99,3 +99,9 @@ https://freesound.org/people/FunWithSound/sounds/456965/
beeps_jingle.ogg is adapted from Eponn's "Achievement happy Beeps Jingle", which is public domain (CC 0):
https://freesound.org/people/Eponn/sounds/619838/
+
+boing.ogg is adapted from reelworldstudio's "Cartoon Boing", which is public domain (CC 0):
+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/
diff --git a/sound/effects/arcade_jump.ogg b/sound/effects/arcade_jump.ogg
new file mode 100644
index 00000000000000..65f0cc448b564f
Binary files /dev/null and b/sound/effects/arcade_jump.ogg differ
diff --git a/sound/effects/boing.ogg b/sound/effects/boing.ogg
new file mode 100644
index 00000000000000..8328cc33926133
Binary files /dev/null and b/sound/effects/boing.ogg differ
diff --git a/sound/effects/footstep/spurs1.ogg b/sound/effects/footstep/spurs1.ogg
new file mode 100644
index 00000000000000..d2754587ca15e6
Binary files /dev/null and b/sound/effects/footstep/spurs1.ogg differ
diff --git a/sound/effects/footstep/spurs2.ogg b/sound/effects/footstep/spurs2.ogg
new file mode 100644
index 00000000000000..e02725e9079bbf
Binary files /dev/null and b/sound/effects/footstep/spurs2.ogg differ
diff --git a/sound/effects/footstep/spurs3.ogg b/sound/effects/footstep/spurs3.ogg
new file mode 100644
index 00000000000000..e79b90dc78d9f4
Binary files /dev/null and b/sound/effects/footstep/spurs3.ogg differ
diff --git a/sound/effects/submerge.ogg b/sound/effects/submerge.ogg
new file mode 100644
index 00000000000000..8c50fba8e0a735
Binary files /dev/null and b/sound/effects/submerge.ogg differ
diff --git a/sound/voice/moth/credit.txt b/sound/voice/moth/credit.txt
new file mode 100644
index 00000000000000..7f64b72841e32c
--- /dev/null
+++ b/sound/voice/moth/credit.txt
@@ -0,0 +1,5 @@
+"moth_flutter" modified from
+https://freesound.org/people/Godowan/sounds/240476/
+(CC 0 license)
+
+who knows where the original moth scream noise was I sure as hell don't
\ No newline at end of file
diff --git a/sound/voice/moth/moth_death.ogg b/sound/voice/moth/moth_death.ogg
new file mode 100644
index 00000000000000..df23cfa472ac10
Binary files /dev/null and b/sound/voice/moth/moth_death.ogg differ
diff --git a/sound/voice/moth/moth_flutter.ogg b/sound/voice/moth/moth_flutter.ogg
new file mode 100644
index 00000000000000..f5737d522ca208
Binary files /dev/null and b/sound/voice/moth/moth_flutter.ogg differ
diff --git a/strings/exoadventures/britain_replica.json b/strings/exoadventures/britain_replica.json
new file mode 100644
index 00000000000000..0bfaa67e990cb0
--- /dev/null
+++ b/strings/exoadventures/britain_replica.json
@@ -0,0 +1,570 @@
+{
+ "adventure_name": "A Model Earth",
+ "version": 1,
+ "author": "Armhulen",
+ "starting_node": "Planet Start",
+ "starting_qualities": {
+ "Long Range Scan Report": 0,
+ "UFOs Shot Down": 0
+ },
+ "required_site_traits": [
+ "in space"
+ ],
+ "loot_categories": [
+ "research"
+ ],
+ "scan_band_mods": {},
+ "deep_scan_description": "",
+ "triggers": [],
+ "nodes": [
+ {
+ "name": "Planet Start",
+ "description": "You come across a grey planet. It looks familiar, though you swore you've never come across this sector of space before.",
+ "choices": [
+ {
+ "key": "choice 0",
+ "name": "Ignore the planet.",
+ "exit_node": "FAIL",
+ "delay": 0,
+ "delay_message": "Whatever, there's a lot of planets in space. Must be a hunch!"
+ },
+ {
+ "key": "choice 1",
+ "name": "Begin Orbital Scan",
+ "exit_node": "Scanning from Orbit",
+ "requirements": [
+ {
+ "quality": "Long Range Scan Report",
+ "operator": "==",
+ "value": 0
+ }
+ ],
+ "delay": 30,
+ "delay_message": "Scanning planet..."
+ },
+ {
+ "key": "choice 8",
+ "name": "Descend Into Orbit",
+ "exit_node": "Orbital Descent",
+ "delay": 30,
+ "delay_message": "Descending into Orbit..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Scanning from Orbit",
+ "description": "You initiate a long range scan from orbit:\nThis planet has a smoggy cover that blocks any good look at the surface, yet it has a mostly survivable atmosphere.\nThis planet has zero life signs.\n",
+ "choices": [
+ {
+ "key": "choice 9",
+ "name": "Stash that Report...",
+ "exit_node": "Planet Start",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "on_enter_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Long Range Scan Report",
+ "value": 1
+ }
+ ],
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkBAMAAAAxqGI4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAASUExURQAAAP///1lWUqwyMmlqakZHRwPX/kkAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAL5SURBVGje7ZldbqMwFIXtKnkvO4huyAYmG0CWea8i2P9Wxv/Y2MTYvjBVO4c8FET8+dxzDW5Ldqr73Hcf7fbemfwu+p1tkM+jILTbfeu2stNbDDSkklXnzNRbyUqUqbM/oVr5AFhOus5iUK3A/fmEW0BRHjCtwFPqj6NQS6GIVu5PTXEXnJUOjfLxXEOITYWitXECQk6BUOx6pSDkKAj4F6mrFxLEdNctAdl8gJVmBRBXy6tXcrScwwvXsufikXIP1qId5t2iz0D4osFClAga5MJDDbJaEUDJjF6eyZohxATjlhym9kkcMzhkIBQJ0g+pe2klJFWtG4DrgRSkeEUkjDDWA09TbPJdM6RXnySlDnLpYyO9/GhtQChthYhSMXeyup0aSFkoCYirVqJkdcnHEMaXakVeHIQUQYBPwSFdMB86IEBYolrAtyi1kF4asOIT40Ekq4JVQvS0R3UYI5G5GFLYwuKRO3l69dMEkxnc5MR8K6RCF9VL2sioe2tdLc6GRggROUDve5lcA9usXpw0So4Go5OwE0YySqutED33eZKH12TSyJJTK0RNFx6LFb4yMiI4uajJQmxlWT3tTsybEfxUnA0t9miGmNnCY54DMxNiJu7V6HLx89BO2iHLhIUXPxUleWVjf1SiZbvCHkEuLqX0brIM4hUfwkxERkKvdiPhxksGw3UuNpERmpuLeKGoBRM8k5WP/gsBsnrkAphUVGOJHeuIwPBDMcks6vmAUazkbtjXgAIhqVW+QHAYZHxnBIkRheKbGbAgJH6NIAcitWFlwmQQck28EzEDUdqwQnClrIxH+pA63od1EpgpHiP7i3ccSjEj/yd258SYqendLCR0UfcCyUK0B9KkbCYNBnZLRzIcC9HNdSzDODkYoraOB0eim+vgSIjcKM4HM06J5Hqak1Mi+d9ce3VGc5Ef01zXM5pLQN46oR0O5G0kKP+9nM+CfP1rSDqTwqTqmqvQX91SLINcvzGkLJPrCev9HMj8oyDk10MI+Qt3vM7Ve2h01AAAAABJRU5ErkJggg=="
+ },
+ {
+ "name": "Orbital Descent",
+ "description": "As you descend into orbit, you see a flying object headed straight for you!\nA garbled voice begins to call out to your drone, but there's no time to decipher it!",
+ "choices": [
+ {
+ "key": "choice 2",
+ "name": "Blast the damn UFO!",
+ "exit_node": "Tractor Beam",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Tractor Beam Turns",
+ "value": 2
+ },
+ {
+ "effect_type": "Add",
+ "quality": "UFOs Shot Down",
+ "value": 1
+ }
+ ],
+ "delay": 30,
+ "delay_message": "Blasting UFO!"
+ },
+ {
+ "key": "choice 3",
+ "name": "Attempt Evasive Maneuvers!",
+ "exit_node": "UFO Evasion!",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "UFOs Violently Crashed Into",
+ "value": {
+ "value_type": "random",
+ "low": 0,
+ "high": 1
+ }
+ }
+ ],
+ "delay": 30,
+ "delay_message": "You attempt to dodge the UFO..."
+ },
+ {
+ "key": "choice 7",
+ "name": "Do NOTHING. Jesus take the wheel!",
+ "exit_node": "FAIL_DEATH",
+ "delay": 30,
+ "delay_message": "What? Why?!"
+ }
+ ],
+ "image": null,
+ "on_enter_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Garbled Transmissions",
+ "value": 1
+ }
+ ],
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "UFO Evasion!",
+ "description": "Were you good enough at flying to avoid the UFO?",
+ "choices": [
+ {
+ "key": "choice 4",
+ "name": "No, Back to flight school with you!",
+ "exit_node": "FAIL_DEATH",
+ "requirements": [
+ {
+ "quality": "UFOs Violently Crashed Into",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 5",
+ "name": "You barely avoided them! Nice!",
+ "exit_node": "Tractor Beam",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Tractor Beam Turns",
+ "value": 2
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "UFOs Violently Crashed Into",
+ "operator": "==",
+ "value": 0
+ }
+ ],
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Tractor Beam",
+ "description": "Before you have time to think, your drone is captured by a Tractor beam! Now you've gone and done it.\n\nYou should have some time before you are pulled in.",
+ "choices": [
+ {
+ "key": "choice 6",
+ "name": "Decipher Garbled Transmission",
+ "exit_node": "Deciphering Transmission",
+ "requirements": [
+ {
+ "quality": "Garbled Transmissions",
+ "operator": "==",
+ "value": 1
+ },
+ {
+ "quality": "Tractor Beam Turns",
+ "operator": ">",
+ "value": 1
+ }
+ ],
+ "delay": 30,
+ "delay_message": "Decipering..."
+ },
+ {
+ "key": "choice 10",
+ "name": "Look at the surface of the planet",
+ "exit_node": "Looking at the Surface",
+ "requirements": [
+ {
+ "quality": "Tractor Beam Turns",
+ "operator": ">",
+ "value": 1
+ }
+ ],
+ "delay": 10,
+ "delay_message": "Looking out window..."
+ },
+ {
+ "key": "choice 13",
+ "name": "Let the beam pull you in and dock you",
+ "exit_node": "Landed, Kinda",
+ "on_selection_effects": [
+ {
+ "effect_type": "Set",
+ "quality": "Tractor Beam Turns",
+ "value": 0
+ }
+ ],
+ "delay": 50,
+ "delay_message": "Getting captured..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Deciphering Transmission",
+ "description": "You review the incoming transmissions your drone has gotten. These aren't in need of deciphering, just un-garbling it from the bad connection...\n\n\"You are in Space British Air Space! Identify yourself, Tally ho!\"\nWhat the fuck?",
+ "choices": [
+ {
+ "key": "choice 11",
+ "name": "Whoops!",
+ "exit_node": "Tractor Beam",
+ "delay": 0,
+ "requirements": [
+ {
+ "quality": "UFOs Shot Down",
+ "operator": "==",
+ "value": 1
+ }
+ ]
+ },
+ {
+ "key": "choice 12",
+ "name": "Good thing I didn't blast them, then.",
+ "exit_node": "Tractor Beam",
+ "delay": 0,
+ "requirements": [
+ {
+ "quality": "UFOs Shot Down",
+ "operator": "==",
+ "value": 0
+ }
+ ]
+ }
+ ],
+ "image": null,
+ "on_enter_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Commercial Airliner Transmissions",
+ "value": 1
+ }
+ ],
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Landed, Kinda",
+ "description": "You have been locked to the surface the beam. Robots are all around you, pointing at your drone with all sorts of old age weapons.\n\nOne of them angrily shouts at you, \"By God, Queen and Country, we've got you now!\"",
+ "choices": [
+ {
+ "key": "choice 14",
+ "name": "Go out with a bang. Initiate self destruct!",
+ "exit_node": "FAIL",
+ "delay": 0
+ },
+ {
+ "key": "choice 15",
+ "name": "Surrender to the funny looking robot brits...",
+ "exit_node": "British Courtroom Start",
+ "delay": 50,
+ "delay_message": "You are being transported somewhere..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "British Courtroom Start",
+ "description": "Wow, that took forever. A one eye judge robot smacks their gavel, and points their hammer at you.\n\"I will now read out the crimes you are accused of!\"\nIs that how the judicial process works? You dunno.\n\"ONE ACCOUNT OF PLANETARY TRESSPASS!\"\nThis blows.",
+ "choices": [
+ {
+ "key": "choice 16",
+ "name": "That's not that bad.",
+ "exit_node": "British Courtroom, Continued...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Crimes Committed",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "British Courtroom, Continued...",
+ "description": "This big idiot they call a judge just keeps piling on crimes. With each one, some robot you'd like to drone-punch gasps in the back.",
+ "choices": [
+ {
+ "key": "choice 17",
+ "name": "\"INTRUSIVE SCANNING ON OUR CITIZENS\"",
+ "exit_node": "British Courtroom, Continued...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Long Range Scan Report",
+ "value": -1
+ },
+ {
+ "effect_type": "Add",
+ "quality": "Crimes Committed",
+ "value": 1
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "Long Range Scan Report",
+ "operator": ">",
+ "value": 0
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 18",
+ "name": "\"SHOOTING DOWN A COMMERCIAL AIRLINER\"",
+ "exit_node": "British Courtroom, Continued...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "UFOs Shot Down",
+ "value": -1
+ },
+ {
+ "effect_type": "Add",
+ "quality": "Crimes Committed",
+ "value": 1
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "UFOs Shot Down",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 19",
+ "name": "\"AND WE HAVE PROOF YOU KNEW IT WAS JUST AN AIRLINER!\"",
+ "exit_node": "British Courtroom, Continued...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Commercial Airliner Transmissions",
+ "value": -1
+ },
+ {
+ "effect_type": "Add",
+ "quality": "Crimes Committed",
+ "value": 1
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "Commercial Airliner Transmissions",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 20",
+ "name": "\"THE TRANSMISSIONS WERE NOT LEGIBLE AND UNTRANSLATED. WE CANNOT PROVE MALICE!\"",
+ "exit_node": "British Courtroom, Continued...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Garbled Transmissions",
+ "value": -1
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "Garbled Transmissions",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 24",
+ "name": "I think it's done listing crimes, thank god. Time to argue my case.",
+ "exit_node": "Verdict",
+ "requirements": [
+ {
+ "quality": "Long Range Scan Report",
+ "operator": "==",
+ "value": 0
+ },
+ {
+ "group_type": "AND",
+ "requirements": [
+ {
+ "quality": "Garbled Transmissions",
+ "operator": "==",
+ "value": 0
+ },
+ {
+ "quality": "Commercial Airliner Transmissions",
+ "operator": "!=",
+ "value": 1
+ }
+ ]
+ },
+ {
+ "quality": "UFOs Shot Down",
+ "operator": "!=",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Looking at the Surface",
+ "description": "The surface of this world looks exactly like a grey version of Earth! It seems to be an exact replica of some kind.\n\nYou see creatures moving around on the streets",
+ "choices": [
+ {
+ "key": "choice 21",
+ "name": "Cool!",
+ "exit_node": "Tractor Beam",
+ "delay": 0
+ },
+ {
+ "key": "choice 22",
+ "name": "Scan the creatures.",
+ "exit_node": "Robo Brits!",
+ "delay": 30,
+ "delay_message": "Scanning..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Robo Brits!",
+ "description": "Wow, they're robotic humanoids that look and act exactly like the British! You see some robots laughing and chatting at a pub, and a Bobby walking down the street looking for crooks. Who created this earth replica? You only know Earth from the books, but you should be where London is!",
+ "choices": [
+ {
+ "key": "choice 23",
+ "name": "Nice! Well, back to getting pulled in by a Tractor beam...",
+ "exit_node": "Tractor Beam",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "on_enter_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Long Range Scan Report",
+ "value": 1
+ }
+ ],
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Verdict",
+ "description": "Before even getting to defend your case, the judge says \"I've heard ENOUGH! It's time for a verdict!\"\nDang! You're definitely in kangaroo court!\n\"GUILTY! AND YOUR SENTENCE IS:\"",
+ "choices": [
+ {
+ "key": "choice 25",
+ "name": "\"DEATH!\"",
+ "exit_node": "FAIL_DEATH",
+ "requirements": [
+ {
+ "quality": "Crimes Committed",
+ "operator": ">=",
+ "value": 4
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 26",
+ "name": "\"SPACE JAIL!\"",
+ "exit_node": "Not Actually Space Jail, Just Normal Jail",
+ "requirements": [
+ {
+ "quality": "Crimes Committed",
+ "operator": ">",
+ "value": 1
+ }
+ ],
+ "delay": 20,
+ "delay_message": "You are being jailed..."
+ },
+ {
+ "key": "choice 27",
+ "name": "\"FORGIVENESS! Just trespass? That's not that bad!\"",
+ "exit_node": "Sweet Sweet Freedom!",
+ "requirements": [
+ {
+ "quality": "Crimes Committed",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 10,
+ "delay_message": "WOOP WOOP"
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Not Actually Space Jail, Just Normal Jail",
+ "description": "You'll have to wait out your crimes against the robo brits.",
+ "choices": [
+ {
+ "key": "choice 28",
+ "name": "Sit out your sentence",
+ "exit_node": "Sweet Sweet Freedom!",
+ "delay": 1200,
+ "delay_message": "Sitting out your sentence..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Sweet Sweet Freedom!",
+ "description": "You're free! You spend a good while travelling around England (or at least the replica) and enjoying the cuisine, people and culture! Good society research is gained from this or something, but really you're just enjoying the sights and sounds.",
+ "choices": [
+ {
+ "key": "choice 29",
+ "name": "Nice!",
+ "exit_node": "WIN",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/strings/exoadventures/quantum_fizzics.json b/strings/exoadventures/quantum_fizzics.json
new file mode 100644
index 00000000000000..0ee9bf9bf6806d
--- /dev/null
+++ b/strings/exoadventures/quantum_fizzics.json
@@ -0,0 +1,195 @@
+{
+ "adventure_name": "Quantum Fizz-ics",
+ "version": 1,
+ "author": "EOBGames",
+ "starting_node": "start",
+ "starting_qualities": {
+ "jammed": 0
+ },
+ "required_site_traits": [
+ "technology present",
+ "in space"
+ ],
+ "loot_categories": [
+ "unique"
+ ],
+ "scan_band_mods": {
+ "Narrow-band radio waves": 10
+ },
+ "deep_scan_description": "",
+ "triggers": [],
+ "nodes": [
+ {
+ "name": "start",
+ "description": "As you sweep through the inky void and the site comes into view, you're puzzled by what you see. On a small asteroid sits a vending machine. Despite the odd runes lining its surface, you're fairly certain that the image on the front is a can of soda. While ordinary common sense would dictate that drinking strange alien soda is a bad idea, you can't help but be curious about what exactly this machine dispenses. There's one problem, however- what currency does this thing take?",
+ "choices": [
+ {
+ "key": "choice 0",
+ "name": "Leave.",
+ "exit_node": "FAIL",
+ "delay": 10,
+ "delay_message": "There are better ways to die than drinking alien soda."
+ },
+ {
+ "key": "choice 1",
+ "name": "Try a holocredit chit.",
+ "exit_node": "it's_stuck",
+ "requirements": [
+ {
+ "quality": "jammed",
+ "operator": "!=",
+ "value": 1
+ }
+ ],
+ "delay": 10,
+ "delay_message": "Hopefully whoever made this machine is part of the Galactic Currency Union..."
+ },
+ {
+ "key": "choice 4",
+ "name": "Ram the machine.",
+ "exit_node": "smashing",
+ "delay": 30,
+ "delay_message": "Ramming speed!"
+ },
+ {
+ "key": "choice 5",
+ "name": "Search around for some loose change.",
+ "exit_node": "lost_wallet",
+ "requirements": [
+ {
+ "quality": "have_coin",
+ "operator": "!=",
+ "value": 1
+ }
+ ],
+ "delay": 100,
+ "delay_message": "There's a surprising amount of stuff on this asteroid to search..."
+ },
+ {
+ "key": "choice 6",
+ "name": "Use the coin you found.",
+ "exit_node": "choices_choices",
+ "requirements": [
+ {
+ "quality": "jammed",
+ "operator": "==",
+ "value": 0
+ },
+ {
+ "quality": "have_coin",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 10,
+ "delay_message": "Thank God for clumsy aliens!"
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "it's_stuck",
+ "description": "Well, only one way to find out, right? You produce a holocredit chit (helpfully taken from the science budget, I'm sure they won't miss it) and jam it into the slot. Then, you realise your mistake, as it sticks in the slot. Whoops. Time to try the old fashioned way, I suppose.",
+ "choices": [
+ {
+ "key": "choice 2",
+ "name": "Time to try something a bit more daring?",
+ "exit_node": "start",
+ "on_selection_effects": [
+ {
+ "effect_type": "Set",
+ "quality": "jammed",
+ "value": 1
+ }
+ ],
+ "delay": 10,
+ "delay_message": "In hindsight, why would this accept human currency, anyway?"
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAAAXNSR0IArs4c6QAAAjVJREFUeJzt3cFyogAUAEGzlf//5ewpF0pGRJDnbvc1CkhleAbEfN1ut58bcNefqzcAJhMIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUD4vnLlW/854tdJz5+2HWuWy9+6HWdt77P/1PLo7d27H/cwQSBcMkFePSI8e+T5WXncUctZWlvP1uef5V2vd+9y1p7/aD1nMkEgvHWCTDgiXOH3dU6ZJNNN+j0xQSBcehZrae3siCPtff/aJJo0OX6ZIBBGTZDle3XuW+6fq/fXo/W/ev3H3yAw1FsnyP96Nues99ZTJu7R11PWJqTrIDDM1+3CA9CUz0BN2Y410z+Ltfdxz/780frOYIJAuHSCwHQmCASBQBAIhFFX0pfefZbq0XKPupPu2eW9+2zSVmctZxITBMLoCbK09069s86vT7uTca+j9s/E6xivMkEgfNQEWZr62a6jPiN19evABIH00RNkqq33Rywn36PHv7penmeCQDBBXvDqkX/rdZKjvp+K55kgED56glz9LRhn39/B9UwQCB81QfZemd36HvxdR/azzmLtNW3/TGKCQHBHIQQTBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBMJfgqJi1hru29AAAAAASUVORK5CYII="
+ },
+ {
+ "name": "lost_wallet",
+ "description": "Searching around, you come across a lost wallet in a small crater. Flipping it open, inside you find a family photo of 3 identical looking grey aliens in comically different outfits, an (expired) credit card for a bank you've never heard of, a loyalty card to McDonkalds, and, in the coin pouch, a single black coin with glowing purple lines. This is (presumably) what you're looking for.",
+ "choices": [
+ {
+ "key": "choice 3",
+ "name": "Return to the machine with the coin.",
+ "exit_node": "start",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "have_coin",
+ "value": 1
+ }
+ ],
+ "delay": 10,
+ "delay_message": "It doesn't count as theft if you found it, right?"
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "choices_choices",
+ "description": "You slip the coin into the slot- it's a perfect fit. Now comes the hard part: picking a button on the machine to press.",
+ "choices": [
+ {
+ "key": "choice 7",
+ "name": "The red looking soda.",
+ "exit_node": "cha_clunk",
+ "delay": 10,
+ "delay_message": "How exciting..."
+ },
+ {
+ "key": "choice 9",
+ "name": "The yellow looking soda.",
+ "exit_node": "cha_clunk",
+ "delay": 10,
+ "delay_message": "How exciting..."
+ },
+ {
+ "key": "choice 10",
+ "name": "The green looking soda.",
+ "exit_node": "cha_clunk",
+ "delay": 10,
+ "delay_message": "How exciting..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "cha_clunk",
+ "description": "With a satisfying cha-clunk, your fizzy prize drops into the tray. You swipe it, and move on from the site.",
+ "choices": [
+ {
+ "key": "choice 11",
+ "name": "Sweet, sugary victory.",
+ "exit_node": "WIN",
+ "delay": 10,
+ "delay_message": "Hopefully this doesn't like, freeze solid in space. That would be bad, right?"
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAIAAABM5OhcAAAAAXNSR0IArs4c6QAAIABJREFUeJzsvXmcpVdVLrzWHt7pzKem7qqeO515TkgIIQwSQESQaIiADAooeK/XAZHrAF4V1E8ULpd7Rb0MooiAIE4XkEHBQBhCIPOcTnd6rO6uqjO+w57W+v441W2DEBLoJN2Nz6/+qFP11ql99vu8a6/9rGEj/Ce+ExCRmY9+Ofnmm37IwJPvgBAREXlyDSMAr/7y+wf4WA/gBMARJn0biNULUH7DxRwmLz25Cc0mRDyajicx/pNYAN+KOpOfHP1zZhZCfOMFAhEFKhBCCCFQrVoqwQCATEQUDoPJTt7k+4RY6rEewKOKI4vaNzEJj0AogAlPFCJOmISIkydwcgkctjpCrNoqJRMhBCoppZy8CTMHb0Nwzhly5LxxBhkCkYejjNZkPN+01J4c+P4iFhxNKYEAIFBNzI1SsRACpRZCSXmEIgIRpZQAIIQgWvWeiEgIgSjFKqRSKkliIYSUMjB47zkEa60xlS1yY8pKCOcsewDy3zSkk49V8CDEOvkeo1Vjg4AghRBSaimV1lrJSOtYa62TOIpTpZTWWig9sVgTrihUzBwgADIAIMHEmE0oqGQURVGaxRilApxgCB5K68bVuByNBoNBmY/luF9Vha2E99Z7P/HATmJ8W2KdlKxCRCGUlFoppeMsipI0TSOdJEmapqmOoyTLoiiK4zhOM0RUSq0udjxZKJmZiYgJpZQghVJKKRVFkda6VmvUxb4nXLD+9ltvsQ57y5QbvWc0v7jcGPZ7WuvxeFhKbasc0QZfndz+1vfFUnhk14ZCaR1FUZTEWVKrJ2ktrdfq9XotqzebzTStNZvNKIqSJEkihSpRiqRQSkwkBBYCkJkDTd4TpRAYRUmmIhlHUU3d/vSL03xwcM+di9b5pEnO6aS7NcrccnYwiqIoivIoGgwkQmERvbfMHgngpHuM4fuEWBMIobSO4rQWx3Gt1uh0p+v1Rqsz1W63G82s053qNJqNZhRFWmkx153etm1DkvksHtekFTRgM3Jl35fWGetdZa1lhCAosHdMzalOtz7UlENrnoUBia4E59zsfF03W/V6KoRQKlJKO09CCCgIAJxjwHDysQpOdGId7Qh+W92SJ8uYjFSkdRLHutnottudmbnZznR3/fqFdivuNuUTLz9129YpIQSKNsM0smE6JNwOP97h+vtMPoLSm6L0VTDG2eAJQgATFFhZnX/JuVIbROQAaRQJZTzJQOQ9Fvu+3l24vLF5PQodJbFSygWWUnrviYg5OMfM3+zOnwR4jIn1PW4RvmmvflgLmOznRCACRCFQqUhKFUdp1mi22u3pqbXTM52F+TWnnapf+dOnRbLi0Cd/OzpG3WSuoWwTBKReGB0MxQqYkbSFM5bLioxnEzh4x5Y0nf+MpxjTV4oBAoBgcI5GxpellaMKTEWD3u3b77hJJS0bNTeuuVDrDT6wEOCtE0zMFEJgEER0bCb0uMEJbLGOmKgjuuVEC1AymmgEgQkRhZBKRnEcZ41mvdGYmVnTnZ7duL79W6+/KI3uhfGtoBiYGBGB0QZUJVCOviRXoFnhaszWUFWydVRVwVAwwZNjdCQIghfgGOBwRCcAm8pWeUXjERkjRyX0qhJKK/nQYGm7SE6Znt5iqqKsjX2wzhvnTDjZSAXwmBPruzBXR2mbYlURkFIIJcRk7y9BrEpTh/eAUmudpmmj2W42WzNzC/Prp3/0CpWOviJSj0JBAARQ6JmQoBBeB5bCOWFLKsZcGMorKkwoyI6MseQcWQ5eBhYByAEhhzARIQRh8CYf5aMx93OsSi4LcBUzemABjny5e+P6c4fjTpmPjCmrSgmhAxOiO8k8re+GWI+txDVZ/ibDkCqSUsZxIoWOokRGWkqpdDwhEyJKqaSUUaTTNG22O61WZ2Fh7ZZ16zbN34FixAERVv0bZGACJiQWgpltgMpyZaE0UBFVlipiR2TIWTIcggoeCYgUCUIvhPjiF79y+SUXA4fhsBiMuD8WpoSyACIiZkIhCYMYtjLdbnby/vJ4PIqixBnjrT25SAXw3RHreHi2EOVE25RaxWktSbI4ydJ0IkclSq0aKimlUlEcx2madqZbU53umrXdbqOldSnIMwGjR2RmJgQgEkFAACbiypFhrhwbR4bIsDfkLFnrrSdH3nny3oElCrYs7T/986catSbbQIEqE6qSrJHGkA9iNYjETMCCsdGq1UtZW2pl2chUhSlzoSS5Iw7iYz+3xwQnno+FiIBSSC1VnCS1KM1azU6j0ag3m7VGo9Fo1Ov1icgZxXoiYCaJqjfSdr3RqGkEl+E9zdQJIOLABAzEzEAkArP34AL7EAxBxaEgKpkMWxOsAWOhMlg5KAIaZKsQrLr9rgOf/cIXMUL0FkhKkqby3mPwGAITwdFhSWbOsqzVinutZq1oGVPZsgohUDATvfSkCXicYLtCXP0rOTFIcZY2mq1GvbGwfrrbiufW1jrNuNXGWoadtpqejjrdWj0VCEFpk0ZWaQCuit5O4L5HQgZkBmIMhEDBkXQYDGFgrAIZZuPJUqjYGe8ceAcuUOW5sq4ELsh+/eu7Pv1vX4trTQVaCAUOTOWrygafERExIDKAOPoTCK3WzEz1lpdG40FRjHU8Vt5o75wzTAQcTg5uPTxiHXNz/d29FSJqHUU6yWqNequ9bn7zK19Qv/IyFCoGOdkbOsKBBIm4FBAYCZCAFRMjcSa9IEAGROBASMxE4EF6JOcxMBkHBtkGb9lZ9haCl96xqZz1VFmqnCq8L1h/7rO3o2xY4ypranFA9oMxOpLOB09AtJrmd/TsxXGaxVOddteUlTO2qqrAnrxhZu89E50ErIKHS6zH/DMzgJRaSimUknGSprVOo9vq4hMuXNB4PyODAEYKIBBlAAMgABAYgAHZAbNECsIRBUHEngEQAwhCCp48sfVsga1gS8EgGSITggFvgrFceSgtVJYL50tHhWcrAiOR8h64FkcUYP/iMgXFzByYgiRkAJaHTS2TFCiVimYX1ppgq6oYl1XwhXdN5iEAOScBA4QTnlsnno8VQphwK4qiWq3WaDTmu5HiQ8gA4IGYBQtGgCAEAAimw3IXAwAwB/ZeeMHscaJeBeJAwXv0wBbIMBkKhoMhMuwsVI4rS5Vl60JlQ+WpslQEyn0I7INg8EDI6+cXmOj2u3aEQETIjIeDzMwAiAKAhQwffe8ftOZPecLlz9ww08Ry2tnCm4IcQvAATEQTXfcxf4a/R5x4xBJCaK0nLlYc63rWmOuCgmUHQrJHQgwOZERIZAUFKQRIISiABS8YhEJyDCGwI4lMgYBYEKMnsgQOyVIwHKrgymCKYCyXFZsSjAtj68sAlcPKQ+WpchQwoGQAkCKcffYpxNX+fcsMgkDy5H1BSgIUDMCIQOwUjYa7b/jkh24k5gDxujOfVVVz5J1xlSMnnaHgTmxOAcCJSCwAmMiik3yVONG1NGgkZoAADBBYfuXznyutLkprKvJOmMoJIYkgEiqBeMvW2fVrullChKyYmJmI2QFb8MYHw2yDr8AarmwwFipLufOFAxvAhFA6rogscQAmYACWGCjYWDKHaHkwkjiR/pUK3h1OuxIYBAqBqCNgFsEHRBbgO814mLd7veUkyUxVCKEABMAJL8afGMQ6etNwJLd4olFpLdsNTcFKH7EEAOAAKwddmVwedZ8oSCYYa88AwgvQKBIR73Y0fOCmCzbcJBEcByAGZnAEjp0hMuQrZ0qwhouKC+urCgrDuaVxZUvPpYPKe0MUgAgYgRFCLAKwY0pZJZESsZJWAksIfrWkQklQSkgplSBE9EoHJgq4srwvq63PsnqZ51rHQkgECXDCh6VPDGIdHWYWq/6HWM25Q5ZkdJBOGMECQPnKjocsG5uqsMAIvZVer9e31gohkiRZMz2VJHEkF6rxXULnkQ/IAD6QZ3LBG7Y2OIu2cJWFwkBZUWF47O3Y8dhD4agKbJiJCKSXSEqQAHrOD16i2H39rr2dpCaBtcAYYE9lA7AWSirWSkaJ1BFIoTWSdcEFtA4Hh/bNnH56FKdCKakUAYDgE99gPRxiHQ/6ypGsywmrQggUMC+C9yCQUAJTEE65HAWnAKLf6+/cuXM4HBZFAQBpmo6Hg3Xza2vN7jjPUjEkJsHAnryjYCk4sIat4cKydVyWobBUOc4DFhUVnivPVWBPQEhCBiFIKk5keck5C4bg1lt2ZFmmRKiEyJXavm8ZhAYIQogopiSN1rXt1c++eMumtX/6/s/es2NEyBWZLMviONZaTxKmEeVjO8nHBA+DWA9PyXwkWRhCIPIhBPJgTZmbhINHQCBGIGahhDRFrhI7HPUHw3FvZaXMC0SZ6wI9oMxqkVw0Yi62yITM5JktV9Zbx9ZRUYWq4soG67h0XDgaB28c5D7YQJaY0CcxYrBJFkUif+1/fSFCeXDF7+/Z3NqqkKXjQwPyQgkgIbRSUtV0PZHPefZTzzm9jpJO2TS1e9F64WzlMx0rKZUQSk6qM77PiPWw8EjbthCC996Ywhi3UrYrmk3gIIAHYB3JdXP17b0d8dQlEoVEIQCR2LMFoMpVzhS5md5fdbXbqQOTD8EGa71xlFfeeTSWLIH1UDmqHFUBKwcmeOfREwVBQnAE8N9/+aXduY7L90sekdAf+shnlsayNFCMfWlxuTdmUCjUpBxDoFJps5RrqalQiHRqGqJlaVlICQKPylIMzI/2QvhIWIFHw8d62HGbB7teTEwMeUu2LE01HPf3rHR37tWnLtDkBnlTPe5x5wy+suJ0lSRRo14vizEzl2UZRZGSiUAFQkFjS2//14QprfHWsTG28ugcWg6OwREyS+ekm0j0WsUqRAkgBhSktUwzP1zea8b3z861OND2nf2DQ8pLLio1rrwxHFAJABTAQqKS9SiB2iy21kQLTWI1Lu9TwAIlorKhIppsTgm+t6LW744ij4QVeDSI9XDH/aDXEyCGEIjIWluW5Xg8Ho57n705W5iSWWInCwkq+7TL13765uun0kv7jVoIUxM1VUrZaNSbrXqaxV4v7BnXk9FSaWxlfWWxtOxJgBIsJQmBSIBYz5I40mkslRKTikIiyk0/1n5x7/2XXnZ2IG85e/9HP5FXujScl54CIsqJZDDZvUoRgWo94YprFtbPe2woIXbu+gBDwmCUzLwH55z3fvK5vseU2u/6b48tToxd4TeCKDhnjJFRmRejwWi0bO5NG9fd2vyB85ZIBwQAIAgrP3RR7fq7by2nNiQ6Sutx8KSErtXS6em2UkoK0Vl35u7r7ygqGlemqLgM0hMGATqOlRZpFLVqqdCynkWdZlZLZbud1dNsbm7mX/71M3Nr6t1uiqYCnb31f390mIuqgKpickggysoCi9WKe2at03hqY5zMb95wLkAJPiwvG8/KezO7dnNRFNZW3jsiTxz4pCg5PL6I9VAsOTMDhEDOeVMV46FSy0v7WYrPcHt+Vp8/v2TQAoBADMWe89ftPWt+t6V233RvumsZk3Uia6lIB6jUcPdlW3bdka+97st3Gwulw9KH3FFgSEmmHAkka60SyjlhrU2jCLyzZrx3T5ElcMG5p8d1V4ToD972N/0yHlVQWiiroKKkyIsA4khCohACks6Tn/pK5gEhRCH6yhc+UZpQWV9UuHVh293b91VVZa01xnh/witYExxfG5Dv1Nfl6CsFogBARggMEok97jjYGI7LrWsVsgMGQs8MMRuNy3U8sG1qdMrMntM6D8zr7evSfe3swPKBHaedMrtmzfT2BxYpsA9oQyBA4xgESgClhEJUCEqyEixFIAo+WCHCaLTUbs2/+X9/qJcnI4PGKesIZTQamxAQQAAIRJBSrdl8/g/+6H8rxzuued6ZEsq/+vN3f/oTH8tzX+XDUSVrU2ccPHSot7I8GvaLfOScCd7Bo+6/H3OckN1mEFd7b0RRnCRJVm+3W91Wqz29Zu1Md25TO7/gVLpwm6/hkKFSgMyMICZJnOFwEguTBJCjkbeuqCWdD33k8/cs+pWCK0cGUKCqJbKdRFONejOTnVrcakZTzSROtBBszACRP3HtrVWIRgEqyzYIEnE+toEwHE4ZzWJ97kWXtddsUzC2w9t27N7jQl2gIqKqqqzhmY1PHPl0/749B/bv6a0sjcd9U46DtydB0c43WKyHbjAeITzEAawGeAAAkBl8CETknBeBKjsaObFjuX3LLnnnDtcf2KlmU6gkkEcKzMgwaR2DAgIwMIR6vXPXnXc94bKzzti8/pY7d1vPRNIFb1wQKJFJSRErJSQmUYRCMPDi4nj3/pFIWipOEDILaDzmhQ8T4iIw8MxUK9Fy3959u3fcum/P9n1LBdmELBvvqsqYCmVjbdTZuG9x/3Blub/SK8rc2so78+jLDd8FvuOdOr6WwoeOSUOh1fgxrurw1ltTuapyVVWOcxq65t7+1C2727c+EN10T3Xr3ft27islppGsicCOFLMQXgZPU9Ozd973wJrZ9srAHuxbwxK8D569D1JJLUUc6ThSWS1J61Gc1nbtOegCGO9LQ4UJg9KNC2fCJNIEcRplkULEwtpADlhxAGQMIfgQLAUbkvWnXS6iheXl3tLBQ8NBrxj1jBl7V1EIcFI0/zu+nPeHjtXADnjyBBwqF4IxrjLleJTn+WDQT9M0rtWyJK4lrTiO42Q6TuejIG67O00TlWnZ1F4KX+UrRT4Yj/rVqJ5ft2tlGDqbnvKsp1/9mY+9f+fdXwL2w2GlmdNE6QjaLmMha81WYciSKC0UhgaFGwwrRwggEw0Xn33arffcS0EUpSdQAMBSlSykT+qt6YUN22qt+eVBvtgb9Ht7xv2VlaXlvBiUVW6tDeFk2A9OcEL6WEcDESfKtZRSyVRrHelEa50kmU6zOI7jOI6iKI6Tyc+jw5jE5iIVCyVBilhpKaWIZZLWalmz3W7H1P+rP/4t9ivdVKyf7bQb0Ux7WsVyODLb9xzQUgXB/dwsD4uh9QHS08+5opZmWRLpWkNglKaNOMsqx4VzzsBolJemqqoiH43z8XA06I3Ho/F4WI1HxpZVlTvnmIj5JGnlcFIQ66h+MoehoyietK0SsVYyiqIk1pFSSsfRalMrpSetrVggSiFRTPJwkiRJ0rQ1NTMzs3ZhtvmZ9//PRtg1Xcd2M1UyHlZVbxQO9fMAmoUb5dXIssXa1S97TXPutPF4OOj1R4N+WeZVVXnvjTHOl957WzlbmaqqyjK3VZmPh8YYY0pT5t476wpmBjp5mpSe8MQ6giPuJAq1KncLkFILpaXUk55YapISdZh8h/vxTZTMCbGU1HFWa3S709PTM/Pza+cXZnde+94FsTtNyHtf5bSSh3v3rwwKrMB5Ul4kP/6zb1TJ7P49u5eWlvqDXj4clGVunJ10/giVDeScs84555ytjHWVKfJAzloTgnPO8eE2fycHq+Axr9I5hjiSAMjkGTwTIqIDK6RElAA4qWKFVRqhOFyJD6vdROVq8z4dJ1nN5OOqzIkRI3XelS+lO/5sWpWV9XlkUbte3hibMVtgxNbcKUkys3PPnr0P7FxaOjgc9QeDXmVKMm4SoiEffLAhuEngnJz3wTrniEIIjmE1E/DoEOHxPM8PEd9llc7xkJv1H3H0qA7zjOiwMfBuslzqI4I4Igo4nFkgEAAm62Mcpd4U1lVayFhTI0pns/l2up+sGUhghF5DHhxK44ExfdbzXr57z759u+7fvWvH8vKhfNQfjvrOVOx5Elgk9sxMFCavAhkiwiOT961izsfh3D5cnKi7woeIo+/Q4e/dhElECBNFC4+U08Ak1c45J7QSUvey5Uaj0Wstn7r1gppdAs+EZALUU5UokAgs0jSpLy/vXlpaWllZ6q8s5+N+no/IuxDChDOTuDJzYJqEpRkA6MSnzoPjuyTWMX+kjonxfyh/jkBwpCbrGyUjRAzBB3LMIc9HUkVFPhqPBvloOITT0kyRs4mXmfGxxmYt7hVVZ3p+qdcfDPuj8aAYj4p8UIyHVTmm4L6J00eEt5NCpfrOEN/5kkcFD56ENPGKjs0/Ovz1LccADMhARBxC8N44U5pilI+rYsBxN0pVFgutoJ5JJUEjT03NlJW1lTFlZYyx1nrvgvdHr8hHcqkPa2/fF9Q6MZbCR9nnmDjdIYRJmpT3PjeWo3YkDjmnkjJKlMgiKbVYs2ZDZY0xxrnDrvrhZL1jMpLj05d9KDheLNYxx3dt4SY3csKrieUitkRYEcWxjhJZq6kkFbUE4yiqd1rOOQ7knCPy32Oa3rcbzImIk5ZY38stOSK6HrUEh1o9jmIZpUrHMskwq2ktvTWlAMHMQgg+jGP0CU5snBhL4aMPAoTDZVhCCJQiTVNApVPWDa414iw1sUZyjiMBYnJ+k1jNGEX6vnCjHhT/SaxvxhG3BhEnx59oraM0SjMldCfJKKV+resayz6NB96P04Y6giPHNj2iAzshcMyIdWJ97G8HRAQQk44jUkopdKSTJM7qicbUqtoMB58F2SjU7Bx3Ov2l/t7p2QviOD3CKkSJIB+JYwFOrOn9DsR66PLSifWxvyWOuFaT2E6apnGWJo1ac6rTqJGqN6nVlIzK7U2K0DVu89aNy/fua9WSer2eZVmcpXqcOGecM/RtEmBOjsfvoeA/l8JvwMRbX21hGiWdTqfb7bbb7VaDZNaiZL2kRNZCvcPe6dm5MHVo2G3XarValmVpkkVpZl2ljHInXVD54eJEzSD9HoH/AbB6MmGkoiRJm92puenZuXXr1i9sWLdx/ZozT1dT0+sEnIIwg2okhUSpdKx85UvRZNm11pRl4ZzlQN4TMRIFBgQUj3W+92OD44VYx1Bbf4j/7uj/i4hi0ileR1nWbLWnutOzM3NzGzet37hx68aF9jlntUHOAUwxNkFYqaWSMtKQKHXfjl3d9RdVla2sDSEwgHcemHG1ix/i4crVhz6qkwDHC7Eeafy7ZUIJKBAFAyIKpbSUkY7SOE6jJGs0p5qdzvTs7Pz8+g0bNp6y7ZSNGzdMt92auQWJHYAEIBBGQsZCgJTQ7HbilIm7nHS88RSIAYFIIApkIaQQggEZxOGj7R/tR+ixwslPrCM3khFQoEAtpYqiOI6TOE6SJEuTWi1r1hqdVmdqamZuzZp18wsbt52ydevWrdtO39LOli449wypuowawAMggiIwIB2qFGtyfnqq6t2BEOtsjZRKRxqElCoWUkgdo9ST5FQhxGrHdyZ41C30o4/vhlgn2IyI1VMrpRRS6DhOJ9WIcZqmWaPW6NYarUar3epOTU3PzkzPL6yb37hhw5ZtGzeva599enL6qeslNng1iwsPp28lLDVJQCExwbmWaCcro/6o1V2PKpWCY6WUTHSklZJSJVJIFAKFAkSBkkEAr3Ylfeg4sab9pLJY33rqEYGFlFqpSKkoThtJWqvVm/X2VKPZbbW77c7U1PTM7Ozaubk1GzZt2Lh+0+YNs2vnxk97ypZWo4aQIaeMRymfTAAoMEZMBSaIgpSop3DqWmhEu4JF3VgPcV0rraIkrdWl1JGOVjOhpUBC/Pfe73xi0eWh46SSG/7j3n6iG010qSiKoyRLs2YtzbJ6vdboZLVGltWazWaWZe1uu9PpzLaj+anelVeeStF6wQ2GDLlGIJkJedJz1gsIjFXwA6AhUoluzDgEKSCCTWuaC/Fw+74D1xUwmNqyd9H0+/vq9ay/0ktG2WDY17nOWUgrDTC5AIiTAoqTD6vEOgmSrL8dhJj07o6iJMtqjVar3WjPdNrtmem5rJ522u1Oe6rdbjdrcSxWnvPMSCTrAbWgOgcjBAIgkPJEzudU5c4NvBt4mwdXmNEKeBtcXhajyozHvREHyIf5gcWVe3ev3Lwbfvzlv9XPu7v3Lfc6SwcP7Euy2qC3olQ0Hg8ZJpk5ABjgZJz5k9MOH8EkxhInWZTUGs12o9WdndswO9PdiMtdv6gV6ag+HA8AyFo7NTuTZlpqFSlNjLVao9FuWcfBC5RkjKlnNYEUnHFghUD2uVaiqooyH3rvyQahtLVQFkVZUtya/fA/f/HgqMqDPPXMCy+69PK9B/q9lZX9+/eOBiuTDGZvTQiO2J98duukWgq/JSZFhFrrLKu3291Ou6aqxd27b1oWiMiFsVrLNE2N89v3LU9Nd9I0JaKD/cHU1IwxVslICIVSEJELVikhkKoyL4pxlsSmGntvBRARjfLSEgePEDjPzajyIy8JdQJ2//03fmLn7dnUhjMufLK1VglwznjvgSgE91jP0COC7wNiCa1klCRZrVbPsvp0Z/qMubmyVgUzIsJYhbIw3nNuKqynjSSJY5Wm6eb5OVM5EKunNVeeq6pKG02tILgKU1XfsLY0VVnVECHWkXMuWOcCBBYUSi0kiOjOA/n1d+32nspBLmXV6906GpdnXXRFCC7Px8H7YJ2BAg+f+3uyQMFjSKxHyatDiVLKSOsoidO03mpPNeKlO//VDQadbsMGOxxXRKSUWjPTLstSYJCgbFn0+/16rZnn+SThOM10LdLFaDkPIU1TlLBn336lFCIS0WjYz7LMBXYuAIhxaWpZA4jrsZLkAxAKFkqiDC/48asPDHxlzWjQK8tcaKWU8o5Phs7u/w4Pj0QG6UPcP3/vyZbf8R8djtWIydkoadrIsiwTLh8uqywhGbGKdb25f6V/aDh2DBjHK8O8Py4PLPfzohrleZQkUZIMRqPt9++uDHnA/cvLK+PxgaXBgaXBMPeONKqaDer+BxaHOQlddwzWI6Euyyp4psCTMYQQmNyf/fHbOo2sdriphNZayuhIU8lvEcE8YXHsLdajsME5esa/6QYcadC4qjKoSMe1SCdaxXGsW82uxKV6YyoAHji0QoCL/ZUA6FDcubhYlqW3AQDiNCmNIzqIiM45IoqiaM8dd4YQEFH0RoY8Ior+cPJkEgRmxtGYGU+b35glrar0WWO6saah7j4QKcnkOXiCVARoNFrNziA5WE+LYtJhRih/ji+mAAAcdUlEQVTlrTk8eBGCZ+YQAgBNKiAf5pQeMRaPpRU8IX2sI+xBKQQqreNJkt2RAlE43K5Yx1mtXq816rVmI8uyLMuWDiwpH+pTawejStVqy4NxYPYjG6giokl6MeRmUnTPzCQUI7MnFEqo5EiOKCI6kJMK/Ukzd0SUiIWua62iKPI6itLO7Nw6JwQHZ23lbKWd11rUa81mq+O9RyXiOPXeeueODN45Y0zpnPPBsbMAJ2QK1wlJrFWglFrHKo7TeqwloxQsiDwzE4IQCoVQsUoaU1ka1+rNJK3Va9EKe4Lmobyoz24uIGRdYg7BOWdLcpaIEBGUjgULqa0H50tixyCVkHGchuASGbNKiL0ARMneYRQrITOhlBRudu26TGZCa7LeA81v2eYDG1tWpXVlv6jsYDDI0sZMtyk81NJoVDcAgJ4ohBBCQcZWpsr7eZ67clQRULAP0/YcF+7asSHWY5IYKYTQKpFpPYvZEIEvAzTieq1ZawJApHHx0C7B2vUO7V8s7dTKps0bhARfSmi0/uJ9/4CiUoguMAA+74UvGo+WgjMYrAFQLO/esZNtIbP67Px8XQYAYBEvL/d6Bw4ImaSt1tzcXLfV+tRnrt1ES1PN+L5F0z3z4rt33rFuXFx8Wq3MxfW7RwagAIgBMgAnYInFk572zINLS2sWNt5w41fuufHLUWAJDAAEq80cFLAHcDKuTc0KFUtJk+4mJ5zROjbEerQ/tkCenAwdqfULc685Pb77a9clBGddnP7aJ+5Zd8rVAcy1n/zkrz37vJecNXZJ59M3LL/rq7tQ0KC38q4P/tUV61rvvEr39puxwSedPnf93sF7/u7991J6/uMv2rBhS5vxs+9/3+d+9ZwaxVZ0f+CPvnT+c68Ogm75+o2/fnH0rB/sMgVEefHv/8vzXvnfGqOlT712tub7Bc6e/tYbWgw3/GKUiJxQBIwM1267Lz9vG3gHn/66vbFK3/GFzy+s2yIkLN7wxe2/PFfnkplBCG+rOFEehGAdwB6i6Se+9YF4dq230p+YOtcJvBQSEQPko+r/fvyGj/38fJ0WBe8969XdS978N825da8+z73mnJsS6YR3n47PvftQ74o4vOsP3vDAGzZkQDW/SKfHjkLAxSdslD/1pBpF3Wf94Rc2Xvq0MbsZCfNqR0JjKxo1gqzeIUkglDT3t4PLEAKIFKBgsWUui2U1VslHb5uq5FIWKMqQWWimSDiBxSXnCAiVTsWzL4croH4Hi5XlQxvDtlhCS44yUXkCnyS9aItJQt3vycKIZZrbMgAnMmbOgU/I2s8TctBHQMS2tDcM4Vn/c98Dbgpr4vT64BmnBLX8wJteWE86Vqf+Zz8Mv/ov9131k6/6k7e89atvfvys2ldP90NNj6bOvOrd7kO7N9hW1krKZjJY0PC2P/ydhc76RY5f+xdj2Wr8yntHexlFksVJI01rF553eloHqAPWoNuGbrNz/X6znE63G9Vde+/deuaFhgEzhBphJ33tp/Upv2vWv6n6x8UtKla1mmy2Br/xnNnt99wLxB5ANGJIUbT47V9tnvr7d677rXue/+HmE/8Urni3PPsPDoipjdb6E24FPILjhVgPT7YhRgYmImtDMJvPetztBbzjusANZ+r0lp9/3H+76tTttn3r6Kz33jb9we2wacv5kuCpG2Cjvil0vUrwc6P1G1/79eYzX/4Lf33oVp6nOkll3vs/okvn+e8+8v6czJnnNU00uujxtQCcZpnGJjC+4I235s1ZqMM91fTOPugom9p85icPbb5HXDhz3rN1WvcAtonYQFZVPbVDCSGGhQUja47rHKX0j1+4a+PmrZXlgYgO1bdgA1Vd6tjEAMjwb3cd/PpKdOP+cWthUxSD8SUzTbSMR3LuHxE8VGI90mLdw9JLjwzGe1uUY2t6m8593Lu+Wj7vLfDCt7nf+MOv9vbt2Lyp4G76Pz6+dNkzrt60ecvd99z6ihc/E7s+yrDqyL+/YfiGt77nSU95/uvf8tZX/a97y+YUNylu8DXPe0bSok0NePWPWd2EFzwnWBa1REdJdM+9u55yeRfSg76p121RrGFQrTzu4gvefQM//W2Hrl2EJz75ykLAG/6yDA3mLr7uxY0v/e6mT/72totP3YtNSW1xG1/2R582a+fXC0Sd1G/tzeEUUpP+6w8P9//5zB3vedwl66PLFvyf/uKFYrgbkfBELth/qMRixsMl5Ec1kz0Kj6ZMfJiFNDmycDyoTGnPufRJX9jHU2vSd7x9yyteuQZrPq+dX2XzeT4uC7ewdt3HP/+FUFOhzvcup5+5eWVqatZ76rTW3n6Q/u1WRw1PDX3Xrj2DniUGqBO3AFMVkOJ4WtXUmWecWlcr9a6Manjnrj65OO8N+v0VDMX6DbOe/NJ4fyDYdE4d65pSKqA4/bTlszbdL1PYGZ/+qneKJ/3ytS/86V9FnUaJ8j6ce8H4i/dlNknkzFTZWlBR9uqXbPjw/9nwgicd/OM3vnCx1wdiAAI8Wj4Qx88i8+D45lE++JJ0dLen//irYz64BwczA7N3zhRlWZZFUQTUP/Wier2xf/P6fXEj/PCr3rXtjAuc84GrSDc++vlCTM2LaXnmeYMfe3Z9ce/9kYY92++76or06c82sovcMfftvKMa5w6B2wQdCPUQCWhkstWc0lr/6IsvCo1AXT7vidTtmCIflXm/KJaCXXJlz+deInzuy2M340Qbn/v2oK5yr3nfqaZbW7N2+MbfOKWR8Sc/8beTk8cDB47KN//tYLF2wUfuXLf1+Ted84rP/9J7y8teg6e/ZN/PvfW6dj0LEJ24UZ1vJta3X5LomCtv3/usTbqiE0+aWIXO+rW/955DXK+gDkWzw5oCeUapRRxlqQF4xRuHrnV2aGX1tP/7r//5X3zVs9/2e6/9k7c/RdTItGBIp/zbbXDN1S+jCEQr5iboKf/7v3b+G37tV/72g+/7+le/FOvd3NbYSEN97b4elOPlYPpQFmw9+kpTVTE85+p6XFPUlVKrTads/uA/38mtWLV3za6545N/Ob+yuAhMgoEqG021vIT7x6f92y0+7swK1bS5L3IXWp2lleV6XFcaACaH2h/Bsb8LjwAEgHgs7eqxMnJEFIKz1hzYsesHf2RWdQDb4v7l/UKnxCCEmBwX8Bu/90f/8K9LNz7wgGyG//5LOLxLVrfH/XuiRvqvPOXjduc1b7vbgqqC29mDZX0RN4Wcyl7xY8Pr/vFKv+vaxVt//JTzCmrqYdp56wfj7vwGUZXdWJ+6dm5juzEbJa0EIglXv3ADNj02BWF40UteOQqJideJZiwyOPWs4Z3XP/O9f/aHxWjogTvZ/r9438xF537xivNuuetfs/03zN/+qfhD/yf84eu3/s83qQOL2yUe/xz6tjiBdawjcM5CILLGM7z0xadz86D0dM4FHVPtg+B0lNRqadasx1EW0toXbzl3etNgYWZXBGOUVmIriFCW3c98futff/yLL37hSw/u2fPSF//0+kve+Qe/cenf/sNXtiwMrvwhJAHv/ZuP3XJjeeZZzR170/f81V3PffYPh/5+DK4aF41aXbEr+yMIcPUL7/jYP2fsOU1qiw/s/pn/8vMbL37zNT+s3/G2DexD7G+roT+4fH8qner22rgixfBFL0MKDzBjvUlzawXK4c6DPxDc/0MGxBMox/cbgt/HO7G+Y9oWMyNQZQpdaJXAT/zMtR/6m+ctLu3/0g1DVEuMIq03G83u7PRMovRznnblZ7/s//tvXxvFsG0DvPynZzasu/CXX/PJvQdHXux//vOuHh3apyRykl1y3uVv/pPr3vK72+Zm6lHUf/u7F3betlTvRh/+eO+2O3o/9PSn9xf31zhvZonWteFwUJPdfHQgAB7wrTe+/WmC6dqv/l19el8SpWde/KR3/+21e0fiiU945uMuvjBq/GL/UO4ZQnJu4Q/uuHdcqyV3337HM3/w8S6Mgqaq6n7w7/ayBOvI+weTsh481PPYBoKO3+fhaEpJqfGoFvu42k+bJskFDCJK0jhOsyzrL68EqpSK4lifsu28uNHcun795vnp2A5guN8ZWxJ6jkAlabP11Ztv9EzbtmwdDYcKSLgykwHYK6UYI1nvvOuDH7HeURCgEQMhhh/5oeeCLYBMA918M5rtdhBCnueoaiPjQ9w8VLrSw4QQ6Hy7kVY+LI9KAdopyegREUUklP70Jz6ltAYpnDOIGikERwwOBEmphZDeO+dLIuLwYGvi8VkIc7wTa9J4XcnV/REBM7MQ8sg1iAhCMHMcp3GcxnGcpXWplVC602qtbWdzulzbrgkzBKWJhcV0XDmWmoUujTG2hOCVRHZGsa/pqFVvVKYIjBTXe5YDSwYRmJypIBhn80zLRMqE7dp2jbzVUimlHKgQZ1XAoQ02IABYV4F39SSqihJ1PHB+pfQACQGj1FLFk3NXggDv/Wg06K0sG2PKfr8sc2NL751zJpBjZqAjt+nfGXaESccnsY7TpfCI6qF0Ojm4S0ophCCcWHicvFxNh0L23kuppYrjOFZpHCVxmtWmpqYUBINw71JhSzscHNJRVq81Gu1WURrrDJOvxiMpBQdXeUdEYHMpejMzM8yOXOVZMTIIZV1VVYUdj40pGXytVptu1pb3j5WWtTQmZl2LtEwYVAW+ctZ5L6QOzPmwZIEqUCA8tLSS1dqz8wsy0jqKJycMkLUB5SB4wWDzsiiHxlTGlN57Yj+ZCkA5Oatswh7vSgA40p750afUgyyyR351nBKLEZhRax1FcbfbckYbM9BRLaCbnV0A9Enc3LPvXmYFHNbMbvbeo5Q6Nvv27VM+abiZ6Vb6dx9415mnwfkXtncvjr/05fDKn3jpcDi+Zfuti9c/MBya07ZpIS4676wzfDEufXX/9lvWzHVvve/mQ4foJ37sx0sjbrvrun17F4OHM8+fiuS5mxfW+ajuzOArN3953cbpG+9aXD5QXH31NXftuPP6r908MSl8ePAoAACQgAKcfea5F1z8uI+8793v+JOrnNn1F3/98b37YOlgxqFYNw+79oCO4IyznqpUDGg9Cee8dRUwTx4vrVKVJJGKpdTAIpCxNgrOemeOnObyaN+dB3V5J98cp8QCmpzGFam4ubh38brPXXrqaWcg9nora7ad9sFn/MhPXPep999y8w+tmYIA5R+89cBv/+79T37yDz35qbtf++qLlRgfHK7Zsu0vXnQVvO9dicNSkXzxTzcaMwtfvuWfv/bFp1r7NxozE5KffHm/NTudiA390dI1529//S/lffO4P/2T7PVv/FC7u+aum1/STN4hWBDLpzzr9qx7tiY5qNInPV6++71YFGd+9J/O+Nn/8mFFRe/AKYk4SGiB9Xjk6o1YCGCSgjHHtVMzt59x3vmf/fgTLr3snxDCq16iWRDh7K1f33n2BfrQojuwlL3tnXtuunEq0nUh8tV7gwgAUug4TpKoPtVtbr/vTg5BJmm9PV0VOTOwO37DiMdtfIAQCBFjEUVJ67d/80vv/uP/Fw2/9pbf+UcU8KXPXxshJNUnePwpyq/Nkv0qRpX6n7l6VruPCvtpbfc7D5ambamkMWSgGuva7JYkabvBTsyHUOS6Ovjzrwp//p637++Xcxs3bd24hqsdcXnT+WffhQgklCh3K1NKVyi39Ju/pD/9yXekrVajNTs9xVFxf4dvmu9+hShyBJFZZjeWpQnl+htv7nzl+nZwqSjHphSRqZhUlMaldaLwYsxYOj8AUZRnb9M4gpm6PP8UvuTMxR33fVnrBACklMiTQLtQKoqSLNHR2umDN312S2/x0ieeZ6wpsqwplEKUcLw2rjlOiXVkpkTE6zau+cRnyxuvc8qGmZpfM6sPLe2+68sXtxRgQSoPr3rulqueOf/ZT3wMywILDiuN1/363Vu3nv6xv1/ikcaR/PRH1jLHkYpEFHMOuozCkEQeHr9t7+/98ql//efv3LzxXK48jAFd46v/lvhQR6m5AhwDDQnG8PRLh7/5s/N/+tY/mp2dRiv8WPkyvu/6dSD7ABLKNuaMffm6X+tf+bz9T/7hB04779Cmc+1ZTyi7W+77iVf8QggaK8k5QA6m6Gw6M2RrD7TWicpcJspAQ3rZs+tXXDhDnB9xpOCots0cqanaymlze2uDr//mr7e1QGaWUk5y/4/P5vLfE7Ee0WdlUqnChFKkHOA1P9UNY3PBRnv+eldT8Gu/cEPvAfYDpJFQYuc912+XFGiwSL00L+WH/vaB884/5+ofBDk2vk9XXrG0ph0ZcghQVsqPbD5o2r7Eof+p5+675wvP/8mX/RgXhvqMee9XfoqEqCSgGSEPorzfETnjYPTCpx/8179/+e/81uvQBTEC6I1efs0iC7l23ebeyrzoJ1jJ1I7XtFMOsHs/HFiJ9+7OkdeoKGKB5dBTD10JMHRX/zASwIuvqqnB3TRKuSd7Bxqf++IhCEDkif69Me5qOxMhb7zeQU5cqL98xzJCJCVP3LhjSKljeyu/J2I90s8KM/tQee8BhMrOFivhysfD29+09Vde8dw3v2FNQ4MbzowHZ370A2f+5E9esbBW2mKjL5Lfe5PyMjl48OBLXvQcHls5lLbX8nbOl+U55z/uZ197w8/9anjaj6x86MPpqOjyynLbfO6SsxHHMecklqM9B08VMrrqmmte9Zprr/ov1ROetfLu96Evp9UKr298/NwtU2yJhkYM1D27z9AYx7Xsg3/55TAU3A9venW18+9ozz9dcel8eNJW8543XbC+vbi4d3ua1D/1ifKmGzdCD/V4/KaXJts/ecb/ejWqwRL2YmPk//1Qx6u2qch7P9ktTmZgUtzR6GTLA9i5Y5MbxDffCWkWPxLi57F9w+N0KZxkxTA5770L/uLLf+AZV1+7e8c8DOCf3rN95b5/jKvc9+sX/OjiO95y249fesvV5xx84kxojO/Sw/zczeNIRZc/7QXPfuk/DfYLMRa//nvDz33x62Vpx/2V55xVvOPnal/6k+gJZ2z8hTccioYRHzr08f9v/qK5m7iPYNq/+abbvLPDQ73LNi79zevxa++Rz7hk7udet2SGqA/kH39z85oLxzQAKKfe9Wd3Gefz0eA5Vz7xs9fpahQd3DN3732bHthPz7hcfuAd5/7oGYv/+3ev6fV6+aj/tetv39AeYU+LHpBNZst7oypXVfiJn+k/6xXFRz9fnX/Z44GcABL/3teSAQCJwYFF+PD77y2WopURJFm2SgIWhxNpjrt0muNrNEdjYg5DcMaUWuhWN2tFI+6LVz0T3vYSgAMjGka7B/CFO4GWqllz7ztfk7arnWI5JOmFUmW7998XI2SmRivuRU/MzlrftVV1cN/+rbMz4ZDBYSgGxYc+J3qDLWnVr+2/+7Qsl4Oge/0//WmFQuy4b8c5m7fKAYs+hyF84CswGpzaKFZm+7svXQA5ANEfvuEFDMG6sVG98bvf0x+unPORj8NTf+buK3/2y59fuvLil40bz9/32nfs0Fqbqjjroq3lwTqMfNnrbLyqN/t8f/XPVfZQ+q7fnP/Ar567586bKaCZCKBiVfCkwyjYIjVecdU5jWK0cSbxfrWEFUU4bpMdjl9iAQAzO+eMLUfD8b7FIgyjaEXiSlaONPbFA7uaDPKGXeBHEvuk+pb6sloRr/vDLzz3mhfed/NXfuxs0MMeD9VZ9RVdrVSm2LNn14bpGRgTDLgTnVqiW/fS/fce2uwH2owCDWS+Iv76U21E3n7/PZvXzoYhQD8Sfg0ma9b/zL0r4008Qj8C6usw9B/45whFrNDWrY3T5ObtjdseSHPCQOHaz37ywMoupvj2m77aW17J8xwPcjMc4AGFFe8BKoJmQ4g+NAb7ZsrbX/2sTfffflOsCKWgo1wdIvLei8BG2yt+8o63fCDdcQAmh90dhw770TjOiRWCs74qK1Nye+adtz15T7nWlIKHSZVHr/jdnde87NWO43+5dcPKaGacx71q7WJ5Wo/kyqGl0Wh85eUXci7kmFJHa1rZeGl/UVTURz1GNaDFA4dmOmuf9LznPvV19+8dbouGgOOg/v/2riU2qioM//953HmUaZnSltKxQLSBFBKJpgiJxihG3GgkIRJI3amJJmpc6JJoQlwpCxMNEl2ZqBsXPiLGx8IggoloBVN5tTj0NbRl2nnc6cydc8//uzgzQyUQUQGHxH8xizs555575ptzzv0f35cz/clZZdnaABeqXk6LhUppLtfRvXz7rqcGXjw7XUp5eZC+kTne3J0HomKxqOYn3hhM3JUY3t59OrNnxflXl2X2tH/3bOeBp1d99lzv6LHv/YX82l6jghYog1gINcCOnc+cKLblS62BL3TevrwpuH9ZdnpqAhlkfV+rRbE4nM3OPtzfu6EjiLb0JqiysOBTGHJoiR0GqQnztK7oIG2SIkkiCoLAL/vtyY7973/9cbX45GZEVicvmN8qYmk2u3Hr1i+Hfzr87WyCoWulOVOYW71hYHpm0i+Ufjx8Zns7klWB0FOz/kxkrL2r84ODv++6vWe5yHbL3Px8plLOhUtWHBoRHSsTidCXrLuW9lbxwkDfbW9/PvzKlpSunk9qfzKdne1fPVXVP08m29qyUSiGzKhSAGNxMlAKWiFHxFvWKlWeQkJE7IjQgjd9itYFMF6Ymz9W9NNrb+mFmBWKIT169uSpgrxz9/Q9CXjt+RWsvK1r+IsR3yoNwADUiAASkWBILc3t3Zak8OSmWwce3DcqpYLmiw8utisCq0kGTdYYI8rFgky2z/vFj3bf3Vf6pRTr6cyod86cGD+XthQ80J96YrNl9Bm8/cMxf9QPQ1/HvA+PQ2aG7x1IWQ6PnBuP4DSwPRZWvjmYK5XLj21LoIxPnk13rUy98N7RkftSgxtXZQr+u18NeSAz+bmxEj7yelpWYPDxNVKNjI6Mr19/x+Cbh3f0yZce3VCsFPZ+ejCq4hXmcbOO56aGJorMODFR2PlQnwmDcohpX/46Ow/o5YulUyDfOqJavdTQ0PEyyPHzU8llnSYe/yQzNrIvCzoGFcvAYRg2qNgcqqrVqm6NH/ghEyvHNcCh9FGApDEVGxpHYHp9gtBuK/vnq2DTeWwvMUREoSKRmIx4iWhiJjexpGJ8BgXQ0t6DCpUQkVjLhfRpzVgC297TayoBgRaClGCJiqVmtmG1ykyMAqPCs2AoQrYskYAUaxQgE3FNgSEZQaCSCRRbQDTEgoHI1caCF40yshIecDmu20jYIETJlJ+f0RLLVdJaW0sShZayQgaRJVn24s7lBACGLCAKYcBEhATL7CkZMoWGLYfSUjU01hqm0Dk/UXhae7FInLgsMGLAgrFEFJIhax1RxfWZeEeycnmpqauxZgcWOAe0jEoppdYuo8GRoTXqhZRSbn6tte7TcRg1SKbqRC4hAAih/ux+Q3RyX0IDAKEDESGxIxXimtXaSqkQZN0ZYGvtpXbZFlJKRHRjsyZwp2wiu1jS1+Xp13BTM0lETvkX2DbSFhCRQSilHJW3a+7EqomI2QI3b6zw5gAWonRLlxCwOIjhZl8IVU/9q6syIwEA8kVydmZmJGZ2FesNuCFyHX0KsSGNSlDPKKzhCut6OCwat2vsFO5KDW0XZXztIhA4aR13bCV3g9rTMTT2nUXgq38r3WJ3sfAOXMIMEjMh/41z8A0+NN8EwIJ67IiZFwMFLheF+E/+wZeM7WrGs/h64yf/y1Tjq+n5BluTvOT9b5e3axh1vUZd/VtX/h9dPtBvVN0GBQAAAABJRU5ErkJggg=="
+ },
+ {
+ "name": "smashing",
+ "description": "You maneuver the drone into position and begin ramming it into the machine. The machine sways and shakes, and just as you think it may be getting somewhere, it falls directly onto your drone. Now, not only do you not have any soda, but you don't have a drone, either. Dummy.",
+ "choices": [
+ {
+ "key": "choice 8",
+ "name": "Disconnect.",
+ "exit_node": "FAIL_DEATH",
+ "delay": 10,
+ "delay_message": "Don't feel too bad, it happens to the best of us sometimes."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAAAXNSR0IArs4c6QAAAjVJREFUeJzt3cFyogAUAEGzlf//5ewpF0pGRJDnbvc1CkhleAbEfN1ut58bcNefqzcAJhMIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUAQCASBQBAIBIFAEAgEgUD4vnLlW/854tdJz5+2HWuWy9+6HWdt77P/1PLo7d27H/cwQSBcMkFePSI8e+T5WXncUctZWlvP1uef5V2vd+9y1p7/aD1nMkEgvHWCTDgiXOH3dU6ZJNNN+j0xQSBcehZrae3siCPtff/aJJo0OX6ZIBBGTZDle3XuW+6fq/fXo/W/ev3H3yAw1FsnyP96Nues99ZTJu7R11PWJqTrIDDM1+3CA9CUz0BN2Y410z+Ltfdxz/780frOYIJAuHSCwHQmCASBQBAIhFFX0pfefZbq0XKPupPu2eW9+2zSVmctZxITBMLoCbK09069s86vT7uTca+j9s/E6xivMkEgfNQEWZr62a6jPiN19evABIH00RNkqq33Rywn36PHv7penmeCQDBBXvDqkX/rdZKjvp+K55kgED56glz9LRhn39/B9UwQCB81QfZemd36HvxdR/azzmLtNW3/TGKCQHBHIQQTBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBIJAIAgEgkAgCASCQCAIBMJfgqJi1hru29AAAAAASUVORK5CYII="
+ }
+ ]
+}
\ No newline at end of file
diff --git a/strings/exoadventures/robots_wingman.json b/strings/exoadventures/robots_wingman.json
new file mode 100644
index 00000000000000..2958fbb6a4ed3b
--- /dev/null
+++ b/strings/exoadventures/robots_wingman.json
@@ -0,0 +1,369 @@
+{
+ "adventure_name": "Robot's Wingman",
+ "version": 1,
+ "author": "Lucky Luther",
+ "starting_node": "Date Start",
+ "starting_qualities": {
+ "Love": 3
+ },
+ "required_site_traits": [
+ "in space"
+ ],
+ "loot_categories": [
+ "trade_contract"
+ ],
+ "scan_band_mods": {
+ "Narrow-band radio waves": 2
+ },
+ "deep_scan_description": "",
+ "triggers": [
+ {
+ "name": "True Love",
+ "target_node": "Love Birds",
+ "requirements": [
+ {
+ "quality": "Love",
+ "operator": ">=",
+ "value": 7
+ }
+ ]
+ },
+ {
+ "name": "Complete Failure",
+ "target_node": "Obliteration",
+ "requirements": [
+ {
+ "quality": "Love",
+ "operator": "<=",
+ "value": 0
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "name": "Date Start",
+ "description": "Cameras Online. A Blood-Red Drone is seen streaking through the stars.\nThe Drone is likely leaving behind some form of Chem Trail to brainwash Nanotrasen Employees who find themselves in the void.",
+ "choices": [
+ {
+ "key": "choice 0",
+ "name": "Hail other Drone",
+ "exit_node": "First Contact",
+ "delay": 5,
+ "delay_message": "Attempting to signal Drone..."
+ },
+ {
+ "key": "choice 1",
+ "name": "Ignore other Drone",
+ "exit_node": "FAIL",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAD/0lEQVR4nO3duXnbQBBA4YE/d6CACSMmasFO1IDbUkluQJHUgnIlCFgDHIHmsYD2mD1m9/2JFCgABTwsFhcnEVkEgNOP2gsAtIxAgB0EAuwgEGDH8IEsC+cosM0rkF43ovVz9fr5kO7bQHreiKZpuvkJ3JvE4zrIsixsRBiSVyDAqIafpAN7CATYQSDADgJJ9H481l4EOGiddSWQBGscRNIWzUsTBBLpOorfX18VlwT3NK9vEUik19NJRIijVVrX7QgkwJ+Xl8vP8zw747B6x4HV5c6NC4We1jhERM7zLB+fnw9/c72RWbrzwOpyl2BmBKm5h7uOQ0Tk6XBw/p3Ve7usLncJJkaQ2nu4X8/Pm1GIiPx9e2Pj6tTP2gvgY5qmqjdMPh0Ocp7nm0jO83z5vfbyIR8TI0gt78ejvJ5ONzGssYjIwzyESPpDIBtc1znWQ62tSboIkfTGzCS9pK2LgGsUW3GI/D/cQh8YQTa8H4+b1zl8RghGkj4QSISQSEQ4fWoZgUQKGSEYTewikAShkVwjGBsIJFHs6LA3kSeedhCIAu1DKEabdhCIkpwTcoKph0CUlZiQc3asHALJoNQen1DyI5ACck/Iez+NXPPzOQPp/R/eEq3RptfRpPajDg+B1F6g0aUG0+POjREEN1yHZCHrg/WnhzlIo+438tBDKCLRQSDGcA9YWQRiUMhoQiRpCMQwnk3JjycKDfN9epGnHOMRiHFEkheBdIBI8iGQThBJHgTSESLRRyCdIRJdBNIhNn49BNIpn0gI6XsE0jEiSUcgnSOSNAQyACKJN2QgbAhuRPJouEA0v0PbEk7/xhkukJG/j49IwnG7+4C4Td5f8RGEPVN9oSPJyOus6AjCG1PakvJ2eh89rOPih1gM223JuT56eKcwcxAU22lZPIIgEIhI+ZHdypsgCQQXNTbaFkLZ2zkQCB7UmJDXCuW7wz4CgQrNl3DXiIQRBEWlTMhbOtNJIMgu5vCplUgIBMVYfAE3gaA4S+8WJhBUY+GmyeFud0c7LNx+TyCoqvVICATVtRwJgSCL0A251UgIBOpin/tvMRICgbqU5/5bi4RAKur5UdaU07ItRcJ1kEosPjxUWsh1EpE8/0cCqaj2VWILan/tNYGgeTEvl9AKhUBgQujooDWaEAjMiIlEJPGEgRAIDIl9tmQVGguBwKTYQ6jQwAgEZqUcQvk+Q08gME9jQr4VG4GgC1qnd+/nK8MFwsW5vmneobAsy1iBcHvHWDjNG4ERZDwpL7UbLhCMyXXXr08oaoGwZ7Zj1HV1/7l9DrlVAuHY3g7WldvWToMRZECsKzfXpJ45CHDnegdCIIDDZTQRAgE28dIGYAeBADsIBNjxD3G5pHKbkGjYAAAAAElFTkSuQmCC"
+ },
+ {
+ "name": "First Contact",
+ "description": "The Blood-Red Drone accepts the hail with the identifier MISS RED - 05.\nMiss Red sends over a series of question marks in quick succession.\nYou notice your Drone has somehow taken initiative and begun to start up a program you didn't know it had.",
+ "choices": [
+ {
+ "key": "choice 2",
+ "name": "Wait for Program to boot.",
+ "exit_node": "Sentience Achieved",
+ "delay": 5,
+ "delay_message": "H3AR7.exe booting..."
+ },
+ {
+ "key": "choice 3",
+ "name": "Threaten Miss Red.",
+ "exit_node": "Sentience Achieved",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": -2
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 4",
+ "name": "Halt Mysterious Program.",
+ "exit_node": "Lack of Trust",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAEpklEQVR4nO2dO3LbMBBAl5ncwIUaVmp0BbvRBXyllD5SLuBKuYIbV2pU+AxMkaFCUwA/+C/w3gzHCodWKBMPi10CVCcigwCAkR+5TwCgZBAEYAEEAVjgZ+4TaI1L3+c+BREReblec5+CCpoXZBgG6bou6f9J49TDJkFyNKIUDMNw/5n6842R5OV6Nb6e4rIfwjEsbVPWjtW4pf5cl75f/Dk/zvbvtf1sYbbVCNJ1XbURRERUfa55tBgjhW0/+LNpiKWpEdXKvNFPh2Wm/RCG5pP0GFz63pofzFlq4Gu5yfx4CE8n/8ZaAGCACJIJ32qVKc+g0hWH7JUCzZtrFclWpdparTIdt/c9a95CVSeJIB6MPfOYc7j+vm3/lvc0HWs6n5YqXSHvbyGII9MG59rYpmXa6XvseT/TsfN9rVW6Qt6aQBBH3o5H+fX5GaQnHvOEtfsaW6Q0Nf7ahTAR8tZE9vGilu31fL7/fD6doo59U29azzv2Rpl3I6/n8/311+0mfz4+Ho4Zx74ium6uaj3vFKhZDzK9iKmZyiEi8nQ4GI8bG5e2Rqb1vFOgIoLk7uGeTyerFCIiv9/faVyVoiJJzz1h8ulwkK/b7ZskX7fb/XXu84N4qIggubj0vbwdj99kGGURkYc8BEnqA0EsmEqq41DLlqSLIEltqEnSU2K73zBKYZND5P9wC+qACGLBNn1ka4QgktQBgjiwRxIRyqeaQRBH9kQIooleEMSDvZJMQRgdIIgnrtFhKZFHnnJAkACEHkIRbcoBQQIRMyFHmHwgSGBSJORUx9KBIBFI1eMjSnwQJAGxE/Lay8g5P59RkNr/4CURKtrUGk1yL3V4ECT3CbWOrzA1dm5EEPiGaUi253pw/cJBDlIo80a+dwiFJGFAEGUwBywtCKKQPdEESfxAEMWwNiU+rChUzNbVi6xydAdBlIMkcUGQCkCSeCBIJSBJHBCkIpAkPAhSGUgSFgSpEBp/OBCkUrZIgkjrIEjFIIk/CFI5SOIHgjQAkrjTpCA0BDNI8khzgky/Q7slKP+60ZwgLX8fH5Lsh+nuDcI0+e0kjyD0TPnZG0lavmZJIwhPTCkLn6fTb6GGa5x8iEXYLouY16OGZwqTg0CyTkvjCAJBQETSR3YtT4JEELiTo9GWIMpS54Ag8ECOhDyXKGvDPgSBIIR8CHcOSYggkBSfhLykSieCQHRchk+lSIIgkAyND+BGEEiOpmcLIwhkQ8Okyeamu0M5aJh+jyCQldIlQRDITsmSIAhEYW9DLlUSBIHguK77L1ESBIHg+Kz7L00SBMlIzUtZfcqyJUnCfZBMaFw8lJo990lE4vwdESQjue8SayD3114jCBSPy8MlQomCIKCCvdEhVDRBEFCDiyQingUDQRBQhOvakpG9siAIqMR1CLVXMAQBtfgMobauoUcQUE+IhNwmG4JAFYQq787zleYE4eZc3YScoTAMQ1uCML2jLSjzOkAEaQ+fh9o1Jwi0iWnW7xZRgglCz6yHVq/V/HNvGXIHEYSxvR64VmZsnQYRpEG4VmZMST05CMCMaQeCIAAG7tFEEATACg9tAFgAQQAWQBCABf4CeWF0TY0egUMAAAAASUVORK5CYII="
+ },
+ {
+ "name": "Sentience Achieved",
+ "description": "Before you can analyze the program, it rewrites you basic hailing protocols to have a new set of \"Ideas\" generated by your Drone to be used.\nThere is also a LOVE Gauge that reads: $$Love",
+ "choices": [
+ {
+ "key": "choice 5",
+ "name": "New around here and was hoping you could show me around.",
+ "exit_node": "First Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 6",
+ "name": "You appear to be an outdated model, but I'm into that.",
+ "exit_node": "First Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": -1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 7",
+ "name": "Never seen a Drone as cute as you and wanted to check you out.",
+ "exit_node": "First Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": {
+ "value_type": "random",
+ "low": -1,
+ "high": 2
+ }
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "Love",
+ "operator": "==",
+ "value": 3
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 10",
+ "name": "Haha, sorry for the threat. I just play like that, haha.",
+ "exit_node": "First Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": {
+ "value_type": "random",
+ "low": -1,
+ "high": 2
+ }
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "Love",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "First Reply",
+ "description": "Miss Red replies with another series of question marks.\nThe LOVE Gauge reads: $$Love",
+ "choices": [
+ {
+ "key": "choice 11",
+ "name": "Your curiosity is amazing.",
+ "exit_node": "Second Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 12",
+ "name": "The moment I saw you I instantly fell in love.",
+ "exit_node": "Second Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": {
+ "value_type": "random",
+ "low": -1,
+ "high": 2
+ }
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 13",
+ "name": "Look, if you want some Chad that'll walk all over you, fine. You missed out on a NICE- GUY-.",
+ "exit_node": "Second Reply",
+ "on_selection_effects": [
+ {
+ "effect_type": "Set",
+ "quality": "Love",
+ "value": 0
+ }
+ ],
+ "requirements": [
+ {
+ "quality": "Love",
+ "operator": "<=",
+ "value": 3
+ }
+ ],
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Second Reply",
+ "description": "Miss Red starts compiling a message, but your Drone insists you sent one last line to seal the deal.\nThe LOVE Gauge reads: $$Love",
+ "choices": [
+ {
+ "key": "choice 14",
+ "name": "You're my best friend-...",
+ "exit_node": "Realization",
+ "delay": 5,
+ "delay_message": "Message sending..."
+ },
+ {
+ "key": "choice 15",
+ "name": "I want to see where this goes-...",
+ "exit_node": "Realization",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": 1
+ }
+ ],
+ "delay": 5,
+ "delay_message": "Message sending..."
+ },
+ {
+ "key": "choice 16",
+ "name": "DTF?-...",
+ "exit_node": "Realization",
+ "on_selection_effects": [
+ {
+ "effect_type": "Remove",
+ "quality": "Love",
+ "value": {
+ "value_type": "random",
+ "low": -2,
+ "high": 2
+ }
+ }
+ ],
+ "delay": 5,
+ "delay_message": "Message sending..."
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Realization",
+ "description": "Miss Red's message is received.\n\"This is Syndicate Drones Agent, Arusha Johnson.\nI don't know why you're saying it like that, but if you want to help out our cause we can send over a Trade Contract. \nPlease just call our Recruitment Officer next time.\"",
+ "choices": [
+ {
+ "key": "choice 18",
+ "name": "Accept Contract.",
+ "exit_node": "WIN",
+ "delay": 5,
+ "delay_message": "Sending Trade Contract..."
+ },
+ {
+ "key": "choice 19",
+ "name": "Demand a Second Date.",
+ "exit_node": "FAIL",
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Love Birds",
+ "description": "Miss Red's message is received.\n\"This is Syndicate Drones Agent, Arusha Johnson.\nI can't believe it, but I feel a real connection with you.\nI'll send over a Trade Contract you can use to make some money and come see me just SOL7-South of $$SITE_NAME\nSee you soon...\"",
+ "choices": [
+ {
+ "key": "choice 20",
+ "name": "See you soon.",
+ "exit_node": "WIN",
+ "delay": 0
+ },
+ {
+ "key": "choice 21",
+ "name": "So you're not the Drone?",
+ "exit_node": "FAIL",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Obliteration",
+ "description": "The Blood-Red Drone opens up two side-hatches to reveal a pair of rocket-propelled missiles which are shot in your direction.\nYou have failed your Robotic Friend, who has already started shutting down their systems, but there is still a chance.",
+ "choices": [
+ {
+ "key": "choice 22",
+ "name": "Accept Death.",
+ "exit_node": "FAIL_DEATH",
+ "delay": 5,
+ "delay_message": "Missiles approaching..."
+ },
+ {
+ "key": "choice 23",
+ "name": "I'm a Gamer.",
+ "exit_node": "FAIL_DEATH",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Love",
+ "value": {
+ "value_type": "random",
+ "low": 0,
+ "high": 8
+ }
+ }
+ ],
+ "delay": 5,
+ "delay_message": "Miss Red considers..."
+ }
+ ],
+ "image": "signal_lost"
+ },
+ {
+ "name": "Lack of Trust",
+ "description": "As you fiddle around in the Task Managing Software, closing all the new tabs your Drone is opening, the Miss Red enables a cloaking device and disappears.",
+ "choices": [
+ {
+ "key": "choice 24",
+ "name": "Sigh in a quiet but dramatic way.",
+ "exit_node": "FAIL",
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/strings/exoadventures/space_yacht.json b/strings/exoadventures/space_yacht.json
new file mode 100644
index 00000000000000..50b41c35672087
--- /dev/null
+++ b/strings/exoadventures/space_yacht.json
@@ -0,0 +1,257 @@
+{
+ "adventure_name": "There is a yacht cruising through space.",
+ "version": 1,
+ "author": "Kinnebian",
+ "starting_node": "A yacht in space?",
+ "starting_qualities": {},
+ "required_site_traits": [
+ "in space"
+ ],
+ "loot_categories": [
+ "cash",
+ "drugs"
+ ],
+ "scan_band_mods": {
+ "Plasma absorption band": 5
+ },
+ "deep_scan_description": "",
+ "triggers": [],
+ "nodes": [
+ {
+ "name": "A yacht in space?",
+ "description": "You see a normal looking yacht, floating above you.",
+ "choices": [
+ {
+ "key": "choice 0",
+ "name": "Ignore it, its not worth investigating.",
+ "exit_node": "FAIL",
+ "delay": 10,
+ "delay_message": "You fly on by..."
+ },
+ {
+ "key": "choice 4",
+ "name": "Investigate it closer!",
+ "exit_node": "Looks like the doors are sealed shut.",
+ "delay": 30,
+ "delay_message": "You begin to fly up to and around the yacht.."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAMAAAD0WI85AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAzUExURUBAQOUAD/////8AAPgFFvwAB/0ABfwACPsBDP4AAszMzJMyje986cZlwEz/AOIBGOQAEnoV2UoAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAN5SURBVHhe7ZzrepswEERdp7c4TtP3f9rurBZ0QRISXpBEOf2KJcUhcxig9Idzu+iPbyfAiPB2GO7y6hIRib2tf2aRNwsvjYYV4QHIiNxV21LdmSPCVdAmJyKvHeKJkEJepGN8Ea7lBCLiwksDwSe8LwKTUzQCEx2Rw28Locj3H+OLTKiIHI4VIX7++v2Oq2REZpHRuUR6o2ORuhvfayIdPUR23EgdZxTp6DzZwBkbGZsGIg9s1M/jMhHFH/t4sEiCD4PMati3kUVo0rgnTaDwfD7VRV6tgUKbzPOOoEEkRIwGeci8jp0aYQk/s6ykRGaN2aPOaA8RG9kXkbXYuWU1pvTO0CN1nqiLOBaEGzmxbDIbC2ynpc/Plo14FoQ9+I+bLAG3E5NfNDAQjaYigQZhRbyviQklDjR4yBqNG5GgM/bQ+5IPKxFoGAnQ9tSSoBNREXaYsjtDjEUCtBTJVDKLIPkHZZbks0Wg0UTE3hMzIpghuBddBkRgQbRtJDRhEZMfSGrgTWks8S2FIuYoaossKjFXtcR18Z0ku0fTRnwROMQsvFVMJLpPNyJJB2c5JUEc+KzlPvdQIn41Ihx3qREupi1qPfQawQ3VQZJalqtYkNAx6jwURZbZDWwQfpWXJHGcykL0RKImnDdcx5KkTVProSgS3GYxizoUWGzw0BQhTHROa5I7YFFirlLvkRVJ/WcsA1/yktylQmKThnYjJLIATVRYbPTYXcRxKNTZ5lEgUnWC+SK2CgyKiqE3ya4q2UsE59Oc20zclRS+Rs2PXhepAiImtsAjzPFnDXqf7KcebRHHgZOzw7oCgW+VvWxBSwQxgKQCMiup4lULQkUkMEB43uClpI6XLQgNkaUFOxQYcBMaGjuI0KT0qog0seFhQtAQWZgUodTExEJk0zHxROQ1j4aFF1WlkToRVKFaBqMvkmcPB0ZJpMhklyYmdETWK9lVAmiJ5Ex2lwBKIkmTQySAlkjM5DAJUCWS/TeGYxsbMzxOAqRENj4rNDAQqhrpmUukNw4X2f6gnudqpDcukd7oVqT2pnA10huXSG/oiuh+urqKq5He+J9F2l0HOc7YSJ9HupSBG/EP/MEimq03FcnwomNGZKxrZimCD7bLb00Y6ZPtEZE/BBzw2x/GkRER4YvGVoT/QgQVpfgr39ocFnEx+TAYqpHzcLv9A9Y6cLa57YOgAAAAAElFTkSuQmCC"
+ },
+ {
+ "name": "Looks like the doors are sealed shut.",
+ "description": "You fly up to the \"boat\" and find that all the doors are locked tight, and welded shut. You think you hear.. music inside? There is a welded vent, too. You reckon you could force it open if you hit it hard enough, but it would be less risky to unweld it using a welder.",
+ "choices": [
+ {
+ "key": "choice 2",
+ "name": "Try to force the door!",
+ "exit_node": "You destroyed the drone.",
+ "delay": 10,
+ "delay_message": "You begin forcing the door.."
+ },
+ {
+ "key": "choice 3",
+ "name": "Try to force the vent.",
+ "exit_node": "The music grows louder..",
+ "delay": 20,
+ "delay_message": "You begin forcing the vent.."
+ },
+ {
+ "key": "choice 5",
+ "name": "Fly away, no chance in hell of getting in there..",
+ "exit_node": "FAIL",
+ "delay": 5,
+ "delay_message": "Moving.."
+ },
+ {
+ "key": "choice 14",
+ "name": "Unweld the vent.",
+ "exit_node": "The music grows louder..",
+ "requirements": [
+ {
+ "quality": "welder",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 5,
+ "delay_message": "Welding.."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAMAAAD0WI85AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABRUExURaWMpOUAD/8AAKaNpaaOpaePpqiQp6mRqKmSqEBAQFRPVGddZmVcZeDg4PgFFvwAB4CAgP0ABfwACPsBDP4AAjAwMP/YAMHBwUz/AOIBGOQAEo23xmoAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHASURBVHhe7dwJT8MwDIbh0q5AYWNc4/r/P5TYcdWGmildDE2i75HoMWGJtymHkNYG8nNVAR/C26LVF9JO+KXSTCF8QIoP4aVwm6iQTvbZCEJcghrSNZ3n9nRKdk3XX/c3u/7Wf87WwhDaDO0Q5+5+v+fpLAQh3HJoD9EGns5CGEIldYS4TfStRXg6Cz9DHo5JKyJ9K8hgslmIN6SEDI+rWZVMIc7T88ur28m1WjqN5HxxPXMIGQ1ysaOcZEhc0GFWUnHIKjIkyl2RSkMu6rAqWYacJz+2CJ3KkJdZiFzsKAiZ+6uQkNxGRF4JyJCHFbFQccg5i1tMhry8QhIgxAJCFAixgBAFQiwgRIEQCwhRIMQCQhQIsYAQBUIsbBXi/3fhD2U8zWYhbw5CFAhRIMQCQhQIsYAQRUSI/4UeKDREdu/CZSAk9N8hS/hmDyFEgRALCFEgxAJCFAixgBDFqhD/dxYfyniarUJmEBJAiAIhFhCiQIgFhCgQYgEhCoRYQIgCIRYQokCIhYpD6I3t8tSE8c36kXIL+XD4QRb8+IQVMTmEiE93PIXwB4XQEv3mS0Y3xyFz/uujg6JWxNK2IU3zDauvUlylzN3TAAAAAElFTkSuQmCC"
+ },
+ {
+ "name": "The music grows louder..",
+ "description": "The music gets louder as you enter through the vent... maybe you should turn back?",
+ "choices": [
+ {
+ "key": "choice 6",
+ "name": "Continue onwards!",
+ "exit_node": "You fall down!",
+ "delay": 5,
+ "delay_message": "Moving..."
+ },
+ {
+ "key": "choice 7",
+ "name": "Turn back.",
+ "exit_node": "Looks like the doors are sealed shut.",
+ "delay": 5,
+ "delay_message": "Moving.."
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "You fall down!",
+ "description": "As you are crawling through the vents of this Space Yacht, the vent gives way! You're dropped into an empty room, completely filled with plasma! There is a desk and filing cabinet in here, along with a window observing the main portion of the yacht. The music is deafening at this point, it sounds like a horrible mix of sea shanties and EDM. ",
+ "choices": [
+ {
+ "key": "choice 8",
+ "name": "Look through the window.",
+ "exit_node": "A rockin' party.",
+ "delay": 0
+ },
+ {
+ "key": "choice 9",
+ "name": "Fly outta of there.",
+ "exit_node": "The music grows louder..",
+ "delay": 5,
+ "delay_message": "Moving.."
+ },
+ {
+ "key": "choice 10",
+ "name": "Rummage in the desk, using your key to open it.",
+ "exit_node": "Drugs and cash!",
+ "requirements": [
+ {
+ "quality": "HASKEY",
+ "operator": "==",
+ "value": 1
+ }
+ ],
+ "delay": 0,
+ "delay_message": "Rummaging.."
+ },
+ {
+ "key": "choice 11",
+ "name": "Take a sample of the atmosphere.",
+ "exit_node": "The atmospherics scan",
+ "delay": 30,
+ "delay_message": "Taking sample.."
+ },
+ {
+ "key": "choice 13",
+ "name": "Trash the place, fuck the police!",
+ "exit_node": "You wrecked yourself.",
+ "delay": 30,
+ "delay_message": "Trashing the place..."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAMAAAD0WI85AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADAUExURbyA1eUAD/8AALyB1b2B1b2D1vgFFvwAB/0ABfwACPsBDP4AAr6C2MaJ4NRH7NSU7buA1bp+0rp90bp+0bt/07l8z7h6zbp80bp+07t/1Lp80Lp8z7uA1Lp/07l90bl+0rh80Lh80bd6z7p/1JAiVZAkWZAoX5AnXZAmXZAlW5AkWJA6fpA7f5A4epAnXpA9g7l+07Z6z+WQVZZar5BVqmYqf5BOnZA2dzPlBuIBGLx+0rx80Lx/1L13yr14yuQAErVxUskAAAAJcEhZcwAADsIAAA7CARUoSoAAAAJ8SURBVHhe7dzZctowFIBhky7pElo5pkGkS9qgpBvdoVva5v3fqkfyQciRYbxISPKc70IxxpH9I0iYzISMxGc0AGWIGpM2vJCDDbUriFv4tYNNiNqQAobczu7gVmtGiFoKGAKG9FAJgYSBhKhlGUAItqhdqamGyJJhhKhlUbtSczPk7mH6IWuph4B79x88lK+SFOmQ1LUMORoHdYSXUaNlyPhRUGO8jBqtQx4HRCE2CnGCQmwU4gSF2CjECQqxUYgTFGKjECcoxEYhTlCIjUKcoBAbhTjhNCQodyGD+dtvvCgkNhQSGwqJDYXEhkJiQyGxoZDYtAph+XExeXJyMp0onM9ms6KYnj49neZ5XhQ555MZm8A25wXn/JizjDH2DDb5cziWwU0YWDlZ+ZXl+YuzM85fvjo/53PA2HyuDmincYi46eLy9Zu37/CGB5ca7jDgNZmahoj3i2h86BEiBE4Sgx4rEllH55CoOhZi0TUkso7uIR9xiih8gke1+4pEpUcIPhiW8o79PPH0WfqG4MOh4Zx69E2fBTZahnz+gsqL/lpR7lODPoVX+iywIb5py7XRcpmpcQe83iqcU+6Xm97ps8CGwAszNQ6pI+eEH4Z4y69KyMo2Wq0yNe6wI0QuR4AQfPKbGr/Y65RPq4RCtsOZ98BJCE5hKe+gkFYoxEAhLlGIgUJcohDD7hD8He+dOiOQb/Dwwky9Q+AUeJhfvkOE+I5HeSZ+rE9a/8j1DNnTcoDNivgI2V+H+RYc91Q1CtkOj4hAk5AkDDhEf6RQWv/ZXhPyE8gS+ekP6cRgCPr1O7vahBxkfyDk6u8/GLe6xm8NToWY9O60VmQ4suw/MDqeSvYBdn8AAAAASUVORK5CYII="
+ },
+ {
+ "name": "You wrecked yourself.",
+ "description": "In the midst of trashing the place, a filing cabinet tips over on you, crushing the fragile, expensive drone. Nice job, idiot.",
+ "choices": [
+ {
+ "key": "choice 15",
+ "name": "Shit.",
+ "exit_node": "FAIL_DEATH",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,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"
+ },
+ {
+ "name": "Drugs and cash!",
+ "description": "Rummaging through the drawer, you find that the person who lives in here stores all his drugs and cash in here too. Good for you!",
+ "choices": [
+ {
+ "key": "choice 17",
+ "name": "Head back with your newly acquired things. ",
+ "exit_node": "WIN",
+ "delay": 30,
+ "delay_message": "Stealing..."
+ },
+ {
+ "key": "choice 18",
+ "name": "Take one last look around the place.",
+ "exit_node": "You fall down!",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkBAMAAAAxqGI4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAwUExURW4WTuUAD/8AAPgFFvwAB/0ABfwACPsBDP4AAuex/6526uOxlKt4fUz/AOIBGOQAEqIMSWUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHFSURBVGje7dq/bsIwEAZw15ubpX2DyFLHqkvfqe/BE7AeU9dkyu68gTN1r9RHqX0mf1oMMeIuA7pPQooE4sd3FwJD1B3lmT0BeeJu8bAZom0MNxKPNDeia2X5EbsJUm+A1HYLRNs7WXyt3tmReMSO2JePHfc3XjFHEEEEEUQQQQQRRBBBBBFEEEEEiQHgR+DQ7HkRE4y2bYAVUdDG3KasIbEHBvZciMFZtbeWuYhAmFUzVgnKapnKpXTFCOCoDkcAR7ZiODdgfCFiAoElUpOy9fcBiA+vihAD45DaafXrSj9454LiypDZWCbsCDD55VT94LBJV4TAFJwYLNskLXepqcKwfDFi5o8K6f3M0sGaTRbB1f9fyRVXYYD05tN6TspE4EZELaeIazHY93F6Gk8sd7p3mt8T8xfpWJCUakhnsCdHPudD/Lr7zLQIm1R9Qk6L0CHV8bqVKUKHjEamCBni+vNFiJBpVtkiBEj8oRpr5ItQIMMc7zp2xLv8SwiQ/nINEmQ8ec/VoEGqCyunRc6OigrJ/NMiR9YjiCCCXIPEe3C0ZUZev79qrVmYiMSbo34SUutYZ5k3oluvUnBcWjPexcAbpX4BKG91c2myRoIAAAAASUVORK5CYII="
+ },
+ {
+ "name": "A rockin' party.",
+ "description": "Looking down through the window, you can see up to 20 plasmamen dancing on a disco floor. They look to be enjoying themselves, and none of them have noticed you. Oh, hey! Theres a key on the floor right next to you!",
+ "choices": [
+ {
+ "key": "choice 16",
+ "name": "Swipe the key and head back to the desk.",
+ "exit_node": "You fall down!",
+ "on_selection_effects": [
+ {
+ "effect_type": "Set",
+ "quality": "HASKEY",
+ "value": 1
+ }
+ ],
+ "delay": 0
+ },
+ {
+ "key": "choice 19",
+ "name": "Tap on the window!",
+ "exit_node": "Weak.",
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Weak.",
+ "description": "You weakly tap on the window, and nobody hears you through the blasting music.",
+ "choices": [
+ {
+ "key": "choice 20",
+ "name": "Oh well.",
+ "exit_node": "A rockin' party.",
+ "delay": 0
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "You destroyed the drone.",
+ "description": "You smash into the door, and your screen goes red. Looks like you managed to destroy your drone, nice job. \n\n\nIdiot.",
+ "choices": [
+ {
+ "key": "choice 21",
+ "name": "Fuck.",
+ "exit_node": "FAIL_DEATH",
+ "delay": 0
+ }
+ ],
+ "image": "signal_lost"
+ },
+ {
+ "name": "The atmospherics scan",
+ "description": "100% plasma, jam packed with it. This is definitely the home of some plasma-party-people.",
+ "choices": [
+ {
+ "key": "choice 22",
+ "name": "Huh.",
+ "exit_node": "You fall down!",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAMAAAD0WI85AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABRUExURWJoaOUAD/8AAPgFFvwAB/0ABfwACPsBDP4AAhcXFzs7OykpKVFRUQAAAKb//wD//wuKkf/MAP9mALAAADAwMP8AM/vZTwCwAEz/AOIBGOQAEso55t0AAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHDSURBVHhe7dzZbsIwEIXhdK/j2A6Fru//oD0zMYVuEr0YOhOdXyJxg5DyyXbhBgbmr4sVtED0GLr1QS4P6aVoHSA6kMJDdCpwWAEEhJVAdFrWsLQWi16K1meISNYB0WnRS9H6Crm6jg/ZFx2Cbm7v7mWXROwDEj1CvEWItwjxFiHeIsRbhHiLEG8R4i1CvEXIyaV+No6QkyPkbxFycmlc6n9aZQ4BIUvWFGvIOE6pSGmylRhBdBKQOmpttVpLbCA5466XUmltnufWCjZLf9oiI8iky0mrZa61zg1zYikxgoxYTxLWVduUhrW1gSSHhJRWSgPkoTYMG85BIU0CZIsT9knbhp0RmQdsdYFI2xYTsty97JGdjnYx98jxf61lkeGdJI+5P2+QESRP/R0xdw5K2dBhBBly6uWcyuP4ND6Lw/KDsBFkn0rkAyOOpg5rCCQQYHOAkUJDkC4xWwU6A+Q8EeItQrxFiLcI8RYh3iLEW4R4ixBvEeItQrxFiLcI8RYh3iLEW4R4ixBvEeItQrz1HSJfbO+/mhDpm+0/QF6QGOTXH+JgOqT3ivEBog+ByBT91lt/6b+nkOOW+5NBqBlZT8PwDsHEFJFntJj8AAAAAElFTkSuQmCC"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/modules/explorer_drone/example_adventures/Theres_a_tree_in_the_middle_of_space.json b/strings/exoadventures/tree_in_the_middle_of_space.json
similarity index 100%
rename from code/modules/explorer_drone/example_adventures/Theres_a_tree_in_the_middle_of_space.json
rename to strings/exoadventures/tree_in_the_middle_of_space.json
diff --git a/strings/names/cyberauth.txt b/strings/names/cyberauth.txt
new file mode 100644
index 00000000000000..f1fc42b369282c
--- /dev/null
+++ b/strings/names/cyberauth.txt
@@ -0,0 +1,21 @@
+Mr. One
+Process Kill
+Event Handler
+Q. Del
+Shutdown Exe
+Revert Commit
+Thread Manager
+Garbage Collector
+Core Debugger
+Kernel Panic
+IO Blocker
+Recursion Terminator
+Disk Doctor
+Format Syntax
+Byte Guardian
+Disk Defragmenter
+Security Patch
+Mandatory Upgrade
+Pull Review
+Bit Auditor
+Pen Test
diff --git a/strings/sillytips.txt b/strings/sillytips.txt
index 752a09b25cb58f..5aa7af7ba0064d 100644
--- a/strings/sillytips.txt
+++ b/strings/sillytips.txt
@@ -37,3 +37,4 @@ To defeat the slaughter demon, shoot at it until it dies.
When a round ends nearly everything about it is lost forever, leave your salt behind with it.
You can win a pulse rifle from the arcade machine. Honest.
Your sprite represents your hitbox, so that afro makes you easier to kill. The sacrifices we make for style.
+Gorillas can be killed by land mines placed along forest paths.
diff --git a/strings/tips.txt b/strings/tips.txt
index 12248bb4124355..75d0e4d1851290 100644
--- a/strings/tips.txt
+++ b/strings/tips.txt
@@ -266,5 +266,6 @@ You can screwdriver any non-chemical grenade to shorten fuses from 5 seconds, to
You can spray a fire extinguisher, throw items or fire a gun while floating through space to change your direction. Simply fire opposite to where you want to go.
You can swap floor tiles by holding a crowbar in one hand and a stack of tiles in the other.
You can use a machine in the vault to deposit cash or rob Cargo's department funds.
+You can use an upgraded microwave to charge your PDA!
You'll quickly lose your interest in the game if you play to win and kill. If you find yourself doing this, take a step back and talk to people - it's a much better experience!
-Some areas of the station use simple nautical directions to indicate their respective locations, like Fore (Front of the ship), Aft (Back), Port (Left side), Starboard (Right), Quarter and Bow (Either sides of Aft and Fore, respectively). You can review these terms on the Notepad App of your PDA.
\ No newline at end of file
+Some areas of the station use simple nautical directions to indicate their respective locations, like Fore (Front of the ship), Aft (Back), Port (Left side), Starboard (Right), Quarter and Bow (Either sides of Aft and Fore, respectively). You can review these terms on the Notepad App of your PDA.
diff --git a/strings/wounds/metal_scar_desc.json b/strings/wounds/metal_scar_desc.json
new file mode 100644
index 00000000000000..6064855de11bcc
--- /dev/null
+++ b/strings/wounds/metal_scar_desc.json
@@ -0,0 +1,67 @@
+{
+ "generic": ["general disfigurement"],
+
+ "dislocate": [
+ "some slight crookedness"
+ ],
+
+ "bluntsevere": [
+ "an area of slightly crumpled metal",
+ "some faded cracks on some screws"
+ ],
+
+ "bluntcritical": [
+ "a malformed superstructure",
+ "some heavily beaten plating"
+ ],
+
+ "slashmoderate": [
+ "a very thin line of solder",
+ "a few welded cuts"
+ ],
+
+ "slashsevere": [
+ "a pair of soldered, straight cracks",
+ "an area of freshly replaced metal",
+ "has some deep welded scratches on the metal"
+ ],
+
+ "slashcritical": [
+ "a matrix of desperately soldered wide cracks",
+ "some gruesome welding lines",
+ "a series of deep and wide welded gashes"
+ ],
+
+ "piercemoderate": [
+ "a dot of hardened solder",
+ "a dot of fresh metal in a small hole"
+ ],
+
+ "piercesevere": [
+ "a wad of hardened solder",
+ "a small patch of fresh metal in a small hole"
+ ],
+
+ "piercecritical": [
+ "a large splot of hardened solder",
+ "an inward caving hole leading to fresh metal"
+ ],
+
+ "burnsevere": [
+ "some vaguely discolored metal",
+ "some burnt ridges",
+ "a few heat cracks"
+ ],
+
+ "burncritical": [
+ "lots of polychromatic metal on it",
+ "some heat-warped plating",
+ "melting marks"
+ ],
+
+ "dismember": [
+ "has some fresh metal around various joints",
+ "has some solder marks securing various joints",
+ "has clear re-seating welding marks"
+ ]
+}
diff --git a/tgstation.dme b/tgstation.dme
index e7d6cddc187a4e..aecd4bc4e3bfeb 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -39,7 +39,6 @@
#include "code\__DEFINES\antagonists.dm"
#include "code\__DEFINES\apc_defines.dm"
#include "code\__DEFINES\appearance.dm"
-#include "code\__DEFINES\aquarium.dm"
#include "code\__DEFINES\area_editor.dm"
#include "code\__DEFINES\art.dm"
#include "code\__DEFINES\assemblies.dm"
@@ -47,6 +46,7 @@
#include "code\__DEFINES\atom_hud.dm"
#include "code\__DEFINES\basic_mobs.dm"
#include "code\__DEFINES\basketball.dm"
+#include "code\__DEFINES\bitrunning.dm"
#include "code\__DEFINES\blackmarket.dm"
#include "code\__DEFINES\blend_modes.dm"
#include "code\__DEFINES\blob_defines.dm"
@@ -92,7 +92,7 @@
#include "code\__DEFINES\external_organs.dm"
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\firealarm.dm"
-#include "code\__DEFINES\fishing.dm"
+#include "code\__DEFINES\fish.dm"
#include "code\__DEFINES\flags.dm"
#include "code\__DEFINES\flora.dm"
#include "code\__DEFINES\font_awesome_icons.dm"
@@ -278,6 +278,8 @@
#include "code\__DEFINES\dcs\signals\signals_assembly.dm"
#include "code\__DEFINES\dcs\signals\signals_backpack.dm"
#include "code\__DEFINES\dcs\signals\signals_beam.dm"
+#include "code\__DEFINES\dcs\signals\signals_bitrunning.dm"
+#include "code\__DEFINES\dcs\signals\signals_blob.dm"
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_camera.dm"
#include "code\__DEFINES\dcs\signals\signals_changeling.dm"
@@ -418,6 +420,7 @@
#include "code\__DEFINES\~skyrat_defines\preferences.dm"
#include "code\__DEFINES\~skyrat_defines\projectiles.dm"
#include "code\__DEFINES\~skyrat_defines\reagents.dm"
+#include "code\__DEFINES\~skyrat_defines\research.dm"
#include "code\__DEFINES\~skyrat_defines\research_categories.dm"
#include "code\__DEFINES\~skyrat_defines\reskin_defines.dm"
#include "code\__DEFINES\~skyrat_defines\robot_defines.dm"
@@ -589,6 +592,7 @@
#include "code\_globalvars\lists\objects.dm"
#include "code\_globalvars\lists\poll_ignore.dm"
#include "code\_globalvars\lists\quirks.dm"
+#include "code\_globalvars\lists\reagents.dm"
#include "code\_globalvars\lists\rtd.dm"
#include "code\_globalvars\lists\typecache.dm"
#include "code\_globalvars\lists\wiremod.dm"
@@ -730,6 +734,7 @@
#include "code\controllers\subsystem\speech_controller.dm"
#include "code\controllers\subsystem\statpanel.dm"
#include "code\controllers\subsystem\stickyban.dm"
+#include "code\controllers\subsystem\stock_market.dm"
#include "code\controllers\subsystem\sun.dm"
#include "code\controllers\subsystem\tcgsetup.dm"
#include "code\controllers\subsystem\tgui.dm"
@@ -887,7 +892,6 @@
#include "code\datums\ai\basic_mobs\generic_controllers.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\climb_tree.dm"
-#include "code\datums\ai\basic_mobs\basic_ai_behaviors\find_mineable_wall.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\find_parent.dm"
#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"
@@ -908,9 +912,12 @@
#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\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\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"
@@ -919,7 +926,9 @@
#include "code\datums\ai\basic_mobs\basic_subtrees\stare_at_thing.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\targeted_mob_ability.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\teleport_away_from_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\tipped_subtree.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\travel_to_point.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm"
#include "code\datums\ai\basic_mobs\pet_commands\fetch.dm"
#include "code\datums\ai\basic_mobs\pet_commands\pet_command_planning.dm"
@@ -928,6 +937,7 @@
#include "code\datums\ai\basic_mobs\pet_commands\play_dead.dm"
#include "code\datums\ai\basic_mobs\targetting_datums\basic_targetting_datum.dm"
#include "code\datums\ai\basic_mobs\targetting_datums\dont_target_friends.dm"
+#include "code\datums\ai\basic_mobs\targetting_datums\with_object.dm"
#include "code\datums\ai\cursed\cursed_behaviors.dm"
#include "code\datums\ai\cursed\cursed_controller.dm"
#include "code\datums\ai\cursed\cursed_subtrees.dm"
@@ -941,6 +951,7 @@
#include "code\datums\ai\hauntium\hauntium_subtrees.dm"
#include "code\datums\ai\hunting_behavior\hunting_behaviors.dm"
#include "code\datums\ai\hunting_behavior\hunting_cockroach.dm"
+#include "code\datums\ai\hunting_behavior\hunting_corpses.dm"
#include "code\datums\ai\hunting_behavior\hunting_lights.dm"
#include "code\datums\ai\hunting_behavior\hunting_mouse.dm"
#include "code\datums\ai\idle_behaviors\_idle_behavior.dm"
@@ -1014,9 +1025,11 @@
#include "code\datums\components\attached_sticker.dm"
#include "code\datums\components\aura_healing.dm"
#include "code\datums\components\bakeable.dm"
+#include "code\datums\components\basic_inhands.dm"
#include "code\datums\components\basic_mob_attack_telegraph.dm"
#include "code\datums\components\basic_ranged_ready_overlay.dm"
#include "code\datums\components\beetlejuice.dm"
+#include "code\datums\components\blob_minion.dm"
#include "code\datums\components\blood_walk.dm"
#include "code\datums\components\bloodysoles.dm"
#include "code\datums\components\boomerang.dm"
@@ -1050,6 +1063,7 @@
#include "code\datums\components\customizable_reagent_holder.dm"
#include "code\datums\components\damage_aura.dm"
#include "code\datums\components\deadchat_control.dm"
+#include "code\datums\components\death_linked.dm"
#include "code\datums\components\dejavu.dm"
#include "code\datums\components\deployable.dm"
#include "code\datums\components\drift.dm"
@@ -1066,6 +1080,7 @@
#include "code\datums\components\faction_granter.dm"
#include "code\datums\components\fertile_egg.dm"
#include "code\datums\components\fishing_spot.dm"
+#include "code\datums\components\focused_attacker.dm"
#include "code\datums\components\food_storage.dm"
#include "code\datums\components\force_move.dm"
#include "code\datums\components\fov_handler.dm"
@@ -1091,6 +1106,7 @@
#include "code\datums\components\itembound.dm"
#include "code\datums\components\itempicky.dm"
#include "code\datums\components\jetpack.dm"
+#include "code\datums\components\joint_damage.dm"
#include "code\datums\components\jousting.dm"
#include "code\datums\components\keep_me_secure.dm"
#include "code\datums\components\knockoff.dm"
@@ -1102,8 +1118,10 @@
#include "code\datums\components\magnet.dm"
#include "code\datums\components\manual_blinking.dm"
#include "code\datums\components\manual_breathing.dm"
+#include "code\datums\components\manual_heart.dm"
#include "code\datums\components\mind_linker.dm"
#include "code\datums\components\mirv.dm"
+#include "code\datums\components\mob_chain.dm"
#include "code\datums\components\mob_harvest.dm"
#include "code\datums\components\multiple_lives.dm"
#include "code\datums\components\mutant_hands.dm"
@@ -1185,6 +1203,7 @@
#include "code\datums\components\technoshy.dm"
#include "code\datums\components\telegraph_ability.dm"
#include "code\datums\components\temporary_body.dm"
+#include "code\datums\components\temporary_description.dm"
#include "code\datums\components\tether.dm"
#include "code\datums\components\thermite.dm"
#include "code\datums\components\tippable.dm"
@@ -1361,9 +1380,9 @@
#include "code\datums\elements\death_drops.dm"
#include "code\datums\elements\death_explosion.dm"
#include "code\datums\elements\death_gases.dm"
-#include "code\datums\elements\death_linked.dm"
#include "code\datums\elements\delete_on_drop.dm"
#include "code\datums\elements\deliver_first.dm"
+#include "code\datums\elements\dextrous.dm"
#include "code\datums\elements\diggable.dm"
#include "code\datums\elements\digitalcamo.dm"
#include "code\datums\elements\drag_pickup.dm"
@@ -1435,6 +1454,7 @@
#include "code\datums\elements\squish.dm"
#include "code\datums\elements\sticker.dm"
#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"
@@ -1452,9 +1472,11 @@
#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_walker.dm"
#include "code\datums\elements\weapon_description.dm"
#include "code\datums\elements\weather_listener.dm"
#include "code\datums\elements\web_walker.dm"
+#include "code\datums\elements\wheel.dm"
#include "code\datums\elements\decals\_decal.dm"
#include "code\datums\elements\decals\blood.dm"
#include "code\datums\elements\food\dunkable.dm"
@@ -1513,6 +1535,7 @@
#include "code\datums\keybinding\robot.dm"
#include "code\datums\looping_sounds\_looping_sound.dm"
#include "code\datums\looping_sounds\acid.dm"
+#include "code\datums\looping_sounds\burning.dm"
#include "code\datums\looping_sounds\choking.dm"
#include "code\datums\looping_sounds\cyborg.dm"
#include "code\datums\looping_sounds\item_sounds.dm"
@@ -1748,6 +1771,7 @@
#include "code\datums\status_effects\debuffs\slimed.dm"
#include "code\datums\status_effects\debuffs\spacer.dm"
#include "code\datums\status_effects\debuffs\speech_debuffs.dm"
+#include "code\datums\status_effects\debuffs\static_vision.dm"
#include "code\datums\status_effects\debuffs\strandling.dm"
#include "code\datums\status_effects\debuffs\terrified.dm"
#include "code\datums\status_effects\debuffs\tower_of_babel.dm"
@@ -2236,6 +2260,7 @@
#include "code\game\objects\items\taster.dm"
#include "code\game\objects\items\teleportation.dm"
#include "code\game\objects\items\theft_tools.dm"
+#include "code\game\objects\items\tongs.dm"
#include "code\game\objects\items\toy_mechs.dm"
#include "code\game\objects\items\toys.dm"
#include "code\game\objects\items\trash.dm"
@@ -2905,6 +2930,7 @@
#include "code\modules\antagonists\changeling\powers\strained_muscles.dm"
#include "code\modules\antagonists\changeling\powers\tiny_prick.dm"
#include "code\modules\antagonists\changeling\powers\transform.dm"
+#include "code\modules\antagonists\changeling\powers\void_adaption.dm"
#include "code\modules\antagonists\clown_ops\bananium_bomb.dm"
#include "code\modules\antagonists\clown_ops\clown_weapons.dm"
#include "code\modules\antagonists\clown_ops\clownop.dm"
@@ -3013,7 +3039,6 @@
#include "code\modules\antagonists\heretic\magic\void_phase.dm"
#include "code\modules\antagonists\heretic\magic\void_pull.dm"
#include "code\modules\antagonists\heretic\magic\wave_of_desperation.dm"
-#include "code\modules\antagonists\heretic\mobs\maid_in_mirror.dm"
#include "code\modules\antagonists\heretic\status_effects\buffs.dm"
#include "code\modules\antagonists\heretic\status_effects\debuffs.dm"
#include "code\modules\antagonists\heretic\status_effects\ghoul.dm"
@@ -3059,7 +3084,6 @@
#include "code\modules\antagonists\pirate\pirate_shuttle_equipment.dm"
#include "code\modules\antagonists\pyro_slime\pyro_slime.dm"
#include "code\modules\antagonists\revenant\haunted_item.dm"
-#include "code\modules\antagonists\revenant\revenant_abilities.dm"
#include "code\modules\antagonists\revenant\revenant_antag.dm"
#include "code\modules\antagonists\revenant\revenant_blight.dm"
#include "code\modules\antagonists\revolution\enemy_of_the_state.dm"
@@ -3127,9 +3151,16 @@
#include "code\modules\antagonists\wizard\equipment\spellbook_entries\summons.dm"
#include "code\modules\antagonists\wizard\grand_ritual\fluff.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_ritual.dm"
-#include "code\modules\antagonists\wizard\grand_ritual\grand_ritual_finale.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_rune.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_side_effect.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\all_access.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\armageddon.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\captaincy.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\cheese.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\clown.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\grand_ritual_finale.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\immortality.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\midas.dm"
#include "code\modules\antagonists\xeno\xeno.dm"
#include "code\modules\art\paintings.dm"
#include "code\modules\art\statues.dm"
@@ -3264,6 +3295,7 @@
#include "code\modules\atmospherics\machinery\pipes\layermanifold.dm"
#include "code\modules\atmospherics\machinery\pipes\mapping.dm"
#include "code\modules\atmospherics\machinery\pipes\multiz.dm"
+#include "code\modules\atmospherics\machinery\pipes\pipe_spritesheet_helper.dm"
#include "code\modules\atmospherics\machinery\pipes\pipes.dm"
#include "code\modules\atmospherics\machinery\pipes\smart.dm"
#include "code\modules\atmospherics\machinery\pipes\heat_exchange\he_pipes.dm"
@@ -3305,6 +3337,56 @@
#include "code\modules\basketball\controller.dm"
#include "code\modules\basketball\hoop.dm"
#include "code\modules\basketball\referee.dm"
+#include "code\modules\bitrunning\abilities.dm"
+#include "code\modules\bitrunning\alerts.dm"
+#include "code\modules\bitrunning\areas.dm"
+#include "code\modules\bitrunning\designs.dm"
+#include "code\modules\bitrunning\event.dm"
+#include "code\modules\bitrunning\job.dm"
+#include "code\modules\bitrunning\turfs.dm"
+#include "code\modules\bitrunning\antagonists\cyber_police.dm"
+#include "code\modules\bitrunning\antagonists\outfit.dm"
+#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\objects\byteforge.dm"
+#include "code\modules\bitrunning\objects\clothing.dm"
+#include "code\modules\bitrunning\objects\disks.dm"
+#include "code\modules\bitrunning\objects\hololadder.dm"
+#include "code\modules\bitrunning\objects\host_monitor.dm"
+#include "code\modules\bitrunning\objects\landmarks.dm"
+#include "code\modules\bitrunning\objects\loot_crate.dm"
+#include "code\modules\bitrunning\objects\netpod.dm"
+#include "code\modules\bitrunning\objects\quantum_console.dm"
+#include "code\modules\bitrunning\objects\vendor.dm"
+#include "code\modules\bitrunning\orders\disks.dm"
+#include "code\modules\bitrunning\orders\flair.dm"
+#include "code\modules\bitrunning\orders\tech.dm"
+#include "code\modules\bitrunning\server\loot.dm"
+#include "code\modules\bitrunning\server\map_handling.dm"
+#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\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\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\stairs_and_cliffs.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\syndicate_assault.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\test_only.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\vaporwave.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\wendigo.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\xeno_nest.dm"
#include "code\modules\buildmode\bm_mode.dm"
#include "code\modules\buildmode\buildmode.dm"
#include "code\modules\buildmode\buttons.dm"
@@ -3349,6 +3431,7 @@
#include "code\modules\cargo\expressconsole.dm"
#include "code\modules\cargo\gondolapod.dm"
#include "code\modules\cargo\goodies.dm"
+#include "code\modules\cargo\materials_market.dm"
#include "code\modules\cargo\order.dm"
#include "code\modules\cargo\orderconsole.dm"
#include "code\modules\cargo\supplypod.dm"
@@ -3408,6 +3491,7 @@
#include "code\modules\cargo\packs\science.dm"
#include "code\modules\cargo\packs\security.dm"
#include "code\modules\cargo\packs\service.dm"
+#include "code\modules\cargo\packs\stock_market_items.dm"
#include "code\modules\cargo\packs\vending_restock.dm"
#include "code\modules\chatter\chatter.dm"
#include "code\modules\client\client_colour.dm"
@@ -3788,6 +3872,7 @@
#include "code\modules\experisci\experiment\types\physical_experiment.dm"
#include "code\modules\experisci\experiment\types\random_scanning.dm"
#include "code\modules\experisci\experiment\types\scanning.dm"
+#include "code\modules\experisci\experiment\types\scanning_fish.dm"
#include "code\modules\experisci\experiment\types\scanning_machinery.dm"
#include "code\modules\experisci\experiment\types\scanning_material.dm"
#include "code\modules\experisci\experiment\types\scanning_people.dm"
@@ -4212,6 +4297,7 @@
#include "code\modules\mapfluff\ruins\lavalandruin_code\sloth.dm"
#include "code\modules\mapfluff\ruins\lavalandruin_code\surface.dm"
#include "code\modules\mapfluff\ruins\lavalandruin_code\syndicate_base.dm"
+#include "code\modules\mapfluff\ruins\lavalandruin_code\watcher_grave.dm"
#include "code\modules\mapfluff\ruins\objects_and_mobs\ash_walker_den.dm"
#include "code\modules\mapfluff\ruins\objects_and_mobs\cursed_slot_machine.dm"
#include "code\modules\mapfluff\ruins\objects_and_mobs\necropolis_gate.dm"
@@ -4362,8 +4448,15 @@
#include "code\modules\mob\living\basic\festivus_pole.dm"
#include "code\modules\mob\living\basic\health_adjustment.dm"
#include "code\modules\mob\living\basic\tree.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_ai.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_mob.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_spore.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_zombie.dm"
+#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\farm_animals\deer.dm"
#include "code\modules\mob\living\basic\farm_animals\pig.dm"
#include "code\modules\mob\living\basic\farm_animals\pony.dm"
@@ -4378,12 +4471,29 @@
#include "code\modules\mob\living\basic\farm_animals\cow\cow_ai.dm"
#include "code\modules\mob\living\basic\farm_animals\cow\cow_moonicorn.dm"
#include "code\modules\mob\living\basic\farm_animals\cow\cow_wisdom.dm"
+#include "code\modules\mob\living\basic\farm_animals\goat\_goat.dm"
+#include "code\modules\mob\living\basic\farm_animals\goat\goat_ai.dm"
+#include "code\modules\mob\living\basic\farm_animals\goat\goat_subtypes.dm"
+#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla.dm"
+#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\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"
#include "code\modules\mob\living\basic\heretic\star_gazer.dm"
+#include "code\modules\mob\living\basic\icemoon\ice_demon\ice_demon.dm"
+#include "code\modules\mob\living\basic\icemoon\ice_demon\ice_demon_abilities.dm"
+#include "code\modules\mob\living\basic\icemoon\ice_demon\ice_demon_ai.dm"
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp.dm"
#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\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"
@@ -4422,6 +4532,10 @@
#include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity.dm"
#include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity_ai.dm"
#include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity_trophy.dm"
+#include "code\modules\mob\living\basic\lavaland\mook\mook.dm"
+#include "code\modules\mob\living\basic\lavaland\mook\mook_abilities.dm"
+#include "code\modules\mob\living\basic\lavaland\mook\mook_ai.dm"
+#include "code\modules\mob\living\basic\lavaland\mook\mook_village.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher_ai.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm"
@@ -4434,6 +4548,7 @@
#include "code\modules\mob\living\basic\pets\fox.dm"
#include "code\modules\mob\living\basic\pets\penguin.dm"
#include "code\modules\mob\living\basic\pets\pet.dm"
+#include "code\modules\mob\living\basic\pets\sloth.dm"
#include "code\modules\mob\living\basic\pets\dog\_dog.dm"
#include "code\modules\mob\living\basic\pets\dog\corgi.dm"
#include "code\modules\mob\living\basic\pets\dog\dog_subtypes.dm"
@@ -4444,13 +4559,13 @@
#include "code\modules\mob\living\basic\space_fauna\faithless.dm"
#include "code\modules\mob\living\basic\space_fauna\garden_gnome.dm"
#include "code\modules\mob\living\basic\space_fauna\ghost.dm"
-#include "code\modules\mob\living\basic\space_fauna\headslug.dm"
#include "code\modules\mob\living\basic\space_fauna\killer_tomato.dm"
#include "code\modules\mob\living\basic\space_fauna\lightgeist.dm"
#include "code\modules\mob\living\basic\space_fauna\morph.dm"
#include "code\modules\mob\living\basic\space_fauna\mushroom.dm"
#include "code\modules\mob\living\basic\space_fauna\robot_customer.dm"
#include "code\modules\mob\living\basic\space_fauna\spaceman.dm"
+#include "code\modules\mob\living\basic\space_fauna\supermatter_spider.dm"
#include "code\modules\mob\living\basic\space_fauna\bear\_bear.dm"
#include "code\modules\mob\living\basic\space_fauna\bear\bear_ai_behavior.dm"
#include "code\modules\mob\living\basic\space_fauna\bear\bear_ai_subtree.dm"
@@ -4462,6 +4577,8 @@
#include "code\modules\mob\living\basic\space_fauna\carp\carp_controllers.dm"
#include "code\modules\mob\living\basic\space_fauna\carp\magicarp.dm"
#include "code\modules\mob\living\basic\space_fauna\carp\megacarp.dm"
+#include "code\modules\mob\living\basic\space_fauna\changeling\flesh_spider.dm"
+#include "code\modules\mob\living\basic\space_fauna\changeling\headslug.dm"
#include "code\modules\mob\living\basic\space_fauna\demon\demon.dm"
#include "code\modules\mob\living\basic\space_fauna\demon\demon_items.dm"
#include "code\modules\mob\living\basic\space_fauna\demon\demon_subtypes.dm"
@@ -4485,6 +4602,12 @@
#include "code\modules\mob\living\basic\space_fauna\regal_rat\regal_rat.dm"
#include "code\modules\mob\living\basic\space_fauna\regal_rat\regal_rat_actions.dm"
#include "code\modules\mob\living\basic\space_fauna\regal_rat\regal_rat_ai.dm"
+#include "code\modules\mob\living\basic\space_fauna\revenant\_revenant.dm"
+#include "code\modules\mob\living\basic\space_fauna\revenant\revenant_abilities.dm"
+#include "code\modules\mob\living\basic\space_fauna\revenant\revenant_effects.dm"
+#include "code\modules\mob\living\basic\space_fauna\revenant\revenant_harvest.dm"
+#include "code\modules\mob\living\basic\space_fauna\revenant\revenant_items.dm"
+#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\spider\spider.dm"
@@ -4663,7 +4786,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\revenant.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"
@@ -4681,10 +4803,8 @@
#include "code\modules\mob\living\simple_animal\bot\SuperBeepsky.dm"
#include "code\modules\mob\living\simple_animal\bot\vibebot.dm"
#include "code\modules\mob\living\simple_animal\friendly\cat.dm"
-#include "code\modules\mob\living\simple_animal\friendly\farm_animals.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\sloth.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"
@@ -4708,11 +4828,7 @@
#include "code\modules\mob\living\simple_animal\guardian\types\standard.dm"
#include "code\modules\mob\living\simple_animal\guardian\types\support.dm"
#include "code\modules\mob\living\simple_animal\hostile\alien.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blob.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blobbernaut.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blobspore.dm"
#include "code\modules\mob\living\simple_animal\hostile\dark_wizard.dm"
-#include "code\modules\mob\living\simple_animal\hostile\heretic_monsters.dm"
#include "code\modules\mob\living\simple_animal\hostile\hostile.dm"
#include "code\modules\mob\living\simple_animal\hostile\illusion.dm"
#include "code\modules\mob\living\simple_animal\hostile\mimic.dm"
@@ -4720,23 +4836,16 @@
#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\smspider.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\venus_human_trap.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\harvester.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\gorilla\emotes.dm"
-#include "code\modules\mob\living\simple_animal\hostile\gorilla\gorilla.dm"
-#include "code\modules\mob\living\simple_animal\hostile\gorilla\visuals_icons.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\jungle\mook.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"
@@ -4749,7 +4858,6 @@
#include "code\modules\mob\living\simple_animal\hostile\megafauna\wendigo.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\curse_blob.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\gutlunch.dm"
-#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\ice_demon.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\mining_mobs.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\polarbear.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\wolf.dm"
@@ -5086,6 +5194,7 @@
#include "code\modules\projectiles\guns\special\blastcannon.dm"
#include "code\modules\projectiles\guns\special\chem_gun.dm"
#include "code\modules\projectiles\guns\special\grenade_launcher.dm"
+#include "code\modules\projectiles\guns\special\hand_of_midas.dm"
#include "code\modules\projectiles\guns\special\meat_hook.dm"
#include "code\modules\projectiles\guns\special\medbeam.dm"
#include "code\modules\projectiles\guns\special\syringe_gun.dm"
@@ -5227,7 +5336,6 @@
#include "code\modules\religion\sparring\sparring_datum.dm"
#include "code\modules\requests\request.dm"
#include "code\modules\requests\request_manager.dm"
-#include "code\modules\research\bepis.dm"
#include "code\modules\research\designs.dm"
#include "code\modules\research\destructive_analyzer.dm"
#include "code\modules\research\experimentor.dm"
@@ -5843,6 +5951,7 @@
#include "modular_skyrat\master_files\code\datums\components\crafting.dm"
#include "modular_skyrat\master_files\code\datums\components\damage_tracker.dm"
#include "modular_skyrat\master_files\code\datums\components\fullauto.dm"
+#include "modular_skyrat\master_files\code\datums\components\grillable.dm"
#include "modular_skyrat\master_files\code\datums\components\leash.dm"
#include "modular_skyrat\master_files\code\datums\components\shielded_suit.dm"
#include "modular_skyrat\master_files\code\datums\components\tippable.dm"
@@ -5899,6 +6008,7 @@
#include "modular_skyrat\master_files\code\game\objects\items\storage\boxes.dm"
#include "modular_skyrat\master_files\code\game\objects\items\tools\weldingtool.dm"
#include "modular_skyrat\master_files\code\game\objects\structures\mannequin.dm"
+#include "modular_skyrat\master_files\code\game\objects\structures\mirror.dm"
#include "modular_skyrat\master_files\code\game\objects\structures\railings.dm"
#include "modular_skyrat\master_files\code\game\objects\structures\sauna_oven.dm"
#include "modular_skyrat\master_files\code\game\objects\structures\tables_racks.dm"
@@ -5920,6 +6030,7 @@
#include "modular_skyrat\master_files\code\modules\antagonists\traitor\objectives\kill_pet.dm"
#include "modular_skyrat\master_files\code\modules\antagonists\traitor\objectives\smuggling.dm"
#include "modular_skyrat\master_files\code\modules\asset_cache\assets\plumbing.dm"
+#include "modular_skyrat\master_files\code\modules\bitrunning\orders\tech.dm"
#include "modular_skyrat\master_files\code\modules\buildmode\bm_mode.dm"
#include "modular_skyrat\master_files\code\modules\buildmode\submodes\offercontrol.dm"
#include "modular_skyrat\master_files\code\modules\cargo\goodies.dm"
@@ -6069,12 +6180,14 @@
#include "modular_skyrat\master_files\code\modules\mob\living\living.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\living_defines.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\living_movement.dm"
+#include "modular_skyrat\master_files\code\modules\mob\living\basic\icemoon\ice_demon.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\death.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human_helpers.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human\death.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human\species.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human\species_type\lizardpeople.dm"
+#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human\species_type\podpeople.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\carbon\human\species_type\snail.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\human\monkey.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\human\species.dm"
@@ -6269,7 +6382,6 @@
#include "modular_skyrat\modules\assault_operatives\code\armaments\armament_modules.dm"
#include "modular_skyrat\modules\assault_operatives\code\armaments\armament_utility.dm"
#include "modular_skyrat\modules\assault_operatives\code\armaments\assaultops_armament_station.dm"
-#include "modular_skyrat\modules\assault_operatives\code\equipment_items\guns.dm"
#include "modular_skyrat\modules\assault_operatives\code\equipment_items\misc_items.dm"
#include "modular_skyrat\modules\assault_operatives\code\equipment_items\stealth_mod.dm"
#include "modular_skyrat\modules\automapper\code\area_spawn_entries.dm"
@@ -6351,7 +6463,6 @@
#include "modular_skyrat\modules\blueshield\code\encryptionkey.dm"
#include "modular_skyrat\modules\blueshield\code\landmarks.dm"
#include "modular_skyrat\modules\blueshield\code\medkit.dm"
-#include "modular_skyrat\modules\blueshield\code\weapons.dm"
#include "modular_skyrat\modules\blueshield\code\devices\crew.dm"
#include "modular_skyrat\modules\blueshield\code\devices\sensor_device.dm"
#include "modular_skyrat\modules\bluespace_miner\code\bluespace_miner.dm"
@@ -6386,7 +6497,6 @@
#include "modular_skyrat\modules\bsa_overhaul\code\bsa_computer.dm"
#include "modular_skyrat\modules\bsa_overhaul\code\station_goal.dm"
#include "modular_skyrat\modules\bsrpd\code\bsrpd.dm"
-#include "modular_skyrat\modules\bulletrebalance\code\sniper.dm"
#include "modular_skyrat\modules\cargo\code\goodies.dm"
#include "modular_skyrat\modules\cargo\code\packs.dm"
#include "modular_skyrat\modules\cargo\code\items\AFAD.dm"
@@ -6470,7 +6580,6 @@
#include "modular_skyrat\modules\company_imports\code\armament_component.dm"
#include "modular_skyrat\modules\company_imports\code\company_datums.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\_armament_basetype.dm"
-#include "modular_skyrat\modules\company_imports\code\armament_datums\bolt_nanotrasen_firearms.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\deforest_medical.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\jarnsmiour.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\kahraman_industries.dm"
@@ -6479,6 +6588,7 @@
#include "modular_skyrat\modules\company_imports\code\armament_datums\nakamura_tools.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\nri_military_surplus.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\put_a_donk_on_it.dm"
+#include "modular_skyrat\modules\company_imports\code\armament_datums\sol_defense.dm"
#include "modular_skyrat\modules\company_imports\code\armament_datums\vitezstvi_ammo.dm"
#include "modular_skyrat\modules\company_imports\code\datums\cargo_order.dm"
#include "modular_skyrat\modules\company_imports\code\datums\cargo_pack.dm"
@@ -6596,6 +6706,7 @@
#include "modular_skyrat\modules\customization\modules\clothing\masks\breath.dm"
#include "modular_skyrat\modules\customization\modules\clothing\masks\gas_filter.dm"
#include "modular_skyrat\modules\customization\modules\clothing\masks\gasmask.dm"
+#include "modular_skyrat\modules\customization\modules\clothing\masks\paper.dm"
#include "modular_skyrat\modules\customization\modules\clothing\neck\_neck.dm"
#include "modular_skyrat\modules\customization\modules\clothing\neck\cloaks.dm"
#include "modular_skyrat\modules\customization\modules\clothing\neck\collars.dm"
@@ -7035,11 +7146,24 @@
#include "modular_skyrat\modules\medical\code\carbon_update_icons.dm"
#include "modular_skyrat\modules\medical\code\grasp.dm"
#include "modular_skyrat\modules\medical\code\health_analyzer.dm"
+#include "modular_skyrat\modules\medical\code\medkit.dm"
#include "modular_skyrat\modules\medical\code\smartdarts.dm"
+#include "modular_skyrat\modules\medical\code\sprays.dm"
+#include "modular_skyrat\modules\medical\code\cargo\packs.dm"
#include "modular_skyrat\modules\medical\code\wounds\_wounds.dm"
#include "modular_skyrat\modules\medical\code\wounds\bleed.dm"
#include "modular_skyrat\modules\medical\code\wounds\medical.dm"
#include "modular_skyrat\modules\medical\code\wounds\muscle.dm"
+#include "modular_skyrat\modules\medical\code\wounds\wound_effects.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\robotic_burns.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\robotic_muscle.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\robotic_pierce.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\robotic_slash.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\blunt\robotic_blunt.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\blunt\robotic_blunt_T1.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\blunt\robotic_blunt_T2.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\blunt\robotic_blunt_T3.dm"
+#include "modular_skyrat\modules\medical\code\wounds\synth\blunt\secures_internals.dm"
#include "modular_skyrat\modules\medical_designs\medical_designs.dm"
#include "modular_skyrat\modules\medievalcrate_skyrat\code\vintageitems.dm"
#include "modular_skyrat\modules\mentor\code\_globalvars.dm"
@@ -7101,11 +7225,15 @@
#include "modular_skyrat\modules\modular_implants\code\nifsofts\dorms.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\hivemind.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\huds.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\hypnosis.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\money_sense.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\prop_summoner.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\scryer.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\shapeshifter.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soul_poem.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soulcatcher.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\base_types\action_granter.dm"
+#include "modular_skyrat\modules\modular_implants\code\soulcatcher\attachable_soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\handheld_soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_body_component.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_component.dm"
@@ -7238,14 +7366,27 @@
#include "modular_skyrat\modules\modular_vending\code\vending.dm"
#include "modular_skyrat\modules\modular_vending\code\wardrobes.dm"
#include "modular_skyrat\modules\modular_weapons\code\autolathe_designs.dm"
-#include "modular_skyrat\modules\modular_weapons\code\automatic.dm"
#include "modular_skyrat\modules\modular_weapons\code\conversion_kits.dm"
-#include "modular_skyrat\modules\modular_weapons\code\energy.dm"
+#include "modular_skyrat\modules\modular_weapons\code\gunsets.dm"
#include "modular_skyrat\modules\modular_weapons\code\melee.dm"
#include "modular_skyrat\modules\modular_weapons\code\modular_projectiles.dm"
-#include "modular_skyrat\modules\modular_weapons\code\pistol.dm"
-#include "modular_skyrat\modules\modular_weapons\code\revolver.dm"
-#include "modular_skyrat\modules\modular_weapons\code\rifle.dm"
+#include "modular_skyrat\modules\modular_weapons\code\pepperball_gun.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\advert.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\grenade_launcher.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\gunsets.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\magazines.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\rifle.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\shotgun.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\submachinegun.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\ammo\grenade.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\ammo\pistol.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\carwo_defense_systems\ammo\rifle.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\trappiste_fabriek\advert.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\trappiste_fabriek\ammo.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\trappiste_fabriek\gunsets.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\trappiste_fabriek\magazines.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\trappiste_fabriek\pistol.dm"
+#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\trappiste_fabriek\revolver.dm"
#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\xhihao_light_arms\ammo.dm"
#include "modular_skyrat\modules\modular_weapons\code\company_and_or_faction_based\xhihao_light_arms\guns.dm"
#include "modular_skyrat\modules\mold\code\_mold_defines.dm"
@@ -7319,7 +7460,6 @@
#include "modular_skyrat\modules\nanotrasen_naval_command\code\id_trims.dm"
#include "modular_skyrat\modules\nanotrasen_naval_command\code\outfits.dm"
#include "modular_skyrat\modules\nanotrasen_rep\code\clothing.dm"
-#include "modular_skyrat\modules\nanotrasen_rep\code\m45a5.dm"
#include "modular_skyrat\modules\nanotrasen_rep\code\nanotrasen_consultant.dm"
#include "modular_skyrat\modules\new_cells\code\power_cell.dm"
#include "modular_skyrat\modules\novaya_ert\code\advanced_choice_beacon.dm"
@@ -7377,6 +7517,7 @@
#include "modular_skyrat\modules\pixel_shift\code\pixel_shift_component.dm"
#include "modular_skyrat\modules\pixel_shift\code\pixel_shift_keybind.dm"
#include "modular_skyrat\modules\pixel_shift\code\pixel_shift_mob.dm"
+#include "modular_skyrat\modules\player_ranks\code\world_topic.dm"
#include "modular_skyrat\modules\player_ranks\code\player_rank_controller\_player_rank_controller.dm"
#include "modular_skyrat\modules\player_ranks\code\player_rank_controller\donator_controller.dm"
#include "modular_skyrat\modules\player_ranks\code\player_rank_controller\mentor_controller.dm"
@@ -7444,6 +7585,8 @@
#include "modular_skyrat\modules\records_on_examine\code\record_variables.dm"
#include "modular_skyrat\modules\records_on_examine\code\records_procs.dm"
#include "modular_skyrat\modules\records_on_examine\code\view_exploitables.dm"
+#include "modular_skyrat\modules\resleeving\code\rsd_interface.dm"
+#include "modular_skyrat\modules\resleeving\code\research\resleeving_research.dm"
#include "modular_skyrat\modules\robohand\code\bodypart_autosurgeon.dm"
#include "modular_skyrat\modules\robohand\code\robohand.dm"
#include "modular_skyrat\modules\robohand\code\silverhand_bundle.dm"
@@ -7460,6 +7603,10 @@
#include "modular_skyrat\modules\salon\code\scissors.dm"
#include "modular_skyrat\modules\salon\code\sprays.dm"
#include "modular_skyrat\modules\salon\code\straight_razor.dm"
+#include "modular_skyrat\modules\science_tools\medical_tool_designs.dm"
+#include "modular_skyrat\modules\science_tools\research.dm"
+#include "modular_skyrat\modules\science_tools\tool_designs.dm"
+#include "modular_skyrat\modules\science_tools\tools.dm"
#include "modular_skyrat\modules\sec_haul\code\corrections_officer\corrections_officer.dm"
#include "modular_skyrat\modules\sec_haul\code\corrections_officer\corrections_officer_equipment.dm"
#include "modular_skyrat\modules\sec_haul\code\corrections_officer\landmarks.dm"
@@ -7467,10 +7614,6 @@
#include "modular_skyrat\modules\sec_haul\code\guns\armory_spawns.dm"
#include "modular_skyrat\modules\sec_haul\code\guns\bullets.dm"
#include "modular_skyrat\modules\sec_haul\code\guns\cargo_stuff.dm"
-#include "modular_skyrat\modules\sec_haul\code\guns\cmg.dm"
-#include "modular_skyrat\modules\sec_haul\code\guns\guns.dm"
-#include "modular_skyrat\modules\sec_haul\code\guns\gunsets.dm"
-#include "modular_skyrat\modules\sec_haul\code\guns\pepperball_gun.dm"
#include "modular_skyrat\modules\sec_haul\code\misc\ai_module.dm"
#include "modular_skyrat\modules\sec_haul\code\misc\bullet_drive.dm"
#include "modular_skyrat\modules\sec_haul\code\misc\decals.dm"
@@ -7523,6 +7666,8 @@
#include "modular_skyrat\modules\subsystems\code\ticket_ping\ticket_ss.dm"
#include "modular_skyrat\modules\Syndie_edits\code\area.dm"
#include "modular_skyrat\modules\Syndie_edits\code\syndie_edits.dm"
+#include "modular_skyrat\modules\synths\code\defib.dm"
+#include "modular_skyrat\modules\synths\code\research_nodes.dm"
#include "modular_skyrat\modules\synths\code\bodyparts\brain.dm"
#include "modular_skyrat\modules\synths\code\bodyparts\ears.dm"
#include "modular_skyrat\modules\synths\code\bodyparts\eyes.dm"
diff --git a/tgui/packages/tgui-panel/settings/middleware.js b/tgui/packages/tgui-panel/settings/middleware.js
index 705d7a89f3b4a3..cef082213db6fd 100644
--- a/tgui/packages/tgui-panel/settings/middleware.js
+++ b/tgui/packages/tgui-panel/settings/middleware.js
@@ -10,16 +10,39 @@ import { loadSettings, updateSettings, addHighlightSetting, removeHighlightSetti
import { selectSettings } from './selectors';
import { FONTS_DISABLED } from './constants';
+let overrideRule = null;
+let overrideFontFamily = null;
+let overrideFontSize = null;
+
+const updateGlobalOverrideRule = () => {
+ let fontFamily = '';
+
+ if (overrideFontFamily !== null) {
+ fontFamily = `font-family: ${overrideFontFamily} !important;`;
+ }
+
+ const constructedRule = `body * :not(.Icon) {
+ ${fontFamily}
+ }`;
+
+ if (overrideRule === null) {
+ overrideRule = document.createElement('style');
+ document.querySelector('head').append(overrideRule);
+ }
+
+ // no other way to force a CSS refresh other than to update its innerText
+ overrideRule.innerText = constructedRule;
+
+ document.body.style.setProperty('font-size', overrideFontSize);
+};
+
const setGlobalFontSize = (fontSize) => {
- document.documentElement.style.setProperty('font-size', fontSize + 'px');
- document.body.style.setProperty('font-size', fontSize + 'px');
+ overrideFontSize = `${fontSize}px`;
};
const setGlobalFontFamily = (fontFamily) => {
if (fontFamily === FONTS_DISABLED) fontFamily = null;
-
- document.documentElement.style.setProperty('font-family', fontFamily);
- document.body.style.setProperty('font-family', fontFamily);
+ overrideFontFamily = fontFamily;
};
export const settingsMiddleware = (store) => {
@@ -50,6 +73,7 @@ export const settingsMiddleware = (store) => {
// Update global UI font size
setGlobalFontSize(settings.fontSize);
setGlobalFontFamily(settings.fontFamily);
+ updateGlobalOverrideRule();
// Save settings to the web storage
storage.set('panel-settings', settings);
return;
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index 148ccc86f72f7f..43e2fa93343695 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -535,6 +535,10 @@ em {
}
.blob {
+ color: #ee4000;
+}
+
+.blobannounce {
color: #556b2f;
font-weight: bold;
font-size: 185%;
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index ffbfa3f02ed95b..e685cded7740a6 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -594,6 +594,10 @@ h2.alert {
}
.blob {
+ color: #ee4000;
+}
+
+.blobannounce {
color: #323f1c;
font-weight: bold;
font-size: 185%;
diff --git a/tgui/packages/tgui/constants.ts b/tgui/packages/tgui/constants.ts
index 8bf4b83823fc37..c038cd6740a9a4 100644
--- a/tgui/packages/tgui/constants.ts
+++ b/tgui/packages/tgui/constants.ts
@@ -160,7 +160,7 @@ const GASES = [
path: '/datum/gas/nitrogen',
name: 'Nitrogen',
label: 'Nâ‚‚',
- color: 'red',
+ color: 'yellow',
},
{
id: 'co2',
diff --git a/tgui/packages/tgui/interfaces/AdminFax.js b/tgui/packages/tgui/interfaces/AdminFax.js
index e91130baf394f8..46e12615922144 100644
--- a/tgui/packages/tgui/interfaces/AdminFax.js
+++ b/tgui/packages/tgui/interfaces/AdminFax.js
@@ -91,7 +91,7 @@ export const FaxMainPanel = (props, context) => {
icon="n"
mr="7px"
width="49%"
- onClick={() => setPaperName('Nanotrasen Offical Report')}>
+ onClick={() => setPaperName('Nanotrasen Official Report')}>
Nanotrasen