"
+
+// This is split into a seperate proc mostly to make errors that happen not break things too much
+/proc/_debug_variable_value(name, value, level, datum/owner, sanitize, display_flags)
+ . = "DISPLAY_ERROR"
+
+ if(isnull(value))
+ return "null"
+
+ if(istext(value))
+ return "\"[VV_HTML_ENCODE(value)]\""
+
+ if(isicon(value))
#ifdef VARSICON
- var/icon/I = icon(value)
+ var/icon/icon_value = icon(value)
var/rnd = rand(1,10000)
- var/rname = "tmp[REF(I)][rnd].png"
- usr << browse_rsc(I, rname)
- item = "[name_part] = ([value]) "
+ var/rname = "tmp[REF(icon_value)][rnd].png"
+ usr << browse_rsc(icon_value, rname)
+ return "([value]) "
#else
- item = "[name_part] = /icon ([value])"
+ return "/icon ([value])"
#endif
- else if(isappearance(value))
+ if(isappearance(value))
var/image/actually_an_appearance = value
- item = "[name_part] = /appearance ([actually_an_appearance.icon])"
+ return "/appearance ([actually_an_appearance.icon])"
- else if (isfile(value))
- item = "[name_part] = '[value]'"
+ if(isfilter(value))
+ var/datum/filter_value = value
+ return "/filter ([filter_value.type] [REF(filter_value)])"
- else if(istype(value,/matrix)) // Needs to be before datum
- var/matrix/M = value
- item = {"[name_part] =
-
-
-
-
[M.a]
[M.d]
0
-
[M.b]
[M.e]
0
-
[M.c]
[M.f]
1
-
-
"} //TODO link to modify_transform wrapper for all matrices
+ if(isfile(value))
+ return "'[value]'"
- else if (isdatum(value))
- var/datum/DV = value
- if ("[DV]" != "[DV.type]") //if the thing as a name var, lets use it.
- item = "[name_part] = [DV] [DV.type] [REF(value)]"
- else
- item = "[name_part] = [DV.type] [REF(value)]"
- if(istype(value,/datum/weakref))
- var/datum/weakref/weakref = value
- item += " (Resolve)"
+ if(isdatum(value))
+ var/datum/datum_value = value
+ return datum_value.debug_variable_value(name, level, owner, sanitize, display_flags)
- else if (islist(value))
- var/list/L = value
+ if(islist(value) || hascall(value, "Cut")) // Some special lists arent detectable as a list through istype, so we check if it has a list proc instead
+ var/list/list_value = value
var/list/items = list()
- if (!(display_flags & VV_ALWAYS_CONTRACT_LIST) && L.len > 0 && !(name == "underlays" || name == "overlays" || L.len > (IS_NORMAL_LIST(L) ? VV_NORMAL_LIST_NO_EXPAND_THRESHOLD : VV_SPECIAL_LIST_NO_EXPAND_THRESHOLD)))
- for (var/i in 1 to L.len)
- var/key = L[i]
+ if (!(display_flags & VV_ALWAYS_CONTRACT_LIST) && list_value.len > 0 && list_value.len <= (IS_NORMAL_LIST(list_value) ? VV_NORMAL_LIST_NO_EXPAND_THRESHOLD : VV_SPECIAL_LIST_NO_EXPAND_THRESHOLD))
+ for (var/i in 1 to list_value.len)
+ var/key = list_value[i]
var/val
- if (IS_NORMAL_LIST(L) && !isnum(key))
- val = L[key]
+ if (IS_NORMAL_LIST(list_value) && !isnum(key))
+ val = list_value[key]
if (isnull(val)) // we still want to display non-null false values, such as 0 or ""
val = key
key = i
items += debug_variable(key, val, level + 1, sanitize = sanitize)
- item = "[name_part] = /list ([L.len])
"
+
+ title = "[thing] ([REF(thing)]) = [type]"
+ var/formatted_type = replacetext("[type]", "/", "/")
+
+ var/list/header = islist ? list("/list") : thing.vv_get_header()
var/ref_line = "@[copytext(refid, 2, -1)]" // get rid of the brackets, add a @ prefix for copy pasting in asay
var/marked_line
- if(holder && holder.marked_datum && holder.marked_datum == D)
+ if(holder && holder.marked_datum && holder.marked_datum == thing)
marked_line = VV_MSG_MARKED
var/tagged_line
- if(holder && LAZYFIND(holder.tagged_datums, D))
- var/tag_index = LAZYFIND(holder.tagged_datums, D)
+ if(holder && LAZYFIND(holder.tagged_datums, thing))
+ var/tag_index = LAZYFIND(holder.tagged_datums, thing)
tagged_line = VV_MSG_TAGGED(tag_index)
var/varedited_line
- if(!islist && (D.datum_flags & DF_VAR_EDITED))
+ if(!islist && (thing.datum_flags & DF_VAR_EDITED))
varedited_line = VV_MSG_EDITED
var/deleted_line
- if(!islist && D.gc_destroyed)
+ if(!islist && thing.gc_destroyed)
deleted_line = VV_MSG_DELETED
var/list/dropdownoptions
@@ -75,28 +79,29 @@
var/link = dropdownoptions[name]
dropdownoptions[i] = ""
else
- dropdownoptions = D.vv_get_dropdown()
+ dropdownoptions = thing.vv_get_dropdown()
var/list/names = list()
if(!islist)
- for(var/V in D.vars)
- names += V
+ for(var/varname in thing.vars)
+ names += varname
+
sleep(1 TICKS)
var/list/variable_html = list()
if(islist)
- var/list/L = D
- for(var/i in 1 to L.len)
- var/key = L[i]
+ var/list/list_value = thing
+ for(var/i in 1 to list_value.len)
+ var/key = list_value[i]
var/value
- if(IS_NORMAL_LIST(L) && IS_VALID_ASSOC_KEY(key))
- value = L[key]
- variable_html += debug_variable(i, value, 0, L)
+ if(IS_NORMAL_LIST(list_value) && IS_VALID_ASSOC_KEY(key))
+ value = list_value[key]
+ variable_html += debug_variable(i, value, 0, list_value)
else
names = sort_list(names)
- for(var/V in names)
- if(D.can_vv_get(V))
- variable_html += D.vv_get_var(V)
+ for(var/varname in names)
+ if(thing.can_vv_get(varname))
+ variable_html += thing.vv_get_var(varname)
var/html = {"
@@ -274,5 +279,5 @@ datumrefresh=[refid];[HrefToken()]'>Refresh
"}
src << browse(html, "window=variables[refid];size=475x650")
-/client/proc/vv_update_display(datum/D, span, content)
- src << output("[span]:[content]", "variables[REF(D)].browser:replace_span")
+/client/proc/vv_update_display(datum/thing, span, content)
+ src << output("[span]:[content]", "variables[REF(thing)].browser:replace_span")
diff --git a/code/modules/antagonists/blob/overmind.dm b/code/modules/antagonists/blob/overmind.dm
index 38e88817de285..d87574c092dcf 100644
--- a/code/modules/antagonists/blob/overmind.dm
+++ b/code/modules/antagonists/blob/overmind.dm
@@ -294,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)
@@ -313,7 +313,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
var/message_a = say_quote(message)
var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]"))
- blob_telepathy(rendered, src)
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, src)
/mob/camera/blob/blob_act(obj/structure/blob/B)
return
diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm
index 171468e3d7e49..de47610564c72 100644
--- a/code/modules/antagonists/changeling/changeling.dm
+++ b/code/modules/antagonists/changeling/changeling.dm
@@ -350,12 +350,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)
/*
@@ -1048,9 +1047,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 f4949306dedc8..1e2d54eb4563f 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
diff --git a/code/modules/antagonists/changeling/powers/defib_grasp.dm b/code/modules/antagonists/changeling/powers/defib_grasp.dm
index 20ff3049c8fdd..135b9b243f721 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/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm
index bf4f8c2b3da3e..54027b7d8a24f 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/void_adaption.dm b/code/modules/antagonists/changeling/powers/void_adaption.dm
new file mode 100644
index 0000000000000..76c0eeffc972d
--- /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/cult.dm b/code/modules/antagonists/cult/cult.dm
index b0d877820d903..0f43b5ae29ef4 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_team.dm b/code/modules/antagonists/cult/cult_team.dm
index 6254ded64a030..1d199a113f5d2 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 30cb3d4467c68..d343e331fad8d 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -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!"))
@@ -892,6 +892,12 @@ structure_check() searches for nearby cultist structures required for the invoca
visible_message(span_warning("A cloud of red mist forms above [src], and from within steps... a [new_human.gender == FEMALE ? "wo":""]man."))
to_chat(user, span_cultitalic("Your blood begins flowing into [src]. You must remain in place and conscious to maintain the forms of those summoned. This will hurt you slowly but surely..."))
var/obj/structure/emergency_shield/cult/weak/N = new(T)
+ if(ghost_to_spawn.mind && ghost_to_spawn.mind.current)
+ new_human.AddComponent( \
+ /datum/component/temporary_body, \
+ old_mind = ghost_to_spawn.mind, \
+ old_body = ghost_to_spawn.mind.current, \
+ )
new_human.key = ghost_to_spawn.key
var/datum/antagonist/cult/created_cultist = new_human.mind?.add_antag_datum(/datum/antagonist/cult)
created_cultist?.silent = TRUE
@@ -1017,7 +1023,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_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm
index 260f56540d042..1f408698f3cfa 100644
--- a/code/modules/antagonists/heretic/heretic_knowledge.dm
+++ b/code/modules/antagonists/heretic/heretic_knowledge.dm
@@ -530,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
diff --git a/code/modules/antagonists/heretic/heretic_living_heart.dm b/code/modules/antagonists/heretic/heretic_living_heart.dm
index 4af93c0da68c8..1766cb4cd765e 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/items/heretic_armor.dm b/code/modules/antagonists/heretic/items/heretic_armor.dm
index 93ab613190b1c..502c52c17fb89 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/blade_lore.dm b/code/modules/antagonists/heretic/knowledge/blade_lore.dm
index 84e266c837458..f2f3b156a2f70 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
*/
diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
index cd1a7ace5407d..c38fcc1a85629 100644
--- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
@@ -294,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/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm
index 966134710b432..84f128b5cfcf9 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
*/
diff --git a/code/modules/antagonists/heretic/knowledge/side_ash_flesh.dm b/code/modules/antagonists/heretic/knowledge/side_ash_flesh.dm
index f7f3c175b2fb0..bf840d6ed27ea 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 a8fb031fed262..d7dfd75a14462 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 8aecfe06e1f16..2dbb44ea4eb7e 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 92e6e381222e4..643fd434af7b5 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/magic/ascended_shapeshift.dm b/code/modules/antagonists/heretic/magic/ascended_shapeshift.dm
index 4848635b443c2..f1d6de56e399f 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/basic/heretic_summon/ash_spirit,
/mob/living/basic/heretic_summon/raw_prophet/ascended,
- /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/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/structures/knock_final.dm b/code/modules/antagonists/heretic/structures/knock_final.dm
index 28a8f90b9f395..f7d0d1a9ea7b0 100644
--- a/code/modules/antagonists/heretic/structures/knock_final.dm
+++ b/code/modules/antagonists/heretic/structures/knock_final.dm
@@ -28,7 +28,7 @@
. = ..()
transform *= 3
if(isnull(monster_types))
- monster_types = subtypesof(/mob/living/simple_animal/hostile/heretic_summon) + subtypesof(/mob/living/basic/heretic_summon) - monster_types_blacklist
+ 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))
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index 2462139044d40..bf213f0b3def0 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/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
index 57eb95a978c52..3ea6488b2d42d 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/spiders/spiders.dm b/code/modules/antagonists/spiders/spiders.dm
index 6d0b86d24d70a..d42f1aea7b391 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 51a54d99e300a..ae28f5dbf4c08 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/art/paintings.dm b/code/modules/art/paintings.dm
index e2448c1aaccf2..9a18a2b026951 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 e4c54929dae19..16dee49c60fea 100644
--- a/code/modules/asset_cache/assets/rcd.dm
+++ b/code/modules/asset_cache/assets/rcd.dm
@@ -2,60 +2,44 @@
name = "rcd-tgui"
/datum/asset/spritesheet/rcd/create_spritesheets()
- //We load airlock icons seperatly from other icons cause they need overlays
-
- //load all category essential icon_states. format is icon_file = list of icon states we need from that file
- var/list/essentials = list(
- 'icons/obj/chairs.dmi' = list("bar"),
- 'icons/obj/machines/wallmounts.dmi' = list("apc", "alarm_bitem", "fire_bitem"),
- 'icons/obj/lighting.dmi' = list("floodlight_c1"),
- 'icons/obj/assemblies/stock_parts.dmi' = list("box_1"),
- 'icons/obj/bed.dmi' = list("bed"),
- '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"),
- )
-
- var/icon/icon
- for(var/icon_file as anything in essentials)
- for(var/icon_state as anything in essentials[icon_file])
- icon = icon(icon = icon_file, icon_state = icon_state)
- if(icon_state == "window0" || icon_state == "rwindow0")
- icon.Blend(icon(icon = 'icons/obj/structures.dmi', icon_state = "grille"), ICON_UNDERLAY)
- Insert(sprite_name = sanitize_css_class_name(icon_state), I = icon)
-
- //for each airlock type we create its overlayed version with the suffix Glass in the sprite name
- var/list/airlocks = list(
- "Standard" = 'icons/obj/doors/airlocks/station/public.dmi',
- "Public" = 'icons/obj/doors/airlocks/public/glass.dmi',
- "Engineering" = 'icons/obj/doors/airlocks/station/engineering.dmi',
- "Atmospherics" = 'icons/obj/doors/airlocks/station/atmos.dmi',
- "Security" = 'icons/obj/doors/airlocks/station/security.dmi',
- "Command" = 'icons/obj/doors/airlocks/station/command.dmi',
- "Medical" = 'icons/obj/doors/airlocks/station/medical.dmi',
- "Research" = 'icons/obj/doors/airlocks/station/research.dmi',
- "Freezer" = 'icons/obj/doors/airlocks/station/freezer.dmi',
- "Virology" = 'icons/obj/doors/airlocks/station/virology.dmi',
- "Mining" = 'icons/obj/doors/airlocks/station/mining.dmi',
- "Maintenance" = 'icons/obj/doors/airlocks/station/maintenance.dmi',
- "External" = 'icons/obj/doors/airlocks/external/external.dmi',
- "External Maintenance" = 'icons/obj/doors/airlocks/station/maintenanceexternal.dmi',
- "Airtight Hatch" = 'icons/obj/doors/airlocks/hatch/centcom.dmi',
- "Maintenance Hatch" = 'icons/obj/doors/airlocks/hatch/maintenance.dmi'
- )
- //these 3 types dont have glass doors
- var/list/exclusion = list("Freezer", "Airtight Hatch", "Maintenance Hatch")
-
- for(var/airlock_name in airlocks)
- //solid door with overlay
- icon = icon(icon = airlocks[airlock_name] , icon_state = "closed" , dir = SOUTH)
- icon.Blend(icon(icon = airlocks[airlock_name], icon_state = "fill_closed", dir = SOUTH), ICON_OVERLAY)
- Insert(sprite_name = sanitize_css_class_name(airlock_name), I = icon)
-
- //exclude these glass types
- if(airlock_name in exclusion)
+ for(var/root_category in GLOB.rcd_designs)
+
+ var/list/category_designs = GLOB.rcd_designs[root_category]
+ if(!length(category_designs))
continue
- //glass door no overlay
- icon = icon(airlocks[airlock_name] , "closed" , SOUTH)
- Insert(sprite_name = sanitize_css_class_name("[airlock_name]Glass"), I = icon)
+ for(var/category in category_designs)
+ var/list/designs = category_designs[category]
+
+ var/sprite_name
+ var/icon/sprite_icon
+ for(var/list/design as anything in designs)
+ var/atom/movable/path = design[RCD_DESIGN_PATH]
+ if(!ispath(path))
+ continue
+ sprite_name = initial(path.name)
+
+ //icon for windows are blended with grills if required and loaded from radial menu
+ if(ispath(path, /obj/structure/window))
+ if(path == /obj/structure/window)
+ sprite_icon = icon(icon = 'icons/hud/radial.dmi', icon_state = "windowsize")
+ else if(path == /obj/structure/window/reinforced)
+ sprite_icon = icon(icon = 'icons/hud/radial.dmi', icon_state = "windowtype")
+ else if(path == /obj/structure/window/fulltile || path == /obj/structure/window/reinforced/fulltile)
+ sprite_icon = icon(icon = initial(path.icon), icon_state = initial(path.icon_state))
+ sprite_icon.Blend(icon(icon = 'icons/obj/structures.dmi', icon_state = "grille"), ICON_UNDERLAY)
+
+ //icons for solid airlocks have an added solid overlay on top of their glass icons
+ else if(ispath(path, /obj/machinery/door/airlock))
+ var/obj/machinery/door/airlock/airlock_path = path
+ var/airlock_icon = initial(airlock_path.icon)
+
+ sprite_icon = icon(icon = airlock_icon, icon_state = "closed")
+ if(!initial(airlock_path.glass))
+ sprite_icon.Blend(icon(icon = airlock_icon, icon_state = "fill_closed"), ICON_OVERLAY)
+
+ //for all other icons we load the paths default icon & icon state
+ else
+ sprite_icon = icon(icon = initial(path.icon), icon_state = initial(path.icon_state))
+
+ Insert(sanitize_css_class_name(sprite_name), sprite_icon)
diff --git a/code/modules/atmospherics/gasmixtures/gas_types.dm b/code/modules/atmospherics/gasmixtures/gas_types.dm
index ae3367ace5932..060b5a12ae9a9 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 e629c14e0fef0..24d1e63e42a25 100644
--- a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
+++ b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
@@ -287,6 +287,8 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
"refID" = REF(vent),
"long_name" = sanitize(vent.name),
"power" = vent.on,
+ "overclock" = vent.fan_overclocked,
+ "integrity" = vent.get_integrity_percentage(),
"checks" = vent.pressure_checks,
"excheck" = vent.pressure_checks & ATMOS_EXTERNAL_BOUND,
"incheck" = vent.pressure_checks & ATMOS_INTERNAL_BOUND,
@@ -356,6 +358,14 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
powering.on = !!params["val"]
powering.atmos_conditions_changed()
powering.update_appearance(UPDATE_ICON)
+
+ if("overclock")
+ if(isnull(vent))
+ return TRUE
+ vent.toggle_overclock()
+ vent.update_appearance(UPDATE_ICON)
+ return TRUE
+
if ("direction")
if (isnull(vent))
return TRUE
diff --git a/code/modules/atmospherics/machinery/air_alarm/air_alarm_interact.dm b/code/modules/atmospherics/machinery/air_alarm/air_alarm_interact.dm
index 3ec8fbdd99b37..3ac90db0c1852 100644
--- a/code/modules/atmospherics/machinery/air_alarm/air_alarm_interact.dm
+++ b/code/modules/atmospherics/machinery/air_alarm/air_alarm_interact.dm
@@ -47,17 +47,15 @@
/obj/machinery/airalarm/rcd_vals(mob/user, obj/item/construction/rcd/the_rcd)
if((buildstage == AIR_ALARM_BUILD_NO_CIRCUIT) && (the_rcd.upgrade & RCD_UPGRADE_SIMPLE_CIRCUITS))
- return list("mode" = RCD_WALLFRAME, "delay" = 2 SECONDS, "cost" = 1)
+ return list("delay" = 2 SECONDS, "cost" = 1)
return FALSE
-/obj/machinery/airalarm/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, passed_mode)
- switch(passed_mode)
- if(RCD_WALLFRAME)
- user.visible_message(span_notice("[user] fabricates a circuit and places it into [src]."), \
- span_notice("You adapt an air alarm circuit and slot it into the assembly."))
- buildstage = AIR_ALARM_BUILD_NO_WIRES
- update_appearance()
- return TRUE
+/obj/machinery/airalarm/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, list/rcd_data)
+ if(rcd_data["[RCD_DESIGN_MODE]"] == RCD_WALLFRAME)
+ balloon_alert(user, "circuit installed")
+ buildstage = AIR_ALARM_BUILD_NO_WIRES
+ update_appearance()
+ return TRUE
return FALSE
/obj/machinery/airalarm/attack_hand_secondary(mob/user, list/modifiers)
diff --git a/code/modules/atmospherics/machinery/atmosmachinery.dm b/code/modules/atmospherics/machinery/atmosmachinery.dm
index 417e5055de755..62c2b562d5129 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/components/unary_devices/vent_pump.dm b/code/modules/atmospherics/machinery/components/unary_devices/vent_pump.dm
index 3422f3e3adfa5..25713e55e37f4 100644
--- a/code/modules/atmospherics/machinery/components/unary_devices/vent_pump.dm
+++ b/code/modules/atmospherics/machinery/components/unary_devices/vent_pump.dm
@@ -15,6 +15,8 @@
shift_underlay_only = FALSE
pipe_state = "uvent"
vent_movement = VENTCRAWL_ALLOWED | VENTCRAWL_CAN_SEE | VENTCRAWL_ENTRANCE_ALLOWED
+ // vents are more complex machinery and so are less resistant to damage
+ max_integrity = 100
///Direction of pumping the gas (ATMOS_DIRECTION_RELEASING or ATMOS_DIRECTION_SIPHONING)
var/pump_direction = ATMOS_DIRECTION_RELEASING
@@ -34,6 +36,18 @@
///area this vent is assigned to
var/area/assigned_area
+ /// Is this vent currently overclocked, removing pressure limits but damaging the fan?
+ var/fan_overclocked = FALSE
+
+ /// Rate of damage per atmos process to the fan when overclocked. Set to 0 to disable damage.
+ var/fan_damage_rate = 0.5
+
+ /// The cached string we show for examine that lets you know how fucked up the fan is.
+ var/examine_condition
+
+ /// Datum for managing the overclock sound loop
+ var/datum/looping_sound/vent_pump_overclock/sound_loop
+
/obj/machinery/atmospherics/components/unary/vent_pump/Initialize(mapload)
if(!id_tag)
id_tag = assign_random_name()
@@ -42,16 +56,44 @@
tool_screentips = string_assoc_nested_list(list(
TOOL_MULTITOOL = list(
SCREENTIP_CONTEXT_LMB = "Log to link later with air sensor",
- )
+ ),
+ TOOL_SCREWDRIVER = list(
+ SCREENTIP_CONTEXT_LMB = "Repair",
+ ),
))
AddElement(/datum/element/contextual_screentip_tools, tool_screentips)
. = ..()
+ sound_loop = new(src)
assign_to_area()
+/obj/machinery/atmospherics/components/unary/vent_pump/on_update_integrity(old_value, new_value)
+ . = ..()
+ var/condition_string
+ switch(get_integrity_percentage())
+ if(1)
+ condition_string = "perfect"
+ if(0.75 to 0.99)
+ condition_string = "good"
+ if(0.50 to 0.74)
+ condition_string = "okay"
+ if(0.25 to 0.49)
+ condition_string = "bad"
+ else
+ condition_string = "terrible"
+ examine_condition = "The fan is in [condition_string] condition."
+
/obj/machinery/atmospherics/components/unary/vent_pump/examine(mob/user)
. = ..()
. += span_notice("You can link it with an air sensor using a multitool.")
+ if(fan_overclocked)
+ . += span_warning("It is currently overclocked causing it to take damage over time.")
+
+ if(get_integrity() > 0)
+ . += span_notice(examine_condition)
+ else
+ . += span_warning("The fan is broken.")
+
/obj/machinery/atmospherics/components/unary/vent_pump/multitool_act(mob/living/user, obj/item/multitool/multi_tool)
if(istype(multi_tool.buffer, /obj/machinery/air_sensor))
var/obj/machinery/air_sensor/sensor = multi_tool.buffer
@@ -63,8 +105,33 @@
multi_tool.set_buffer(src)
return TOOL_ACT_TOOLTYPE_SUCCESS
+/obj/machinery/atmospherics/components/unary/vent_pump/screwdriver_act(mob/living/user, obj/item/tool)
+ var/time_to_repair = (10 SECONDS) * (1 - get_integrity_percentage())
+ if(!time_to_repair)
+ return FALSE
+
+ balloon_alert(user, "repairing vent...")
+ if(do_after(user, time_to_repair, src))
+ balloon_alert(user, "vent repaired")
+ repair_damage(max_integrity)
+
+ else
+ balloon_alert(user, "interrupted!")
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/atmospherics/components/unary/vent_pump/atom_fix()
+ set_is_operational(TRUE)
+ update_appearance()
+ return ..()
+
+/obj/machinery/atmospherics/components/unary/vent_pump/atom_break(damage_flag)
+ set_is_operational(FALSE)
+ update_appearance()
+ return ..()
+
/obj/machinery/atmospherics/components/unary/vent_pump/Destroy()
disconnect_from_area()
+ QDEL_NULL(sound_loop)
var/area/vent_area = get_area(src)
if(vent_area)
@@ -107,6 +174,17 @@
. = ..()
disconnect_from_area(area_to_unregister)
+/obj/machinery/atmospherics/components/unary/vent_pump/update_overlays()
+ . = ..()
+ if(!powered())
+ return
+
+ if(get_integrity() <= 0)
+ . += mutable_appearance(icon, "broken")
+
+ else if(fan_overclocked)
+ . += mutable_appearance(icon, "overclocked")
+
/obj/machinery/atmospherics/components/unary/vent_pump/update_icon_nopipes()
cut_overlays()
if(showpipe)
@@ -144,6 +222,20 @@
else // pump_direction == SIPHONING
icon_state = "vent_in"
+/obj/machinery/atmospherics/components/unary/vent_pump/proc/toggle_overclock(from_break = FALSE)
+ fan_overclocked = !fan_overclocked
+
+ if(from_break)
+ playsound(src, 'sound/machines/fan_break.ogg', 100)
+ fan_overclocked = FALSE
+
+ if(fan_overclocked)
+ sound_loop.start()
+ else
+ sound_loop.stop()
+
+ update_appearance()
+
/obj/machinery/atmospherics/components/unary/vent_pump/process_atmos()
if(!is_operational)
return
@@ -154,6 +246,17 @@
var/turf/open/us = loc
if(!istype(us))
return
+
+ var/current_integrity = get_integrity()
+ if(fan_overclocked)
+ take_damage(fan_damage_rate, sound_effect=FALSE)
+ if(current_integrity == 0)
+ on = FALSE
+ set_is_operational(FALSE)
+ toggle_overclock(from_break = TRUE)
+ return
+
+ var/percent_integrity = get_integrity_percentage()
var/datum/gas_mixture/air_contents = airs[1]
var/datum/gas_mixture/environment = us.return_air()
var/environment_pressure = environment.return_pressure()
@@ -168,9 +271,13 @@
if(pressure_delta > 0)
if(air_contents.temperature > 0)
- if(environment_pressure >= 50 * ONE_ATMOSPHERE)
+ if(!fan_overclocked && (environment_pressure >= 50 * ONE_ATMOSPHERE))
return FALSE
+
var/transfer_moles = (pressure_delta * environment.volume) / (air_contents.temperature * R_IDEAL_GAS_EQUATION)
+ if(!fan_overclocked && (percent_integrity < 1))
+ transfer_moles *= percent_integrity
+
var/datum/gas_mixture/removed = air_contents.remove(transfer_moles)
if(!removed || !removed.total_moles())
@@ -187,9 +294,12 @@
pressure_delta = min(pressure_delta, (internal_pressure_bound - air_contents.return_pressure()))
if(pressure_delta > 0 && environment.temperature > 0)
- if(air_contents.return_pressure() >= 50 * ONE_ATMOSPHERE)
+ if(!fan_overclocked && (air_contents.return_pressure() >= 50 * ONE_ATMOSPHERE))
return FALSE
+
var/transfer_moles = (pressure_delta * air_contents.volume) / (environment.temperature * R_IDEAL_GAS_EQUATION)
+ if(!fan_overclocked && (percent_integrity < 1))
+ transfer_moles *= percent_integrity
var/datum/gas_mixture/removed = loc.remove_air(transfer_moles)
diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm
index b8ad06e25abb1..77a29b5461d4f 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)
@@ -280,3 +294,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 9cda298ccd4c9..472cb3ed2034a 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 02c550fd55859..06812aaa6496f 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 fd43e315527b4..72c9c74278123 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 00ef058333a40..a6bfcc533d240 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 e340d7f54ccf1..5841486ba9bbc 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 03ef32b435453..83870ee210b67 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 bdfbda9ba6c78..09c8a3a9367ae 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 cfc24ab82912b..7e14b8a98063e 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 0000000000000..9642442a9733f
--- /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 c1c128c2e808a..c94caf3117476 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 95e5839469fd6..7c530bace5fcf 100644
--- a/code/modules/atmospherics/machinery/pipes/smart.dm
+++ b/code/modules/atmospherics/machinery/pipes/smart.dm
@@ -14,7 +14,7 @@ GLOBAL_LIST_INIT(atmos_components, typecacheof(list(/obj/machinery/atmospherics)
var/connections = NONE
/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/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 3f28631726758..28c0bd93f2554 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -34,8 +34,8 @@ GLOBAL_LIST_INIT(gas_id_to_canister, init_gas_id_to_canister())
desc = "A canister for the storage of gas."
icon = 'icons/obj/pipes_n_cables/canisters.dmi'
icon_state = "#mapme"
- greyscale_config = /datum/greyscale_config/canister/hazard
- greyscale_colors = "#ffff00#000000"
+ greyscale_config = /datum/greyscale_config/canister
+ greyscale_colors = "#6b6b80"
density = TRUE
volume = 2000
armor_type = /datum/armor/portable_atmospherics_canister
@@ -156,7 +156,7 @@ GLOBAL_LIST_INIT(gas_id_to_canister, init_gas_id_to_canister())
gas_type = /datum/gas/antinoblium
filled = 1
greyscale_config = /datum/greyscale_config/canister/double_stripe
- greyscale_colors = "#9b5d7f#368bff"
+ greyscale_colors = "#333333#fefb30"
/obj/machinery/portable_atmospherics/canister/bz
name = "\improper BZ canister"
@@ -167,8 +167,8 @@ GLOBAL_LIST_INIT(gas_id_to_canister, init_gas_id_to_canister())
/obj/machinery/portable_atmospherics/canister/carbon_dioxide
name = "Carbon dioxide canister"
gas_type = /datum/gas/carbon_dioxide
- greyscale_config = /datum/greyscale_config/canister
- greyscale_colors = "#4e4c48"
+ greyscale_config = /datum/greyscale_config/canister/double_stripe
+ greyscale_colors = "#4e4c48#eaeaea"
/obj/machinery/portable_atmospherics/canister/freon
name = "Freon canister"
@@ -202,8 +202,8 @@ GLOBAL_LIST_INIT(gas_id_to_canister, init_gas_id_to_canister())
name = "Hydrogen canister"
gas_type = /datum/gas/hydrogen
filled = 1
- greyscale_config = /datum/greyscale_config/canister/stripe
- greyscale_colors = "#bdc2c0#ffffff"
+ greyscale_config = /datum/greyscale_config/canister/double_stripe
+ greyscale_colors = "#eaeaea#be3455"
/obj/machinery/portable_atmospherics/canister/miasma
name = "Miasma canister"
@@ -215,8 +215,8 @@ GLOBAL_LIST_INIT(gas_id_to_canister, init_gas_id_to_canister())
/obj/machinery/portable_atmospherics/canister/nitrogen
name = "Nitrogen canister"
gas_type = /datum/gas/nitrogen
- greyscale_config = /datum/greyscale_config/canister
- greyscale_colors = "#d41010"
+ greyscale_config = /datum/greyscale_config/canister/double_stripe
+ greyscale_colors = "#e9ff5c#f4fce8"
/obj/machinery/portable_atmospherics/canister/nitrous_oxide
name = "Nitrous oxide canister"
diff --git a/code/modules/bitrunning/components/avatar_connection.dm b/code/modules/bitrunning/components/avatar_connection.dm
index 2a151d05066d0..24f42d8f3e519 100644
--- a/code/modules/bitrunning/components/avatar_connection.dm
+++ b/code/modules/bitrunning/components/avatar_connection.dm
@@ -68,7 +68,7 @@
/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), override = TRUE)
+ 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()
@@ -79,7 +79,9 @@
/// 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))
diff --git a/code/modules/bitrunning/designs.dm b/code/modules/bitrunning/designs.dm
new file mode 100644
index 0000000000000..4e7bca1c1a8dd
--- /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/objects/byteforge.dm b/code/modules/bitrunning/objects/byteforge.dm
new file mode 100644
index 0000000000000..e4543601ce9d1
--- /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/host_monitor.dm b/code/modules/bitrunning/objects/host_monitor.dm
index f59ca61cbd080..c35edea6319f8 100644
--- a/code/modules/bitrunning/objects/host_monitor.dm
+++ b/code/modules/bitrunning/objects/host_monitor.dm
@@ -2,10 +2,10 @@
name = "host monitor"
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2)
- desc = "A complex medical device that, when attached to an avatar's data stream, can detect the user of their host's health."
+ 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 = "gps-b"
+ icon_state = "host_monitor"
inhand_icon_state = "electronic"
item_flags = NOBLUDGEON
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
diff --git a/code/modules/bitrunning/objects/landmarks.dm b/code/modules/bitrunning/objects/landmarks.dm
index d78283c6a8b23..d727025c92864 100644
--- a/code/modules/bitrunning/objects/landmarks.dm
+++ b/code/modules/bitrunning/objects/landmarks.dm
@@ -44,11 +44,6 @@
qdel(src)
-/// Where the crates get ported to station
-/obj/effect/landmark/bitrunning/station_reward_spawn
- name = "Bitrunning rewards spawn"
- icon_state = "station"
-
/// Where the exit hololadder spawns
/obj/effect/landmark/bitrunning/hololadder_spawn
name = "Bitrunning hololadder spawn"
@@ -68,3 +63,41 @@
/obj/effect/landmark/bitrunning/safehouse_spawn
name = "Bitrunning safehouse spawn"
icon_state = "safehouse"
+
+///Swaps the locations of an encrypted crate in the area with another randomly selected crate.
+///Randomizes names, so you have to inspect crates manually.
+/obj/effect/landmark/bitrunning/crate_replacer
+ name = "Bitrunning Goal Crate Randomizer"
+ icon_state = "crate"
+
+/obj/effect/landmark/bitrunning/crate_replacer/Initialize(mapload)
+ . = ..()
+
+ #ifndef UNIT_TESTS
+ var/list/crate_list = list()
+ var/obj/structure/closet/crate/secure/bitrunning/encrypted/encrypted_crate
+ var/area/my_area = get_area(src)
+
+ for(var/turf/area_turf as anything in my_area.get_contained_turfs())
+ for(var/obj/structure/closet/crate/crate_to_check in area_turf)
+ if(istype(crate_to_check, /obj/structure/closet/crate/secure/bitrunning/encrypted))
+ encrypted_crate = crate_to_check
+ crate_to_check.desc += span_hypnophrase(" This feels like the crate we're looking for!")
+ else
+ crate_list += crate_to_check
+ crate_to_check.name = "Unidentified Crate"
+
+ if(!encrypted_crate)
+ stack_trace("Bitrunning Goal Crate Randomizer failed to find an encrypted crate to swap positions for.")
+ return
+ if(!length(crate_list))
+ stack_trace("Bitrunning Goal Crate Randomizer failed to find any NORMAL crates to swap positions for.")
+ return
+
+ var/original_location = encrypted_crate.loc
+ var/obj/structure/closet/crate/selected_crate = pick(crate_list)
+
+ encrypted_crate.abstract_move(selected_crate.loc)
+ selected_crate.abstract_move(original_location)
+
+ #endif
diff --git a/code/modules/bitrunning/objects/netpod.dm b/code/modules/bitrunning/objects/netpod.dm
index 349a304f04b78..d92da961b86a3 100644
--- a/code/modules/bitrunning/objects/netpod.dm
+++ b/code/modules/bitrunning/objects/netpod.dm
@@ -189,7 +189,7 @@
return TRUE
/obj/machinery/netpod/ui_interact(mob/user, datum/tgui/ui)
- if(!is_operational)
+ if(!is_operational || occupant)
return
ui = SStgui.try_update_ui(user, src, ui)
@@ -242,20 +242,12 @@
to_chat(player, span_notice("The machine disconnects itself and begins to drain."))
open_machine()
-/**
- * ### Disconnect occupant
- * If this goes smoothly, should reconnect a receiving mind to the occupant's body
- *
- * This is the second stage of the process - if you want to disconn avatars start at the mind first
- */
+/// Handles occupant post-disconnection effects like damage, sounds, etc
/obj/machinery/netpod/proc/disconnect_occupant(forced = FALSE)
- var/mob/living/mob_occupant = occupant
- if(isnull(occupant) || !isliving(occupant))
- return
-
connected = FALSE
- if(mob_occupant.stat == DEAD)
+ var/mob/living/mob_occupant = occupant
+ if(isnull(occupant) || !isliving(occupant) || mob_occupant.stat == DEAD)
open_machine()
return
@@ -347,8 +339,9 @@
return
server_ref = WEAKREF(server)
- RegisterSignal(server, COMSIG_BITRUNNER_SERVER_UPGRADED, PROC_REF(on_server_upgraded), override = TRUE)
- RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_COMPLETE, PROC_REF(on_domain_complete), override = TRUE)
+ 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
@@ -395,6 +388,7 @@
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
@@ -409,7 +403,15 @@
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)
diff --git a/code/modules/bitrunning/objects/bit_vendor.dm b/code/modules/bitrunning/objects/vendor.dm
similarity index 100%
rename from code/modules/bitrunning/objects/bit_vendor.dm
rename to code/modules/bitrunning/objects/vendor.dm
diff --git a/code/modules/bitrunning/antagonists/outfit.dm b/code/modules/bitrunning/outfits.dm
similarity index 74%
rename from code/modules/bitrunning/antagonists/outfit.dm
rename to code/modules/bitrunning/outfits.dm
index db57af561f8ad..27ef8029a18ed 100644
--- a/code/modules/bitrunning/antagonists/outfit.dm
+++ b/code/modules/bitrunning/outfits.dm
@@ -41,3 +41,17 @@
officer_uniform.has_sensor = NO_SENSORS
officer_uniform.sensor_mode = SENSOR_OFF
user.update_suit_sensors()
+
+/datum/outfit/echolocator
+ name = "Bitrunning Echolocator"
+ glasses = /obj/item/clothing/glasses/blindfold
+ ears = /obj/item/radio/headset/psyker //Navigating without these is horrible.
+ uniform = /obj/item/clothing/under/abductor
+ gloves = /obj/item/clothing/gloves/fingerless
+ shoes = /obj/item/clothing/shoes/jackboots
+ suit = /obj/item/clothing/suit/jacket/trenchcoat
+ id = /obj/item/card/id/advanced
+
+/datum/outfit/echolocator/post_equip(mob/living/carbon/human/user, visualsOnly)
+ . = ..()
+ user.psykerize()
diff --git a/code/modules/bitrunning/server/loot.dm b/code/modules/bitrunning/server/loot.dm
index 29b730aae784f..8b3af95607c64 100644
--- a/code/modules/bitrunning/server/loot.dm
+++ b/code/modules/bitrunning/server/loot.dm
@@ -16,14 +16,16 @@
/// Generates a reward based on the given domain
/obj/machinery/quantum_server/proc/generate_loot()
- if(!length(receive_turfs) && !locate_receive_turfs())
+ 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/turf/dest_turf = pick(receive_turfs)
- if(isnull(dest_turf))
+ 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
@@ -34,11 +36,11 @@
certificate.name = "certificate of domain completion"
certificate.update_appearance()
- var/obj/structure/closet/crate/secure/bitrunning/decrypted/reward_crate = new(dest_turf, generated_domain, bonus)
+ var/obj/structure/closet/crate/secure/bitrunning/decrypted/reward_crate = new(src, generated_domain, bonus)
reward_crate.manifest = certificate
reward_crate.update_appearance()
- spark_at_location(reward_crate)
+ chosen_forge.start_to_spawn(reward_crate)
return TRUE
/// Returns the markdown text containing domain completion information
diff --git a/code/modules/bitrunning/server/map_handling.dm b/code/modules/bitrunning/server/map_handling.dm
index 02126c290f774..741fad476f0a8 100644
--- a/code/modules/bitrunning/server/map_handling.dm
+++ b/code/modules/bitrunning/server/map_handling.dm
@@ -1,3 +1,4 @@
+#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)
@@ -119,7 +120,6 @@
new /obj/structure/closet/crate/secure/bitrunning/encrypted(pick(crate_turfs))
return TRUE
-#define ONLY_TURF 1 // There should only ever be one turf at the bottom left of the map.
/// Loads the safehouse
/obj/machinery/quantum_server/proc/initialize_safehouse()
@@ -160,7 +160,8 @@
/// Deletes all the tile contents
/obj/machinery/quantum_server/proc/scrub_vdom()
- SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR) // just in case
+ 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]
diff --git a/code/modules/bitrunning/server/quantum_server.dm b/code/modules/bitrunning/server/quantum_server.dm
index 404a31cca6a77..b869fb7f02e2a 100644
--- a/code/modules/bitrunning/server/quantum_server.dm
+++ b/code/modules/bitrunning/server/quantum_server.dm
@@ -48,8 +48,6 @@
var/servo_bonus = 0
/// The turfs we can place a hololadder on.
var/turf/exit_turfs = list()
- /// The turfs on station where we generate loot.
- var/turf/receive_turfs = list()
/obj/machinery/quantum_server/Initialize(mapload)
. = ..()
@@ -84,18 +82,17 @@
avatar_connection_refs.Cut()
spawned_threat_refs.Cut()
QDEL_NULL(exit_turfs)
- QDEL_NULL(receive_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(0)
+ set_light(l_on = FALSE)
return ..()
set_light_color(is_ready ? LIGHT_COLOR_BABY_BLUE : LIGHT_COLOR_FIRE)
- set_light(2, 1.5)
+ set_light(l_range = 2, l_power = 1.5, l_on = TRUE)
return ..()
diff --git a/code/modules/bitrunning/server/util.dm b/code/modules/bitrunning/server/util.dm
index c4f1319cd12dd..f4dbada9ef6f0 100644
--- a/code/modules/bitrunning/server/util.dm
+++ b/code/modules/bitrunning/server/util.dm
@@ -107,14 +107,14 @@
return shuffle(mutation_candidate_refs)
-/// Locates any turfs with crate out landmarks
-/obj/machinery/quantum_server/proc/locate_receive_turfs()
- for(var/obj/effect/landmark/bitrunning/station_reward_spawn/spawner in GLOB.landmarks_list)
- if(IN_GIVEN_RANGE(src, spawner, MAX_DISTANCE))
- receive_turfs += get_turf(spawner)
- qdel(spawner)
+/// Locates any turfs with forges on them
+/obj/machinery/quantum_server/proc/get_nearby_forges()
+ var/list/obj/machinery/byteforge/nearby_forges = list()
- return length(receive_turfs) > 0
+ 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()
@@ -132,10 +132,10 @@
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/crate)
- playsound(crate, 'sound/magic/blink.ogg', 50, TRUE)
+/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(crate))
+ sparks.set_up(5, 1, get_turf(cache))
sparks.start()
#undef REDACTED
diff --git a/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
index 871c2cb1338e2..a6fb3e921e054 100644
--- a/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
+++ b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
@@ -8,12 +8,12 @@
map_name = "beach_bar"
safehouse_path = /datum/map_template/safehouse/mine
-/obj/item/reagent_containers/cup/glass/drinkingglass/virtual_domain
+/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/virtual_domain/Initialize(mapload, vol)
+/obj/item/reagent_containers/cup/glass/drinkingglass/filled/virtual_domain/Initialize(mapload, vol)
. = ..()
AddComponent(/datum/component/bitrunning_points, \
diff --git a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
index 4deacb4f9c592..01d58e3980381 100644
--- a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
+++ b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
@@ -28,11 +28,7 @@
/datum/reagent/gondola_mutation_toxin/virtual_domain
name = "Advanced Tranquility"
-
-/datum/reagent/gondola_mutation_toxin/virtual_domain/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/virtual_domain(), FALSE, TRUE)
+ gondola_disease = /datum/disease/transformation/gondola/virtual_domain
/datum/disease/transformation/gondola/virtual_domain
stage_prob = 9
diff --git a/code/modules/bitrunning/virtual_domain/domains/psyker_shuffle.dm b/code/modules/bitrunning/virtual_domain/domains/psyker_shuffle.dm
new file mode 100644
index 0000000000000..2ca32bce98340
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/psyker_shuffle.dm
@@ -0,0 +1,12 @@
+/datum/lazy_template/virtual_domain/psyker_shuffle
+ name = "Crate Chaos"
+ cost = BITRUNNER_COST_LOW
+ desc = "Sneak into an abandoned corner of the virtual world, where they store all of the crates. \
+ Warning -- Virtual domain does not support visual display. This mission must be completed using echolocation."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ help_text = "Getting used to echolocation may be difficult. Remember to walk slowly, and carefully inspect every crate you come across."
+ key = "psyker_shuffle"
+ map_name = "psyker_shuffle"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/bathroom
+ forced_outfit = /datum/outfit/echolocator
diff --git a/code/modules/bitrunning/virtual_domain/domains/psyker_zombies.dm b/code/modules/bitrunning/virtual_domain/domains/psyker_zombies.dm
new file mode 100644
index 0000000000000..6d545f7c652f1
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/psyker_zombies.dm
@@ -0,0 +1,14 @@
+/datum/lazy_template/virtual_domain/psyker_zombies
+ name = "Infected Domain"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Another neglected corner of the virtual world. This one had to be abandoned due to zombie virus. \
+ Warning -- Virtual domain does not support visual display. This mission must be completed using echolocation."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ help_text = "This once-beloved virtual domain has been corrupted by a virus, rendering it unstable, full of holes, and full of ZOMBIES! \
+ There should be a Mystery Box nearby to help get you armed. Get armed, and finish what the cyber-police started!"
+ key = "psyker_zombies"
+ map_name = "psyker_zombies"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/bathroom
+ forced_outfit = /datum/outfit/echolocator
+ extra_loot = list(/obj/item/radio/headset/psyker = 1) //Looks cool, might make your local burdened chaplain happy.
diff --git a/code/modules/bitrunning/virtual_domain/safehouses.dm b/code/modules/bitrunning/virtual_domain/safehouses.dm
index bb42f690ac7ec..6504d447f28c4 100644
--- a/code/modules/bitrunning/virtual_domain/safehouses.dm
+++ b/code/modules/bitrunning/virtual_domain/safehouses.dm
@@ -46,6 +46,9 @@
/datum/map_template/safehouse/ice
filename = "ice.dmm"
+/datum/map_template/safehouse/bathroom
+ filename = "bathroom.dmm"
+
/**
* Your safehouse here
* /datum/map_template/safehouse/your_type
diff --git a/code/modules/capture_the_flag/ctf_equipment.dm b/code/modules/capture_the_flag/ctf_equipment.dm
index 5ec412f9017e9..e822ae2dbb156 100644
--- a/code/modules/capture_the_flag/ctf_equipment.dm
+++ b/code/modules/capture_the_flag/ctf_equipment.dm
@@ -165,7 +165,7 @@
/obj/item/ammo_casing/energy/instakill
projectile_type = /obj/projectile/beam/instakill
- e_cost = 0
+ e_cost = 0 // Not possible to use the macro
select_name = "DESTROY"
/obj/projectile/beam/instakill
diff --git a/code/modules/capture_the_flag/ctf_game.dm b/code/modules/capture_the_flag/ctf_game.dm
index 4ab831f706094..1f6c44c293a95 100644
--- a/code/modules/capture_the_flag/ctf_game.dm
+++ b/code/modules/capture_the_flag/ctf_game.dm
@@ -151,6 +151,13 @@
new_team_member.prefs.safe_transfer_prefs_to(player_mob, is_antag = TRUE)
if(player_mob.dna.species.outfit_important_for_life)
player_mob.set_species(/datum/species/human)
+
+ player_mob.AddComponent( \
+ /datum/component/temporary_body, \
+ old_mind = new_team_member.mob.mind, \
+ old_body = new_team_member.mob.mind.current, \
+ )
+
player_mob.ckey = new_team_member.ckey
if(isnull(ctf_player_component))
var/datum/component/ctf_player/player_component = player_mob.mind.AddComponent(/datum/component/ctf_player, team, ctf_game, ammo_type)
diff --git a/code/modules/cargo/exports.dm b/code/modules/cargo/exports.dm
index a9d408c8699b9..44a628740bd52 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/gondolapod.dm b/code/modules/cargo/gondolapod.dm
index 5ee9f856823e1..f3dca7a3dca10 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/materials_market.dm b/code/modules/cargo/materials_market.dm
index dd56128241d4e..e2bedd2f19abf 100644
--- a/code/modules/cargo/materials_market.dm
+++ b/code/modules/cargo/materials_market.dm
@@ -11,7 +11,7 @@
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/list/exportable_material_items = list(
+ 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,
@@ -130,12 +130,18 @@
data["canOrderCargo"] = can_buy_via_budget
return data
-/obj/machinery/materials_market/ui_act(action, params)
+/obj/machinery/materials_market/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
- if(!isliving(usr))
+
+ //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"]
@@ -149,32 +155,41 @@
break
if(!material_bought)
CRASH("Invalid material name passed to materials market!")
- var/mob/living/living_user = usr
- var/datum/bank_account/account_payable = SSeconomy.get_dep_account(ACCOUNT_CAR)
- if(ordering_private)
- var/obj/item/card/id/used_id_card = living_user.get_idcard(TRUE)
+
+ //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)
-
- var/cost = SSstock_market.materials_prices[material_bought] * quantity
+ 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!")
- if(!account_payable)
- say("No bank account detected!")
- return
+ 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
+ // 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)
- if(order.orderer == living_user && order.orderer_rank == "Galactic Materials Market")
+ // 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)
@@ -182,29 +197,24 @@
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)
- account_payable.adjust_money(-(cost) , "Materials Market Purchase") //Add the extra price to the total
return
- account_payable.adjust_money(-(CARGO_CRATE_VALUE) , "Materials Market Purchase") //Here is where we factor in the base cost of a crate
+
//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 = living_user, \
- cost = SSstock_market.materials_prices[material_bought] * quantity, \
+ 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,
- reason = "",
- paying_account = account_payable,
- department_destination = null,
- coupon = null,
- charge_on_purchase = FALSE,
- manifest_can_fail = FALSE,
+ paying_account = is_ordering_private ? account_payable : null,
cost_type = "credit",
- can_be_cancelled = FALSE,
+ can_be_cancelled = FALSE
)
say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
SSshuttle.shopping_list += new_order
@@ -214,7 +224,6 @@
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."
diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm
index 7abe5f7601e62..784b870fea084 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
index 04b2eac4acf74..9744bdf7400ea 100644
--- a/code/modules/cargo/packs/stock_market_items.dm
+++ b/code/modules/cargo/packs/stock_market_items.dm
@@ -14,7 +14,7 @@
var/amount
/datum/supply_pack/market_materials/get_cost()
- for(var/datum/material/mat in SSstock_market.materials_prices)
+ for(var/datum/material/mat as anything in SSstock_market.materials_prices)
if(material == mat)
return SSstock_market.materials_prices[mat] * amount
diff --git a/code/modules/cargo/supplypod.dm b/code/modules/cargo/supplypod.dm
index 4373699495988..99e6472791bbb 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.
@@ -288,6 +291,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)
@@ -310,7 +315,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
@@ -482,7 +487,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)
@@ -532,7 +538,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/preferences.dm b/code/modules/client/preferences.dm
index bc643cc9d45a9..dab23c78e17ff 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -509,6 +509,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
character.icon_render_keys = list()
character.update_body(is_creating = TRUE)
+ SEND_SIGNAL(character, COMSIG_HUMAN_PREFS_APPLIED)
/// Returns whether the parent mob should have the random hardcore settings enabled. Assumes it has a mind.
/datum/preferences/proc/should_be_random_hardcore(datum/job/job, datum/mind/mind)
diff --git a/code/modules/clothing/head/hat.dm b/code/modules/clothing/head/hat.dm
index ccc8e903f3394..89244adcf31bc 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/helmet.dm b/code/modules/clothing/head/helmet.dm
index f8cd88923ec19..2960260255fbc 100644
--- a/code/modules/clothing/head/helmet.dm
+++ b/code/modules/clothing/head/helmet.dm
@@ -178,7 +178,7 @@
toggle_message = "You pull the visor down on"
alt_toggle_message = "You push the visor up on"
armor_type = /datum/armor/toggleable_riot
- flags_inv = HIDEEARS|HIDEFACE|HIDESNOUT
+ flags_inv = HIDEHAIR|HIDEEARS|HIDEFACE|HIDESNOUT
strip_delay = 80
actions_types = list(/datum/action/item_action/toggle)
visor_flags_inv = HIDEFACE|HIDESNOUT
diff --git a/code/modules/clothing/shoes/cowboy.dm b/code/modules/clothing/shoes/cowboy.dm
index 05792a72cbd96..0aa518bc1364d 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/suits/wintercoats.dm b/code/modules/clothing/suits/wintercoats.dm
index c970c7f9b0e11..7ade6df6f7c37 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 d4e41696219d8..d3ef481557bec 100644
--- a/code/modules/clothing/under/costume.dm
+++ b/code/modules/clothing/under/costume.dm
@@ -359,6 +359,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/events/ghost_role/revenant_event.dm b/code/modules/events/ghost_role/revenant_event.dm
index 27f3597a7ad2a..f739a3e13d46b 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 6cdc66472c1a7..abc57d33a0758 100644
--- a/code/modules/events/ghost_role/sentience.dm
+++ b/code/modules/events/ghost_role/sentience.dm
@@ -11,13 +11,13 @@ GLOBAL_LIST_INIT(high_priority_sentience, 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/bot/secbot/beepsky,
/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/wizard/petsplosion.dm b/code/modules/events/wizard/petsplosion.dm
index 70248bf0ebb6a..e670fa910a1f0 100644
--- a/code/modules/events/wizard/petsplosion.dm
+++ b/code/modules/events/wizard/petsplosion.dm
@@ -13,12 +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/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/experiment/types/scanning_fish.dm b/code/modules/experisci/experiment/types/scanning_fish.dm
index 8397801086994..52e58c9104ccb 100644
--- a/code/modules/experisci/experiment/types/scanning_fish.dm
+++ b/code/modules/experisci/experiment/types/scanning_fish.dm
@@ -58,7 +58,7 @@ GLOBAL_LIST_EMPTY(scanned_fish_by_techweb)
message += ""
for(var/atom_type in required_atoms)
for(var/obj/item/fish/fish_path as anything in scanned[atom_type])
- message += "[initial(fish_path.name)]"
+ message += "\n[initial(fish_path.name)]"
message += ""
examine_list += message
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index 06ac03d64aceb..71617b4de07b3 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -176,7 +176,7 @@
difficulty += comp.fish_source.calculate_difficulty(reward_path, rod, user, src)
difficulty = clamp(round(difficulty), 1, 100)
- if(HAS_TRAIT(user, TRAIT_REVEAL_FISH))
+ 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"
/**
@@ -203,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)
@@ -242,7 +244,8 @@
/datum/fishing_challenge/proc/on_line_deleted(datum/source)
SIGNAL_HANDLER
fishing_line = null
- send_alert(user.is_holding(used_rod) ? "line snapped" : "rod dropped")
+ ///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)
@@ -309,7 +312,7 @@
phase = BITING_PHASE
// Trashing animation
playsound(lure, 'sound/effects/fish_splash.ogg', 100)
- if(HAS_TRAIT(user, TRAIT_REVEAL_FISH))
+ 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!!!")
diff --git a/code/modules/food_and_drinks/machinery/deep_fryer.dm b/code/modules/food_and_drinks/machinery/deep_fryer.dm
index 73114812c2dc8..9b5d5b006988f 100644
--- a/code/modules/food_and_drinks/machinery/deep_fryer.dm
+++ b/code/modules/food_and_drinks/machinery/deep_fryer.dm
@@ -5,12 +5,13 @@
/// Global typecache of things which should never be fried.
GLOBAL_LIST_INIT(oilfry_blacklisted_items, typecacheof(list(
- /obj/item/reagent_containers/cup,
- /obj/item/reagent_containers/syringe,
- /obj/item/reagent_containers/condiment,
+ /obj/item/bodybag/bluespace,
/obj/item/delivery,
/obj/item/his_grace,
- /obj/item/bodybag/bluespace,
+ /obj/item/mod/control,
+ /obj/item/reagent_containers/condiment,
+ /obj/item/reagent_containers/cup,
+ /obj/item/reagent_containers/syringe,
)))
/obj/machinery/deepfryer
diff --git a/code/modules/food_and_drinks/machinery/microwave.dm b/code/modules/food_and_drinks/machinery/microwave.dm
index 8060bf150d36e..d52e2213a5b28 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
+/// 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,44 +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(
- 0,
- 3,
- -3,
- 4,
- -4,
- 2,
- -2,
- )
-
. = ..()
// 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)
@@ -152,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
@@ -165,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 ..()
@@ -232,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()
@@ -258,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)
@@ -270,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
@@ -330,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
@@ -351,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)
@@ -369,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.
@@ -380,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
@@ -388,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
@@ -411,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
@@ -429,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)
@@ -438,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)
@@ -469,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)
*/
@@ -490,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))
@@ -500,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)
@@ -508,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()
@@ -522,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!"))
@@ -534,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
@@ -562,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/recipes/soup_mixtures.dm b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
index ad2caa84ca625..b35bc128d321e 100644
--- a/code/modules/food_and_drinks/recipes/soup_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
@@ -85,7 +85,7 @@
if(!length(required_ingredients))
return
- // If a food item is supposed to be made, remove relevant ingredients from the pot, then make the item
+ // If a food item is supposed to be made, remove relevant ingredients from the pot, then make the item
if(!isnull(resulting_food_path))
var/list/tracked_ingredients
LAZYINITLIST(tracked_ingredients)
@@ -112,7 +112,7 @@
for(var/reagent_path as anything in required_reagents)
holder.add_reagent(reagent_path,(required_reagents[reagent_path])*(created_volume-ingredient_max_multiplier))
-
+
// This only happens if we're being instant reacted so let's just skip to what we really want
if(isnull(reaction))
testing("Soup reaction of type [type] instant reacted, cleaning up.")
@@ -851,9 +851,9 @@
/datum/chemical_reaction/food/soup/monkey
required_reagents = list(
- /datum/reagent/water = 25,
+ /datum/reagent/water = 20,
/datum/reagent/consumable/flour = 5,
- /datum/reagent/consumable/salt = 5,
+ /datum/reagent/water/salt = 10,
/datum/reagent/consumable/blackpepper = 5,
)
required_ingredients = list(
@@ -2103,7 +2103,7 @@
name = "\improper Hong Kong macaroni soup"
icon = 'icons/obj/food/martian.dmi'
icon_state = "hong_kong_macaroni"
- drink_type = MEAT | VEGETABLES | GRAIN
+ drink_type = MEAT | VEGETABLES | GRAIN
/datum/chemical_reaction/food/soup/hong_kong_macaroni
required_reagents = list(
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_guide.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_guide.dm
index ea6d10cb6686b..c02a7243368bd 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_guide.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_guide.dm
@@ -365,6 +365,11 @@
result = /obj/item/food/watermelonslice
category = CAT_SALAD
+/datum/crafting_recipe/food/knife/appleslice
+ reqs = list(/obj/item/food/grown/apple = 1)
+ result = /obj/item/food/appleslice
+ category = CAT_SALAD
+
/datum/crafting_recipe/food/knife/kamaboko_slice
reqs = list(/obj/item/food/kamaboko = 1)
result = /obj/item/food/kamaboko_slice
diff --git a/code/modules/hallucination/fake_sound.dm b/code/modules/hallucination/fake_sound.dm
index 0a783df3adadf..ec578f101d376 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 30b537afbc7e8..fa51e103cca33 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/hydroponics/grown/apple.dm b/code/modules/hydroponics/grown/apple.dm
index b994749c2f812..0079f63ec9023 100644
--- a/code/modules/hydroponics/grown/apple.dm
+++ b/code/modules/hydroponics/grown/apple.dm
@@ -26,6 +26,9 @@
tastes = list("apple" = 1)
distill_reagent = /datum/reagent/consumable/ethanol/hcider
+/obj/item/food/grown/apple/make_processable()
+ AddElement(/datum/element/processable, TOOL_KNIFE, /obj/item/food/appleslice, 5, 20, screentip_verb = "Slice")
+
// Gold Apple
/obj/item/seeds/apple/gold
name = "pack of golden apple seeds"
@@ -41,6 +44,9 @@
reagents_add = list(/datum/reagent/gold = 0.2, /datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
rarity = 40 // Alchemy!
+/obj/item/food/grown/apple/gold/make_processable()
+ return // You're going to break your knife!
+
/obj/item/food/grown/apple/gold
seed = /obj/item/seeds/apple/gold
name = "golden apple"
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index ebb22cc63a0af..22ea8d5334e3b 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/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
index a214eb48e0cc9..805c72a326749 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/library/bibles.dm b/code/modules/library/bibles.dm
index 5d3c6e276b429..8a10a058341fe 100644
--- a/code/modules/library/bibles.dm
+++ b/code/modules/library/bibles.dm
@@ -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/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
index 5e459758ee405..aa201292f72a2 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 0000000000000..75ac48c2467c4
--- /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/meteors/meteor_spawning.dm b/code/modules/meteors/meteor_spawning.dm
index eac365bc2a83e..97c359d03bfba 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/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm
index caee1bbac8d2b..4ceddb09854ca 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/mob/inventory.dm b/code/modules/mob/inventory.dm
index 7e288ebcee560..4660a1b054651 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 aebb54770d702..4ad94270837ea 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)
. = ..()
@@ -138,6 +148,7 @@
/mob/living/basic/death(gibbed)
. = ..()
if(basic_mob_flags & DEL_ON_DEATH)
+ ghostize(can_reenter_corpse = FALSE)
qdel(src)
else
health = 0
@@ -191,7 +202,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 +218,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 +285,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/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm
index 7871536135608..5682edf933907 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 0000000000000..f2e55cceb86b3
--- /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 8c5fc8eae37b3..30b3099487282 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/bee/bee_ai_behavior.dm b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm
index 017eac3cb51db..3c8b018cc935e 100644
--- a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm
+++ b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm
@@ -85,7 +85,7 @@
/datum/targetting_datum/basic/bee
-/datum/targetting_datum/basic/bee/can_attack(mob/living/owner, atom/target)
+/datum/targetting_datum/basic/bee/can_attack(mob/living/owner, atom/target, vision_range)
if(!isliving(target))
return FALSE
. = ..()
diff --git a/code/modules/mob/living/basic/farm_animals/chicken/chicken.dm b/code/modules/mob/living/basic/farm_animals/chicken/chicken.dm
index 89f5174f15e0d..99ff6b5274829 100644
--- a/code/modules/mob/living/basic/farm_animals/chicken/chicken.dm
+++ b/code/modules/mob/living/basic/farm_animals/chicken/chicken.dm
@@ -88,8 +88,6 @@ GLOBAL_VAR_INIT(chicken_count, 0)
planning_subtrees = list(
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/flee_target,
- /datum/ai_planning_subtree/target_retaliate,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/chicken,
)
diff --git a/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm b/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm
index cdfb0868f9e02..7db589334ffc3 100644
--- a/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm
+++ b/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm
@@ -62,7 +62,7 @@
///moonicorns will not attack people holding something that could tame them.
/datum/targetting_datum/basic/allow_items/moonicorn
-/datum/targetting_datum/basic/allow_items/moonicorn/can_attack(mob/living/living_mob, atom/the_target)
+/datum/targetting_datum/basic/allow_items/moonicorn/can_attack(mob/living/living_mob, atom/the_target, vision_range)
. = ..()
if(!.)
return FALSE
diff --git a/code/modules/mob/living/basic/farm_animals/deer.dm b/code/modules/mob/living/basic/farm_animals/deer.dm
index 445ad831e5951..7907e6684431f 100644
--- a/code/modules/mob/living/basic/farm_animals/deer.dm
+++ b/code/modules/mob/living/basic/farm_animals/deer.dm
@@ -34,9 +34,8 @@
/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,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
index f698e5015e8c7..db49ae8df9ffd 100644
--- a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
+++ b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
@@ -34,7 +34,10 @@
blood_volume = BLOOD_VOLUME_NORMAL
ai_controller = /datum/ai_controller/basic_controller/goat
-
+ /// How often will we develop an evil gleam in our eye?
+ var/gleam_delay = 20 SECONDS
+ /// Time until we can next gleam evilly
+ COOLDOWN_DECLARE(gleam_cooldown)
/// List of stuff (flora) that we want to eat
var/static/list/edibles = list(
/obj/structure/alien/resin/flower_bud,
@@ -85,15 +88,14 @@
return COMPONENT_HOSTILE_NO_ATTACK
-/// If we are being attacked by someone who we are already retaliating against, give a nice fluff message.
+/// If we are being attacked by someone, give a nice fluff message. But only once in a while.
/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)
+ if (!COOLDOWN_FINISHED(src, gleam_cooldown))
return
-
visible_message(
span_danger("[src] gets an evil-looking gleam in [p_their()] eye."),
)
+ COOLDOWN_START(src, gleam_cooldown, gleam_delay)
/// 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)
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
index 41fc448a3b6ac..f8463b1967537 100644
--- a/code/modules/mob/living/basic/farm_animals/goat/goat_ai.dm
+++ b/code/modules/mob/living/basic/farm_animals/goat/goat_ai.dm
@@ -9,6 +9,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
+ /datum/ai_planning_subtree/capricious_retaliate, // Capricious like Capra, get it?
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
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
index 0638163d03248..19d50fb38097a 100644
--- a/code/modules/mob/living/basic/farm_animals/goat/goat_subtypes.dm
+++ b/code/modules/mob/living/basic/farm_animals/goat/goat_subtypes.dm
@@ -6,7 +6,7 @@
. = ..()
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
+ . += 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 0000000000000..b0926b41811a1
--- /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 0000000000000..814e56487bf54
--- /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 0000000000000..c62307542777d
--- /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 20166d8139191..94133336c4d49 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/pig.dm b/code/modules/mob/living/basic/farm_animals/pig.dm
index 46655e503411a..c0ad3f6b349be 100644
--- a/code/modules/mob/living/basic/farm_animals/pig.dm
+++ b/code/modules/mob/living/basic/farm_animals/pig.dm
@@ -29,11 +29,11 @@
ai_controller = /datum/ai_controller/basic_controller/pig
/mob/living/basic/pig/Initialize(mapload)
+ . = ..()
AddElement(/datum/element/pet_bonus, "oinks!")
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/ai_flee_while_injured)
make_tameable()
- . = ..()
///wrapper for the tameable component addition so you can have non tamable cow subtypes
/mob/living/basic/pig/proc/make_tameable()
@@ -47,7 +47,7 @@
/datum/ai_controller/basic_controller/pig
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
diff --git a/code/modules/mob/living/basic/farm_animals/pony.dm b/code/modules/mob/living/basic/farm_animals/pony.dm
index 4bc09391cb718..e1765f0fdbac6 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!")
@@ -72,7 +106,7 @@
/datum/ai_controller/basic_controller/pony
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
@@ -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 92d40e0228e2d..19fd0120ba7e6 100644
--- a/code/modules/mob/living/basic/farm_animals/rabbit.dm
+++ b/code/modules/mob/living/basic/farm_animals/rabbit.dm
@@ -49,8 +49,7 @@
/datum/ai_controller/basic_controller/rabbit
blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE,
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/farm_animals/sheep.dm b/code/modules/mob/living/basic/farm_animals/sheep.dm
index 691f1db14e3ce..ac320d8080419 100644
--- a/code/modules/mob/living/basic/farm_animals/sheep.dm
+++ b/code/modules/mob/living/basic/farm_animals/sheep.dm
@@ -81,8 +81,7 @@
/datum/ai_controller/basic_controller/sheep
blackboard = list(
- BB_BASIC_MOB_FLEEING = TRUE,
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/health_adjustment.dm b/code/modules/mob/living/basic/health_adjustment.dm
index 4ae129d9d6616..9644a1a629905 100644
--- a/code/modules/mob/living/basic/health_adjustment.dm
+++ b/code/modules/mob/living/basic/health_adjustment.dm
@@ -18,9 +18,7 @@
return . - bruteloss
/mob/living/basic/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- if(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(BRUTE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_brute_loss(amount, forced, required_bodytype))
return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -28,9 +26,7 @@
. = 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(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(BURN, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -38,9 +34,7 @@
. = 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(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(OXY, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ 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)
@@ -48,9 +42,7 @@
. = 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(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(TOX, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_tox_loss(amount, forced, required_biotype))
return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -58,7 +50,7 @@
. = adjust_health(amount * damage_coeff[TOX] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/basic/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
- if(on_damage_adjustment(CLONE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_clone_loss(amount, forced, required_biotype))
return 0
if(forced)
. = adjust_health(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -66,7 +58,7 @@
. = 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(on_damage_adjustment(STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_stamina_loss(amount, forced, required_biotype))
return 0
. = staminaloss
if(forced)
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 0000000000000..b2d4d8b4d2947
--- /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 0000000000000..6f31b3ce7c523
--- /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
index 05ef707faf4cf..3c60a9b653c32 100644
--- a/code/modules/mob/living/basic/heretic/flesh_worm.dm
+++ b/code/modules/mob/living/basic/heretic/flesh_worm.dm
@@ -38,7 +38,7 @@
AddElement(\
/datum/element/amputating_limbs,\
surgery_time = 0 SECONDS,\
- surgery_verb = "tearing",\
+ surgery_verb = "tears",\
minimum_stat = CONSCIOUS,\
snip_chance = 10,\
target_zones = GLOB.arm_zones,\
diff --git a/code/modules/mob/living/basic/heretic/heretic_summon.dm b/code/modules/mob/living/basic/heretic/heretic_summon.dm
index cf1bcf80aad83..a99508ff65968 100644
--- a/code/modules/mob/living/basic/heretic/heretic_summon.dm
+++ b/code/modules/mob/living/basic/heretic/heretic_summon.dm
@@ -22,6 +22,10 @@
response_harm_simple = "tear"
death_message = "implodes into itself."
+ unsuitable_atmos_damage = 0
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
+
combat_mode = TRUE
ai_controller = null
speak_emote = list("screams")
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 b53bbe147d3df..0976576255305 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/maid_in_the_mirror/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/rust_walker.dm b/code/modules/mob/living/basic/heretic/rust_walker.dm
new file mode 100644
index 0000000000000..070326c0281ae
--- /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 0000000000000..960f875365bfa
--- /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 0000000000000..79c9ee9bd583e
--- /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 0000000000000..28ddd38324ac6
--- /dev/null
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm
@@ -0,0 +1,117 @@
+/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_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/jungle/mega_arachnid/mega_arachnid_ai.dm b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
index c88178135dc54..8964ebf837ec1 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 c489d6c888d4a..a997a8d266b1a 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 58efaf1f81b46..fe1c4150315b0 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/legion/legion_ai.dm b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
index 6b3525cb32ab6..310bbeb708ebb 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,
)
@@ -32,7 +31,7 @@
/// Target nearby friendlies if they are hurt (and are not themselves Legions)
/datum/targetting_datum/basic/attack_until_dead/legion
-/datum/targetting_datum/basic/attack_until_dead/legion/faction_check(mob/living/living_mob, mob/living/the_target)
+/datum/targetting_datum/basic/attack_until_dead/legion/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
if (!living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly))
return FALSE
if (istype(the_target, living_mob.type))
diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
index d47ae15b9759c..a048fe77ab146 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
@@ -39,7 +39,7 @@
AddElement(/datum/element/basic_eating, food_types = target_foods)
AddElement(\
/datum/element/amputating_limbs,\
- surgery_verb = "snipping",\
+ surgery_verb = "begins snipping",\
target_zones = GLOB.arm_zones,\
)
charge = new(src)
@@ -74,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 8e4dfe9e29463..7b6926fca04e7 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 0000000000000..da833437715c6
--- /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 0000000000000..cfc359bd54fcc
--- /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 0000000000000..6a04742d47152
--- /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(datum/ai_controller/controller, 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 0000000000000..e3a091f6f0e49
--- /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 a25234817f31e..348bbcfcaa7d0 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/minebots/minebot_ai.dm b/code/modules/mob/living/basic/minebots/minebot_ai.dm
index a4b082f5dd1bb..33e9821dbc4bb 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/fox.dm b/code/modules/mob/living/basic/pets/fox.dm
index 578a64ba08dd8..150abdc676bc7 100644
--- a/code/modules/mob/living/basic/pets/fox.dm
+++ b/code/modules/mob/living/basic/pets/fox.dm
@@ -38,9 +38,9 @@
/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
+ BB_ALWAYS_IGNORE_FACTION = TRUE,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/ours_or_smaller,
+ BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -61,12 +61,6 @@
/datum/ai_planning_subtree/random_speech/fox,
)
-// Foxes will attack other station pets regardless of faction.
-/datum/targetting_datum/basic/of_size/ours_or_smaller/ignore_faction
-
-/datum/targetting_datum/basic/of_size/ours_or_smaller/ignore_faction/faction_check(mob/living/living_mob, mob/living/the_target)
- return FALSE
-
// The captain's fox, Renault
/mob/living/basic/pet/fox/renault
name = "Renault"
diff --git a/code/modules/mob/living/simple_animal/friendly/sloth.dm b/code/modules/mob/living/basic/pets/sloth.dm
similarity index 50%
rename from code/modules/mob/living/simple_animal/friendly/sloth.dm
rename to code/modules/mob/living/basic/pets/sloth.dm
index 8e308c58d12d2..aaeeb218b2c0c 100644
--- a/code/modules/mob/living/simple_animal/friendly/sloth.dm
+++ b/code/modules/mob/living/basic/pets/sloth.dm
@@ -1,63 +1,68 @@
-GLOBAL_DATUM(cargo_sloth, /mob/living/simple_animal/sloth)
+GLOBAL_DATUM(cargo_sloth, /mob/living/basic/sloth)
-/mob/living/simple_animal/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")
- 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)
+ 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
- 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
+ speed = 10 // speed is fucking weird man. they aren't fast though don't worry
+ butcher_results = list(/obj/item/food/meat/slab = 3)
- footstep_type = FOOTSTEP_MOB_CLAW
+ ai_controller = /datum/ai_controller/basic_controller/sloth
-/mob/living/simple_animal/sloth/Initialize(mapload)
+/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
- if(!GLOB.cargo_sloth && mapload && is_station_level(z))
- GLOB.cargo_sloth = src
+ GLOB.cargo_sloth = src
-/mob/living/simple_animal/sloth/Destroy()
+/mob/living/basic/sloth/Destroy()
if(GLOB.cargo_sloth == src)
GLOB.cargo_sloth = null
return ..()
-//Cargo Sloth
-/mob/living/simple_animal/sloth/paperwork
+/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
-//Cargo Sloth 2
-
-/mob/living/simple_animal/sloth/citrus
+/mob/living/basic/sloth/citrus
name = "Citrus"
desc = "Cargo's pet sloth. She's dressed in a horrible sweater."
icon_state = "cool_sloth"
@@ -66,3 +71,26 @@ GLOBAL_DATUM(cargo_sloth, /mob/living/simple_animal/sloth)
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,
+ BB_FLEE_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/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 414f28a1e9af5..924cf854276d1 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 d220325ca15f5..fc6997896b0d2 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 b30970145352b..9d967c5a8b009 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm
@@ -9,8 +9,8 @@
*/
/datum/ai_controller/basic_controller/carp
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends()
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -35,8 +35,9 @@
*/
/datum/ai_controller/basic_controller/carp/pet
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends()
+ BB_ALWAYS_IGNORE_FACTION = TRUE,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
)
ai_traits = STOP_MOVING_WHEN_PULLED
planning_subtrees = list(
@@ -78,8 +79,8 @@
*/
/datum/ai_controller/basic_controller/carp/passive
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends()
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
)
ai_traits = STOP_MOVING_WHEN_PULLED
planning_subtrees = list(
diff --git a/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm b/code/modules/mob/living/basic/space_fauna/changeling/flesh_spider.dm
new file mode 100644
index 0000000000000..c73b008d6b48b
--- /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 c2d8c751cde90..741ac27712f8f 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/eyeball/eyeball_ai_subtree.dm b/code/modules/mob/living/basic/space_fauna/eyeball/eyeball_ai_subtree.dm
index a3c8e22071d5d..b3033b27a4bd3 100644
--- a/code/modules/mob/living/basic/space_fauna/eyeball/eyeball_ai_subtree.dm
+++ b/code/modules/mob/living/basic/space_fauna/eyeball/eyeball_ai_subtree.dm
@@ -22,7 +22,7 @@
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_the_blind, BB_BLIND_TARGET, BB_EYE_DAMAGE_THRESHOLD)
-/datum/targetting_datum/basic/eyeball/can_attack(mob/living/owner, atom/target)
+/datum/targetting_datum/basic/eyeball/can_attack(mob/living/owner, atom/target, vision_range)
. = ..()
if(!.)
return FALSE
diff --git a/code/modules/mob/living/basic/space_fauna/ghost.dm b/code/modules/mob/living/basic/space_fauna/ghost.dm
index 058c2d2cd3f40..35afaade990c8 100644
--- a/code/modules/mob/living/basic/space_fauna/ghost.dm
+++ b/code/modules/mob/living/basic/space_fauna/ghost.dm
@@ -93,7 +93,7 @@
/datum/ai_controller/basic_controller/ghost
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/space_fauna/lightgeist.dm b/code/modules/mob/living/basic/space_fauna/lightgeist.dm
index c70588f4502b4..9ab6ffe677855 100644
--- a/code/modules/mob/living/basic/space_fauna/lightgeist.dm
+++ b/code/modules/mob/living/basic/space_fauna/lightgeist.dm
@@ -97,7 +97,7 @@
/// Type of limb we can heal
var/required_bodytype = BODYTYPE_ORGANIC
-/datum/targetting_datum/lightgeist/can_attack(mob/living/living_mob, mob/living/target)
+/datum/targetting_datum/lightgeist/can_attack(mob/living/living_mob, mob/living/target, vision_range)
if (!isliving(target) || target.stat == DEAD)
return FALSE
if (!(heal_biotypes & target.mob_biotypes))
diff --git a/code/modules/mob/living/basic/space_fauna/mushroom.dm b/code/modules/mob/living/basic/space_fauna/mushroom.dm
index e6d47e2db5cc9..96280db29235b 100644
--- a/code/modules/mob/living/basic/space_fauna/mushroom.dm
+++ b/code/modules/mob/living/basic/space_fauna/mushroom.dm
@@ -73,7 +73,7 @@
stat_attack = DEAD
///we only attacked another mushrooms
-/datum/targetting_datum/basic/mushroom/faction_check(mob/living/living_mob, mob/living/the_target)
+/datum/targetting_datum/basic/mushroom/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
return !living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly)
/datum/ai_planning_subtree/find_and_hunt_target/mushroom_food
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 ef92c7b3b76b1..8a7013b962373 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,8 +1,7 @@
/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,
+ BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
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 0000000000000..b3c6935c92efe
--- /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 0de2be1d17c80..3ea62afd9f80d 100644
--- a/code/modules/antagonists/revenant/revenant_abilities.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
@@ -1,123 +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"
@@ -171,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
@@ -184,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
@@ -202,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
@@ -227,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
@@ -242,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)
@@ -267,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)
@@ -316,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)
@@ -357,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 +313,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 0000000000000..0eeec231973ee
--- /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 0000000000000..b8bb05db48414
--- /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 0000000000000..a9e17a9b305ff
--- /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 0000000000000..7dd391c17e477
--- /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/spaceman.dm b/code/modules/mob/living/basic/space_fauna/spaceman.dm
index 0ad2c4cfb8ce4..5851bfa531b37 100644
--- a/code/modules/mob/living/basic/space_fauna/spaceman.dm
+++ b/code/modules/mob/living/basic/space_fauna/spaceman.dm
@@ -32,7 +32,7 @@
/datum/ai_controller/basic_controller/spaceman
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction,
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
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 f998b7b53cd90..7dd61b72070df 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
@@ -30,7 +30,7 @@
/// Used by Araneus, who only attacks those who attack first. He is house-trained and will not web up the HoS office.
/datum/ai_controller/basic_controller/giant_spider/retaliate
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
planning_subtrees = list(
@@ -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 6df2eb427f411..8cb7d8398bf36 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 4bd773f6d0abb..53b48129e2ed4 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider.dm
@@ -49,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 7108983c31051..c949b438683cb 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 bce35146ecfc6..d2ea5e8a831d0 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/wumborian_fugu/inflation.dm b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/inflation.dm
index d459648892b01..a9e2b538bdd74 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 9d3a09c5348f2..e405ee3755abf 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 abe5c25117b6d..bb81fd29c4d50 100644
--- a/code/modules/mob/living/basic/vermin/crab.dm
+++ b/code/modules/mob/living/basic/vermin/crab.dm
@@ -77,9 +77,9 @@
///The basic ai controller for crabs
/datum/ai_controller/basic_controller/crab
blackboard = list(
- BB_BASIC_MOB_FLEEING = FALSE,
+ BB_ALWAYS_IGNORE_FACTION = 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,
+ BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm
index 46e175c5323bb..87d549ac5234e 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/basic/vermin/space_bat.dm b/code/modules/mob/living/basic/vermin/space_bat.dm
index 3ce1b41173dd5..232febf0f97d2 100644
--- a/code/modules/mob/living/basic/vermin/space_bat.dm
+++ b/code/modules/mob/living/basic/vermin/space_bat.dm
@@ -42,7 +42,7 @@
///Controller for space bats, has nothing unique, just retaliation.
/datum/ai_controller/basic_controller/space_bat
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(),
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_traits = STOP_MOVING_WHEN_PULLED
diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm
index cbec374449c0d..bc0d2e3d8e81a 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."))
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 8c2111511236b..bbfd68f8186b8 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/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 199590e3dc637..eb72b273453a4 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 ..()
diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm
index cdd6900c22b6e..a8c008bc41d84 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 d297296a4ec74..040bf76a3db02 100644
--- a/code/modules/mob/living/carbon/damage_procs.dm
+++ b/code/modules/mob/living/carbon/damage_procs.dm
@@ -56,9 +56,7 @@
return amount
/mob/living/carbon/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- if(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(BRUTE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_brute_loss(amount, forced, required_bodytype))
return 0
if(amount > 0)
. = take_overall_damage(brute = amount, updating_health = updating_health, forced = forced, required_bodytype = required_bodytype)
@@ -75,9 +73,7 @@
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 0
- if(on_damage_adjustment(BURN, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
return 0
if(amount > 0)
. = take_overall_damage(burn = amount, updating_health = updating_health, forced = forced, required_bodytype = required_bodytype)
@@ -94,11 +90,7 @@
return adjustFireLoss(diff, updating_health, forced, required_bodytype)
/mob/living/carbon/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
- if(!forced && (status_flags & GODMODE))
- return 0
- if(!forced && !(mob_biotypes & required_biotype))
- return 0
- if(on_damage_adjustment(TOX, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ 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
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index 9d8f7fb5f63fd..5cf1a02a9aabb 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -487,7 +487,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
else if(old_species.exotic_bloodtype && !exotic_bloodtype)
human_who_gained_species.dna.blood_type = random_blood_type()
- //Resets blood if it is excessively high for some reason
+ //Resets blood if it is excessively high so they don't gib
normalize_blood(human_who_gained_species)
if(ishuman(human_who_gained_species))
@@ -1117,9 +1117,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
// ATTACK PROCS //
//////////////////
-/datum/species/proc/spec_updatehealth(mob/living/carbon/human/H)
- return
-
/datum/species/proc/help(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style)
if(SEND_SIGNAL(target, COMSIG_CARBON_PRE_HELP, user, attacker_style) & COMPONENT_BLOCK_HELP_ACT)
return TRUE
@@ -1874,12 +1871,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/human.dm b/code/modules/mob/living/carbon/human/human.dm
index ba110f26e01c6..a4cab428111e3 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -923,7 +923,6 @@
/mob/living/carbon/human/updatehealth()
. = ..()
- dna?.species.spec_updatehealth(src)
if(HAS_TRAIT(src, TRAIT_IGNOREDAMAGESLOWDOWN))
remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown)
remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying)
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 d49da7e84d658..ccb36715e852e 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -481,12 +481,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)
@@ -515,8 +510,7 @@ 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]"
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 58a46da81a488..349742a15e536 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 f17abdfc868ff..570aa91dc342c 100644
--- a/code/modules/mob/living/carbon/human/species_types/android.dm
+++ b/code/modules/mob/living/carbon/human/species_types/android.dm
@@ -50,3 +50,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 e59aa11375ff8..1599b0a23c015 100644
--- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm
+++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm
@@ -6,6 +6,17 @@
TRAIT_NOBREATH,
TRAIT_NOHUNGER,
TRAIT_USES_SKINTONES,
+ TRAIT_ADVANCEDTOOLUSER, // Normally applied by brain but we don't have one
+ TRAIT_LITERATE,
+ TRAIT_CAN_STRIP,
+ )
+ bodypart_overrides = list(
+ BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left,
+ BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right,
+ BODY_ZONE_HEAD = /obj/item/bodypart/head/dullahan,
+ BODY_ZONE_L_LEG = /obj/item/bodypart/leg/left,
+ BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right,
+ BODY_ZONE_CHEST = /obj/item/bodypart/chest,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
mutant_bodyparts = list("wings" = "None")
@@ -20,11 +31,9 @@
/// The dullahan relay that's associated with the owner, used to handle many things such as talking and hearing.
var/obj/item/dullahan_relay/my_head
-
/// Did our owner's first client connection get handled yet? Useful for when some proc needs to be called once we're sure that a client has moved into our owner, like for Dullahans.
var/owner_first_client_connection_handled = FALSE
-
/datum/species/dullahan/check_roundstart_eligible()
if(check_holidays(HALLOWEEN))
return TRUE
@@ -33,62 +42,59 @@
/datum/species/dullahan/on_species_gain(mob/living/carbon/human/human, datum/species/old_species)
. = ..()
human.lose_hearing_sensitivity(TRAIT_GENERIC)
- var/obj/item/bodypart/head/head = human.get_bodypart(BODY_ZONE_HEAD)
-
- if(head)
- head.drop_limb()
+ RegisterSignal(human, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(on_gained_part))
- if(!QDELETED(head)) //drop_limb() deletes the limb if no drop location exists and character setup dummies are located in nullspace.
- head.throwforce = 25
- my_head = new /obj/item/dullahan_relay(head, human)
- human.put_in_hands(head)
- head.show_organs_on_examine = FALSE
- head.speech_span = null // so we don't look roboty when talking through it
+ var/obj/item/bodypart/head/head = human.get_bodypart(BODY_ZONE_HEAD)
+ head?.drop_limb()
+ if(QDELETED(head)) //drop_limb() deletes the limb if no drop location exists and character setup dummies are located in nullspace.
+ return
+ my_head = new /obj/item/dullahan_relay(head, human)
+ human.put_in_hands(head)
+
+ // We want to give the head some boring old eyes just so it doesn't look too jank on the head sprite.
+ head.eyes = new /obj/item/organ/internal/eyes(head)
+ head.eyes.eye_color_left = human.eye_color_left
+ head.eyes.eye_color_right = human.eye_color_right
+ human.update_body()
+ head.update_icon_dropped()
+ human.set_safe_hunger_level()
+ RegisterSignal(head, COMSIG_QDELETING, PROC_REF(on_head_destroyed))
- // We want to give the head some boring old eyes just so it doesn't look too jank on the head sprite.
- head.eyes = new /obj/item/organ/internal/eyes(head)
- head.eyes.eye_color_left = human.eye_color_left
- head.eyes.eye_color_right = human.eye_color_right
- human.update_body()
- head.update_icon_dropped()
+/// If we gained a new body part, it had better not be a head
+/datum/species/dullahan/proc/on_gained_part(mob/living/carbon/human/dullahan, obj/item/bodypart/part)
+ SIGNAL_HANDLER
+ if (part.body_zone != BODY_ZONE_HEAD)
+ return
+ my_head = null
+ dullahan.investigate_log("has been gibbed by having an illegal head put on [dullahan.p_their()] shoulders.", INVESTIGATE_DEATHS)
+ dullahan.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.
- human.set_safe_hunger_level()
+/// If our head is destroyed, so are we
+/datum/species/dullahan/proc/on_head_destroyed()
+ SIGNAL_HANDLER
+ var/mob/living/human = my_head?.owner
+ if (QDELETED(human))
+ return // guess we already died
+ my_head = null
+ human.investigate_log("has been gibbed by the loss of [human.p_their()] head.", INVESTIGATE_DEATHS)
+ human.gib(DROP_ALL_REMAINS)
/datum/species/dullahan/on_species_loss(mob/living/carbon/human/human)
. = ..()
-
if(my_head)
var/obj/item/bodypart/head/detached_head = my_head.loc
+ UnregisterSignal(detached_head, COMSIG_QDELETING)
my_head.owner = null
QDEL_NULL(my_head)
if(detached_head)
qdel(detached_head)
+ UnregisterSignal(human, COMSIG_CARBON_ATTACH_LIMB)
human.regenerate_limb(BODY_ZONE_HEAD, FALSE)
human.become_hearing_sensitive()
prevent_perspective_change = FALSE
human.reset_perspective(human)
-/datum/species/dullahan/spec_life(mob/living/carbon/human/human, seconds_per_tick, times_fired)
- . = ..()
- 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(DROP_ALL_REMAINS)
- return
-
- if(my_head.loc.name != human.real_name && istype(my_head.loc, /obj/item/bodypart/head))
- var/obj/item/bodypart/head/detached_head = my_head.loc
- detached_head.real_name = human.real_name
- detached_head.name = human.real_name
- detached_head.brain.name = "[human.name]'s brain"
-
- var/obj/item/bodypart/head/illegal_head = human.get_bodypart(BODY_ZONE_HEAD)
- 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(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)
if(eyes)
@@ -114,6 +120,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 \
@@ -210,11 +218,15 @@
. = ..()
if(!new_owner)
return INITIALIZE_HINT_QDEL
+ var/obj/item/bodypart/head/detached_head = loc
+ if (!istype(detached_head))
+ return INITIALIZE_HINT_QDEL
owner = new_owner
START_PROCESSING(SSobj, src)
RegisterSignal(owner, COMSIG_CLICK_SHIFT, PROC_REF(examinate_check))
RegisterSignal(owner, COMSIG_CARBON_REGENERATE_LIMBS, PROC_REF(unlist_head))
RegisterSignal(owner, COMSIG_LIVING_REVIVE, PROC_REF(retrieve_head))
+ RegisterSignal(owner, COMSIG_HUMAN_PREFS_APPLIED, PROC_REF(update_prefs_name))
become_hearing_sensitive(ROUNDSTART_TRAIT)
/obj/item/dullahan_relay/Destroy()
@@ -223,9 +235,20 @@
return ..()
/obj/item/dullahan_relay/process()
- if(!istype(loc, /obj/item/bodypart/head) || QDELETED(owner))
- . = PROCESS_KILL
- qdel(src)
+ if(istype(loc, /obj/item/bodypart/head) && !QDELETED(owner))
+ return
+ qdel(src)
+ return PROCESS_KILL
+
+/// Updates our names after applying name prefs
+/obj/item/dullahan_relay/proc/update_prefs_name(mob/living/carbon/human/wearer)
+ SIGNAL_HANDLER
+ var/obj/item/bodypart/head/detached_head = loc
+ if (!istype(detached_head))
+ return // It's so over
+ detached_head.real_name = wearer.real_name
+ detached_head.name = wearer.real_name
+ detached_head.brain.name = "[wearer.name]'s brain"
/obj/item/dullahan_relay/proc/examinate_check(mob/user, atom/source)
SIGNAL_HANDLER
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 c4e7eaa5889e0..232067041012d 100644
--- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm
+++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm
@@ -55,24 +55,24 @@
QDEL_NULL(ethereal_light)
return ..()
-/datum/species/ethereal/on_species_gain(mob/living/carbon/new_ethereal, datum/species/old_species, pref_load)
+/datum/species/ethereal/on_species_gain(mob/living/carbon/human/new_ethereal, datum/species/old_species, pref_load)
. = ..()
if(!ishuman(new_ethereal))
return
- var/mob/living/carbon/human/ethereal = new_ethereal
- default_color = ethereal.dna.features["ethcolor"]
+ default_color = new_ethereal.dna.features["ethcolor"]
fixed_hair_color = default_color
r1 = GETREDPART(default_color)
g1 = GETGREENPART(default_color)
b1 = GETBLUEPART(default_color)
- RegisterSignal(ethereal, COMSIG_ATOM_EMAG_ACT, PROC_REF(on_emag_act))
- RegisterSignal(ethereal, COMSIG_ATOM_EMP_ACT, PROC_REF(on_emp_act))
- RegisterSignal(ethereal, COMSIG_LIGHT_EATER_ACT, PROC_REF(on_light_eater))
- RegisterSignal(ethereal, COMSIG_HIT_BY_SABOTEUR, PROC_REF(on_saboteur))
- ethereal_light = ethereal.mob_light(light_type = /obj/effect/dummy/lighting_obj/moblight/species)
- spec_updatehealth(ethereal)
+ RegisterSignal(new_ethereal, COMSIG_ATOM_EMAG_ACT, PROC_REF(on_emag_act))
+ RegisterSignal(new_ethereal, COMSIG_ATOM_EMP_ACT, PROC_REF(on_emp_act))
+ RegisterSignal(new_ethereal, COMSIG_HIT_BY_SABOTEUR, PROC_REF(on_saboteur))
+ RegisterSignal(new_ethereal, COMSIG_LIGHT_EATER_ACT, PROC_REF(on_light_eater))
+ RegisterSignal(new_ethereal, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(refresh_light_color))
+ ethereal_light = new_ethereal.mob_light(light_type = /obj/effect/dummy/lighting_obj/moblight/species)
+ refresh_light_color(new_ethereal)
new_ethereal.set_safe_hunger_level()
- update_mail_goodies(ethereal)
+ update_mail_goodies(new_ethereal)
var/obj/item/organ/internal/heart/ethereal/ethereal_heart = new_ethereal.get_organ_slot(ORGAN_SLOT_HEART)
ethereal_heart.ethereal_color = default_color
@@ -82,10 +82,13 @@
limb.update_limb(is_creating = TRUE)
/datum/species/ethereal/on_species_loss(mob/living/carbon/human/former_ethereal, datum/species/new_species, pref_load)
- UnregisterSignal(former_ethereal, COMSIG_ATOM_EMAG_ACT)
- UnregisterSignal(former_ethereal, COMSIG_ATOM_EMP_ACT)
- UnregisterSignal(former_ethereal, COMSIG_LIGHT_EATER_ACT)
- UnregisterSignal(former_ethereal, COMSIG_HIT_BY_SABOTEUR)
+ UnregisterSignal(former_ethereal, list(
+ COMSIG_ATOM_EMAG_ACT,
+ COMSIG_ATOM_EMP_ACT,
+ COMSIG_HIT_BY_SABOTEUR,
+ COMSIG_LIGHT_EATER_ACT,
+ COMSIG_LIVING_HEALTH_UPDATE,
+ ))
QDEL_NULL(ethereal_light)
return ..()
@@ -109,9 +112,9 @@
features["ethcolor"] = GLOB.color_list_ethereal[pick(GLOB.color_list_ethereal)]
return features
-/datum/species/ethereal/spec_updatehealth(mob/living/carbon/human/ethereal)
- . = ..()
- if(!ethereal_light)
+/datum/species/ethereal/proc/refresh_light_color(mob/living/carbon/human/ethereal)
+ SIGNAL_HANDLER
+ if(isnull(ethereal_light))
return
if(default_color != ethereal.dna.features["ethcolor"])
var/new_color = ethereal.dna.features["ethcolor"]
@@ -138,39 +141,38 @@
ethereal.set_facial_haircolor(dead_color, override = TRUE, update = FALSE)
ethereal.set_haircolor(dead_color, override = TRUE, update = TRUE)
-/datum/species/ethereal/proc/on_emp_act(mob/living/carbon/human/H, severity, protection)
+/datum/species/ethereal/proc/on_emp_act(mob/living/carbon/human/source, severity, protection)
SIGNAL_HANDLER
if(protection & EMP_PROTECT_SELF)
return
EMPeffect = TRUE
- spec_updatehealth(H)
- to_chat(H, span_notice("You feel the light of your body leave you."))
+ refresh_light_color(source)
+ to_chat(source, span_notice("You feel the light of your body leave you."))
switch(severity)
if(EMP_LIGHT)
- addtimer(CALLBACK(src, PROC_REF(stop_emp), H), 10 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE) //We're out for 10 seconds
+ addtimer(CALLBACK(src, PROC_REF(stop_emp), source), 10 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE) //We're out for 10 seconds
if(EMP_HEAVY)
- addtimer(CALLBACK(src, PROC_REF(stop_emp), H), 20 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE) //We're out for 20 seconds
+ addtimer(CALLBACK(src, PROC_REF(stop_emp), source), 20 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE) //We're out for 20 seconds
-/datum/species/ethereal/proc/on_saboteur(datum/source, disrupt_duration)
+/datum/species/ethereal/proc/on_saboteur(mob/living/carbon/human/source, disrupt_duration)
SIGNAL_HANDLER
- var/mob/living/carbon/human/our_target = source
EMPeffect = TRUE
- spec_updatehealth(our_target)
- to_chat(our_target, span_warning("Something inside of you crackles in a bad way."))
- our_target.take_bodypart_damage(burn = 3, wound_bonus = CANT_WOUND)
- addtimer(CALLBACK(src, PROC_REF(stop_emp), our_target), disrupt_duration, TIMER_UNIQUE|TIMER_OVERRIDE)
+ refresh_light_color(source)
+ to_chat(source, span_warning("Something inside of you crackles in a bad way."))
+ source.take_bodypart_damage(burn = 3, wound_bonus = CANT_WOUND)
+ addtimer(CALLBACK(src, PROC_REF(stop_emp), source), disrupt_duration, TIMER_UNIQUE|TIMER_OVERRIDE)
return COMSIG_SABOTEUR_SUCCESS
-/datum/species/ethereal/proc/on_emag_act(mob/living/carbon/human/H, mob/user)
+/datum/species/ethereal/proc/on_emag_act(mob/living/carbon/human/source, mob/user)
SIGNAL_HANDLER
if(emageffect)
return FALSE
emageffect = TRUE
if(user)
- to_chat(user, span_notice("You tap [H] on the back with your card."))
- H.visible_message(span_danger("[H] starts flickering in an array of colors!"))
- handle_emag(H)
- addtimer(CALLBACK(src, PROC_REF(stop_emag), H), 2 MINUTES) //Disco mode for 2 minutes! This doesn't affect the ethereal at all besides either annoying some players, or making someone look badass.
+ to_chat(user, span_notice("You tap [source] on the back with your card."))
+ source.visible_message(span_danger("[source] starts flickering in an array of colors!"))
+ handle_emag(source)
+ addtimer(CALLBACK(src, PROC_REF(stop_emag), source), 2 MINUTES) //Disco mode for 2 minutes! This doesn't affect the ethereal at all besides either annoying some players, or making someone look badass.
return TRUE
/// Special handling for getting hit with a light eater
@@ -179,23 +181,22 @@
source.emp_act(EMP_LIGHT)
return COMPONENT_BLOCK_LIGHT_EATER
-/datum/species/ethereal/proc/stop_emp(mob/living/carbon/human/H)
+/datum/species/ethereal/proc/stop_emp(mob/living/carbon/human/ethereal)
EMPeffect = FALSE
- spec_updatehealth(H)
- to_chat(H, span_notice("You feel more energized as your shine comes back."))
-
+ refresh_light_color(ethereal)
+ to_chat(ethereal, span_notice("You feel more energized as your shine comes back."))
-/datum/species/ethereal/proc/handle_emag(mob/living/carbon/human/H)
+/datum/species/ethereal/proc/handle_emag(mob/living/carbon/human/ethereal)
if(!emageffect)
return
current_color = GLOB.color_list_ethereal[pick(GLOB.color_list_ethereal)]
- spec_updatehealth(H)
- addtimer(CALLBACK(src, PROC_REF(handle_emag), H), 5) //Call ourselves every 0.5 seconds to change color
+ refresh_light_color(ethereal)
+ addtimer(CALLBACK(src, PROC_REF(handle_emag), ethereal), 0.5 SECONDS)
-/datum/species/ethereal/proc/stop_emag(mob/living/carbon/human/H)
+/datum/species/ethereal/proc/stop_emag(mob/living/carbon/human/ethereal)
emageffect = FALSE
- spec_updatehealth(H)
- H.visible_message(span_danger("[H] stops flickering and goes back to their normal state!"))
+ refresh_light_color(ethereal)
+ ethereal.visible_message(span_danger("[ethereal] stops flickering and goes back to their normal state!"))
/datum/species/ethereal/get_features()
var/list/features = ..()
@@ -211,6 +212,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 \
@@ -274,7 +280,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,
@@ -289,6 +295,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 4114d6810dd9b..a6a229d0b2c46 100644
--- a/code/modules/mob/living/carbon/human/species_types/felinid.dm
+++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm
@@ -138,6 +138,10 @@
cat_ears.color = human_for_preview.hair_color
human_for_preview.update_body()
+/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 7f1d111211569..c36252bbcb2dc 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 9df07a8e49635..19427def4d2ed 100644
--- a/code/modules/mob/living/carbon/human/species_types/golems.dm
+++ b/code/modules/mob/living/carbon/human/species_types/golems.dm
@@ -56,6 +56,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/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
index 30c374033fca4..bd662d63ad142 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -192,6 +192,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)
@@ -492,6 +497,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
@@ -661,6 +670,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 d94d3390b866e..e02813826b13a 100644
--- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
@@ -87,6 +87,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 \
@@ -153,6 +157,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
*/
@@ -182,6 +190,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 4d39ece432971..f8aee4d83b17c 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 0447b1f18695d..a337b8cee0578 100644
--- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
@@ -60,6 +60,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 5263101680b88..9facc517c1c79 100644
--- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
@@ -144,6 +144,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 cd0a0c99449f6..0190996567d13 100644
--- a/code/modules/mob/living/carbon/human/species_types/podpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
@@ -74,6 +74,10 @@
if(chem.type == /datum/reagent/toxin/plantbgone)
affected.adjustToxLoss(3 * REM * seconds_per_tick)
+/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 687e304938135..c628d5ffef688 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 db753732fdcd6..9e2d59d03007b 100644
--- a/code/modules/mob/living/carbon/human/species_types/skeletons.dm
+++ b/code/modules/mob/living/carbon/human/species_types/skeletons.dm
@@ -55,6 +55,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 41a6166812865..32fc0161ea21f 100644
--- a/code/modules/mob/living/carbon/human/species_types/snail.dm
+++ b/code/modules/mob/living/carbon/human/species_types/snail.dm
@@ -25,6 +25,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 46d507e4999de..c07f0478c03ea 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -69,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 28ebe93bd7b08..d07badff51ef2 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!\""
@@ -197,7 +201,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 355f5bf61ca1c..f3b03cdab35e0 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -458,7 +458,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
@@ -688,7 +689,7 @@
var/datum/reagent/bits = bile
if(istype(bits, /datum/reagent/consumable))
var/datum/reagent/consumable/goodbit = bile
- fullness += goodbit.get_nutriment_factor() * goodbit.volume / goodbit.metabolization_rate
+ fullness += goodbit.get_nutriment_factor(src) * goodbit.volume / goodbit.metabolization_rate
continue
fullness += 0.6 * bits.volume / bits.metabolization_rate //not food takes up space
diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm
index 82425b27a5ba9..45a35bb6e158b 100644
--- a/code/modules/mob/living/damage_procs.dm
+++ b/code/modules/mob/living/damage_procs.dm
@@ -159,23 +159,24 @@
return TRUE
-/// Should be called by any adjustXLoss proc to send signalling information, returns a bit flag which may indicate that we don't want to make any adjustment
-/mob/living/proc/on_damage_adjustment(damage_type, amount, forced)
- return SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_DAMAGE, damage_type, amount, forced)
-
/mob/living/proc/getBruteLoss()
return bruteloss
-/mob/living/proc/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
+/mob/living/proc/can_adjust_brute_loss(amount, forced, required_bodytype)
if(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(BRUTE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ 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 FALSE
+ return 0
if(updating_health)
updatehealth()
@@ -195,19 +196,24 @@
/mob/living/proc/getOxyLoss()
return oxyloss
-/mob/living/proc/adjustOxyLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL, required_respiration_type = ALL)
+/mob/living/proc/can_adjust_oxy_loss(amount, forced, required_biotype, required_respiration_type)
if(!forced)
if(status_flags & GODMODE)
- return 0
+ 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
- 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 0
- else
- if(!(affected_lungs.respiration_type & required_respiration_type)) // otherwise use the lungs' respiration_type
- return 0
- if(on_damage_adjustment(OXY, amount, forced) & COMPONENT_IGNORE_CHANGE)
+/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)
@@ -240,13 +246,16 @@
/mob/living/proc/getToxLoss()
return toxloss
-/mob/living/proc/adjustToxLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
- 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(on_damage_adjustment(TOX, amount, forced) & COMPONENT_IGNORE_CHANGE)
- return 0
- if(!forced && !(mob_biotypes & required_biotype))
+ 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
@@ -271,10 +280,15 @@
/mob/living/proc/getFireLoss()
return fireloss
-/mob/living/proc/adjustFireLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
+/mob/living/proc/can_adjust_fire_loss(amount, forced, required_bodytype)
if(!forced && (status_flags & GODMODE))
- return 0
- if(on_damage_adjustment(BURN, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ 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)
@@ -298,10 +312,15 @@
/mob/living/proc/getCloneLoss()
return cloneloss
-/mob/living/proc/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype = ALL)
+/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 0
- if(on_damage_adjustment(CLONE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ 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)
@@ -336,10 +355,15 @@
/mob/living/proc/getStaminaLoss()
return staminaloss
+/mob/living/proc/can_adjust_stamina_loss(amount, forced, required_biotype)
+ if(!forced && (!(mob_biotypes & required_biotype) || status_flags & GODMODE))
+ return FALSE
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ADJUST_STAMINA_DAMAGE, STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ return FALSE
+ return TRUE
+
/mob/living/proc/adjustStaminaLoss(amount, updating_stamina = TRUE, forced = FALSE, required_biotype = ALL)
- if(!forced && ((status_flags & GODMODE) || required_biotype && !(mob_biotypes & required_biotype)))
- return 0
- if(on_damage_adjustment(STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_stamina_loss(amount, forced, required_biotype))
return 0
. = staminaloss
staminaloss = clamp((staminaloss + (amount * CONFIG_GET(number/damage_multiplier))), 0, max_stamina)
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index 9b0a078c1acc8..356653745e80b 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)
@@ -114,7 +117,7 @@
for(var/bile in reagents.reagent_list)
var/datum/reagent/consumable/bits = bile
if(bits)
- fullness += bits.get_nutriment_factor() * bits.volume / bits.metabolization_rate
+ fullness += bits.get_nutriment_factor(src) * bits.volume / bits.metabolization_rate
return fullness
/**
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index ba5c04ec4602a..d2ccf6e8f87d0 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1233,7 +1233,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))
@@ -1414,6 +1414,7 @@
/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,
@@ -1431,7 +1432,6 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet/cat,
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index aa3743a44b411..f31fd60b131fc 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -452,7 +452,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_say.dm b/code/modules/mob/living/living_say.dm
index 50aacd2fdf0e1..26d0dd01a4db4 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -382,8 +382,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
@@ -399,7 +398,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/robot/inventory.dm b/code/modules/mob/living/silicon/robot/inventory.dm
index 7df2e7d339065..be713b429a6b5 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/simple_animal/damage_procs.dm b/code/modules/mob/living/simple_animal/damage_procs.dm
index 0c06049288d09..9640dbb9de440 100644
--- a/code/modules/mob/living/simple_animal/damage_procs.dm
+++ b/code/modules/mob/living/simple_animal/damage_procs.dm
@@ -19,7 +19,7 @@
toggle_ai(AI_ON)
/mob/living/simple_animal/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
- if(on_damage_adjustment(BRUTE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_brute_loss(amount, forced, required_bodytype))
return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -27,7 +27,7 @@
. = 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(on_damage_adjustment(BURN, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_fire_loss(amount, forced, required_bodytype))
return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -35,7 +35,7 @@
. = 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(on_damage_adjustment(OXY, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ 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)
@@ -43,7 +43,7 @@
. = 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(on_damage_adjustment(TOX, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_tox_loss(amount, forced, required_biotype))
return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -51,7 +51,7 @@
. = adjustHealth(amount * damage_coeff[TOX] * CONFIG_GET(number/damage_multiplier), updating_health, forced)
/mob/living/simple_animal/adjustCloneLoss(amount, updating_health = TRUE, forced = FALSE, required_biotype)
- if(on_damage_adjustment(CLONE, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_clone_loss(amount, forced, required_biotype))
return 0
if(forced)
. = adjustHealth(amount * CONFIG_GET(number/damage_multiplier), updating_health, forced)
@@ -59,7 +59,7 @@
. = 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(on_damage_adjustment(STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
+ if(!can_adjust_stamina_loss(amount, forced, required_biotype))
return 0
if(forced)
staminaloss = max(0, min(max_staminaloss, staminaloss + amount))
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 598695588bc57..7f6a398b9dafc 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 69c2ac3e8cf73..90391dff58546 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/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
index ea5e0685b53fe..4ca35a673577e 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 dc4d8a004c6e7..d2fbfc33c877d 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]")
- 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/hostile/constructs/constructs.dm b/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
index 23f7590dc8e24..31150a4dc89c1 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 6f5952382c67d..0000000000000
--- 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(drop_bitflags=DROP_BRAIN)
- if(drop_bitflags & DROP_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 39dfe8f7d899e..0000000000000
--- 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 090709d052744..0000000000000
--- a/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm
+++ /dev/null
@@ -1,120 +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/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/mook.dm b/code/modules/mob/living/simple_animal/hostile/jungle/mook.dm
deleted file mode 100644
index 444635f2dc344..0000000000000
--- 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/mining_mobs/ice_demon.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm
deleted file mode 100644
index 9d84fe2e1fe82..0000000000000
--- 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/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/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm
index 6059e7efcc22b..b1c8c55cacc66 100644
--- a/code/modules/mob/living/simple_animal/hostile/ooze.dm
+++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm
@@ -64,7 +64,7 @@
var/consumption_amount = min(reagents.get_reagent_amount(reagent.type), ooze_metabolism_modifier * REAGENTS_METABOLISM * seconds_per_tick)
if(istype(reagent, /datum/reagent/consumable))
var/datum/reagent/consumable/consumable = reagent
- nutrition_change += consumption_amount * consumable.get_nutriment_factor()
+ nutrition_change += consumption_amount * consumable.get_nutriment_factor(src)
reagents.remove_reagent(reagent.type, consumption_amount)
adjust_ooze_nutrition(nutrition_change)
diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
index f87e78009a972..c536da22cd146 100644
--- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
+++ b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
@@ -1,5 +1,7 @@
/// The darkness threshold for space dragon when choosing a color
#define DARKNESS_THRESHOLD 50
+/// Any interactions executed by the space dragon
+#define DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION "space dragon interaction"
/**
* # Space Dragon
@@ -71,8 +73,6 @@
var/gust_tiredness = 30
/// Determines whether or not Space Dragon is in the middle of using wing gust. If set to true, prevents him from moving and doing certain actions.
var/using_special = FALSE
- /// Determines whether or not Space Dragon is currently tearing through a wall.
- var/tearing_wall = FALSE
/// The color of the space dragon.
var/chosen_color
/// Minimum devastation damage dealt coefficient based on max health
@@ -86,6 +86,7 @@
AddElement(/datum/element/simple_flying)
add_traits(list(TRAIT_SPACEWALK, TRAIT_FREE_HYPERSPACE_MOVEMENT, TRAIT_NO_FLOATING_ANIM, TRAIT_HEALS_FROM_CARP_RIFTS), INNATE_TRAIT)
AddElement(/datum/element/content_barfer)
+ RegisterSignal(src, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(before_attack))
/mob/living/simple_animal/hostile/space_dragon/Login()
. = ..()
@@ -121,41 +122,65 @@
eaten.forceMove(loc)
eaten.Paralyze(5 SECONDS)
-/mob/living/simple_animal/hostile/space_dragon/AttackingTarget()
+/mob/living/simple_animal/hostile/space_dragon/proc/before_attack(datum/source, atom/target)
+ SIGNAL_HANDLER
if(using_special)
- return
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
if(target == src)
to_chat(src, span_warning("You almost bite yourself, but then decide against it."))
- return
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ if(DOING_INTERACTION(src, DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION)) // patience grasshopper
+ target.balloon_alert(src, "finish current action first!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ if(ismecha(target))
+ target.take_damage(50, BRUTE, MELEE, 1)
+ return // don't block the rest of the attack chain
+
if(iswallturf(target))
- if(tearing_wall)
- return
- tearing_wall = TRUE
- var/turf/closed/wall/thewall = target
- to_chat(src, span_warning("You begin tearing through the wall..."))
- playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
- var/timetotear = 40
- if(istype(target, /turf/closed/wall/r_wall))
- timetotear = 120
- if(do_after(src, timetotear, target = thewall))
- if(isopenturf(thewall))
- return
- thewall.dismantle_wall(1)
- playsound(src, 'sound/effects/meteorimpact.ogg', 100, TRUE)
- tearing_wall = FALSE
- return
+ INVOKE_ASYNC(src, PROC_REF(tear_down_wall), target)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
if(isliving(target)) //Swallows corpses like a snake to regain health.
- var/mob/living/L = target
- if(L.stat == DEAD)
- to_chat(src, span_warning("You begin to swallow [L] whole..."))
- if(do_after(src, 30, target = L))
- if(eat(L))
- adjustHealth(-L.maxHealth * 0.25)
- return
- . = ..()
- if(ismecha(target))
- var/obj/vehicle/sealed/mecha/M = target
- M.take_damage(50, BRUTE, MELEE, 1)
+ var/mob/living/living_target = target
+ if(living_target.stat != DEAD)
+ return // go ham on slapping the shit out of them buddy
+
+ INVOKE_ASYNC(src, PROC_REF(eat_this_corpse), living_target)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Handles tearing down a wall. Returns TRUE if successful, FALSE otherwise.
+/mob/living/simple_animal/hostile/space_dragon/proc/tear_down_wall(turf/closed/wall/wall_target)
+ to_chat(src, span_warning("You begin tearing through the wall..."))
+ playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
+
+ var/time_to_tear = 4 SECONDS
+ if(istype(wall_target, /turf/closed/wall/r_wall))
+ time_to_tear = 12 SECONDS
+
+ if(!do_after(src, time_to_tear, target = wall_target, interaction_key = DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION))
+ return FALSE
+
+ if(isopenturf(wall_target))
+ return FALSE // well the thing was destroyed while we were sleeping so that's nice, but we didn't successfully tear it down. whatever
+
+ wall_target.dismantle_wall(devastated = TRUE)
+ playsound(src, 'sound/effects/meteorimpact.ogg', 100, TRUE)
+ return TRUE
+
+/// Handles eating a corpse, giving us a bit of health back. Returns TRUE if we were sucessful in eating, FALSE otherwise.
+/mob/living/simple_animal/hostile/space_dragon/proc/eat_this_corpse(mob/living/corpse)
+ to_chat(src, span_warning("You begin to swallow the body of [corpse] whole..."))
+
+ if(!do_after(src, 3 SECONDS, target = corpse, interaction_key = DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION))
+ return FALSE
+ if(!eat(corpse))
+ return FALSE
+
+ adjustHealth(-(corpse.maxHealth * 0.25))
+ return TRUE
/mob/living/simple_animal/hostile/space_dragon/ranged_secondary_attack(atom/target, modifiers)
if(using_special)
@@ -416,3 +441,4 @@
mind.add_antag_datum(/datum/antagonist/space_dragon)
#undef DARKNESS_THRESHOLD
+#undef DOAFTER_SOURCE_SPACE_DRAGON_INTERACTION
diff --git a/code/modules/mob/living/simple_animal/revenant.dm b/code/modules/mob/living/simple_animal/revenant.dm
deleted file mode 100644
index 6e2ec11afeac8..0000000000000
--- 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 3b16ab685eb27..85c146b29c827 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,9 +448,6 @@
/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
@@ -560,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/mob.dm b/code/modules/mob/mob.dm
index c5a6fd74e7251..cb6469d16a836 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -937,10 +937,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
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index b0b3014f5b1f9..6d94df367802b 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -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_update_icons.dm b/code/modules/mob/mob_update_icons.dm
index 8a6464ee1825a..b8b84f8782afe 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/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 09ce3b3c65c8c..290177f5baf50 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/mob_corpses.dm b/code/modules/mob_spawn/corpses/mob_corpses.dm
index 5dd709bde66aa..476c3f70a8491 100644
--- a/code/modules/mob_spawn/corpses/mob_corpses.dm
+++ b/code/modules/mob_spawn/corpses/mob_corpses.dm
@@ -350,3 +350,7 @@
/datum/outfit/prey_pod_victim
name = "Prey Pod Victim"
uniform = /obj/item/clothing/under/rank/rnd/roboticist
+
+/obj/effect/mob_spawn/corpse/human/cyber_police
+ name = "Dead Cyber Police"
+ outfit = /datum/outfit/cyber_police
diff --git a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
index 060f7e178be2f..ce02c6894aee8 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/spider_roles.dm b/code/modules/mob_spawn/ghost_roles/spider_roles.dm
index e3dbea6b1ba7b..fb3d470f5aa80 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 96a75842b1b61..8ab475dce015d 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 b282ea7d8b2c3..3bfff7855f03c 100644
--- a/code/modules/mob_spawn/mob_spawn.dm
+++ b/code/modules/mob_spawn/mob_spawn.dm
@@ -281,6 +281,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)
@@ -298,6 +303,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/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm
index cf6cc4b2bc273..b724892da7e1c 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 bba55b4474efb..6e3e69cdccfc5 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 9c097b2fb9b02..dc9538cf3580d 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/pai/defense.dm b/code/modules/pai/defense.dm
index b5eb177fddcf4..75c437c2546e0 100644
--- a/code/modules/pai/defense.dm
+++ b/code/modules/pai/defense.dm
@@ -65,7 +65,7 @@
/mob/living/silicon/pai/ignite_mob(silent)
return FALSE
-/mob/living/silicon/pai/proc/take_holo_damage(type, amount)
+/mob/living/silicon/pai/proc/take_holo_damage(amount)
holochassis_health = clamp((holochassis_health - amount), -50, HOLOCHASSIS_MAX_HEALTH)
if(holochassis_health < 0)
fold_in(force = TRUE)
@@ -73,23 +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)
- if(on_damage_adjustment(BRUTE, amount, forced) & COMPONENT_IGNORE_CHANGE)
- return 0
- 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)
- if(on_damage_adjustment(BURN, amount, forced) & COMPONENT_IGNORE_CHANGE)
- return 0
- return take_holo_damage(amount)
-
-/mob/living/silicon/pai/adjustStaminaLoss(amount, updating_stamina, forced = FALSE, required_biotype)
- if(on_damage_adjustment(STAMINA, amount, forced) & COMPONENT_IGNORE_CHANGE)
- return 0
- 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/pai.dm b/code/modules/pai/pai.dm
index f38017dedf64d..3998470f74878 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -235,6 +235,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/fax.dm b/code/modules/paperwork/fax.dm
index 055ac8bba4e6a..a03c79f44d066 100644
--- a/code/modules/paperwork/fax.dm
+++ b/code/modules/paperwork/fax.dm
@@ -6,6 +6,7 @@ GLOBAL_VAR_INIT(nt_fax_department, pick("NT HR Department", "NT Legal Department
icon = 'icons/obj/machines/fax.dmi'
icon_state = "fax"
density = TRUE
+ anchored_tabletop_offset = 6
power_channel = AREA_USAGE_EQUIP
max_integrity = 100
pass_flags = PASSTABLE
diff --git a/code/modules/photography/camera/other.dm b/code/modules/photography/camera/other.dm
index e9aa5d94e597a..166517d055fba 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/plumbing/plumbers/acclimator.dm b/code/modules/plumbing/plumbers/acclimator.dm
index da5c4529a4230..6b7a8caba4ac8 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/teleporter.dm b/code/modules/plumbing/plumbers/teleporter.dm
index a8e6e7ae3ac55..7bb098eae4e06 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_tool_act.dm b/code/modules/power/apc/apc_tool_act.dm
index 8884786a4eb0f..55c9b34a35086 100644
--- a/code/modules/power/apc/apc_tool_act.dm
+++ b/code/modules/power/apc/apc_tool_act.dm
@@ -161,26 +161,25 @@
if(machine_stat & BROKEN)
balloon_alert(user, "frame is too damaged!")
return FALSE
- return list("mode" = RCD_WALLFRAME, "delay" = 2 SECONDS, "cost" = 1)
+ return list("delay" = 2 SECONDS, "cost" = 1)
if(!cell)
if(machine_stat & MAINT)
balloon_alert(user, "no board for a cell!")
return FALSE
- return list("mode" = RCD_WALLFRAME, "delay" = 5 SECONDS, "cost" = 10)
+ return list("delay" = 5 SECONDS, "cost" = 10)
balloon_alert(user, "has both board and cell!")
return FALSE
-/obj/machinery/power/apc/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, passed_mode)
- if(!(the_rcd.upgrade & RCD_UPGRADE_SIMPLE_CIRCUITS) || passed_mode != RCD_WALLFRAME)
+/obj/machinery/power/apc/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, list/rcd_data)
+ if(!(the_rcd.upgrade & RCD_UPGRADE_SIMPLE_CIRCUITS) || rcd_data["[RCD_DESIGN_MODE]"] != RCD_WALLFRAME)
return FALSE
if(!has_electronics)
if(machine_stat & BROKEN)
balloon_alert(user, "frame is too damaged!")
return
- user.visible_message(span_notice("[user] fabricates a circuit and places it into [src]."))
balloon_alert(user, "control board placed")
has_electronics = TRUE
locked = TRUE
@@ -194,8 +193,7 @@
C.forceMove(src)
cell = C
chargecount = 0
- user.visible_message(span_notice("[user] fabricates a weak power cell and places it into [src]."), \
- span_warning("Your [the_rcd.name] whirrs with strain as you create a weak power cell and place it into [src]!"))
+ balloon_alert(user, "power cell installed")
update_appearance()
return TRUE
diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm
index aa7b216d39182..0d0bb09c9975d 100644
--- a/code/modules/power/cell.dm
+++ b/code/modules/power/cell.dm
@@ -23,7 +23,7 @@
///Current charge in cell units
var/charge = 0
///Maximum charge in cell units
- var/maxcharge = 1000
+ var/maxcharge = STANDARD_CELL_CHARGE
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*7, /datum/material/glass=SMALL_MATERIAL_AMOUNT*0.5)
grind_results = list(/datum/reagent/lithium = 15, /datum/reagent/iron = 5, /datum/reagent/silicon = 5)
///If the cell has been booby-trapped by injecting it with plasma. Chance on use() to explode.
@@ -31,7 +31,7 @@
///If the power cell was damaged by an explosion, chance for it to become corrupted and function the same as rigged.
var/corrupted = FALSE
///how much power is given every tick in a recharger
- var/chargerate = 100
+ var/chargerate = STANDARD_CELL_CHARGE * 0.1
///If true, the cell will state it's maximum charge in it's description
var/ratingdesc = TRUE
///If it's a grown that acts as a battery, add a wire overlay to it.
@@ -51,7 +51,7 @@
create_reagents(5, INJECTABLE | DRAINABLE)
if (override_maxcharge)
maxcharge = override_maxcharge
- rating = max(round(maxcharge / 10000, 1), 1)
+ rating = max(round(maxcharge / (STANDARD_CELL_CHARGE * 10), 1), 1)
if(!charge)
charge = maxcharge
if(empty)
@@ -82,7 +82,7 @@
. = COMPONENT_ITEM_CHARGED
if(prob(80))
- maxcharge -= 200
+ maxcharge -= STANDARD_CELL_CHARGE * 0.2
if(maxcharge <= 1) // Div by 0 protection
maxcharge = 1
@@ -281,7 +281,7 @@
/obj/item/stock_parts/cell/crap
name = "\improper Nanotrasen brand rechargeable AA battery"
desc = "You can't top the plasma top." //TOTALLY TRADEMARK INFRINGEMENT
- maxcharge = 500
+ maxcharge = STANDARD_CELL_CHARGE * 0.5
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*0.4)
/obj/item/stock_parts/cell/crap/empty
@@ -290,18 +290,18 @@
/obj/item/stock_parts/cell/upgraded
name = "upgraded power cell"
desc = "A power cell with a slightly higher capacity than normal!"
- maxcharge = 2500
+ maxcharge = STANDARD_CELL_CHARGE * 2.5
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*0.5)
- chargerate = 1000
+ chargerate = STANDARD_CELL_CHARGE
/obj/item/stock_parts/cell/upgraded/plus
name = "upgraded power cell+"
desc = "A power cell with an even higher capacity than the base model!"
- maxcharge = 5000
+ maxcharge = STANDARD_CELL_CHARGE * 5
/obj/item/stock_parts/cell/secborg
name = "security borg rechargeable D battery"
- maxcharge = 600 //600 max charge / 100 charge per shot = six shots
+ maxcharge = STANDARD_CELL_CHARGE * 0.6
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*0.4)
/obj/item/stock_parts/cell/secborg/empty
@@ -309,38 +309,38 @@
/obj/item/stock_parts/cell/mini_egun
name = "miniature energy gun power cell"
- maxcharge = 600
+ maxcharge = STANDARD_CELL_CHARGE * 0.6
/obj/item/stock_parts/cell/hos_gun
name = "X-01 multiphase energy gun power cell"
- maxcharge = 1200
+ maxcharge = STANDARD_CELL_CHARGE * 1.2
/obj/item/stock_parts/cell/pulse //200 pulse shots
name = "pulse rifle power cell"
- maxcharge = 40000
- chargerate = 1500
+ maxcharge = STANDARD_CELL_CHARGE * 40
+ chargerate = STANDARD_CELL_CHARGE * 1.5
/obj/item/stock_parts/cell/pulse/carbine //25 pulse shots
name = "pulse carbine power cell"
- maxcharge = 5000
+ maxcharge = STANDARD_CELL_CHARGE * 5
/obj/item/stock_parts/cell/pulse/pistol //10 pulse shots
name = "pulse pistol power cell"
- maxcharge = 2000
+ maxcharge = STANDARD_CELL_CHARGE * 2
/obj/item/stock_parts/cell/ninja
name = "black power cell"
icon_state = "bscell"
- maxcharge = 10000
+ maxcharge = STANDARD_CELL_CHARGE * 10
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*0.6)
- chargerate = 2000
+ chargerate = STANDARD_CELL_CHARGE * 2
/obj/item/stock_parts/cell/high
name = "high-capacity power cell"
icon_state = "hcell"
- maxcharge = 10000
+ maxcharge = STANDARD_CELL_CHARGE * 10
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*0.6)
- chargerate = 1500
+ chargerate = STANDARD_CELL_CHARGE * 1.5
/obj/item/stock_parts/cell/high/empty
empty = TRUE
@@ -348,9 +348,9 @@
/obj/item/stock_parts/cell/super
name = "super-capacity power cell"
icon_state = "scell"
- maxcharge = 20000
+ maxcharge = STANDARD_CELL_CHARGE * 20
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT * 3)
- chargerate = 2000
+ chargerate = STANDARD_CELL_CHARGE * 2
/obj/item/stock_parts/cell/super/empty
empty = TRUE
@@ -358,9 +358,9 @@
/obj/item/stock_parts/cell/hyper
name = "hyper-capacity power cell"
icon_state = "hpcell"
- maxcharge = 30000
+ maxcharge = STANDARD_CELL_CHARGE * 30
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT * 4)
- chargerate = 3000
+ chargerate = STANDARD_CELL_CHARGE * 3
/obj/item/stock_parts/cell/hyper/empty
empty = TRUE
@@ -369,9 +369,9 @@
name = "bluespace power cell"
desc = "A rechargeable transdimensional power cell."
icon_state = "bscell"
- maxcharge = 40000
+ maxcharge = STANDARD_CELL_CHARGE * 40
custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*6)
- chargerate = 4000
+ chargerate = STANDARD_CELL_CHARGE * 4
/obj/item/stock_parts/cell/bluespace/empty
empty = TRUE
@@ -392,7 +392,7 @@
desc = "An alien power cell that produces energy seemingly out of nowhere."
icon = 'icons/obj/antags/abductor.dmi'
icon_state = "cell"
- maxcharge = 50000
+ maxcharge = STANDARD_CELL_CHARGE * 50
ratingdesc = FALSE
/obj/item/stock_parts/cell/infinite/abductor/Initialize(mapload)
@@ -405,7 +405,7 @@
icon = 'icons/obj/service/hydroponics/harvest.dmi'
icon_state = "potato"
charge = 100
- maxcharge = 300
+ maxcharge = STANDARD_CELL_CHARGE * 0.3
charge_light_type = null
connector_type = null
custom_materials = null
@@ -415,7 +415,7 @@
/obj/item/stock_parts/cell/emproof
name = "\improper EMP-proof cell"
desc = "An EMP-proof cell."
- maxcharge = 500
+ maxcharge = STANDARD_CELL_CHARGE * 0.5
/obj/item/stock_parts/cell/emproof/Initialize(mapload)
AddElement(/datum/element/empprotection, EMP_PROTECT_SELF)
@@ -433,15 +433,15 @@
icon = 'icons/mob/simple/slimes.dmi'
icon_state = "yellow slime extract"
custom_materials = null
- maxcharge = 5000
+ maxcharge = STANDARD_CELL_CHARGE * 5
charge_light_type = null
connector_type = "slimecore"
/obj/item/stock_parts/cell/beam_rifle
name = "beam rifle capacitor"
desc = "A high powered capacitor that can provide huge amounts of energy in an instant."
- maxcharge = 50000
- chargerate = 5000 //Extremely energy intensive
+ maxcharge = STANDARD_CELL_CHARGE * 50
+ chargerate = STANDARD_CELL_CHARGE * 5 //Extremely energy intensive
/obj/item/stock_parts/cell/beam_rifle/corrupt()
return
@@ -455,7 +455,7 @@
/obj/item/stock_parts/cell/emergency_light
name = "miniature power cell"
desc = "A tiny power cell with a very low power capacity. Used in light fixtures to power them in the event of an outage."
- maxcharge = 120 //Emergency lights use 0.2 W per tick, meaning ~10 minutes of emergency power from a cell
+ maxcharge = STANDARD_CELL_CHARGE * 0.12 //Emergency lights use 0.2 W per tick, meaning ~10 minutes of emergency power from a cell
custom_materials = list(/datum/material/glass = SMALL_MATERIAL_AMOUNT*0.2)
w_class = WEIGHT_CLASS_TINY
@@ -470,7 +470,7 @@
name = "crystal power cell"
desc = "A very high power cell made from crystallized plasma"
icon_state = "crystal_cell"
- maxcharge = 50000
+ maxcharge = STANDARD_CELL_CHARGE * 50
chargerate = 0
charge_light_type = null
connector_type = "crystal"
@@ -478,7 +478,7 @@
grind_results = null
/obj/item/stock_parts/cell/inducer_supply
- maxcharge = 5000
+ maxcharge = STANDARD_CELL_CHARGE * 5
#undef CELL_DRAIN_TIME
#undef CELL_POWER_GAIN
diff --git a/code/modules/power/floodlight.dm b/code/modules/power/floodlight.dm
index f36c9b1303866..7155ce6aa2383 100644
--- a/code/modules/power/floodlight.dm
+++ b/code/modules/power/floodlight.dm
@@ -120,6 +120,12 @@
return
..()
+/obj/structure/floodlight_frame/completed
+ name = "floodlight frame"
+ desc = "A bare metal frame that looks like a floodlight. Requires a light tube to complete."
+ icon_state = "floodlight_c3"
+ state = FLOODLIGHT_NEEDS_LIGHTS
+
/obj/machinery/power/floodlight
name = "floodlight"
desc = "A pole with powerful mounted lights on it. Due to its high power draw, it must be powered by a direct connection to a wire node."
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index 99dd421452cf3..be2cccbcde43c 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/shotgun.dm b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
index ac3cc859b63b8..da5d4161286e0 100644
--- a/code/modules/projectiles/ammunition/ballistic/shotgun.dm
+++ b/code/modules/projectiles/ammunition/ballistic/shotgun.dm
@@ -135,6 +135,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/_energy.dm b/code/modules/projectiles/ammunition/energy/_energy.dm
index 8ff8f6510caf3..d90da88db9457 100644
--- a/code/modules/projectiles/ammunition/energy/_energy.dm
+++ b/code/modules/projectiles/ammunition/energy/_energy.dm
@@ -4,7 +4,7 @@
caliber = ENERGY
projectile_type = /obj/projectile/energy
slot_flags = null
- var/e_cost = 100 //The amount of energy a cell needs to expend to create this shot.
+ var/e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE) //The amount of energy a cell needs to expend to create this shot.
var/select_name = CALIBER_ENERGY
fire_sound = 'sound/weapons/laser.ogg'
firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/energy
diff --git a/code/modules/projectiles/ammunition/energy/ebow.dm b/code/modules/projectiles/ammunition/energy/ebow.dm
index 0eee10a58e532..535ea126576cb 100644
--- a/code/modules/projectiles/ammunition/energy/ebow.dm
+++ b/code/modules/projectiles/ammunition/energy/ebow.dm
@@ -1,7 +1,7 @@
/obj/item/ammo_casing/energy/bolt
projectile_type = /obj/projectile/energy/bolt
select_name = "bolt"
- e_cost = 500
+ e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE * 0.5)
fire_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg' // Even for non-suppressed crossbows, this is the most appropriate sound
/obj/item/ammo_casing/energy/bolt/halloween
diff --git a/code/modules/projectiles/ammunition/energy/gravity.dm b/code/modules/projectiles/ammunition/energy/gravity.dm
index 5b781189c5282..fe73ad26883ed 100644
--- a/code/modules/projectiles/ammunition/energy/gravity.dm
+++ b/code/modules/projectiles/ammunition/energy/gravity.dm
@@ -1,5 +1,5 @@
/obj/item/ammo_casing/energy/gravity
- e_cost = 0
+ e_cost = 0 // Not possible to use the macro
fire_sound = 'sound/weapons/wave.ogg'
select_name = "gravity"
delay = 50
diff --git a/code/modules/projectiles/ammunition/energy/laser.dm b/code/modules/projectiles/ammunition/energy/laser.dm
index fbf9b289aa079..c47181688a482 100644
--- a/code/modules/projectiles/ammunition/energy/laser.dm
+++ b/code/modules/projectiles/ammunition/energy/laser.dm
@@ -1,32 +1,40 @@
/obj/item/ammo_casing/energy/laser
projectile_type = /obj/projectile/beam/laser
- e_cost = 83
+ e_cost = LASER_SHOTS(12, STANDARD_CELL_CHARGE)
select_name = "kill"
/obj/item/ammo_casing/energy/laser/hellfire
projectile_type = /obj/projectile/beam/laser/hellfire
- e_cost = 100
+ e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE)
select_name = "maim"
-/obj/item/ammo_casing/energy/laser/hellfire/antique
- e_cost = 100
-
/obj/item/ammo_casing/energy/lasergun
projectile_type = /obj/projectile/beam/laser
- e_cost = 62.5
+ e_cost = LASER_SHOTS(16, STANDARD_CELL_CHARGE)
+ select_name = "kill"
+
+/obj/item/ammo_casing/energy/lasergun/carbine
+ projectile_type = /obj/projectile/beam/laser/carbine
+ e_cost = LASER_SHOTS(40, STANDARD_CELL_CHARGE)
select_name = "kill"
+ fire_sound = 'sound/weapons/laser2.ogg'
+
+/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
+ e_cost = LASER_SHOTS(5, STANDARD_CELL_CHARGE)
select_name = "kill"
/obj/item/ammo_casing/energy/laser/hos
- e_cost = 120
+ e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE * 1.2)
/obj/item/ammo_casing/energy/laser/musket
projectile_type = /obj/projectile/beam/laser/musket
- e_cost = 1000
+ e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/laser/musket/prime
projectile_type = /obj/projectile/beam/laser/musket/prime
@@ -38,7 +46,7 @@
/obj/item/ammo_casing/energy/chameleon
projectile_type = /obj/projectile/energy/chameleon
- e_cost = 0
+ e_cost = 0 // Can't really use the macro here, unfortunately
var/projectile_vars = list()
/obj/item/ammo_casing/energy/chameleon/ready_proj()
@@ -78,7 +86,7 @@
/obj/item/ammo_casing/energy/laser/pulse
projectile_type = /obj/projectile/beam/pulse
- e_cost = 200
+ e_cost = LASER_SHOTS(200, STANDARD_CELL_CHARGE * 40)
select_name = "DESTROY"
fire_sound = 'sound/weapons/pulse.ogg'
@@ -100,7 +108,7 @@
/obj/item/ammo_casing/energy/xray
projectile_type = /obj/projectile/beam/xray
- e_cost = 50
+ e_cost = LASER_SHOTS(20, STANDARD_CELL_CHARGE)
fire_sound = 'sound/weapons/laser3.ogg'
/obj/item/ammo_casing/energy/mindflayer
@@ -116,7 +124,7 @@
/obj/item/ammo_casing/energy/nanite
projectile_type = /obj/projectile/bullet/c10mm //henk
select_name = "bullet"
- e_cost = 120
+ e_cost = LASER_SHOTS(8, STANDARD_CELL_CHARGE)
fire_sound = 'sound/weapons/thermalpistol.ogg'
/obj/item/ammo_casing/energy/nanite/inferno
diff --git a/code/modules/projectiles/ammunition/energy/lmg.dm b/code/modules/projectiles/ammunition/energy/lmg.dm
index fbd5916613e39..632044f065203 100644
--- a/code/modules/projectiles/ammunition/energy/lmg.dm
+++ b/code/modules/projectiles/ammunition/energy/lmg.dm
@@ -2,5 +2,5 @@
projectile_type = /obj/projectile/bullet/c3d
select_name = "spraydown"
fire_sound = 'sound/weapons/gun/smg/shot.ogg'
- e_cost = 20
+ e_cost = LASER_SHOTS(30, STANDARD_CELL_CHARGE * 0.6)
firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
diff --git a/code/modules/projectiles/ammunition/energy/plasma.dm b/code/modules/projectiles/ammunition/energy/plasma.dm
index 00de4a90ffee7..e660903bdc95d 100644
--- a/code/modules/projectiles/ammunition/energy/plasma.dm
+++ b/code/modules/projectiles/ammunition/energy/plasma.dm
@@ -3,9 +3,9 @@
select_name = "plasma burst"
fire_sound = 'sound/weapons/plasma_cutter.ogg'
delay = 15
- e_cost = 25
+ e_cost = LASER_SHOTS(40, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/plasma/adv
projectile_type = /obj/projectile/plasma/adv
delay = 10
- e_cost = 10
+ e_cost = LASER_SHOTS(100, STANDARD_CELL_CHARGE)
diff --git a/code/modules/projectiles/ammunition/energy/portal.dm b/code/modules/projectiles/ammunition/energy/portal.dm
index 8bdd697f1bfc6..0ef63491f11d4 100644
--- a/code/modules/projectiles/ammunition/energy/portal.dm
+++ b/code/modules/projectiles/ammunition/energy/portal.dm
@@ -1,6 +1,6 @@
/obj/item/ammo_casing/energy/wormhole
projectile_type = /obj/projectile/beam/wormhole
- e_cost = 0
+ e_cost = 0 // Can't use the macro
harmful = FALSE
fire_sound = 'sound/weapons/pulse3.ogg'
select_name = "blue"
diff --git a/code/modules/projectiles/ammunition/energy/special.dm b/code/modules/projectiles/ammunition/energy/special.dm
index 24fba4b9ba492..f2fc274ee8a10 100644
--- a/code/modules/projectiles/ammunition/energy/special.dm
+++ b/code/modules/projectiles/ammunition/energy/special.dm
@@ -5,7 +5,7 @@
/obj/item/ammo_casing/energy/ion/hos
projectile_type = /obj/projectile/ion/weak
- e_cost = 300
+ e_cost = LASER_SHOTS(4, STANDARD_CELL_CHARGE * 1.2)
/obj/item/ammo_casing/energy/declone
projectile_type = /obj/projectile/energy/declone
@@ -30,12 +30,12 @@
/obj/item/ammo_casing/energy/flora/revolution
projectile_type = /obj/projectile/energy/florarevolution
select_name = "revolution"
- e_cost = 250
+ e_cost = LASER_SHOTS(4, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/temp
projectile_type = /obj/projectile/temp
select_name = "freeze"
- e_cost = 250
+ e_cost = LASER_SHOTS(40, STANDARD_CELL_CHARGE * 10)
fire_sound = 'sound/weapons/pulse3.ogg'
/obj/item/ammo_casing/energy/temp/hot
@@ -60,23 +60,23 @@
/obj/item/ammo_casing/energy/tesla_cannon
fire_sound = 'sound/magic/lightningshock.ogg'
- e_cost = 30
+ e_cost = LASER_SHOTS(33, STANDARD_CELL_CHARGE)
select_name = "shock"
projectile_type = /obj/projectile/energy/tesla_cannon
/obj/item/ammo_casing/energy/shrink
projectile_type = /obj/projectile/beam/shrink
select_name = "shrink ray"
- e_cost = 200
+ e_cost = LASER_SHOTS(5, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/marksman
projectile_type = /obj/projectile/bullet/marksman
select_name = "marksman nanoshot"
- e_cost = 0
+ e_cost = 0 // Can't use the macro
fire_sound = 'sound/weapons/gun/revolver/shot_alt.ogg'
/obj/item/ammo_casing/energy/fisher
projectile_type = /obj/projectile/energy/fisher
select_name = "light-buster"
- e_cost = 250
+ e_cost = LASER_SHOTS(2, STANDARD_CELL_CHARGE * 0.5)
fire_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg' // fwip fwip fwip fwip
diff --git a/code/modules/projectiles/ammunition/energy/stun.dm b/code/modules/projectiles/ammunition/energy/stun.dm
index 0a34ab1782c6b..ee2f9fa17eef5 100644
--- a/code/modules/projectiles/ammunition/energy/stun.dm
+++ b/code/modules/projectiles/ammunition/energy/stun.dm
@@ -2,33 +2,33 @@
projectile_type = /obj/projectile/energy/electrode
select_name = "stun"
fire_sound = 'sound/weapons/taser.ogg'
- e_cost = 200
+ e_cost = LASER_SHOTS(5, STANDARD_CELL_CHARGE)
harmful = FALSE
/obj/item/ammo_casing/energy/electrode/spec
- e_cost = 100
+ e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/electrode/gun
fire_sound = 'sound/weapons/gun/pistol/shot.ogg'
- e_cost = 100
+ e_cost = LASER_SHOTS(10, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/electrode/old
- e_cost = 1000
+ e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/disabler
projectile_type = /obj/projectile/beam/disabler
select_name = "disable"
- e_cost = 50
+ e_cost = LASER_SHOTS(20, STANDARD_CELL_CHARGE)
fire_sound = 'sound/weapons/taser2.ogg'
harmful = FALSE
/obj/item/ammo_casing/energy/disabler/hos
- e_cost = 60
+ e_cost = LASER_SHOTS(20, STANDARD_CELL_CHARGE * 1.2)
/obj/item/ammo_casing/energy/disabler/smoothbore
projectile_type = /obj/projectile/beam/disabler/smoothbore
- e_cost = 1000
+ e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/disabler/smoothbore/prime
projectile_type = /obj/projectile/beam/disabler/smoothbore/prime
- e_cost = 500
+ e_cost = LASER_SHOTS(2, STANDARD_CELL_CHARGE)
diff --git a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
index 5d33b3fce5170..9f7ab7e354c94 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 355ed3575a814..b9ac1af0cca12 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/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 2b62416fe7f42..f87f473ae459b 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."
diff --git a/code/modules/projectiles/guns/energy/beam_rifle.dm b/code/modules/projectiles/guns/energy/beam_rifle.dm
index 8869da14a59e3..a451b14de9aeb 100644
--- a/code/modules/projectiles/guns/energy/beam_rifle.dm
+++ b/code/modules/projectiles/guns/energy/beam_rifle.dm
@@ -433,7 +433,7 @@
/obj/item/ammo_casing/energy/beam_rifle/hitscan
projectile_type = /obj/projectile/beam/beam_rifle/hitscan
select_name = "beam"
- e_cost = 10000
+ e_cost = LASER_SHOTS(5, 50000) // Beam rifle has a custom cell
fire_sound = 'sound/weapons/beam_sniper.ogg'
/obj/projectile/beam/beam_rifle
diff --git a/code/modules/projectiles/guns/energy/dueling.dm b/code/modules/projectiles/guns/energy/dueling.dm
index a766ba5667fdf..7b3929d7574cf 100644
--- a/code/modules/projectiles/guns/energy/dueling.dm
+++ b/code/modules/projectiles/guns/energy/dueling.dm
@@ -292,7 +292,7 @@
//Casing
/obj/item/ammo_casing/energy/duel
- e_cost = 0
+ e_cost = 0 // Can't use the macro
projectile_type = /obj/projectile/energy/duel
var/setting
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index 8652fd0248065..a5dd70b17cb13 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -165,7 +165,7 @@
/obj/item/ammo_casing/energy/kinetic
projectile_type = /obj/projectile/kinetic
select_name = "kinetic"
- e_cost = 500
+ e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE * 0.5)
fire_sound = 'sound/weapons/kenetic_accel.ogg' // fine spelling there chap
/obj/item/ammo_casing/energy/kinetic/ready_proj(atom/target, mob/living/user, quiet, zone_override = "")
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index 2d5249ef50c65..4c716f679efe3 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -35,6 +35,26 @@
desc = "An older model of the basic lasergun, no longer used by Nanotrasen's private security or military forces. Nevertheless, it is still quite deadly and easy to maintain, making it a favorite amongst pirates and other outlaws."
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"
@@ -59,7 +79,7 @@
selfcharge = 1
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF
flags_1 = PREVENT_CONTENTS_EXPLOSION_1
- ammo_type = list(/obj/item/ammo_casing/energy/laser/hellfire/antique)
+ ammo_type = list(/obj/item/ammo_casing/energy/laser/hellfire)
/obj/item/gun/energy/laser/captain/scattershot
name = "scatter shot laser rifle"
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 6bce420a02bcb..37d36b13f6ece 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -1143,13 +1143,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
@@ -1157,3 +1161,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 454684e1bee32..08a71bff1b86a 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/reagents/chemistry/equilibrium.dm b/code/modules/reagents/chemistry/equilibrium.dm
index 7d7aff20fae3e..f46c636c50f59 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 6388bc4221246..298b0077cb2e9 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.
+ * 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.
- * Check subtypes controls whether it should it should also include subtypes: ispath(type, reagent) versus type == reagent.
+ * 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)
@@ -841,7 +860,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)
@@ -883,7 +908,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)
@@ -894,19 +926,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
*
@@ -968,16 +987,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)
@@ -1090,7 +1109,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
*/
@@ -1115,7 +1134,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()
@@ -1123,7 +1141,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))
@@ -1223,7 +1241,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
@@ -1261,43 +1279,55 @@
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
@@ -1339,56 +1369,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
@@ -1399,42 +1447,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
@@ -1456,8 +1473,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
@@ -1523,8 +1544,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
@@ -1564,21 +1591,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
*
@@ -1610,16 +1622,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"
@@ -1627,7 +1634,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)
@@ -1978,7 +1985,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)
@@ -2096,13 +2103,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/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index 46ac1eda36074..9b388c660857d 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
@@ -60,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.
@@ -119,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
@@ -173,11 +154,21 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list(
*
*/
/datum/reagent/proc/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ SHOULD_CALL_PARENT(TRUE)
current_cycle++
if(length(reagent_removal_skip_list))
return
- if(holder)
- holder.remove_reagent(type, metabolization_rate * affected_mob.metabolism_efficiency * seconds_per_tick) //By default it slowly disappears.
+ if(isnull(holder))
+ return
+
+ var/metabolizing_out = metabolization_rate * seconds_per_tick
+ if(!(chemical_flags & REAGENT_UNAFFECTED_BY_METABOLISM))
+ if(chemical_flags & REAGENT_REVERSE_METABOLISM)
+ metabolizing_out /= affected_mob.metabolism_efficiency
+ else
+ metabolizing_out *= affected_mob.metabolism_efficiency
+
+ holder.remove_reagent(type, metabolizing_out)
/// 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)
@@ -187,7 +178,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
@@ -222,10 +213,6 @@ Primarily used in reagents/reaction_agents
if(holder)
holder.remove_reagent(type, metabolization_rate * affected_mob.metabolism_efficiency * seconds_per_tick)
-/// Called by [/datum/reagents/proc/conditional_update_move]
-/datum/reagent/proc/on_move(mob/affected_mob)
- return
-
/// Called after add_reagents creates a new reagent.
/datum/reagent/proc/on_new(data)
if(data)
diff --git a/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/cat2_medicine_reagents.dm
index 262058b334d71..0c1dd26e17399 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.
diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
index 98ddd72db2329..be3069e7d4683 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
@@ -2212,6 +2212,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/consumable/ethanol/mauna_loa/on_mob_life(mob/living/carbon/drinker, seconds_per_tick, times_fired)
+ . = ..()
// 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))
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 00f5018447844..fb6e6ea2c0066 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -19,16 +19,18 @@
/// affects mood, typically higher for mixed drinks with more complex recipes'
var/quality = 0
+/datum/reagent/consumable/New()
+ . = ..()
+ // All food reagents function at a fixed rate
+ chemical_flags |= REAGENT_UNAFFECTED_BY_METABOLISM
+
/datum/reagent/consumable/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- current_cycle++
- 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))
+ . = ..()
+ if(!ishuman(affected_mob) || HAS_TRAIT(affected_mob, TRAIT_NOHUNGER))
return
- if(holder)
- holder.remove_reagent(type, metabolization_rate * seconds_per_tick)
+
+ var/mob/living/carbon/human/affected_human = affected_mob
+ affected_human.adjust_nutrition(get_nutriment_factor(affected_mob) * REM * seconds_per_tick)
/datum/reagent/consumable/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -53,8 +55,9 @@
if(isitem(the_real_food) && !is_reagent_container(the_real_food))
exposed_mob.add_mob_memory(/datum/memory/good_food, food = the_real_food)
-/datum/reagent/consumable/proc/get_nutriment_factor()
- return nutriment_factor * REAGENTS_METABOLISM * (purity * 2)
+/// Gets just how much nutrition this reagent is worth for the passed mob
+/datum/reagent/consumable/proc/get_nutriment_factor(mob/living/carbon/eater)
+ return nutriment_factor * REAGENTS_METABOLISM * purity * 2
/datum/reagent/consumable/nutriment
name = "Nutriment"
@@ -257,15 +260,12 @@
brute_heal = 0
burn_heal = 0
-/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
+/datum/reagent/consumable/nutriment/mineral/get_nutriment_factor(mob/living/carbon/eater)
+ if(HAS_TRAIT(eater, TRAIT_ROCK_EATER))
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)
+
+ // You cannot eat rocks, it gives no nutrition
+ return 0
/datum/reagent/consumable/sugar
name = "Sugar"
@@ -476,6 +476,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
@@ -650,6 +652,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
@@ -754,6 +758,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
@@ -866,7 +872,7 @@
/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)
+ affected_mob.adjust_nutrition(-3 * REM * get_nutriment_factor(affected_mob) * seconds_per_tick)
////Lavaland Flora Reagents////
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 e2b703a9c1386..b317bfafa1ac7 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
@@ -496,6 +496,7 @@ 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)
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!
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index ff1671852c1e0..68eefcdf0cd44 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -9,12 +9,10 @@
/datum/reagent/medicine
taste_description = "bitterness"
-/datum/reagent/medicine/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- current_cycle++
- if(length(reagent_removal_skip_list))
- return
- 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/New()
+ . = ..()
+ // All medicine metabolizes out slower / stay longer if you have a better metabolism
+ chemical_flags |= REAGENT_REVERSE_METABOLISM
/datum/reagent/medicine/leporazine
name = "Leporazine"
@@ -928,7 +926,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
@@ -1371,7 +1368,6 @@
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)
. = ..()
@@ -1622,6 +1618,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/silibinin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
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
@@ -1666,6 +1663,7 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/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
var/need_mob_update
need_mob_update = affected_mob.adjustBruteLoss(-healamount * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 3a699da266d23..e42cfc55107d7 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -303,6 +303,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
@@ -335,7 +337,7 @@
color = "#E0E8EF" // rgb: 224, 232, 239
self_consuming = TRUE //divine intervention won't be limited by the lack of a liver
ph = 7.5 //God is alkaline
- chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_CLEANS
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_CLEANS|REAGENT_UNAFFECTED_BY_METABOLISM // Operates at fixed metabolism for balancing memes.
default_container = /obj/item/reagent_containers/cup/glass/bottle/holywater
/datum/glass_style/drinking_glass/holywater
@@ -344,6 +346,15 @@
desc = "A glass of holy water."
icon_state = "glass_clear"
+/datum/reagent/water/holywater/on_new(list/data)
+ // Tracks the total amount of deciseconds that the reagent has been metab'd for, for the purpose of deconversion
+ if(isnull(data))
+ data = list("deciseconds_metabolized" = 0)
+ else if(isnull(data["deciseconds_metabolized"]))
+ data["deciseconds_metabolized"] = 0
+
+ return ..()
+
// Holy water. Unlike water, which is nuked, stays in and heals the plant a little with the power of the spirits. Also ALSO increases instability.
/datum/reagent/water/holywater/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
mytray.adjust_waterlevel(round(volume))
@@ -354,53 +365,49 @@
. = ..()
ADD_TRAIT(affected_mob, TRAIT_HOLY, type)
-/datum/reagent/water/holywater/on_mob_add(mob/living/affected_mob, amount)
- . = ..()
- if(data)
- 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)
+/datum/reagent/water/holywater/on_mob_add(mob/living/affected_mob, amount)
. = ..()
- if(IS_CULTIST(exposed_mob))
- 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!"))
+ if(IS_CULTIST(affected_mob))
+ to_chat(affected_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(!data)
- data = list("misc" = 0)
- data["misc"] += seconds_per_tick SECONDS * REM
- affected_mob.adjust_jitter_up_to(4 SECONDS * seconds_per_tick, 20 SECONDS)
+ data["deciseconds_metabolized"] += (seconds_per_tick * 1 SECONDS * REM)
+
+ affected_mob.adjust_jitter_up_to(4 SECONDS * REM * seconds_per_tick, 20 SECONDS)
+
if(IS_CULTIST(affected_mob))
for(var/datum/action/innate/cult/blood_magic/BM in affected_mob.actions)
- to_chat(affected_mob, span_cultlarge("Your blood rites falter as holy water scours your body!"))
+ var/removed_any = FALSE
for(var/datum/action/innate/cult/blood_spell/BS in BM.spells)
+ removed_any = TRUE
qdel(BS)
- if(data["misc"] >= (25 SECONDS)) // 10 units
- affected_mob.adjust_stutter_up_to(4 SECONDS * seconds_per_tick, 20 SECONDS)
+ if(removed_any)
+ to_chat(affected_mob, span_cultlarge("Your blood rites falter as holy water scours your body!"))
+
+ if(data["deciseconds_metabolized"] >= (25 SECONDS)) // 10 units
+ affected_mob.adjust_stutter_up_to(4 SECONDS * REM * seconds_per_tick, 20 SECONDS)
affected_mob.set_dizzy_if_lower(10 SECONDS)
if(IS_CULTIST(affected_mob) && SPT_PROB(10, seconds_per_tick))
affected_mob.say(pick("Av'te Nar'Sie","Pa'lid Mors","INO INO ORA ANA","SAT ANA!","Daim'niodeis Arc'iai Le'eones","R'ge Na'sie","Diabo us Vo'iscum","Eld' Mon Nobis"), forced = "holy water")
if(prob(10))
affected_mob.visible_message(span_danger("[affected_mob] starts having a seizure!"), span_userdanger("You have a seizure!"))
affected_mob.Unconscious(12 SECONDS)
- to_chat(affected_mob, "[pick("Your blood is your bond - you are nothing without it", "Do not forget your place", \
- "All that power, and you still fail?", "If you cannot scour this poison, I shall scour your meager life!")].")
- if(data["misc"] >= (1 MINUTES)) // 24 units
+ to_chat(affected_mob, span_cultlarge("[pick("Your blood is your bond - you are nothing without it", "Do not forget your place", \
+ "All that power, and you still fail?", "If you cannot scour this poison, I shall scour your meager life!")]."))
+
+ if(data["deciseconds_metabolized"] >= (1 MINUTES)) // 24 units
if(IS_CULTIST(affected_mob))
affected_mob.mind.remove_antag_datum(/datum/antagonist/cult)
- affected_mob.Unconscious(100)
+ affected_mob.Unconscious(10 SECONDS)
affected_mob.remove_status_effect(/datum/status_effect/jitter)
affected_mob.remove_status_effect(/datum/status_effect/speech/stutter)
- 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
- if(holder)
- holder.remove_reagent(type, 1 * REAGENTS_METABOLISM * seconds_per_tick) //fixed consumption to prevent balancing going out of whack
+ holder?.remove_reagent(type, volume) // maybe this is a little too perfect and a max() cap on the statuses would be better??
/datum/reagent/water/holywater/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -466,6 +473,11 @@
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
@@ -480,6 +492,16 @@
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
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)
@@ -489,6 +511,10 @@
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"
description = "YOUR FLESH! IT BURNS!"
@@ -2180,15 +2206,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"
@@ -2201,13 +2229,15 @@
/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)
. = ..()
@@ -2562,11 +2592,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
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index 7f4acabef6eb9..390c3d7bfd4dd 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
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index 9083de70902e7..d49976ac10eed 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/cups/_cup.dm b/code/modules/reagents/reagent_containers/cups/_cup.dm
index 7ccc3209ab0d7..c15ba1a017f65 100644
--- a/code/modules/reagents/reagent_containers/cups/_cup.dm
+++ b/code/modules/reagents/reagent_containers/cups/_cup.dm
@@ -124,7 +124,7 @@
return
var/trans = reagents.trans_to(target, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You transfer [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)
@@ -136,7 +136,7 @@
return
var/trans = target.reagents.trans_to(src, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You fill [src] with [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()
@@ -157,7 +157,7 @@
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
var/trans = target.reagents.trans_to(src, amount_per_transfer_from_this, transferred_by = user)
- to_chat(user, span_notice("You fill [src] with [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 cf01c2cd4c2a0..ac8d0af0d1c6b 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 4499030642d93..d4c752751b8bc 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/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 0a2c96dddd598..3b0bf62c26781 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -532,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."
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 7852bdb367e56..441e6f0038b5e 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -476,6 +476,7 @@
"gibber",
"griddle",
"microwave",
+ "microwave_engineering",
"monkey_recycler",
"oven",
"processor",
diff --git a/code/modules/security_levels/keycard_authentication.dm b/code/modules/security_levels/keycard_authentication.dm
index 66bf8b3bc93f3..6d3d9326d6d41 100644
--- a/code/modules/security_levels/keycard_authentication.dm
+++ b/code/modules/security_levels/keycard_authentication.dm
@@ -55,11 +55,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/spells/spell_types/conjure/simian.dm b/code/modules/spells/spell_types/conjure/simian.dm
index 556a78e50127c..aa9aabc681009 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/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index f929d2a779860..2e140bdbf26e7 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -1280,9 +1280,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
@@ -1299,16 +1300,14 @@
burn_damage *= 2
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 37b6cef989750..99591daaa4b2b 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/bodyparts/species_parts/misc_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
index 408afea6679d8..b8b7d427b67b2 100644
--- a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
@@ -387,6 +387,25 @@
burn_modifier = 1.25
speed_modifier = 0.75 //big fungus big fungus
+/// Dullahan head preserves organs inside it
+/obj/item/bodypart/head/dullahan
+ throwforce = 25 // It's also a potent weapon
+ show_organs_on_examine = FALSE
+ speech_span = null
+
+/obj/item/bodypart/head/dullahan/Entered(obj/item/organ/arrived, atom/old_loc, list/atom/old_locs)
+ . = ..()
+ if (!isorgan(arrived))
+ return
+ arrived.organ_flags |= ORGAN_FROZEN
+
+/obj/item/bodypart/head/dullahan/Exited(obj/item/organ/gone, direction)
+ . = ..()
+ if (!isorgan(gone))
+ return
+ gone.organ_flags &= ~ORGAN_FROZEN
+
+
//GOLEM
/obj/item/bodypart/head/golem
icon = 'icons/mob/human/species/golems.dmi'
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm b/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm
index 86dacdd98ffee..1ea3a1bf9c4ab 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_chest.dm
@@ -52,31 +52,50 @@
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())
need_mob_update += owner.adjustOxyLoss(-5, updating_health = FALSE)
@@ -84,15 +103,34 @@
if(owner.getBruteLoss())
need_mob_update += owner.adjustBruteLoss(-2, updating_health = FALSE)
revive_cost += 40
+ body_damage_patched = TRUE
if(owner.getFireLoss())
need_mob_update += owner.adjustFireLoss(-2, updating_health = FALSE)
revive_cost += 40
+ body_damage_patched = TRUE
if(owner.getToxLoss())
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)
. = ..()
if(!owner || . & EMP_PROTECT_SELF)
diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm
index 71dc305ac5f82..faf67a4596f2c 100644
--- a/code/modules/surgery/organs/internal/lungs/_lungs.dm
+++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm
@@ -514,6 +514,7 @@
if(prob(20))
n2o_euphoria = EUPHORIA_ACTIVE
breather.emote(pick("giggle", "laugh"))
+ breather.set_drugginess(30 SECONDS)
else
n2o_euphoria = EUPHORIA_INACTIVE
return
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index d4839542122d3..c6fdecbd29bc0 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"
diff --git a/code/modules/unit_tests/bitrunning.dm b/code/modules/unit_tests/bitrunning.dm
new file mode 100644
index 0000000000000..568eeeed8c133
--- /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/changeling.dm b/code/modules/unit_tests/changeling.dm
index 644a7247ad1d9..c01105ce9c44c 100644
--- a/code/modules/unit_tests/changeling.dm
+++ b/code/modules/unit_tests/changeling.dm
@@ -48,6 +48,10 @@
if(isnull(final_icon))
final_icon = icon('icons/effects/effects.dmi', "nothing")
+ // If we have a lot of dna features with a lot of parts (icons)
+ // This'll eventually runtime into a bad icon operation
+ // So we're recaching the icons here to prevent it from failing
+ final_icon = icon(final_icon)
final_icon.Insert(getFlatIcon(ling, no_anim = TRUE), dir = SOUTH, frame = last_frame)
final_icon.Insert(getFlatIcon(victim, no_anim = TRUE), dir = NORTH, frame = last_frame)
diff --git a/code/modules/unit_tests/rcd.dm b/code/modules/unit_tests/rcd.dm
index 27ab66fa0be3b..49e9f8461fd0a 100644
--- a/code/modules/unit_tests/rcd.dm
+++ b/code/modules/unit_tests/rcd.dm
@@ -15,7 +15,7 @@
engineer.put_in_hands(rcd, forced = TRUE)
- rcd.mode = RCD_MACHINE
+ rcd.mode = RCD_STRUCTURE
var/list/adjacent_turfs = get_adjacent_open_turfs(engineer)
@@ -24,7 +24,7 @@
var/turf/adjacent_turf = adjacent_turfs[1]
for(var/i in 1 to 10)
- adjacent_turf.rcd_act(engineer, rcd, rcd.mode)
+ adjacent_turf.rcd_act(engineer, rcd, list("[RCD_DESIGN_MODE]" = rcd.mode, "[RCD_DESIGN_PATH]" = /obj/structure/frame/machine/secured))
var/frame_count = 0
for(var/obj/structure/frame/machine_frame in adjacent_turf.contents)
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 ac412207c236d..d8e9c548db997 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/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 4e9b10a80ee7b..a4ef1a1a28255 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -64,7 +64,6 @@
/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,
@@ -74,7 +73,6 @@
/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,
@@ -88,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,
@@ -103,17 +98,11 @@
/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/ash_spirit,
- /mob/living/simple_animal/hostile/heretic_summon/maid_in_the_mirror,
- /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,
@@ -177,7 +166,6 @@
/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,
@@ -197,15 +185,11 @@
/mob/living/simple_animal/pet/gondola,
/mob/living/simple_animal/pet/gondola/gondolapod,
/mob/living/simple_animal/pet/gondola/virtual_domain,
- /mob/living/simple_animal/revenant,
/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,
// DO NOT ADD NEW ENTRIES TO THIS LIST
// READ THE COMMENT ABOVE
diff --git a/code/modules/uplink/uplink_items/ammunition.dm b/code/modules/uplink/uplink_items/ammunition.dm
index 292f87ffe13b3..e88727812528d 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 93087be9dd00e..67676edcd7da9 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 912c28e2bcb2b..a930784fe1462 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 0cdcf3a7bb7f6..f1788c6e1dec3 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 27145ccf004df..72f40b00dfca3 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/job.dm b/code/modules/uplink/uplink_items/job.dm
index 6dcd48dee839c..2dc7fdf153cc5 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 be7c163bd861d..8a1c301830a09 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 491f8e8e99d6d..2f205a9d0bd69 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/config/blanks.json b/config/blanks.json
index 299fa67a594b3..f3b38d67bdb4b 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/lavaruinblacklist.txt b/config/lavaruinblacklist.txt
index 6f90b1f254e1b..d40c4386d31de 100644
--- a/config/lavaruinblacklist.txt
+++ b/config/lavaruinblacklist.txt
@@ -23,20 +23,21 @@
#_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_ufo_crash.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
-#_maps/RandomRuins/LavaRuins/lavaland_surface_tomb.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_hierophant.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_pizzaparty.dmm
#_maps/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_hermit.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_gaia.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/AnywhereRuins/fountain_hall.dmm
-#_maps/RandomRuins/LavaRuins/lavaland_surface_phonebooth.dmm
\ No newline at end of file
+#_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_wwiioutpost.dmm
+#_maps/RandomRuins/LavaRuins/lavaland_surface_xeno_nest.dmm
diff --git a/html/changelogs/AutoChangeLog-pr-78583.yml b/html/changelogs/AutoChangeLog-pr-78583.yml
new file mode 100644
index 0000000000000..d5723ae152162
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-78583.yml
@@ -0,0 +1,6 @@
+author: "ZephyrTFA"
+delete-after: True
+changes:
+ - rscadd: "Vent Pumps can now be overclocked, do some light reading in the air alarm to figure out what this means."
+ - balance: "Vent Pumps now have fan integrity, the damaging of which reduces their ability to move air."
+ - sound: "Overclocking sounds, including spool, stop, and loop"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78669.yml b/html/changelogs/AutoChangeLog-pr-78669.yml
deleted file mode 100644
index 519ec9c826a0a..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78669.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "unit0016"
-delete-after: True
-changes:
- - bugfix: "It is no longer possible to chasm yourself on the geode. Again."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78674.yml b/html/changelogs/AutoChangeLog-pr-78674.yml
deleted file mode 100644
index 2fd8072593f9c..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78674.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "necromanceranne"
-delete-after: True
-changes:
- - 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'."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78742.yml b/html/changelogs/AutoChangeLog-pr-78742.yml
deleted file mode 100644
index c9f9775b94cff..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78742.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Ghommie"
-delete-after: True
-changes:
- - bugfix: "Fixed silent catwalks."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78745.yml b/html/changelogs/AutoChangeLog-pr-78745.yml
deleted file mode 100644
index ba964a0876856..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78745.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "Ghommie"
-delete-after: True
-changes:
- - 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."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78747.yml b/html/changelogs/AutoChangeLog-pr-78747.yml
deleted file mode 100644
index 6549830c58da7..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78747.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Sealed101"
-delete-after: True
-changes:
- - bugfix: "fixed bad food not having bad food reagents"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78754.yml b/html/changelogs/AutoChangeLog-pr-78754.yml
deleted file mode 100644
index c19e3e967f2fc..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78754.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "timothymtorres"
-delete-after: True
-changes:
- - 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."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78759.yml b/html/changelogs/AutoChangeLog-pr-78759.yml
deleted file mode 100644
index ddd3a3ba173e9..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78759.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "san7890"
-delete-after: True
-changes:
- - 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."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78766.yml b/html/changelogs/AutoChangeLog-pr-78766.yml
deleted file mode 100644
index 92544abeaec67..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78766.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "GPeckman"
-delete-after: True
-changes:
- - bugfix: "The AI can no longer turn you off if you shapeshift into a robot."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78768.yml b/html/changelogs/AutoChangeLog-pr-78768.yml
deleted file mode 100644
index 1c1fea677fb14..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78768.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "GPeckman"
-delete-after: True
-changes:
- - bugfix: "Adminheal will now properly clear negative mutations as intended."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78771.yml b/html/changelogs/AutoChangeLog-pr-78771.yml
deleted file mode 100644
index e55b8dc436782..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78771.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "GPeckman"
-delete-after: True
-changes:
- - bugfix: "Borgs will no longer become permanently upside-down if tipped over by multiple people at the same time."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78773.yml b/html/changelogs/AutoChangeLog-pr-78773.yml
deleted file mode 100644
index 6b661f241e31e..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78773.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "GPeckman"
-delete-after: True
-changes:
- - bugfix: "Engineering borgs can no longer grab and drop their own iron/glass sheet module."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78774.yml b/html/changelogs/AutoChangeLog-pr-78774.yml
deleted file mode 100644
index 960314de4262a..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78774.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-author: "Melbert"
-delete-after: True
-changes:
- - 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%"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78775.yml b/html/changelogs/AutoChangeLog-pr-78775.yml
deleted file mode 100644
index cc4a35246834d..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78775.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "GPeckman"
-delete-after: True
-changes:
- - 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."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78776.yml b/html/changelogs/AutoChangeLog-pr-78776.yml
deleted file mode 100644
index bcc4dd4a02b16..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78776.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Likteer"
-delete-after: True
-changes:
- - rscadd: "Fake moustaches are now poorly slapped on top of what you're wearing"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78783.yml b/html/changelogs/AutoChangeLog-pr-78783.yml
deleted file mode 100644
index 57d3955572cb3..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-78783.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Kapu1178"
-delete-after: True
-changes:
- - bugfix: "Blood once again appears as small drops instead of splatters during minor bleeding."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78809.yml b/html/changelogs/AutoChangeLog-pr-78809.yml
new file mode 100644
index 0000000000000..e2c06429f4d0f
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-78809.yml
@@ -0,0 +1,4 @@
+author: "Iamgoofball"
+delete-after: True
+changes:
+ - balance: "Allows spacemen to use age-appropriate drugs by making it so you can now huff N2O to get high."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78914.yml b/html/changelogs/AutoChangeLog-pr-78914.yml
new file mode 100644
index 0000000000000..0d9e5a3217f21
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-78914.yml
@@ -0,0 +1,4 @@
+author: "starrm4nn"
+delete-after: True
+changes:
+ - bugfix: "makes the riot helmet hide hair like other sec helmets"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-78972.yml b/html/changelogs/AutoChangeLog-pr-78972.yml
new file mode 100644
index 0000000000000..ee28a7c42d525
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-78972.yml
@@ -0,0 +1,4 @@
+author: "carlarctg"
+delete-after: True
+changes:
+ - bugfix: "Fixes Monkey's Delight recipe"
\ No newline at end of file
diff --git a/html/changelogs/archive/2023-10.yml b/html/changelogs/archive/2023-10.yml
index f7035bd489938..ff449e2e69689 100644
--- a/html/changelogs/archive/2023-10.yml
+++ b/html/changelogs/archive/2023-10.yml
@@ -171,3 +171,395 @@
neocloudy:
- bugfix: MetaStation disposal pipes from Cargo to Disposals/the rest of the station
are working again.
+2023-10-07:
+ CoiledLamb:
+ - qol: allows janitor keys to be stored in janitor wintercoats and janibets
+ - qol: gives empty fireaxe and mech removal crowbars cabinets directional helpers
+ GPeckman:
+ - bugfix: Borgs will no longer become permanently upside-down if tipped over by
+ multiple people at the same time.
+ - bugfix: Adminheal will now properly clear negative mutations as intended.
+ - bugfix: The AI can no longer turn you off if you shapeshift into a robot.
+ - rscadd: The laser carbine, a weak but fully automatic sidegrade to the normal
+ laser gun, can now be ordered from cargo.
+ - bugfix: Engineering borgs can no longer grab and drop their own iron/glass sheet
+ module.
+ - 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.
+ Ghommie:
+ - bugfix: Fixed silent catwalks.
+ - 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.
+ Isratosh:
+ - spellcheck: '"offical" has been officially corrected to "official" in several
+ official locations.'
+ Jacquerel:
+ - refactor: Rust Walkers, Ash Spirits, Flesh Stalkers, and The Maid in the Mirror
+ now use the basic mob framework. Please report any unusual behaviour.
+ Kapu1178:
+ - bugfix: Blood once again appears as small drops instead of splatters during minor
+ bleeding.
+ Likteer:
+ - rscadd: Fake moustaches are now poorly slapped on top of what you're wearing
+ Melbert:
+ - refactor: Refactors how ethereals update their color when damaged.
+ - 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%
+ ReezeBL:
+ - bugfix: fixed a PDA's messenger TGUI issue with handling of destroyed recipients.
+ Sealed101:
+ - bugfix: fixed bad food not having bad food reagents
+ necromanceranne:
+ - 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'.
+ oranges:
+ - rscadd: Dogs now react to centrist grillers more realistically
+ san7890:
+ - 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.
+ timothymtorres:
+ - 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.
+ unit0016:
+ - bugfix: It is no longer possible to chasm yourself on the geode. Again.
+2023-10-08:
+ Comxy:
+ - bugfix: Spider types get properly checked again.
+ Ghommie:
+ - bugfix: People who are irremediably bald can still grow a beard with barber aid.
+ Hatterhat:
+ - 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.
+ Jacquerel:
+ - bugfix: Flesh Worms will move smoothly more consistently.
+ 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
+ YehnBeep:
+ - qol: '"prison" intercoms have been renamed to "receive-only" intercoms to make
+ it clearer they cannot transmit.'
+ carlarctg:
+ - qol: Added slapcrafting to unloaded tech shells, click on them with ingredients
+ to quickly craft your shell.
+ san7890:
+ - refactor: Sloths are now basic mobs, however their overall sluggish behavior shouldn't
+ have changed much- let us know if anything is broken.
+ timothymtorres:
+ - bugfix: Fix bodies now lose fire stacks while husked.
+2023-10-09:
+ Ben10Omintrix:
+ - refactor: ice demons have been refactored into basic mbos. please report any bugs
+ - rscadd: ice demons now have a unique trophy
+ IndieanaJones:
+ - bugfix: Slaughter/Laughter demon melee cooldowns have been fixed and now attack
+ at the regular player character attack speed
+ Jacquerel:
+ - 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.
+ distributivgesetz:
+ - qol: Supermatter shards can now be fastened with right click too. Now, just don't
+ forget to use a wrench.
+2023-10-10:
+ BlueMemesauce:
+ - bugfix: fixed gibbing from having too much blood not working in some cases
+ 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
+ FlufflesTheDog:
+ - bugfix: Virtual domain gondola meat will no longer have a small chance to turn
+ you into a weaker gondola variant
+ GPeckman:
+ - bugfix: Warm donk-pockets should now have omnizine in them again.
+ HWSensum:
+ - balance: Reviver Implant now able to revive dead people.
+ Hatterhat:
+ - bugfix: Necropolis tendrils and other mining mob spawners can be hit in melee
+ again.
+ Jacquerel:
+ - 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.
+ 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.
+ SyncIt21:
+ - 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
+ 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.
+ YehnBeep:
+ - qol: The autopsy tray (and surgery trays) can now hold the autopsy scanner
+ admeeer:
+ - code_imp: made an eensy teensie weensie change to some supermatter boilerplate
+ jlsnow301:
+ - 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.'
+ lizardqueenlexi:
+ - bugfix: Metastation disposals will no longer infinitely loop garbage around the
+ station.
+ mc-oofert:
+ - code_imp: COMSIG_GLOB_LIGHT_MECHANISM_COMPLETED is now COMSIG_GLOB_PUZZLE_COMPLETED
+ ninjanomnom:
+ - bugfix: Images are once more displayed as images in vv instead of as an appearance
+ - rscadd: Pipes now have a colored visual display that shows their contents at a
+ glance.
+ san7890:
+ - 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.
+2023-10-11:
+ GPeckman:
+ - bugfix: Borg modules can no longer be sold by pirates.
+ Iamgoofball:
+ - bugfix: Fixes a few runtimes with TTS and skips some code if TTS isn't enabled.
+ Jacquerel:
+ - 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: 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).
+ - bugfix: Heretic mobs will not be summoned with AI enabled, and won't turn into
+ small animals instead of summoning a flesh stalker.
+ JohnFulpWillard:
+ - bugfix: Antiglow now probably has negative glow power.
+ Melbert:
+ - bugfix: Miner's Salve, Sterilizine, and Space Cleaner now all properly affect
+ burn wounds
+ Sealed101:
+ - bugfix: you can no longer polymorph belt into a holoparasite
+ ViktorKoL:
+ - bugfix: fixed some faulty research connections in between heretic's blade and
+ rust paths.
+ carlarctg:
+ - rscadd: Adds practice carbines to all firing ranges. They don't deal damage.
+ - 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.
+ jlsnow301:
+ - 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
+ lizardqueenlexi:
+ - 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.
+ mc-oofert:
+ - bugfix: count station food verb now counts food only onstation
+ necromanceranne:
+ - balance: Unholy water acts as a coagulant for cultists.
+ nikothedude:
+ - bugfix: Wound promotion and demotion no longer removes gauze from the limb
+ timothymtorres:
+ - sound: Add burning sound loop to bonfires and fireplaces
+ - code_imp: Improved fireplaces to only process when lit
+ vinylspiders:
+ - bugfix: fixes a runtime in organ on_death()
+ - bugfix: using a magic mirror to change gender or skintone will now update your
+ icon properly to match your selection
+ - bugfix: bitrunners will no longer be lumped in with assistants on the crew monitor
+ console's display
+ - bugfix: moved a garbage spawner on Tramstation that was causing random runtimes
+ due to sometimes spawning in space depending on which module got loaded
+2023-10-12:
+ Ghommie:
+ - 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.
+ Isratosh:
+ - admin: Gondola supplypods are functional again.
+ Jacquerel:
+ - refactor: Gorillas now use the basic mob framework. Please report any unusual
+ side effects.
+ - rscadd: Adds a new lavaland ruin where you can find a unique egg.
+ - 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.
+ LT3:
+ - 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
+ - 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
+ SyncIt21:
+ - 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
+ - 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
+ ViktorKoL:
+ - sound: the blood cult's rise to power is now accompanied by several new sound
+ effects
+ mc-oofert:
+ - 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
+ vinylspiders:
+ - image: you can now change the style of lipstick to be higher or lower on the face
+ by alt-clicking the lipstick tube
+2023-10-13:
+ EuSouAFazer:
+ - qol: The rollerdome is now better - the dance floor works now, and the bar is
+ groovier.
+ Jacquerel:
+ - bugfix: Dullahans can read, strip people, and utilise tools.
+ - bugfix: Dullahan brains and eyes will not decay while inside their living severed
+ head.
+ LT3:
+ - qol: Canisters can now be built in one step, no upgrading required
+ Paxilmaniac:
+ - image: Inhands for the Sakhno and related rifles will no longer be way too high
+ or big
+ Rhials:
+ - rscadd: Two new psyker-oriented virtual domains -- Crate Chaos and Infected Domain.
+ - rscadd: Map helper for cyber-police corpse spawn.
+ - rscadd: Map helper for swapping the encrypted crate in an area with a random crate
+ from that same area.
+ jlsnow301:
+ - bugfix: Fixed the errant bluescreen in the camera console.
+ mc-oofert:
+ - code_imp: basicmobs that delete on death, ghost before dying
+ san7890:
+ - 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.
+2023-10-14:
+ BlueMemesauce:
+ - bugfix: Modsuits can no longer be deepfried
+ DrDiasyl:
+ - sound: laser2.ogg sound has been changed. Now laser carbine uses it.
+ - image: Laser carbine and orange laser sprite have been improved.
+ IndieanaJones:
+ - bugfix: Space Dragon can break walls, eat corpses and destroy mechs more efficiently
+ again
+ - bugfix: Player-controlled lavaland elites can once again return to their tumor
+ after winning their fight
+ Jacquerel:
+ - bugfix: '"Mirror Walk" is once more the domain of the Maid in the Mirror rather
+ than "every heretic summon"'
+ - bugfix: Heretic mobs can once again survive space
+ - bugfix: Pete's anger management training has worn off, and he will once again
+ sometimes pick a fight with you for absolutely no reason.
+ - qol: Attacking a goat will not spam messages so frequently.
+ LT3:
+ - image: Nitrogen canisters are now yellow, antinob are grey/yellow, empty are grey,
+ hydrogen are red/white
+ MTandi:
+ - bugfix: The crew is instructed to place fax machines properly in the center of
+ a table without hanging.
+ Melbert:
+ - bugfix: Fixes Mauna Loa, Monover, Silibinin, Granibitaluri not exiting your system
+ on metabolism
+ - bugfix: Fixes holy water exiting your system at double the rate on metabolism
+ - bugfix: Holy Water no longer spams cultists with big text every time, it's much
+ more tame now
+ Rhials:
+ - qol: You can now return to your old body after being summoned by a manifest rune.
+ - qol: You can now return to your old body after dying in CTF.
+ - qol: You can now return to your old body after dying in the Medisim Shuttle battle
+ area.
+ - qol: You can no longer suicide in CTF areas, for integrity purposes.
+ bun235:
+ - rscadd: targetting someone's arm with *slap now has a unique message
+ dragomagol:
+ - qol: apples can now be sliced
+ mc-oofert:
+ - bugfix: sqdl2 query readout displays location of turfs properly
+ ninjanomnom:
+ - admin: VV can now display the contents of special byond lists like filters, or
+ client.images
+ - admin: VV on images now displays the image in the header
+ - admin: VV can now display filters and includes their type
+ san7890:
+ - bugfix: Space Dragons can now, once again, tear down walls and eat corpses. They
+ also have regained their special damage modifier when attacking mechs.
diff --git a/icons/hud/radial.dmi b/icons/hud/radial.dmi
index f4c4ab7693e98..42d5c451018ae 100644
Binary files a/icons/hud/radial.dmi and b/icons/hud/radial.dmi differ
diff --git a/icons/mob/human/human_face.dmi b/icons/mob/human/human_face.dmi
index 6985cf07eee49..6530b300aa676 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/weapons/guns_lefthand.dmi b/icons/mob/inhands/weapons/guns_lefthand.dmi
index 013e6e4745854..d0caa04a33a03 100644
Binary files a/icons/mob/inhands/weapons/guns_lefthand.dmi and b/icons/mob/inhands/weapons/guns_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_righthand.dmi b/icons/mob/inhands/weapons/guns_righthand.dmi
index 14bf9e762c753..e4842aca23aa6 100644
Binary files a/icons/mob/inhands/weapons/guns_righthand.dmi and b/icons/mob/inhands/weapons/guns_righthand.dmi differ
diff --git a/icons/mob/simple/animal.dmi b/icons/mob/simple/animal.dmi
index 8e3affff4a90a..01670b07d9389 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 c9265b22a0ad2..fbc38d29d99de 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 38b78cf468f1f..f68e3db4a6cb9 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 2be68ef4c6696..808fdc59d9bae 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/card.dmi b/icons/obj/card.dmi
index 4172a0a3641a7..a5e34e9cc27cb 100644
Binary files a/icons/obj/card.dmi and b/icons/obj/card.dmi differ
diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi
index d89ee6e5d6408..fe74b6c11c5c7 100644
Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ
diff --git a/icons/obj/fluff/general.dmi b/icons/obj/fluff/general.dmi
index 3f990111c3c74..2628eea874694 100644
Binary files a/icons/obj/fluff/general.dmi and b/icons/obj/fluff/general.dmi differ
diff --git a/icons/obj/food/egg.dmi b/icons/obj/food/egg.dmi
index c7661fca918f0..58908d8247913 100644
Binary files a/icons/obj/food/egg.dmi and b/icons/obj/food/egg.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index 97ded07df214b..46d7459e0b0b7 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/obj/machines/atmospherics/unary_devices.dmi b/icons/obj/machines/atmospherics/unary_devices.dmi
index 3350500fde950..6a929f211b8dc 100644
Binary files a/icons/obj/machines/atmospherics/unary_devices.dmi and b/icons/obj/machines/atmospherics/unary_devices.dmi differ
diff --git a/icons/obj/machines/bitrunning.dmi b/icons/obj/machines/bitrunning.dmi
index a910a16b35cf8..b3f8ad63a6c99 100644
Binary files a/icons/obj/machines/bitrunning.dmi and b/icons/obj/machines/bitrunning.dmi differ
diff --git a/icons/obj/machines/microwave.dmi b/icons/obj/machines/microwave.dmi
index 427a5d9daaee6..7a72ba5d3dca3 100644
Binary files a/icons/obj/machines/microwave.dmi and b/icons/obj/machines/microwave.dmi differ
diff --git a/icons/obj/mining_zones/artefacts.dmi b/icons/obj/mining_zones/artefacts.dmi
index f3f7d00e4eef8..d4c603834d21b 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 0000000000000..0262adcaeb241
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 0000000000000..97643036fbe3b
Binary files /dev/null and b/icons/obj/pipes_n_cables/!pipes_bitmask.dmi differ
diff --git a/icons/obj/pipes_n_cables/atmos.dmi b/icons/obj/pipes_n_cables/atmos.dmi
index 65cbdc672db74..91badbf3ccf9b 100644
Binary files a/icons/obj/pipes_n_cables/atmos.dmi and b/icons/obj/pipes_n_cables/atmos.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 0000000000000..d0d2f7ff7bb80
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 7a382fb55c5e4..0000000000000
Binary files a/icons/obj/pipes_n_cables/pipes_bitmask.dmi and /dev/null differ
diff --git a/icons/obj/service/hydroponics/harvest.dmi b/icons/obj/service/hydroponics/harvest.dmi
index 28a43776a50a0..57a127cd9975e 100644
Binary files a/icons/obj/service/hydroponics/harvest.dmi and b/icons/obj/service/hydroponics/harvest.dmi differ
diff --git a/icons/obj/weapons/guns/energy.dmi b/icons/obj/weapons/guns/energy.dmi
index 97b75335b91a0..9a86e65329fb4 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/projectiles.dmi b/icons/obj/weapons/guns/projectiles.dmi
index e1c70c4f5ade4..05ad142ff58d3 100644
Binary files a/icons/obj/weapons/guns/projectiles.dmi and b/icons/obj/weapons/guns/projectiles.dmi differ
diff --git a/sound/ambience/antag/bloodcult/bloodcult_eyes.ogg b/sound/ambience/antag/bloodcult/bloodcult_eyes.ogg
new file mode 100644
index 0000000000000..38c223b1ad858
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 0000000000000..bd22934fd301b
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 0000000000000..a01ef30a1d487
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 3ae6c797dd33b..4f000ad82f95c 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -105,3 +105,8 @@ 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/
+
+laser2.ogg is adapted with 3 SFX made by junggle (CC 4), inferno (CC Sampling+), humanoide9000 (CC 0):
+https://freesound.org/people/junggle/sounds/28917/
+https://freesound.org/people/inferno/sounds/18397/
+https://freesound.org/people/humanoide9000/sounds/330293/
diff --git a/sound/effects/footstep/spurs1.ogg b/sound/effects/footstep/spurs1.ogg
new file mode 100644
index 0000000000000..d2754587ca15e
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 0000000000000..e02725e9079bb
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 0000000000000..e79b90dc78d9f
Binary files /dev/null and b/sound/effects/footstep/spurs3.ogg differ
diff --git a/sound/machines/fan_break.ogg b/sound/machines/fan_break.ogg
new file mode 100644
index 0000000000000..ca0549333ad66
Binary files /dev/null and b/sound/machines/fan_break.ogg differ
diff --git a/sound/machines/fan_loop.ogg b/sound/machines/fan_loop.ogg
new file mode 100644
index 0000000000000..9c7820548f670
Binary files /dev/null and b/sound/machines/fan_loop.ogg differ
diff --git a/sound/machines/fan_start.ogg b/sound/machines/fan_start.ogg
new file mode 100644
index 0000000000000..a0d11c3e969aa
Binary files /dev/null and b/sound/machines/fan_start.ogg differ
diff --git a/sound/machines/fan_stop.ogg b/sound/machines/fan_stop.ogg
new file mode 100644
index 0000000000000..84d39c3ee5a85
Binary files /dev/null and b/sound/machines/fan_stop.ogg differ
diff --git a/sound/weapons/laser2.ogg b/sound/weapons/laser2.ogg
index 5e22e30d4a0d8..7fd3969b2adf3 100644
Binary files a/sound/weapons/laser2.ogg and b/sound/weapons/laser2.ogg differ
diff --git a/strings/sillytips.txt b/strings/sillytips.txt
index 752a09b25cb58..5aa7af7ba0064 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 c96394adb2657..51dd6a635d160 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/tgstation.dme b/tgstation.dme
index 96b0f791a98c1..bb2da82fed3ee 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -66,7 +66,6 @@
#include "code\__DEFINES\communications.dm"
#include "code\__DEFINES\computers.dm"
#include "code\__DEFINES\configuration.dm"
-#include "code\__DEFINES\construction.dm"
#include "code\__DEFINES\cooldowns.dm"
#include "code\__DEFINES\crafting.dm"
#include "code\__DEFINES\crushing.dm"
@@ -134,7 +133,6 @@
#include "code\__DEFINES\map_switch.dm"
#include "code\__DEFINES\mapping.dm"
#include "code\__DEFINES\maps.dm"
-#include "code\__DEFINES\materials.dm"
#include "code\__DEFINES\maths.dm"
#include "code\__DEFINES\matrices.dm"
#include "code\__DEFINES\MC.dm"
@@ -267,6 +265,10 @@
#include "code\__DEFINES\atmospherics\atmos_mapping_helpers.dm"
#include "code\__DEFINES\atmospherics\atmos_mob_interaction.dm"
#include "code\__DEFINES\atmospherics\atmos_piping.dm"
+#include "code\__DEFINES\construction\actions.dm"
+#include "code\__DEFINES\construction\material.dm"
+#include "code\__DEFINES\construction\rcd.dm"
+#include "code\__DEFINES\construction\structures.dm"
#include "code\__DEFINES\dcs\flags.dm"
#include "code\__DEFINES\dcs\helpers.dm"
#include "code\__DEFINES\dcs\signals\mapping.dm"
@@ -486,6 +488,7 @@
#include "code\_globalvars\lighting.dm"
#include "code\_globalvars\logging.dm"
#include "code\_globalvars\phobias.dm"
+#include "code\_globalvars\rcd.dm"
#include "code\_globalvars\religion.dm"
#include "code\_globalvars\tgui.dm"
#include "code\_globalvars\time_vars.dm"
@@ -494,6 +497,7 @@
#include "code\_globalvars\lists\ambience.dm"
#include "code\_globalvars\lists\client.dm"
#include "code\_globalvars\lists\color.dm"
+#include "code\_globalvars\lists\crafting.dm"
#include "code\_globalvars\lists\flavor_misc.dm"
#include "code\_globalvars\lists\icons.dm"
#include "code\_globalvars\lists\keybindings.dm"
@@ -504,6 +508,8 @@
#include "code\_globalvars\lists\objects.dm"
#include "code\_globalvars\lists\poll_ignore.dm"
#include "code\_globalvars\lists\quirks.dm"
+#include "code\_globalvars\lists\rcd.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"
@@ -803,7 +809,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"
@@ -818,15 +823,19 @@
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\write_on_paper.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\attack_adjacent_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\attack_obstacle_in_path.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\capricious_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\climb_tree.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_paper_and_write.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_parent.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\flee_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\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"
@@ -835,6 +844,7 @@
#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"
@@ -845,6 +855,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"
@@ -932,6 +943,7 @@
#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"
@@ -1012,6 +1024,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"
@@ -1108,6 +1121,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"
@@ -1286,6 +1300,7 @@
#include "code\datums\elements\death_gases.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"
@@ -1357,6 +1372,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"
@@ -1374,6 +1390,7 @@
#include "code\datums\elements\waddling.dm"
#include "code\datums\elements\wall_engraver.dm"
#include "code\datums\elements\wall_smasher.dm"
+#include "code\datums\elements\wall_walker.dm"
#include "code\datums\elements\weapon_description.dm"
#include "code\datums\elements\weather_listener.dm"
#include "code\datums\elements\web_walker.dm"
@@ -1436,11 +1453,13 @@
#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"
#include "code\datums\looping_sounds\machinery_sounds.dm"
#include "code\datums\looping_sounds\music.dm"
+#include "code\datums\looping_sounds\vents.dm"
#include "code\datums\looping_sounds\weather.dm"
#include "code\datums\mapgen\_MapGenerator.dm"
#include "code\datums\mapgen\CaveGenerator.dm"
@@ -1796,7 +1815,6 @@
#include "code\game\machinery\barsigns.dm"
#include "code\game\machinery\botlaunchpad.dm"
#include "code\game\machinery\buttons.dm"
-#include "code\game\machinery\canister_frame.dm"
#include "code\game\machinery\cell_charger.dm"
#include "code\game\machinery\civilian_bounties.dm"
#include "code\game\machinery\constructable_frame.dm"
@@ -1989,6 +2007,7 @@
#include "code\game\objects\effects\poster_demotivational.dm"
#include "code\game\objects\effects\poster_motivational.dm"
#include "code\game\objects\effects\powerup.dm"
+#include "code\game\objects\effects\rcd.dm"
#include "code\game\objects\effects\spiderwebs.dm"
#include "code\game\objects\effects\step_triggers.dm"
#include "code\game\objects\effects\wanted_poster.dm"
@@ -2830,6 +2849,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"
@@ -2938,7 +2958,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"
@@ -2984,7 +3003,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"
@@ -3196,6 +3214,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"
@@ -3240,15 +3259,16 @@
#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\outfits.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\bit_vendor.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"
@@ -3257,6 +3277,7 @@
#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"
@@ -3279,6 +3300,8 @@
#include "code\modules\bitrunning\virtual_domain\domains\legion.dm"
#include "code\modules\bitrunning\virtual_domain\domains\pipedream.dm"
#include "code\modules\bitrunning\virtual_domain\domains\pirates.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\psyker_shuffle.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\psyker_zombies.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"
@@ -4195,6 +4218,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"
@@ -4351,6 +4375,8 @@
#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"
@@ -4368,14 +4394,26 @@
#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"
@@ -4414,6 +4452,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"
@@ -4426,6 +4468,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"
@@ -4436,7 +4479,6 @@
#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"
@@ -4455,6 +4497,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"
@@ -4478,6 +4522,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"
@@ -4656,7 +4706,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"
@@ -4676,7 +4725,6 @@
#include "code\modules\mob\living\simple_animal\friendly\cat.dm"
#include "code\modules\mob\living\simple_animal\friendly\gondola.dm"
#include "code\modules\mob\living\simple_animal\friendly\pet.dm"
-#include "code\modules\mob\living\simple_animal\friendly\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"
@@ -4701,7 +4749,6 @@
#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\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"
@@ -4711,20 +4758,14 @@
#include "code\modules\mob\living\simple_animal\hostile\skeleton.dm"
#include "code\modules\mob\living\simple_animal\hostile\space_dragon.dm"
#include "code\modules\mob\living\simple_animal\hostile\vatbeast.dm"
-#include "code\modules\mob\living\simple_animal\hostile\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"
@@ -4737,7 +4778,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"
diff --git a/tgui/packages/tgui/constants.ts b/tgui/packages/tgui/constants.ts
index 270ce9873bd6e..db6b33ca7d20d 100644
--- a/tgui/packages/tgui/constants.ts
+++ b/tgui/packages/tgui/constants.ts
@@ -159,7 +159,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 e91130baf394f..46e1261592214 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