")
/mob/camera/blob/proc/add_points(points)
blob_points = clamp(blob_points + points, 0, max_blob_points)
@@ -291,14 +312,8 @@ GLOBAL_LIST_EMPTY(blob_nodes)
src.log_talk(message, LOG_SAY)
var/message_a = say_quote(message)
- var/rendered = span_big("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]")
-
- for(var/mob/M in GLOB.mob_list)
- if(isovermind(M) || isblobmonster(M))
- to_chat(M, rendered)
- if(isobserver(M))
- var/link = FOLLOW_LINK(M, src)
- to_chat(M, "[link] [rendered]")
+ var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]"))
+ blob_telepathy(rendered, src)
/mob/camera/blob/blob_act(obj/structure/blob/B)
return
@@ -324,8 +339,8 @@ GLOBAL_LIST_EMPTY(blob_nodes)
else
return FALSE
else
- var/area/A = get_area(NewLoc)
- if(isgroundlessturf(NewLoc) || istype(A, /area/shuttle)) //if unplaced, can't go on shuttles or goundless tiles
+ var/area/check_area = get_area(NewLoc)
+ if(isgroundlessturf(NewLoc) || istype(check_area, /area/shuttle)) //if unplaced, can't go on shuttles or groundless tiles
return FALSE
forceMove(NewLoc)
return TRUE
diff --git a/code/modules/antagonists/blob/powers.dm b/code/modules/antagonists/blob/powers.dm
index c75a41a48ff..04054f6df85 100644
--- a/code/modules/antagonists/blob/powers.dm
+++ b/code/modules/antagonists/blob/powers.dm
@@ -196,38 +196,25 @@
var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as a [blobstrain.name] blobbernaut?", ROLE_BLOB, ROLE_BLOB, 50)
- factory.is_creating_blobbernaut = FALSE
-
if(!length(candidates))
to_chat(src, span_warning("You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later."))
add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST)
- factory.blobbernaut = null //players must answer rapidly
+ factory.assign_blobbernaut(null)
return FALSE
- factory.modify_max_integrity(initial(factory.max_integrity) * 0.25) //factories that produced a blobbernaut have much lower health
- factory.update_appearance()
- factory.visible_message(span_warning("The blobbernaut [pick("rips", "tears", "shreds")] its way out of the factory blob!"))
- playsound(factory.loc, 'sound/effects/splat.ogg', 50, TRUE)
-
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobber = new /mob/living/simple_animal/hostile/blob/blobbernaut(get_turf(factory))
- flick("blobbernaut_produce", blobber)
-
- factory.blobbernaut = blobber
- blobber.factory = factory
- blobber.overmind = src
- blobber.update_icons()
- blobber.adjustHealth(blobber.maxHealth * 0.5)
- blob_mobs += blobber
-
+ var/mob/living/basic/blob_minion/blobbernaut/minion/blobber = new(get_turf(factory))
+ assume_direct_control(blobber)
+ factory.assign_blobbernaut(blobber)
var/mob/dead/observer/player = pick(candidates)
- blobber.key = player.key
+ blobber.assign_key(player.key, blobstrain)
+ RegisterSignal(blobber, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_blobbernaut_attacked))
- SEND_SOUND(blobber, sound('sound/effects/blobattack.ogg'))
- SEND_SOUND(blobber, sound('sound/effects/attackblob.ogg'))
- to_chat(blobber, span_infoplain("You are powerful, hard to kill, and slowly regenerate near nodes and cores, [span_cultlarge("but will slowly die if not near the blob")] or if the factory that made you is killed."))
- to_chat(blobber, span_infoplain("You can communicate with other blobbernauts and overminds telepathically by attempting to speak normally"))
- to_chat(blobber, span_infoplain("Your overmind's blob reagent is: [blobstrain.name]!"))
- to_chat(blobber, span_infoplain("The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]"))
+/// When one of our boys attacked something, we sometimes want to perform extra effects
+/mob/camera/blob/proc/on_blobbernaut_attacked(mob/living/basic/blobbynaut, atom/target, success)
+ SIGNAL_HANDLER
+ if (!success)
+ return
+ blobstrain.blobbernaut_attack(target, blobbynaut)
/** Moves the core */
/mob/camera/blob/proc/relocate_core()
@@ -358,10 +345,11 @@
var/list/surrounding_turfs = TURF_NEIGHBORS(tile)
if(!length(surrounding_turfs))
return FALSE
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore as anything in blob_mobs)
- if(isturf(spore.loc) && get_dist(spore, tile) <= 35 && !spore.key)
- spore.LoseTarget()
- spore.Goto(pick(surrounding_turfs), spore.move_to_delay)
+ for(var/mob/living/basic/blob_mob as anything in blob_mobs)
+ if(!isturf(blob_mob.loc) || get_dist(blob_mob, tile) > 35 || blob_mob.key)
+ continue
+ blob_mob.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
+ blob_mob.ai_controller.set_blackboard_key(BB_TRAVEL_DESTINATION, pick(surrounding_turfs))
/** Opens the reroll menu to change strains */
/mob/camera/blob/proc/strain_reroll()
diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm
index 3580e3f3bf8..e206d97c26b 100644
--- a/code/modules/antagonists/blob/structures/_blob.dm
+++ b/code/modules/antagonists/blob/structures/_blob.dm
@@ -129,13 +129,13 @@
return FALSE //oh no we failed
/obj/structure/blob/proc/ConsumeTile()
- for(var/atom/A in loc)
- if(!A.can_blob_attack())
+ for(var/atom/thing in loc)
+ if(!thing.can_blob_attack())
continue
- if(isliving(A) && overmind && !isblobmonster(A)) // Make sure to inject strain-reagents with automatic attacks when needed.
- overmind.blobstrain.attack_living(A)
+ if(isliving(thing) && overmind && !HAS_TRAIT(thing, TRAIT_BLOB_ALLY)) // Make sure to inject strain-reagents with automatic attacks when needed.
+ overmind.blobstrain.attack_living(thing)
continue // Don't smack them twice though
- A.blob_act(src)
+ thing.blob_act(src)
if(iswallturf(loc))
loc.blob_act(src) //don't ask how a wall got on top of the core, just eat it
@@ -227,10 +227,10 @@
/obj/structure/blob/zap_act(power, zap_flags)
if(overmind)
if(overmind.blobstrain.tesla_reaction(src, power))
- take_damage(power * 0.0025, BURN, ENERGY)
+ take_damage(power * 3.125e-6, BURN, ENERGY)
else
- take_damage(power * 0.0025, BURN, ENERGY)
- power -= power * 0.0025 //You don't get to do it for free
+ take_damage(power * 3.125e-6, BURN, ENERGY)
+ power -= power * 2.5e-3 //You don't get to do it for free
return ..() //You don't get to do it for free
/obj/structure/blob/extinguish()
diff --git a/code/modules/antagonists/blob/structures/factory.dm b/code/modules/antagonists/blob/structures/factory.dm
index 7f28dcce224..cee7e9a0ac5 100644
--- a/code/modules/antagonists/blob/structures/factory.dm
+++ b/code/modules/antagonists/blob/structures/factory.dm
@@ -10,12 +10,12 @@
armor_type = /datum/armor/structure_blob/factory
///How many spores this factory can have.
var/max_spores = BLOB_FACTORY_MAX_SPORES
- ///The list of spores
- var/list/spores = list()
+ ///The list of spores and zombies
+ var/list/spores_and_zombies = list()
COOLDOWN_DECLARE(spore_delay)
var/spore_cooldown = BLOBMOB_SPORE_SPAWN_COOLDOWN
///Its Blobbernaut, if it has spawned any.
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobbernaut
+ var/mob/living/basic/blob_minion/blobbernaut/minion/blobbernaut
///Used in blob/powers.dm, checks if it's already trying to spawn a blobbernaut to prevent issues.
var/is_creating_blobbernaut = FALSE
@@ -32,15 +32,8 @@
overmind.factory_blobs += src
/obj/structure/blob/special/factory/Destroy()
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore in spores)
- to_chat(spore, span_userdanger("Your factory was destroyed! You can no longer sustain yourself."))
- spore.death()
- spores = null
- if(blobbernaut)
- blobbernaut.factory = null
- to_chat(blobbernaut, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
- blobbernaut.throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
- blobbernaut = null
+ spores_and_zombies = null
+ blobbernaut = null
if(overmind)
overmind.factory_blobs -= src
return ..()
@@ -49,13 +42,57 @@
. = ..()
if(blobbernaut)
return
- if(spores.len >= max_spores)
+ if(length(spores_and_zombies) >= max_spores)
return
if(!COOLDOWN_FINISHED(src, spore_delay))
return
COOLDOWN_START(src, spore_delay, spore_cooldown)
- var/mob/living/simple_animal/hostile/blob/blobspore/BS = new (loc, src)
- if(overmind) //if we don't have an overmind, we don't need to do anything but make a spore
- BS.overmind = overmind
- BS.update_icons()
- overmind.blob_mobs.Add(BS)
+ var/mob/living/basic/blob_minion/created_spore = (overmind) ? overmind.create_spore(loc) : new(loc)
+ register_mob(created_spore)
+ RegisterSignal(created_spore, COMSIG_BLOB_ZOMBIFIED, PROC_REF(on_zombie_created))
+
+/// Tracks the existence of a mob in our mobs list
+/obj/structure/blob/special/factory/proc/register_mob(mob/living/basic/blob_minion/blob_mob)
+ spores_and_zombies |= blob_mob
+ blob_mob.link_to_factory(src)
+ RegisterSignal(blob_mob, COMSIG_LIVING_DEATH, PROC_REF(on_spore_died))
+ RegisterSignal(blob_mob, COMSIG_QDELETING, PROC_REF(on_spore_lost))
+
+/// When a spore or zombie dies reset our spawn cooldown so we don't instantly replace it
+/obj/structure/blob/special/factory/proc/on_spore_died(mob/living/dead_spore)
+ SIGNAL_HANDLER
+ COOLDOWN_START(src, spore_delay, spore_cooldown)
+
+/// When a spore is deleted remove it from our list
+/obj/structure/blob/special/factory/proc/on_spore_lost(mob/living/dead_spore)
+ SIGNAL_HANDLER
+ spores_and_zombies -= dead_spore
+
+/// When a spore makes a zombie add it to our mobs list
+/obj/structure/blob/special/factory/proc/on_zombie_created(mob/living/spore, mob/living/zombie)
+ SIGNAL_HANDLER
+ register_mob(zombie)
+
+/// Produce a blobbernaut
+/obj/structure/blob/special/factory/proc/assign_blobbernaut(mob/living/new_naut)
+ is_creating_blobbernaut = FALSE
+ if (isnull(new_naut))
+ return
+
+ modify_max_integrity(initial(max_integrity) * 0.25) //factories that produced a blobbernaut have much lower health
+ visible_message(span_boldwarning("The blobbernaut [pick("rips", "tears", "shreds")] its way out of the factory blob!"))
+ playsound(loc, 'sound/effects/splat.ogg', 50, TRUE)
+
+ blobbernaut = new_naut
+ blobbernaut.link_to_factory(src)
+ RegisterSignals(new_naut, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_blobbernaut_death))
+ update_appearance(UPDATE_ICON)
+
+/// When our brave soldier dies, reset our max integrity
+/obj/structure/blob/special/factory/proc/on_blobbernaut_death(mob/living/death_naut)
+ SIGNAL_HANDLER
+ if (isnull(blobbernaut) || blobbernaut != death_naut)
+ return
+ blobbernaut = null
+ max_integrity = initial(max_integrity)
+ update_appearance(UPDATE_ICON)
diff --git a/code/modules/antagonists/changeling/powers/absorb.dm b/code/modules/antagonists/changeling/powers/absorb.dm
index c2ae89933d1..cee0f0da5b9 100644
--- a/code/modules/antagonists/changeling/powers/absorb.dm
+++ b/code/modules/antagonists/changeling/powers/absorb.dm
@@ -95,31 +95,14 @@
//Some of target's recent speech, so the changeling can attempt to imitate them better.
//Recent as opposed to all because rounds tend to have a LOT of text.
- var/list/recent_speech = list()
- var/list/say_log = list()
- var/log_source = target.logging
- for(var/log_type in log_source)
- var/nlog_type = text2num(log_type)
- if(nlog_type & LOG_SAY)
- var/list/reversed = log_source[log_type]
- if(islist(reversed))
- say_log = reverse_range(reversed.Copy())
- break
-
- if(LAZYLEN(say_log) > LING_ABSORB_RECENT_SPEECH)
- recent_speech = say_log.Copy(say_log.len-LING_ABSORB_RECENT_SPEECH+1,0) //0 so len-LING_ARS+1 to end of list
- else
- for(var/spoken_memory in say_log)
- if(recent_speech.len >= LING_ABSORB_RECENT_SPEECH)
- break
- recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
+ var/list/recent_speech = target.copy_recent_speech()
if(recent_speech.len)
changeling.antag_memory += "Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]! "
to_chat(owner, span_boldnotice("Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!"))
for(var/spoken_memory in recent_speech)
- changeling.antag_memory += "\"[recent_speech[spoken_memory]]\" "
- to_chat(owner, span_notice("\"[recent_speech[spoken_memory]]\""))
+ changeling.antag_memory += "\"[spoken_memory]\" "
+ to_chat(owner, span_notice("\"[spoken_memory]\""))
changeling.antag_memory += "We have no more knowledge of [target]'s speech patterns. "
to_chat(owner, span_boldnotice("We have no more knowledge of [target]'s speech patterns."))
diff --git a/code/modules/antagonists/changeling/powers/panacea.dm b/code/modules/antagonists/changeling/powers/panacea.dm
index 25a267e03df..9fe7613cc4c 100644
--- a/code/modules/antagonists/changeling/powers/panacea.dm
+++ b/code/modules/antagonists/changeling/powers/panacea.dm
@@ -14,7 +14,9 @@
var/list/bad_organs = list(
user.get_organ_by_type(/obj/item/organ/internal/empowered_borer_egg), // SKYRAT EDIT ADDITION
user.get_organ_by_type(/obj/item/organ/internal/body_egg),
- user.get_organ_by_type(/obj/item/organ/internal/zombie_infection))
+ user.get_organ_by_type(/obj/item/organ/internal/legion_tumour),
+ user.get_organ_by_type(/obj/item/organ/internal/zombie_infection),
+ )
try_to_mutant_cure(user) //SKYRAT EDIT ADDITION
diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm
index 3b2a13c2cb5..062412f70e9 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -886,7 +886,7 @@ structure_check() searches for nearby cultist structures required for the invoca
new_human.equipOutfit(/datum/outfit/ghost_cultist) //give them armor
new_human.apply_status_effect(/datum/status_effect/cultghost) //ghosts can't summon more ghosts
new_human.set_invis_see(SEE_INVISIBLE_OBSERVER)
- ADD_TRAIT(new_human, TRAIT_NOBREATH, INNATE_TRAIT)
+ new_human.add_traits(list(TRAIT_NOBREATH, TRAIT_PERMANENTLY_MORTAL), INNATE_TRAIT) // permanently mortal can be removed once this is a bespoke kind of mob
ghosts++
playsound(src, 'sound/magic/exit_blood.ogg', 50, TRUE)
visible_message(span_warning("A cloud of red mist forms above [src], and from within steps... a [new_human.gender == FEMALE ? "wo":""]man."))
diff --git a/code/modules/antagonists/ert/ert.dm b/code/modules/antagonists/ert/ert.dm
index 0088660ec14..116d19f5164 100644
--- a/code/modules/antagonists/ert/ert.dm
+++ b/code/modules/antagonists/ert/ert.dm
@@ -12,8 +12,8 @@
antagpanel_category = ANTAG_GROUP_ERT
suicide_cry = "FOR NANOTRASEN!!"
count_against_dynamic_roll_chance = FALSE
- // Not 'true' antags, cannot induct to prevent issues
- antag_flags = NONE
+ // Not 'true' antags, this disables certain interactions that assume the owner is a baddie
+ antag_flags = FLAG_FAKE_ANTAG
var/datum/team/ert/ert_team
var/leader = FALSE
var/datum/outfit/outfit = /datum/outfit/centcom/ert/security
diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm
index a0f0d86d33f..ccc7fce6ecf 100644
--- a/code/modules/antagonists/heretic/heretic_antag.dm
+++ b/code/modules/antagonists/heretic/heretic_antag.dm
@@ -34,6 +34,8 @@
var/heretic_path = PATH_START
/// A sum of how many knowledge points this heretic CURRENTLY has. Used to research.
var/knowledge_points = 2 //SKYRAT EDIT - ORIGINAL 1
+ /// How many side path points the heretic has. He gains one of these per main path that splits into two sidepaths. These can be used in place of knowledge points for side paths only.
+ var/side_path_points = 0
/// The time between gaining influence passively. The heretic gain +1 knowledge points every this duration of time.
var/passive_gain_timer = 20 MINUTES
/// Assoc list of [typepath] = [knowledge instance]. A list of all knowledge this heretic's reserached.
@@ -85,6 +87,7 @@
var/list/data = list()
data["charges"] = knowledge_points
+ data["side_charges"] = side_path_points
data["total_sacrifices"] = total_sacrifices
data["ascended"] = ascended
@@ -98,7 +101,10 @@
knowledge_data["desc"] = initial(knowledge.desc)
knowledge_data["gainFlavor"] = initial(knowledge.gain_text)
knowledge_data["cost"] = initial(knowledge.cost)
- knowledge_data["disabled"] = (initial(knowledge.cost) > knowledge_points)
+ if(initial(knowledge.route) == PATH_SIDE)
+ knowledge_data["disabled"] = (initial(knowledge.cost) > knowledge_points + side_path_points)
+ else
+ knowledge_data["disabled"] = (initial(knowledge.cost) > knowledge_points)
// Final knowledge can't be learned until all objectives are complete.
if(ispath(knowledge, /datum/heretic_knowledge/ultimate))
@@ -142,13 +148,22 @@
if(!ispath(researched_path))
CRASH("Heretic attempted to learn non-heretic_knowledge path! (Got: [researched_path])")
- if(initial(researched_path.cost) > knowledge_points)
- return TRUE
+ // If side path and has path points, buy!
+ var/coupon = FALSE
+ if((initial(researched_path.route) == PATH_SIDE) && side_path_points)
+ coupon = TRUE
+ // else try normal purchase
+ else if(initial(researched_path.cost) > knowledge_points)
+ return
+
if(!gain_knowledge(researched_path))
return TRUE
- log_heretic_knowledge("[key_name(owner)] gained knowledge: [initial(researched_path.name)]")
- knowledge_points -= initial(researched_path.cost)
+ log_heretic_knowledge("[key_name(owner)] gained knowledge: [initial(researched_path.name)][coupon ? "(via free side-path point)" : ""]")
+ if(coupon)
+ side_path_points -= initial(researched_path.cost)
+ else
+ knowledge_points -= initial(researched_path.cost)
return TRUE
/datum/antagonist/heretic/submit_player_objective(retain_existing = FALSE, retain_escape = TRUE, force = FALSE)
@@ -305,14 +320,14 @@
* * drawing_time - how long the do_after takes to make the rune
* * additional checks - optional callbacks to be ran while drawing the rune
*/
-/datum/antagonist/heretic/proc/try_draw_rune(mob/living/user, turf/target_turf, drawing_time = 30 SECONDS, additional_checks)
+/datum/antagonist/heretic/proc/try_draw_rune(mob/living/user, turf/target_turf, drawing_time = 20 SECONDS, additional_checks)
for(var/turf/nearby_turf as anything in RANGE_TURFS(1, target_turf))
if(!isopenturf(nearby_turf) || is_type_in_typecache(nearby_turf, blacklisted_rune_turfs))
target_turf.balloon_alert(user, "invalid placement for rune!")
return
if(locate(/obj/effect/heretic_rune) in range(3, target_turf))
- target_turf.balloon_alert(user, "to close to another rune!")
+ target_turf.balloon_alert(user, "too close to another rune!")
return
if(drawing_rune)
@@ -330,16 +345,16 @@
* * drawing_time - how long the do_after takes to make the rune
* * additional checks - optional callbacks to be ran while drawing the rune
*/
-/datum/antagonist/heretic/proc/draw_rune(mob/living/user, turf/target_turf, drawing_time = 30 SECONDS, additional_checks)
+/datum/antagonist/heretic/proc/draw_rune(mob/living/user, turf/target_turf, drawing_time = 20 SECONDS, additional_checks)
drawing_rune = TRUE
var/rune_colour = path_to_rune_color[heretic_path]
target_turf.balloon_alert(user, "drawing rune...")
var/obj/effect/temp_visual/drawing_heretic_rune/drawing_effect
- if (drawing_time >= (30 SECONDS))
- drawing_effect = new(target_turf, rune_colour)
- else
+ if (drawing_time < (10 SECONDS))
drawing_effect = new /obj/effect/temp_visual/drawing_heretic_rune/fast(target_turf, rune_colour)
+ else
+ drawing_effect = new(target_turf, rune_colour)
if(!do_after(user, drawing_time, target_turf, extra_checks = additional_checks))
target_turf.balloon_alert(user, "interrupted!")
@@ -602,7 +617,7 @@
if(!admin.client?.holder)
to_chat(admin, span_warning("You shouldn't be using this!"))
return
-
+
var/mob/living/pawn = owner.current
pawn.equip_to_slot_if_possible(new /obj/item/clothing/neck/heretic_focus(get_turf(pawn)), ITEM_SLOT_NECK, TRUE, TRUE)
to_chat(pawn, span_hypnophrase("The Mansus has manifested you a focus."))
@@ -774,8 +789,8 @@
target_amount = main_path_length
// Add in the base research we spawn with, otherwise it'd be too easy.
target_amount += length(GLOB.heretic_start_knowledge)
- // And add in some buffer, to require some sidepathing.
- target_amount += rand(2, 4)
+ // And add in some buffer, to require some sidepathing, especially since heretics get some free side paths.
+ target_amount += rand(5, 7)
update_explanation_text()
/datum/objective/heretic_research/update_explanation_text()
diff --git a/code/modules/antagonists/heretic/heretic_focus.dm b/code/modules/antagonists/heretic/heretic_focus.dm
index b7c79b6d6ca..45bbf743b8c 100644
--- a/code/modules/antagonists/heretic/heretic_focus.dm
+++ b/code/modules/antagonists/heretic/heretic_focus.dm
@@ -46,7 +46,7 @@
if(!IS_HERETIC(user))
return
- if(!(source.slot_flags & slot))
+ if(source.slot_flags && !(source.slot_flags & slot))
return
ADD_TRAIT(user, TRAIT_ALLOW_HERETIC_CASTING, ELEMENT_TRAIT(source))
diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm
index b51c2a5dcb8..ea22955d83e 100644
--- a/code/modules/antagonists/heretic/heretic_knowledge.dm
+++ b/code/modules/antagonists/heretic/heretic_knowledge.dm
@@ -25,11 +25,16 @@
var/list/banned_knowledge = list()
/// Assoc list of [typepaths we need] to [amount needed].
/// If set, this knowledge allows the heretic to do a ritual on a transmutation rune with the components set.
+ /// If one of the items in the list is a list, it's treated as 'any of these items will work'
var/list/required_atoms
/// Paired with above. If set, the resulting spawned atoms upon ritual completion.
var/list/result_atoms = list()
+ /// If set, required_atoms checks for these *exact* types and doesn't allow them to be ingredients.
+ var/list/banned_atom_types = list()
/// Cost of knowledge in knowledge points
var/cost = 0
+ /// If true, adds side path points according to value. Only main branch powers that split into sidepaths should have this.
+ var/adds_sidepath_points = 0
/// The priority of the knowledge. Higher priority knowledge appear higher in the ritual list.
/// Number itself is completely arbitrary. Does not need to be set for non-ritual knowledge.
var/priority = 0
@@ -58,6 +63,8 @@
if(gain_text)
to_chat(user, span_warning("[gain_text]"))
+ // Usually zero
+ our_heretic.side_path_points += adds_sidepath_points
on_gain(user, our_heretic)
/**
@@ -112,6 +119,18 @@
/datum/heretic_knowledge/proc/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc)
return TRUE
+/**
+ * Parses specific items into a more reaadble form.
+ * Can be overriden by knoweldge subtypes.
+ */
+/datum/heretic_knowledge/proc/parse_required_item(atom/item_path, number_of_things)
+ // If we need a human, there is a high likelihood we actually need a (dead) body
+ if(ispath(item_path, /mob/living/carbon/human))
+ return "bod[number_of_things > 1 ? "ies" : "y"]"
+ if(ispath(item_path, /mob/living))
+ return "carcass[number_of_things > 1 ? "es" : ""] of any kind"
+ return "[initial(item_path.name)]\s"
+
/**
* Called whenever the knowledge's associated ritual is completed successfully.
*
@@ -158,9 +177,12 @@
var/obj/item/stack/sac_stack = sacrificed
var/how_much_to_use = 0
for(var/requirement in required_atoms)
- if(istype(sacrificed, requirement))
- how_much_to_use = min(required_atoms[requirement], sac_stack.amount)
- break
+ if(islist(requirement) && !is_type_in_list(sacrificed, requirement))
+ continue
+ if(!istype(sacrificed, requirement))
+ continue
+ how_much_to_use = min(required_atoms[requirement], sac_stack.amount)
+ break
sac_stack.use(how_much_to_use)
continue
@@ -264,7 +286,7 @@
/datum/heretic_knowledge/mark
abstract_parent_type = /datum/heretic_knowledge/mark
mutually_exclusive = TRUE
- cost = 2
+ cost = 1
/// The status effect typepath we apply on people on mansus grasp.
var/datum/status_effect/eldritch/mark_type
diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm
index 503e066d0e3..67a3bbc16b4 100644
--- a/code/modules/antagonists/heretic/influences.dm
+++ b/code/modules/antagonists/heretic/influences.dm
@@ -260,7 +260,8 @@
// Using a codex will give you two knowledge points for draining.
if(!being_drained && istype(weapon, /obj/item/codex_cicatrix))
var/obj/item/codex_cicatrix/codex = weapon
- codex.open_animation()
+ if(!codex.book_open)
+ codex.attack_self(user) // open booke
INVOKE_ASYNC(src, PROC_REF(drain_influence), user, 2)
return TRUE
diff --git a/code/modules/antagonists/heretic/items/forbidden_book.dm b/code/modules/antagonists/heretic/items/forbidden_book.dm
index 80721c97592..ff570801c5f 100644
--- a/code/modules/antagonists/heretic/items/forbidden_book.dm
+++ b/code/modules/antagonists/heretic/items/forbidden_book.dm
@@ -9,8 +9,6 @@
w_class = WEIGHT_CLASS_SMALL
/// Helps determine the icon state of this item when it's used on self.
var/book_open = FALSE
- /// id for timer
- var/timer_id
/obj/item/codex_cicatrix/Initialize(mapload)
. = ..()
@@ -31,6 +29,7 @@
. += span_notice("Can be used to tap influences for additional knowledge points.")
. += span_notice("Can also be used to draw or remove transmutation runes with ease.")
+ . += span_notice("Additionally, it can work as a focus for your spells in a pinch, though a more specialized relic is recommended, as this may get dropped in combat.")
/obj/item/codex_cicatrix/attack_self(mob/user, modifiers)
. = ..()
@@ -39,8 +38,12 @@
if(book_open)
close_animation()
+ RemoveElement(/datum/element/heretic_focus)
+ w_class = WEIGHT_CLASS_SMALL
else
open_animation()
+ AddElement(/datum/element/heretic_focus)
+ w_class = WEIGHT_CLASS_NORMAL
/obj/item/codex_cicatrix/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
. = ..()
@@ -52,7 +55,7 @@
return
if(isopenturf(target))
- heretic_datum.try_draw_rune(user, target, drawing_time = 12 SECONDS)
+ heretic_datum.try_draw_rune(user, target, drawing_time = 8 SECONDS)
return TRUE
/// Plays a little animation that shows the book opening and closing.
@@ -61,12 +64,8 @@
flick("[base_icon_state]_opening", src)
book_open = TRUE
- timer_id = addtimer(CALLBACK(src, PROC_REF(close_animation)), 5 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
-
/// Plays a closing animation and resets the icon state.
/obj/item/codex_cicatrix/proc/close_animation()
icon_state = base_icon_state
flick("[base_icon_state]_closing", src)
book_open = FALSE
-
- deltimer(timer_id)
diff --git a/code/modules/antagonists/heretic/knowledge/ash_lore.dm b/code/modules/antagonists/heretic/knowledge/ash_lore.dm
index df29ba9efa0..52e25f6a8b1 100644
--- a/code/modules/antagonists/heretic/knowledge/ash_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/ash_lore.dm
@@ -72,9 +72,9 @@
name = "Ashen Passage"
desc = "Grants you Ashen Passage, a silent but short range jaunt."
gain_text = "He knew how to walk between the planes."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/ash_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/summon/fire_shark,
/datum/heretic_knowledge/medallion,
)
@@ -127,6 +127,7 @@
The mask instills fear into heathens who witness it, causing stamina damage, hallucinations, and insanity. \
It can also be forced onto a heathen, to make them unable to take it off..."
gain_text = "The Nightwatcher was lost. That's what the Watch believed. Yet he walked the world, unnoticed by the masses."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/ash,
/datum/heretic_knowledge/reroll_targets,
@@ -165,6 +166,7 @@
If any victims afflicted are in critical condition, they will also instantly die."
gain_text = "The fire was inescapable, and yet, life remained in his charred body. \
The Nightwatcher was a particular man, always watching."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/ash_final,
/datum/heretic_knowledge/summon/ashy,
diff --git a/code/modules/antagonists/heretic/knowledge/blade_lore.dm b/code/modules/antagonists/heretic/knowledge/blade_lore.dm
index 01358807fce..84e266c8374 100644
--- a/code/modules/antagonists/heretic/knowledge/blade_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/blade_lore.dm
@@ -101,10 +101,10 @@
towards your attacker. This effect can only trigger once every 20 seconds."
gain_text = "The footsoldier was known to be a fearsome duelist. \
Their general quickly appointed them as their personal Champion."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/limited_amount/risen_corpse,
/datum/heretic_knowledge/mark/blade_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/armor,
)
cost = 1
@@ -243,6 +243,7 @@
you gain increased resistance to gaining wounds and resistance to batons."
gain_text = "In time, it was he who stood alone among the bodies of his former comrades, awash in blood, none of it his own. \
He was without rival, equal, or purpose."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/blade,
/datum/heretic_knowledge/reroll_targets,
@@ -372,6 +373,7 @@
at a target, dealing damage and causing bleeding."
gain_text = "Without thinking, I took the knife of a fallen soldier and threw with all my might. My aim was true! \
The Torn Champion smiled at their first taste of agony, and with a nod, their blades became my own."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/summon/maid_in_mirror,
/datum/heretic_knowledge/ultimate/blade_final,
diff --git a/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm b/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm
index 9e4d77bd95c..75ee0cd5916 100644
--- a/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm
@@ -70,9 +70,9 @@
However, people with a star mark will get transported along with another person using the rune."
gain_text = "The distant stars crept into my dreams, roaring and screaming without reason. \
I spoke, and heard my own words echoed back."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/cosmic_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/essence,
/datum/heretic_knowledge/summon/fire_shark,
)
@@ -114,6 +114,7 @@
desc = "Fires a projectile that moves very slowly and creates cosmic fields on impact. \
Anyone hit by the projectile will recieve burn damage, a knockdown, and give people in a three tile range a star mark."
gain_text = "The Beast was behind me now at all times, with each sacrifice words of affirmation coursed through me."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/cosmic,
/datum/heretic_knowledge/reroll_targets,
@@ -208,6 +209,7 @@
desc = "Grants you Cosmic Expansion, a spell that creates a 3x3 area of cosmic fields around you. \
Nearby beings will also receive a star mark."
gain_text = "The ground now shook beneath me. The Beast inhabited me, and their voice was intoxicating."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/cosmic_final,
/datum/heretic_knowledge/eldritch_coin,
@@ -253,7 +255,7 @@
var/mob/living/basic/heretic_summon/star_gazer/star_gazer_mob = new /mob/living/basic/heretic_summon/star_gazer(loc)
star_gazer_mob.maxHealth = INFINITY
star_gazer_mob.health = INFINITY
- user.AddElement(/datum/element/death_linked, star_gazer_mob)
+ user.AddComponent(/datum/component/death_linked, star_gazer_mob)
star_gazer_mob.AddComponent(/datum/component/obeys_commands, star_gazer_commands)
star_gazer_mob.AddComponent(/datum/component/damage_aura, range = 7, burn_damage = 0.5, simple_damage = 0.5, immune_factions = list(FACTION_HERETIC), current_owner = user)
star_gazer_mob.befriend(user)
diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
index 07fa2718185..915165ad768 100644
--- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
@@ -126,9 +126,9 @@
Voiceless Dead are mute ghouls and only have 50 health, but can use Bloody Blades effectively. \
You can only create two at a time."
gain_text = "I found notes of a dark ritual, unfinished... yet still, I pushed forward."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/flesh_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/void_cloak,
/datum/heretic_knowledge/medallion,
)
@@ -239,6 +239,7 @@
the ability to link minds to communicate with ease, but are very fragile and weak in combat."
gain_text = "I could not continue alone. I was able to summon The Uncanny Man to help me see more. \
The screams... once constant, now silenced by their wretched appearance. Nothing was out of reach."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/flesh,
/datum/heretic_knowledge/reroll_targets,
@@ -280,6 +281,7 @@
Stalkers can jaunt, release EMPs, shapeshift into animals or automatons, and are strong in combat."
gain_text = "I was able to combine my greed and desires to summon an eldritch beast I had never seen before. \
An ever shapeshifting mass of flesh, it knew well my goals. The Marshal approved."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/flesh_final,
/datum/heretic_knowledge/summon/ashy,
diff --git a/code/modules/antagonists/heretic/knowledge/general_side.dm b/code/modules/antagonists/heretic/knowledge/general_side.dm
index 9c3fbe9d447..2dc2719227b 100644
--- a/code/modules/antagonists/heretic/knowledge/general_side.dm
+++ b/code/modules/antagonists/heretic/knowledge/general_side.dm
@@ -39,19 +39,3 @@
return FALSE
return TRUE
-
-/datum/heretic_knowledge/codex_cicatrix
- name = "Codex Cicatrix"
- desc = "Allows you to transmute a bible, a fountain pen, and hide from an animal (or human) to create a Codex Cicatrix. \
- The Codex Cicatrix can be used when draining influences to gain additional knowledge, but comes at greater risk of being noticed. \
- It can also be used to draw and remove transmutation runes easier."
- gain_text = "The occult leaves fragments of knowledge and power anywhere and everywhere. The Codex Cicatrix is one such example. \
- Within the leather-bound faces and age old pages, a path into the Mansus is revealed."
- required_atoms = list(
- /obj/item/book/bible = 1,
- /obj/item/pen/fountain = 1,
- /obj/item/stack/sheet/animalhide = 1,
- )
- result_atoms = list(/obj/item/codex_cicatrix)
- cost = 1
- route = PATH_SIDE
diff --git a/code/modules/antagonists/heretic/knowledge/knock_lore.dm b/code/modules/antagonists/heretic/knowledge/knock_lore.dm
index fcb2c6ceb4c..6879f527b6b 100644
--- a/code/modules/antagonists/heretic/knowledge/knock_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/knock_lore.dm
@@ -70,7 +70,7 @@
/datum/heretic_knowledge/knock_grasp/proc/on_secondary_mansus_grasp(mob/living/source, atom/target)
SIGNAL_HANDLER
-
+
if(ismecha(target))
var/obj/vehicle/sealed/mecha/mecha = target
mecha.dna_lock = null
@@ -89,7 +89,7 @@
var/turf/target_turf = get_turf(target)
SEND_SIGNAL(target_turf, COMSIG_ATOM_MAGICALLY_UNLOCKED, src, source)
playsound(target, 'sound/magic/hereticknock.ogg', 100, TRUE, -1)
-
+
return COMPONENT_USE_HAND
/datum/heretic_knowledge/key_ring
@@ -99,6 +99,7 @@
You can use it in-hand to change its form to a card you fused. \
Does not preserve the card used in the ritual."
gain_text = "Gateways shall open before me, my very will ensnaring reality."
+ adds_sidepath_points = 1
required_atoms = list(
/obj/item/storage/wallet = 1,
/obj/item/stack/rods = 1,
@@ -143,6 +144,7 @@
desc = "Grants you Burglar's Finesse, a single-target spell \
that puts a random item from the victims backpack into your hand."
gain_text = "Their trinkets will be mine, as will their lives in due time."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/spell/apetra_vulnera,
/datum/heretic_knowledge/spell/opening_blast,
@@ -171,6 +173,7 @@
While in refuge, you cannot use your hands or spells, and you are immune to slowdown. \
You are invincible but unable to harm anything. Cancelled by being hit with an anti-magic item."
gain_text = "Then I saw my my own reflection cascaded mind-numbingly enough times that I was but a haze."
+ adds_sidepath_points = 1
next_knowledge = list(/datum/heretic_knowledge/ultimate/knock_final)
route = PATH_KNOCK
spell_to_add = /datum/action/cooldown/spell/caretaker
diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm
index bbb98f84bd6..1eeaab69bdb 100644
--- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm
@@ -80,9 +80,9 @@
name = "Leeching Walk"
desc = "Grants you passive healing and resistance to batons while standing over rust."
gain_text = "The speed was unparalleled, the strength unnatural. The Blacksmith was smiling."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/rust_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/armor,
/datum/heretic_knowledge/essence,
/datum/heretic_knowledge/entropy_pulse,
@@ -169,6 +169,7 @@
desc = "Grants you Aggressive Spread, a spell that spreads rust to nearby surfaces. \
Already rusted surfaces are destroyed."
gain_text = "All wise men know well not to visit the Rusted Hills... Yet the Blacksmith's tale was inspiring."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/rust,
/datum/heretic_knowledge/reroll_targets,
@@ -198,6 +199,7 @@
at friend or foe wildly. Also rusts and destroys and surfaces it hits."
gain_text = "The corrosion was unstoppable. The rust was unpleasable. \
The Blacksmith was gone, and you hold their blade. Champions of hope, the Rustbringer is nigh!"
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/rust_final,
/datum/heretic_knowledge/summon/rusty,
diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm
index 8f36b866510..eb766392290 100644
--- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm
@@ -212,3 +212,79 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge())
spell_to_add = /datum/action/cooldown/spell/shadow_cloak
cost = 0
route = PATH_START
+
+/**
+ * Codex Cicatrixi is available at the start:
+ * This allows heretics to choose if they want to rush all the influences and take them stealthily, or
+ * Construct a codex and take what's left with more points.
+ * Another downside to having the book is strip searches, which means that it's not just a free nab, at least until you get exposed - and when you do, you'll probably need the faster drawing speed.
+ * Overall, it's a tradeoff between speed and stealth or power.
+ */
+/datum/heretic_knowledge/codex_cicatrix
+ name = "Codex Cicatrix"
+ desc = "Allows you to transmute a book, any unique pen (anything but generic pens), and your pick from any carcass (animal or human), leather, or hide to create a Codex Cicatrix. \
+ The Codex Cicatrix can be used when draining influences to gain additional knowledge, but comes at greater risk of being noticed. \
+ It can also be used to draw and remove transmutation runes easier, and as a spell focus in a pinch."
+ gain_text = "The occult leaves fragments of knowledge and power anywhere and everywhere. The Codex Cicatrix is one such example. \
+ Within the leather-bound faces and age old pages, a path into the Mansus is revealed."
+ required_atoms = list(
+ /obj/item/book = 1,
+ /obj/item/pen = 1,
+ list(/mob/living, /obj/item/stack/sheet/leather, /obj/item/stack/sheet/animalhide) = 1,
+ )
+ banned_atom_types = list(/obj/item/pen)
+ result_atoms = list(/obj/item/codex_cicatrix)
+ cost = 1
+ route = PATH_START
+ priority = MAX_KNOWLEDGE_PRIORITY - 3 // Least priority out of the starting knowledges, as it's an optional boon.
+
+/datum/heretic_knowledge/codex_cicatrix/parse_required_item(atom/item_path, number_of_things)
+ if(item_path == /obj/item/pen)
+ return "unique type of pen"
+ return ..()
+
+/datum/heretic_knowledge/codex_cicatrix/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/body in atoms)
+ if(body.stat != DEAD)
+ continue
+
+ selected_atoms += body
+ return TRUE
+ return FALSE
+
+/datum/heretic_knowledge/codex_cicatrix/cleanup_atoms(list/selected_atoms)
+ var/mob/living/body = locate() in selected_atoms
+ if(!body)
+ return
+ // A golem or an android doesn't have skin!
+ var/exterior_text = "skin"
+ // If carbon, it's the limb. If not, it's the body.
+ var/ripped_thing = body
+
+ // We will check if it's a carbon's body.
+ // If it is, we will damage a random bodypart, and check that bodypart for its body type, to select between 'skin' or 'exterior'.
+ if(iscarbon(body))
+ var/mob/living/carbon/carbody = body
+ var/obj/item/bodypart/bodypart = pick(carbody.bodyparts)
+ ripped_thing = bodypart
+ bodypart.receive_damage(25, sharpness = SHARP_EDGED)
+ if(!(bodypart.bodytype & BODYTYPE_ORGANIC))
+ exterior_text = "exterior"
+ else
+ // If it is not a carbon mob, we will just check biotypes and damage it directly.
+ if(body.mob_biotypes & (MOB_MINERAL|MOB_ROBOTIC))
+ exterior_text = "exterior"
+ body.apply_damage(25, BRUTE)
+
+ // Procure book for flavor text. This is why we call parent at the end.
+ var/obj/item/book/le_book = locate() in selected_atoms
+ if(!le_book)
+ stack_trace("Somehow, no book in codex cicatrix selected atoms! [english_list(selected_atoms)]")
+ playsound(body, 'sound/items/poster_ripped.ogg', 100, TRUE)
+ body.do_jitter_animation()
+ body.visible_message(span_danger("An awful ripping sound is heard as [ripped_thing]'s [exterior_text] is ripped straight out, wrapping around [le_book || "the book"], turning into an eldritch shade of blue!"))
+ return ..()
diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm
index 57db8636818..a5e21472517 100644
--- a/code/modules/antagonists/heretic/knowledge/void_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm
@@ -81,9 +81,9 @@
You can still take damage due to a lack of pressure."
gain_text = "I found a thread of cold breath. It lead me to a strange shrine, all made of crystals. \
Translucent and white, a depiction of a nobleman stood before me."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/mark/void_mark,
- /datum/heretic_knowledge/codex_cicatrix,
/datum/heretic_knowledge/void_cloak,
/datum/heretic_knowledge/limited_amount/risen_corpse,
)
@@ -127,6 +127,7 @@
Additionally causes damage to heathens around your original and target destination."
gain_text = "The entity calls themself the Aristocrat. They effortlessly walk through air like \
nothing - leaving a harsh, cold breeze in their wake. They disappear, and I am left in the blizzard."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/blade_upgrade/void,
/datum/heretic_knowledge/reroll_targets,
@@ -161,6 +162,7 @@
desc = "Grants you Void Pull, a spell that pulls all nearby heathens towards you, stunning them briefly."
gain_text = "All is fleeting, but what else stays? I'm close to ending what was started. \
The Aristocrat reveals themselves to me again. They tell me I am late. Their pull is immense, I cannot turn back."
+ adds_sidepath_points = 1
next_knowledge = list(
/datum/heretic_knowledge/ultimate/void_final,
/datum/heretic_knowledge/spell/cleave,
diff --git a/code/modules/antagonists/heretic/magic/star_touch.dm b/code/modules/antagonists/heretic/magic/star_touch.dm
index de3a56128de..ba8c2a56391 100644
--- a/code/modules/antagonists/heretic/magic/star_touch.dm
+++ b/code/modules/antagonists/heretic/magic/star_touch.dm
@@ -73,11 +73,13 @@
/obj/item/melee/touch_attack/star_touch/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/effect_remover, \
+ AddComponent(\
+ /datum/component/effect_remover, \
success_feedback = "You remove %THEEFFECT.", \
tip_text = "Clear rune", \
on_clear_callback = CALLBACK(src, PROC_REF(after_clear_rune)), \
- effects_we_clear = list(/obj/effect/cosmic_rune))
+ effects_we_clear = list(/obj/effect/cosmic_rune), \
+ )
/*
* Callback for effect_remover component.
diff --git a/code/modules/antagonists/heretic/structures/knock_final.dm b/code/modules/antagonists/heretic/structures/knock_final.dm
index 85face85609..c8a2058eb9f 100644
--- a/code/modules/antagonists/heretic/structures/knock_final.dm
+++ b/code/modules/antagonists/heretic/structures/knock_final.dm
@@ -5,39 +5,68 @@
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
icon = 'icons/obj/anomaly.dmi'
icon_state = "bhole3"
- color = "#53277E"
- light_color = "#53277E" //cooler purple
+ color = COLOR_VOID_PURPLE
+ light_color = COLOR_VOID_PURPLE
light_range = 20
anchored = TRUE
density = FALSE
layer = HIGH_PIPE_LAYER //0.01 above sigil layer used by heretic runes
move_resist = INFINITY
+ /// Who is our daddy?
var/datum/mind/ascendee
- ///a static list of heretic summons, this shouldnt even matter enough to be static but whatever
+ /// True if we're currently checking for ghost opinions
+ var/gathering_candidates = TRUE
+ ///a static list of heretic summons we cam create, automatically populated from heretic monster subtypes
var/static/list/monster_types
+ /// A static list of heretic summons which we should not create
+ var/static/list/monster_types_blacklist = list(
+ /mob/living/basic/heretic_summon/star_gazer,
+ /mob/living/simple_animal/hostile/heretic_summon/armsy,
+ /mob/living/simple_animal/hostile/heretic_summon/armsy/prime,
+ )
-/obj/structure/knock_tear/Initialize(mapload, ascendant)
+/obj/structure/knock_tear/Initialize(mapload, datum/mind/ascendant_mind)
. = ..()
transform *= 3
- if(!monster_types)
- monster_types = subtypesof(/mob/living/simple_animal/hostile/heretic_summon) - /mob/living/simple_animal/hostile/heretic_summon/armsy/prime
- if(ascendant)
- ascendee = ascendant
+ if(isnull(monster_types))
+ monster_types = subtypesof(/mob/living/simple_animal/hostile/heretic_summon) + subtypesof(/mob/living/basic/heretic_summon) - monster_types_blacklist
+ if(!isnull(ascendant_mind))
+ ascendee = ascendant_mind
+ RegisterSignals(ascendant_mind.current, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(end_madness))
SSpoints_of_interest.make_point_of_interest(src)
INVOKE_ASYNC(src, PROC_REF(poll_ghosts))
+/// Ask ghosts if they want to make some noise
/obj/structure/knock_tear/proc/poll_ghosts()
var/list/candidates = poll_ghost_candidates("Would you like to be a random eldritch monster attacking the crew?", ROLE_SENTIENCE, ROLE_SENTIENCE, 10 SECONDS, POLL_IGNORE_HERETIC_MONSTER)
while(LAZYLEN(candidates))
var/mob/dead/observer/candidate = pick_n_take(candidates)
ghost_to_monster(candidate, should_ask = FALSE)
+ gathering_candidates = FALSE
+
+/// Destroy the rift if you kill the heretic
+/obj/structure/knock_tear/proc/end_madness(datum/former_master)
+ SIGNAL_HANDLER
+ var/turf/our_turf = get_turf(src)
+ playsound(our_turf, 'sound/magic/castsummon.ogg', vol = 100, vary = TRUE)
+ visible_message(span_boldwarning("The rip in space spasms and disappears!"))
+ UnregisterSignal(former_master, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING)) // Just in case they die THEN delete
+ new /obj/effect/temp_visual/destabilising_tear(our_turf)
+ qdel(src)
/obj/structure/knock_tear/attack_ghost(mob/user)
. = ..()
- if(.)
+ if(. || gathering_candidates)
return
ghost_to_monster(user)
+/obj/structure/knock_tear/examine(mob/user)
+ . = ..()
+ if (!isobserver(user) || gathering_candidates)
+ return
+ . += span_notice("You can use this to enter the world as a foul monster.")
+
+/// Turn a ghost into an 'orrible beast
/obj/structure/knock_tear/proc/ghost_to_monster(mob/dead/observer/user, should_ask = TRUE)
if(should_ask)
var/ask = tgui_alert(user, "Become a monster?", "Ascended Rift", list("Yes", "No"))
@@ -61,7 +90,26 @@
/obj/structure/knock_tear/move_crushed(atom/movable/pusher, force = MOVE_FORCE_DEFAULT, direction)
return FALSE
-/obj/structure/knock_tear/Destroy(force) //this shouldnt happen but hey
+/obj/structure/knock_tear/Destroy(force)
if(ascendee)
ascendee = null
return ..()
+
+/obj/effect/temp_visual/destabilising_tear
+ name = "destabilised tear"
+ icon = 'icons/obj/anomaly.dmi'
+ icon_state = "bhole3"
+ color = COLOR_VOID_PURPLE
+ light_color = COLOR_VOID_PURPLE
+ light_range = 20
+ layer = HIGH_PIPE_LAYER
+ duration = 1 SECONDS
+
+/obj/effect/temp_visual/destabilising_tear/Initialize(mapload)
+ . = ..()
+ transform *= 3
+ animate(src, transform = matrix().Scale(3.2), time = 0.15 SECONDS)
+ animate(transform = matrix().Scale(0.2), time = 0.75 SECONDS)
+ animate(transform = matrix().Scale(3, 0), time = 0.1 SECONDS)
+ animate(src, color = COLOR_WHITE, time = 0.25 SECONDS, flags = ANIMATION_PARALLEL)
+ animate(color = COLOR_VOID_PURPLE, time = 0.3 SECONDS)
diff --git a/code/modules/antagonists/heretic/transmutation_rune.dm b/code/modules/antagonists/heretic/transmutation_rune.dm
index 7e63040af1d..cd49feb3f17 100644
--- a/code/modules/antagonists/heretic/transmutation_rune.dm
+++ b/code/modules/antagonists/heretic/transmutation_rune.dm
@@ -90,6 +90,7 @@
// A copy of our requirements list.
// We decrement the values of to determine if enough of each key is present.
var/list/requirements_list = ritual.required_atoms.Copy()
+ var/list/banned_atom_types = ritual.banned_atom_types.Copy()
// A list of all atoms we've selected to use in this recipe.
var/list/selected_atoms = list()
@@ -105,8 +106,16 @@
// We already have enough of this type, skip
if(requirements_list[req_type] <= 0)
continue
- if(!istype(nearby_atom, req_type))
+ // If req_type is a list of types, check all of them for one match.
+ if(islist(req_type))
+ if(!(is_type_in_list(nearby_atom, req_type)))
+ continue
+ else if(!istype(nearby_atom, req_type))
continue
+ // if list has items, check if the strict type is banned.
+ if(length(banned_atom_types))
+ if(nearby_atom.type in banned_atom_types)
+ continue
// This item is a valid type. Add it to our selected atoms list.
selected_atoms |= nearby_atom
@@ -122,7 +131,7 @@
// All of the atoms have been checked, let's see if the ritual was successful
var/list/what_are_we_missing = list()
- for(var/atom/req_type as anything in requirements_list)
+ for(var/req_type in requirements_list)
var/number_of_things = requirements_list[req_type]
// <= 0 means it's fulfilled, skip
if(number_of_things <= 0)
@@ -130,10 +139,16 @@
// > 0 means it's unfilfilled - the ritual has failed, we should tell them why
// Lets format the thing they're missing and put it into our list
- var/formatted_thing = "[number_of_things] [initial(req_type.name)]\s"
- if(ispath(req_type, /mob/living/carbon/human))
- // If we need a human, there is a high likelihood we actually need a (dead) body
- formatted_thing = "[number_of_things] [number_of_things > 1 ? "bodies":"body"]"
+ var/formatted_thing = "[number_of_things] "
+ if(islist(req_type))
+ var/list/req_type_list = req_type
+ var/list/req_text_list = list()
+ for(var/atom/possible_type as anything in req_type_list)
+ req_text_list += ritual.parse_required_item(possible_type)
+ formatted_thing += english_list(req_text_list, and_text = "or")
+
+ else
+ formatted_thing = ritual.parse_required_item(req_type)
what_are_we_missing += formatted_thing
@@ -180,6 +195,7 @@
return ritual_result
+
/// A 3x3 heretic rune. The kind heretics actually draw in game.
/obj/effect/heretic_rune/big
icon = 'icons/effects/96x96.dmi'
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index a6c6616ad48..49d464fde16 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
@@ -9,7 +9,6 @@
#define MALF_AI_ROLL_COOLDOWN 1 SECONDS + MALF_AI_ROLL_TIME
#define MALF_AI_ROLL_DAMAGE 75
#define MALF_AI_ROLL_CRIT_CHANCE 5 //percent
-#define MALF_AI_ROLL_MAX_DISTANCE 1 //anything further away than this, and the roll will fail
GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list(
/obj/machinery/field/containment,
@@ -1138,6 +1137,11 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
enable_text = span_notice("Your inner servos shift as you prepare to roll around. Click adjacent tiles to roll onto them!")
disable_text = span_notice("You disengage your rolling protocols.")
+ /// How long does it take for us to roll?
+ var/roll_over_time = MALF_AI_ROLL_TIME
+ /// On top of [roll_over_time], how long does it take for the ability to cooldown?
+ var/roll_over_cooldown = MALF_AI_ROLL_COOLDOWN
+
/datum/action/innate/ai/ranged/core_tilt/New()
. = ..()
desc = "[desc] It has [uses] use\s remaining."
@@ -1152,7 +1156,7 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
return FALSE
var/mob/living/silicon/ai/ai_caller = caller
- if (ai_caller.incapacitated())
+ if (ai_caller.incapacitated() || !isturf(ai_caller.loc))
return FALSE
var/turf/target = get_turf(clicked_on)
@@ -1163,30 +1167,32 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
target.balloon_alert(ai_caller, "can't roll on yourself!")
return FALSE
- if (get_dist(ai_caller, target) > MALF_AI_ROLL_MAX_DISTANCE)
- target.balloon_alert(ai_caller, "too far!")
- return FALSE
-
var/picked_dir = get_dir(ai_caller, target)
+ if (!picked_dir)
+ return FALSE
+ var/turf/temp_target = get_step(ai_caller, picked_dir) // we can move during the timer so we cant just pass the ref
- new /obj/effect/temp_visual/telegraphing/vending_machine_tilt(target, MALF_AI_ROLL_TIME)
+ new /obj/effect/temp_visual/telegraphing/vending_machine_tilt(temp_target, roll_over_time)
ai_caller.balloon_alert_to_viewers("rolling...")
- addtimer(CALLBACK(src, PROC_REF(do_roll_over), ai_caller, picked_dir), MALF_AI_ROLL_TIME)
+ addtimer(CALLBACK(src, PROC_REF(do_roll_over), ai_caller, picked_dir), roll_over_time)
adjust_uses(-1)
if(uses)
desc = "[initial(desc)] It has [uses] use\s remaining."
build_all_button_icons()
- COOLDOWN_START(src, time_til_next_tilt, MALF_AI_ROLL_COOLDOWN)
+ COOLDOWN_START(src, time_til_next_tilt, roll_over_cooldown)
/datum/action/innate/ai/ranged/core_tilt/proc/do_roll_over(mob/living/silicon/ai/ai_caller, picked_dir)
+ if (ai_caller.incapacitated() || !isturf(ai_caller.loc)) // prevents bugs where the ai is carded and rolls
+ return
+
var/turf/target = get_step(ai_caller, picked_dir) // in case we moved we pass the dir not the target turf
if (isnull(target))
return
- var/paralyze_time = clamp(6 SECONDS, 0 SECONDS, (MALF_AI_ROLL_COOLDOWN * 0.9)) //the clamp prevents stunlocking as the max is always a little less than the cooldown between rolls
+ var/paralyze_time = clamp(6 SECONDS, 0 SECONDS, (roll_over_cooldown * 0.9)) //the clamp prevents stunlocking as the max is always a little less than the cooldown between rolls
return ai_caller.fall_and_crush(target, MALF_AI_ROLL_DAMAGE, MALF_AI_ROLL_CRIT_CHANCE, null, paralyze_time, picked_dir, rotation = get_rotation_from_dir(picked_dir))
@@ -1321,4 +1327,3 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module))
#undef MALF_AI_ROLL_TIME
#undef MALF_AI_ROLL_DAMAGE
#undef MALF_AI_ROLL_CRIT_CHANCE
-#undef MALF_AI_ROLL_MAX_DISTANCE
diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm
index e88730f7026..3e759bdc362 100644
--- a/code/modules/antagonists/revolution/revolution.dm
+++ b/code/modules/antagonists/revolution/revolution.dm
@@ -397,6 +397,9 @@
/// List of all ex-revs. Useful because dynamic removes antag status when it ends, so this can be kept for the roundend report.
var/list/ex_revs = list()
+ /// The objective of the heads of staff, aka to kill the headrevs.
+ var/list/datum/objective/mutiny/heads_objective = list()
+
/// Proc called on periodic timer.
/// Updates the rev team's objectives to make sure all heads are targets, useful when new heads latejoin.
/// Propagates all objectives to all revs.
@@ -472,11 +475,34 @@
/// Checks if heads have won
/datum/team/revolution/proc/check_heads_victory()
- for(var/datum/mind/rev_mind in get_head_revolutionaries())
- var/turf/rev_turf = get_turf(rev_mind.current)
- if(!considered_afk(rev_mind) && considered_alive(rev_mind) && is_station_level(rev_turf.z))
- return FALSE
- return TRUE
+ // List of headrevs we're currently tracking
+ var/list/included_headrevs = list()
+ // List of current headrevs
+ var/list/current_headrevs = get_head_revolutionaries()
+ // A copy of the head of staff objective list, since we're going to be modifying the original list.
+ var/list/heads_objective_copy = heads_objective.Copy()
+
+ var/objective_complete = TRUE
+ // Here, we check current head of staff objectives and remove them if the target doesn't exist as a headrev anymore
+ for(var/datum/objective/mutiny/objective in heads_objective_copy)
+ if(!(objective.target in current_headrevs))
+ heads_objective -= objective
+ continue
+ if(!objective.check_completion())
+ objective_complete = FALSE
+ included_headrevs += objective.target
+
+ // Here, we check current headrevs and add them as objectives if they didn't exist as a head of staff objective before.
+ // Additionally, we make sure the objective is not completed by running the check_completion check on them.
+ for(var/datum/mind/rev_mind as anything in current_headrevs)
+ if(!(rev_mind in included_headrevs))
+ var/datum/objective/mutiny/objective = new()
+ objective.target = rev_mind
+ if(!objective.check_completion())
+ objective_complete = FALSE
+ heads_objective += objective
+
+ return objective_complete
/// Updates the state of the world depending on if revs won or loss.
/// Returns who won, at which case this method should no longer be called.
diff --git a/code/modules/antagonists/traitor/objectives/abstract/target_player.dm b/code/modules/antagonists/traitor/objectives/abstract/target_player.dm
index 76471eb244a..c08f8cd46ee 100644
--- a/code/modules/antagonists/traitor/objectives/abstract/target_player.dm
+++ b/code/modules/antagonists/traitor/objectives/abstract/target_player.dm
@@ -16,3 +16,18 @@
/// The target that we need to target.
var/mob/living/target
+
+/datum/traitor_objective/target_player/Destroy(force)
+ set_target(null)
+ return ..()
+
+/datum/traitor_objective/target_player/proc/set_target(mob/living/new_target)
+ if(target)
+ UnregisterSignal(target, COMSIG_QDELETING)
+ target = new_target
+ if(target)
+ RegisterSignal(target, COMSIG_QDELETING, PROC_REF(target_deleted))
+
+/datum/traitor_objective/target_player/proc/target_deleted(datum/source)
+ SIGNAL_HANDLER
+ set_target(null)
diff --git a/code/modules/antagonists/traitor/objectives/assassination.dm b/code/modules/antagonists/traitor/objectives/assassination.dm
index 4efcbb111be..ab3211aedd8 100644
--- a/code/modules/antagonists/traitor/objectives/assassination.dm
+++ b/code/modules/antagonists/traitor/objectives/assassination.dm
@@ -107,20 +107,13 @@
return //in their pockets please
succeed_objective()
-/datum/traitor_objective/target_player/assassinate/calling_card/generate_objective(datum/mind/generating_for, list/possible_duplicates)
- . = ..()
- if(!.) //didn't generate
- return FALSE
- RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_qdeleted))
-
/datum/traitor_objective/target_player/assassinate/calling_card/ungenerate_objective()
- UnregisterSignal(target, COMSIG_QDELETING)
. = ..() //unsets kill target
if(card)
UnregisterSignal(card, COMSIG_ITEM_EQUIPPED)
card = null
-/datum/traitor_objective/target_player/assassinate/calling_card/on_target_qdeleted()
+/datum/traitor_objective/target_player/assassinate/calling_card/target_deleted()
//you cannot plant anything on someone who is gone gone, so even if this happens after you're still liable to fail
fail_objective(penalty_cost = telecrystal_penalty)
@@ -228,7 +221,7 @@
return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME
var/datum/mind/target_mind = pick(possible_targets)
- target = target_mind.current
+ set_target(target_mind.current)
replace_in_name("%TARGET%", target.real_name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death))
@@ -236,17 +229,17 @@
/datum/traitor_objective/target_player/assassinate/ungenerate_objective()
UnregisterSignal(target, COMSIG_LIVING_DEATH)
- target = null
+ set_target(null)
///proc for checking for special states that invalidate a target
/datum/traitor_objective/target_player/assassinate/proc/special_target_filter(list/possible_targets)
return
-/datum/traitor_objective/target_player/assassinate/proc/on_target_qdeleted()
- SIGNAL_HANDLER
+/datum/traitor_objective/target_player/assassinate/target_deleted()
if(objective_state == OBJECTIVE_STATE_INACTIVE)
//don't take an objective target of someone who is already obliterated
fail_objective()
+ return ..()
/datum/traitor_objective/target_player/assassinate/proc/on_target_death()
SIGNAL_HANDLER
diff --git a/code/modules/antagonists/traitor/objectives/demoralise_assault.dm b/code/modules/antagonists/traitor/objectives/demoralise_assault.dm
index 095c646cb99..fe26864e4fc 100644
--- a/code/modules/antagonists/traitor/objectives/demoralise_assault.dm
+++ b/code/modules/antagonists/traitor/objectives/demoralise_assault.dm
@@ -55,9 +55,7 @@
/datum/traitor_objective/target_player/assault/ungenerate_objective()
UnregisterSignal(target, COMSIG_ATOM_WAS_ATTACKED)
UnregisterSignal(target, COMSIG_LIVING_DEATH)
- UnregisterSignal(target, COMSIG_QDELETING)
-
- target = null
+ set_target(null)
/datum/traitor_objective/target_player/assault/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things
@@ -102,7 +100,7 @@
var/datum/mind/target_mind = pick(possible_targets)
- target = target_mind.current
+ set_target(target_mind.current)
replace_in_name("%TARGET%", target.real_name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
@@ -110,7 +108,6 @@
replace_in_name("%COUNT%", attacks_required)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death))
- RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_qdeleted))
return TRUE
@@ -120,11 +117,10 @@
buttons += add_ui_button("[attacks_required - attacks_inflicted]", "This tells you how many more times you have to attack the target player to succeed.", "hand-rock-o", "none")
return buttons
-/datum/traitor_objective/target_player/assault/proc/on_target_qdeleted()
- SIGNAL_HANDLER
-
+/datum/traitor_objective/target_player/assault/target_deleted()
//don't take an objective target of someone who is already obliterated
fail_objective()
+ return ..()
/datum/traitor_objective/target_player/assault/proc/on_target_death()
SIGNAL_HANDLER
diff --git a/code/modules/antagonists/traitor/objectives/eyesnatching.dm b/code/modules/antagonists/traitor/objectives/eyesnatching.dm
index 0540d83601c..4a307f8d8e0 100644
--- a/code/modules/antagonists/traitor/objectives/eyesnatching.dm
+++ b/code/modules/antagonists/traitor/objectives/eyesnatching.dm
@@ -102,7 +102,7 @@
return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME
var/datum/mind/target_mind = pick(possible_targets)
- target = target_mind.current
+ set_target(target_mind.current)
replace_in_name("%TARGET%", target_mind.name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
diff --git a/code/modules/antagonists/traitor/objectives/infect.dm b/code/modules/antagonists/traitor/objectives/infect.dm
index 7784b8228c5..b1bb868e903 100644
--- a/code/modules/antagonists/traitor/objectives/infect.dm
+++ b/code/modules/antagonists/traitor/objectives/infect.dm
@@ -122,7 +122,7 @@
return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME
var/datum/mind/target_mind = pick(possible_targets)
- target = target_mind.current
+ set_target(target_mind.current)
replace_in_name("%TARGET%", target.real_name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death))
@@ -130,17 +130,17 @@
/datum/traitor_objective/target_player/infect/ungenerate_objective()
UnregisterSignal(target, COMSIG_LIVING_DEATH)
- target = null
+ set_target(null)
///proc for checking for special states that invalidate a target
/datum/traitor_objective/target_player/infect/proc/special_target_filter(list/possible_targets)
return
-/datum/traitor_objective/target_player/infect/proc/on_target_qdeleted()
- SIGNAL_HANDLER
+/datum/traitor_objective/target_player/infect/target_deleted()
if(objective_state == OBJECTIVE_STATE_INACTIVE)
//don't take an objective target of someone who is already obliterated
fail_objective()
+ return ..()
/datum/traitor_objective/target_player/infect/proc/on_target_death()
SIGNAL_HANDLER
diff --git a/code/modules/antagonists/traitor/objectives/kidnapping.dm b/code/modules/antagonists/traitor/objectives/kidnapping.dm
index 55d633ced94..4a463b2b616 100644
--- a/code/modules/antagonists/traitor/objectives/kidnapping.dm
+++ b/code/modules/antagonists/traitor/objectives/kidnapping.dm
@@ -13,7 +13,7 @@
var/pod_called = FALSE
/// How much TC do we get from sending the target alive
var/alive_bonus = 0
- /// All stripped targets belongings
+ /// All stripped targets belongings (weakrefs)
var/list/target_belongings = list()
duplicate_type = /datum/traitor_objective/target_player
@@ -157,7 +157,7 @@
return FALSE
var/datum/mind/target_mind = pick(possible_targets)
- target = target_mind.current
+ set_target(target_mind.current)
AddComponent(/datum/component/traitor_objective_register, target, fail_signals = list(COMSIG_QDELETING))
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
@@ -172,7 +172,7 @@
return TRUE
/datum/traitor_objective/target_player/kidnapping/ungenerate_objective()
- target = null
+ set_target(null)
dropoff_area = null
/datum/traitor_objective/target_player/kidnapping/on_objective_taken(mob/user)
@@ -234,7 +234,7 @@
var/unequipped = sent_mob.transferItemToLoc(belonging)
if (!unequipped)
continue
- target_belongings.Add(belonging)
+ target_belongings.Add(WEAKREF(belonging))
var/datum/bank_account/cargo_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
@@ -303,7 +303,10 @@
continue
sent_mob.dropItemToGround(belonging) // No souvenirs, except shoes and t-shirts
- for(var/obj/item/belonging in target_belongings)
+ for(var/datum/weakref/belonging_ref in target_belongings)
+ var/obj/item/belonging = belonging_ref.resolve()
+ if(!belonging)
+ continue
belonging.forceMove(return_pod)
sent_mob.forceMove(return_pod)
diff --git a/code/modules/antagonists/valentines/valentine.dm b/code/modules/antagonists/valentines/valentine.dm
index 8883625fb1a..79ae9fa8baa 100644
--- a/code/modules/antagonists/valentines/valentine.dm
+++ b/code/modules/antagonists/valentines/valentine.dm
@@ -4,8 +4,8 @@
show_in_antagpanel = FALSE
prevent_roundtype_conversion = FALSE
suicide_cry = "FOR MY LOVE!!"
- // Not 'true' antags, cannot induct
- antag_flags = NONE
+ // Not 'true' antags, this disables certain interactions that assume the owner is a baddie
+ antag_flags = FLAG_FAKE_ANTAG
var/datum/mind/date
count_against_dynamic_roll_chance = FALSE
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/all_access.dm b/code/modules/antagonists/wizard/grand_ritual/finales/all_access.dm
new file mode 100644
index 00000000000..07958ed94a7
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/all_access.dm
@@ -0,0 +1,17 @@
+/// Open all of the doors
+/datum/grand_finale/all_access
+ name = "Connection"
+ desc = "The ultimate use of your gathered power! Unlock every single door that they have! Nobody will be able to keep you out now, or anyone else for that matter!"
+ icon = 'icons/mob/actions/actions_spells.dmi'
+ icon_state = "knock"
+
+/datum/grand_finale/all_access/trigger(mob/living/carbon/human/invoker)
+ message_admins("[key_name(invoker)] removed all door access requirements")
+ for(var/obj/machinery/door/target_door as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door))
+ if(is_station_level(target_door.z))
+ target_door.unlock()
+ target_door.req_access = list()
+ target_door.req_one_access = list()
+ INVOKE_ASYNC(target_door, TYPE_PROC_REF(/obj/machinery/door/airlock, open))
+ CHECK_TICK
+ priority_announce("AULIE OXIN FIERA!!", null, 'sound/magic/knock.ogg', sender_override = "[invoker.real_name]")
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm b/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm
new file mode 100644
index 00000000000..876f2475d55
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/armageddon.dm
@@ -0,0 +1,60 @@
+#define DOOM_SINGULARITY "singularity"
+#define DOOM_TESLA "tesla"
+#define DOOM_METEORS "meteors"
+
+/// Kill yourself and probably a bunch of other people
+/datum/grand_finale/armageddon
+ name = "Annihilation"
+ desc = "This crew have offended you beyond the realm of pranks. Make the ultimate sacrifice to teach them a lesson your elders can really respect. \
+ YOU WILL NOT SURVIVE THIS."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ minimum_time = 90 MINUTES // This will probably immediately end the round if it gets finished.
+ ritual_invoke_time = 60 SECONDS // Really give the crew some time to interfere with this one.
+ dire_warning = TRUE
+ glow_colour = "#be000048"
+ /// Things to yell before you die
+ var/static/list/possible_last_words = list(
+ "Flames and ruin!",
+ "Dooooooooom!!",
+ "HAHAHAHAHAHA!! AHAHAHAHAHAHAHAHAA!!",
+ "Hee hee hee!! Hoo hoo hoo!! Ha ha haaa!!",
+ "Ohohohohohoho!!",
+ "Cower in fear, puny mortals!",
+ "Tremble before my glory!",
+ "Pick a god and pray!",
+ "It's no use!",
+ "If the gods wanted you to live, they would not have created me!",
+ "God stays in heaven out of fear of what I have created!",
+ "Ruination is come!",
+ "All of creation, bend to my will!",
+ )
+
+/datum/grand_finale/armageddon/trigger(mob/living/carbon/human/invoker)
+ priority_announce(pick(possible_last_words), null, 'sound/magic/voidblink.ogg', sender_override = "[invoker.real_name]")
+ var/turf/current_location = get_turf(invoker)
+ invoker.gib()
+
+ var/static/list/doom_options = list()
+ if (!length(doom_options))
+ doom_options = list(DOOM_SINGULARITY, DOOM_TESLA)
+ if (!SSmapping.config.planetary)
+ doom_options += DOOM_METEORS
+
+ switch(pick(doom_options))
+ if (DOOM_SINGULARITY)
+ var/obj/singularity/singulo = new(current_location)
+ singulo.energy = 300
+ if (DOOM_TESLA)
+ var/obj/energy_ball/tesla = new (current_location)
+ tesla.energy = 200
+ if (DOOM_METEORS)
+ var/datum/dynamic_ruleset/roundstart/meteor/meteors = new()
+ meteors.meteordelay = 0
+ var/datum/game_mode/dynamic/mode = SSticker.mode
+ mode.execute_roundstart_rule(meteors) // Meteors will continue until morale is crushed.
+ priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS)
+
+#undef DOOM_SINGULARITY
+#undef DOOM_TESLA
+#undef DOOM_METEORS
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/captaincy.dm b/code/modules/antagonists/wizard/grand_ritual/finales/captaincy.dm
new file mode 100644
index 00000000000..d1a3c1afaf7
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/captaincy.dm
@@ -0,0 +1,113 @@
+/// Become the official Captain of the station
+/datum/grand_finale/usurp
+ name = "Usurpation"
+ desc = "The ultimate use of your gathered power! Rewrite time such that you have been Captain of this station the whole time."
+ icon = 'icons/obj/card.dmi'
+ icon_state = "card_gold"
+
+/datum/grand_finale/usurp/trigger(mob/living/carbon/human/invoker)
+ message_admins("[key_name(invoker)] has replaced the Captain")
+ var/list/former_captains = list()
+ var/list/other_crew = list()
+ SEND_SOUND(world, sound('sound/magic/timeparadox2.ogg'))
+
+ for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
+ if (!crewmate.mind)
+ continue
+ crewmate.Unconscious(3 SECONDS) // Everyone falls unconscious but not everyone gets told about a new captain
+ if (crewmate == invoker || IS_HUMAN_INVADER(crewmate))
+ continue
+ to_chat(crewmate, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
+ Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
+ the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
+ Everything is just as it was and always has been. \n\n\
+ A stray thought sticks in the forefront of your mind. \n\
+ [span_hypnophrase("I'm so glad that [invoker.real_name] is our legally appointed Captain!")] \n\
+ Is... that right?"))
+ if (is_captain_job(crewmate.mind.assigned_role))
+ former_captains += crewmate
+ demote_to_assistant(crewmate)
+ continue
+ if (crewmate.stat != DEAD)
+ other_crew += crewmate
+
+ dress_candidate(invoker)
+ GLOB.manifest.modify(invoker.real_name, JOB_CAPTAIN, JOB_CAPTAIN)
+ minor_announce("Captain [invoker.real_name] on deck!")
+
+ // Enlist some crew to try and restore the natural order
+ for (var/mob/living/carbon/human/former_captain as anything in former_captains)
+ create_vendetta(former_captain.mind, invoker.mind)
+ for (var/mob/living/carbon/human/random_crewmate as anything in other_crew)
+ if (prob(10))
+ create_vendetta(random_crewmate.mind, invoker.mind)
+
+/**
+ * Anyone who thought they were Captain is in for a nasty surprise, and won't be very happy about it
+ */
+/datum/grand_finale/usurp/proc/demote_to_assistant(mob/living/carbon/human/former_captain)
+ var/obj/effect/particle_effect/fluid/smoke/exit_poof = new(get_turf(former_captain))
+ exit_poof.lifetime = 2 SECONDS
+
+ former_captain.unequip_everything()
+ if(isplasmaman(former_captain))
+ former_captain.equipOutfit(/datum/outfit/plasmaman)
+ former_captain.internal = former_captain.get_item_for_held_index(2)
+ else
+ former_captain.equipOutfit(/datum/outfit/job/assistant)
+
+ GLOB.manifest.modify(former_captain.real_name, JOB_ASSISTANT, JOB_ASSISTANT)
+ var/list/valid_turfs = list()
+ // Used to be into prison but that felt a bit too mean
+ for (var/turf/exile_turf as anything in get_area_turfs(/area/station/maintenance, subtypes = TRUE))
+ if (isspaceturf(exile_turf) || exile_turf.is_blocked_turf())
+ continue
+ valid_turfs += exile_turf
+ do_teleport(former_captain, pick(valid_turfs), no_effects = TRUE)
+ var/obj/effect/particle_effect/fluid/smoke/enter_poof = new(get_turf(former_captain))
+ enter_poof.lifetime = 2 SECONDS
+
+/**
+ * Does some item juggling to try to dress you as both a Wizard and Captain without deleting any items you have bought.
+ * ID, headset, and uniform are forcibly replaced. Other slots are only filled if unoccupied.
+ * We could forcibly replace shoes and gloves too but people might miss their insuls or... meown shoes?
+ */
+/datum/grand_finale/usurp/proc/dress_candidate(mob/living/carbon/human/invoker)
+ // Won't be needing these
+ var/obj/id = invoker.get_item_by_slot(ITEM_SLOT_ID)
+ QDEL_NULL(id)
+ var/obj/headset = invoker.get_item_by_slot(ITEM_SLOT_EARS)
+ QDEL_NULL(headset)
+ // We're about to take off your pants so those are going to fall out
+ var/obj/item/pocket_L = invoker.get_item_by_slot(ITEM_SLOT_LPOCKET)
+ var/obj/item/pocket_R = invoker.get_item_by_slot(ITEM_SLOT_RPOCKET)
+ // In case we try to put a PDA there
+ var/obj/item/belt = invoker.get_item_by_slot(ITEM_SLOT_BELT)
+ belt?.moveToNullspace()
+
+ var/obj/pants = invoker.get_item_by_slot(ITEM_SLOT_ICLOTHING)
+ QDEL_NULL(pants)
+ invoker.equipOutfit(/datum/outfit/job/wizard_captain)
+ // And put everything back!
+ equip_to_slot_then_hands(invoker, ITEM_SLOT_BELT, belt)
+ equip_to_slot_then_hands(invoker, ITEM_SLOT_LPOCKET, pocket_L)
+ equip_to_slot_then_hands(invoker, ITEM_SLOT_RPOCKET, pocket_R)
+
+/// An outfit which replaces parts of a wizard's clothes with captain's clothes but keeps the robes
+/datum/outfit/job/wizard_captain
+ name = "Captain (Wizard Transformation)"
+ jobtype = /datum/job/captain
+ id = /obj/item/card/id/advanced/gold
+ id_trim = /datum/id_trim/job/captain
+ uniform = /obj/item/clothing/under/rank/captain/parade
+ belt = /obj/item/modular_computer/pda/heads/captain
+ ears = /obj/item/radio/headset/heads/captain/alt
+ glasses = /obj/item/clothing/glasses/sunglasses
+ gloves = /obj/item/clothing/gloves/captain
+ shoes = /obj/item/clothing/shoes/laceup
+ accessory = /obj/item/clothing/accessory/medal/gold/captain
+ backpack_contents = list(
+ /obj/item/melee/baton/telescopic = 1,
+ /obj/item/station_charter = 1,
+ )
+ box = null
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/cheese.dm b/code/modules/antagonists/wizard/grand_ritual/finales/cheese.dm
new file mode 100644
index 00000000000..714cd62659b
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/cheese.dm
@@ -0,0 +1,49 @@
+/**
+ * Gives the wizard a defensive/mood buff and a Wabbajack, a juiced up chaos staff that will surely break something.
+ * Everyone but the wizard goes crazy, suffers major brain damage, and is given a vendetta against the wizard.
+ * Already insane people are instead cured of their madness, ignoring any other effects as the station around them loses its marbles.
+ */
+/datum/grand_finale/cheese
+ // we don't set name, desc and others, thus we won't appear in the radial choice of a normal finale rune
+ dire_warning = TRUE
+ minimum_time = 45 MINUTES //i'd imagine speedrunning this would be crummy, but the wizard's average lifespan is barely reaching this point
+
+/datum/grand_finale/cheese/trigger(mob/living/invoker)
+ message_admins("[key_name(invoker)] has summoned forth The Wabbajack and cursed the crew with madness!")
+ priority_announce("Danger: Extremely potent reality altering object has been summoned on station. Immediate evacuation advised. Brace for impact.", "Central Command Higher Dimensional Affairs", 'sound/effects/glassbr1.ogg')
+
+ for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
+ if (isnull(crewmate.mind))
+ continue
+ if (crewmate == invoker) //everyone but the wizard is royally fucked, no matter who they are
+ continue
+ if (crewmate.has_trauma_type(/datum/brain_trauma/mild/hallucinations)) //for an already insane person, this is retribution
+ to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble..."))
+ to_chat(crewmate, span_nicegreen("...but as the moment passes, you realise that whatever eldritch power behind the event happened to affect you \
+ has resonated within the ruins of your already shattered mind, creating a singularity of mental instability! \
+ As it collapses unto itself, you feel... at peace, finally."))
+ if(crewmate.has_quirk(/datum/quirk/insanity))
+ crewmate.remove_quirk(/datum/quirk/insanity)
+ else
+ crewmate.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE)
+ else
+ //everyone else gets to relish in madness
+ //yes killing their mood will also trigger mood hallucinations
+ create_vendetta(crewmate.mind, invoker.mind)
+ to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble. \n\
+ You feel your inner psyche shatter into a myriad pieces of jagged glass of colors unknown to the universe, \
+ infinitely reflecting a blinding, maddening light coming from the innermost sanctums of your destroyed mind. \n\
+ After a brief pause which felt like a millenia, one phrase rebounds ceaselessly in your head, imbued with the false hope of absolution... \n\
+ [invoker] must die."))
+ var/datum/brain_trauma/mild/hallucinations/added_trauma = new()
+ added_trauma.resilience = TRAUMA_RESILIENCE_ABSOLUTE
+ crewmate.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_DEATH - 25, BRAIN_DAMAGE_DEATH - 25) //you'd better hope chap didn't pick a hypertool
+ crewmate.gain_trauma(added_trauma)
+ crewmate.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_despair)
+
+ //drip our wizard out
+ invoker.apply_status_effect(/datum/status_effect/blessing_of_insanity)
+ invoker.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_elation)
+ var/obj/item/gun/magic/staff/chaos/true_wabbajack/the_wabbajack = new
+ invoker.put_in_active_hand(the_wabbajack)
+ to_chat(invoker, span_mind_control("Your every single instinct and rational thought is screaming at you as [the_wabbajack] appears in your firm grip..."))
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/clown.dm b/code/modules/antagonists/wizard/grand_ritual/finales/clown.dm
new file mode 100644
index 00000000000..bda79c908c0
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/clown.dm
@@ -0,0 +1,76 @@
+/// Dress the crew as magical clowns
+/datum/grand_finale/clown
+ name = "Jubilation"
+ desc = "The ultimate use of your gathered power! Rewrite time so that everyone went to clown college! Now they'll prank each other for you!"
+ icon = 'icons/obj/clothing/masks.dmi'
+ icon_state = "clown"
+ glow_colour = "#ffff0048"
+
+/datum/grand_finale/clown/trigger(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/victim as anything in GLOB.human_list)
+ victim.Unconscious(3 SECONDS)
+ if (!victim.mind || IS_HUMAN_INVADER(victim) || victim == invoker)
+ continue
+ if (HAS_TRAIT(victim, TRAIT_CLOWN_ENJOYER))
+ victim.add_mood_event("clown_world", /datum/mood_event/clown_world)
+ to_chat(victim, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
+ Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
+ the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
+ Everything is just as it was and always has been. \n\n\
+ A stray thought sticks in the forefront of your mind. \n\
+ [span_hypnophrase("I'm so glad that I work at Clown Research Station [station_name()]!")] \n\
+ Is... that right?"))
+ if (is_clown_job(victim.mind.assigned_role))
+ var/datum/action/cooldown/spell/conjure_item/clown_pockets/new_spell = new(victim)
+ new_spell.Grant(victim)
+ continue
+ if (!ismonkey(victim)) // Monkeys cannot yet wear clothes
+ dress_as_magic_clown(victim)
+ if (prob(15))
+ create_vendetta(victim.mind, invoker.mind)
+
+/**
+ * Clown enjoyers who are effected by this become ecstatic, they have achieved their life's dream.
+ * This moodlet is equivalent to the one for simply being a traitor.
+ */
+/datum/mood_event/clown_world
+ mood_change = 4
+
+/datum/mood_event/clown_world/add_effects(param)
+ description = "I LOVE working at Clown Research Station [station_name()]!!"
+
+/// Dress the passed mob as a magical clown, self-explanatory
+/datum/grand_finale/clown/proc/dress_as_magic_clown(mob/living/carbon/human/victim)
+ var/obj/effect/particle_effect/fluid/smoke/poof = new(get_turf(victim))
+ poof.lifetime = 2 SECONDS
+
+ var/obj/item/tank/internal = victim.internal
+ // We're about to take off your pants so those are going to fall out
+ var/obj/item/pocket_L = victim.get_item_by_slot(ITEM_SLOT_LPOCKET)
+ var/obj/item/pocket_R = victim.get_item_by_slot(ITEM_SLOT_RPOCKET)
+ var/obj/item/id = victim.get_item_by_slot(ITEM_SLOT_ID)
+ var/obj/item/belt = victim.get_item_by_slot(ITEM_SLOT_BELT)
+
+ var/obj/pants = victim.get_item_by_slot(ITEM_SLOT_ICLOTHING)
+ var/obj/mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
+ QDEL_NULL(pants)
+ QDEL_NULL(mask)
+ if(isplasmaman(victim))
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/under/plasmaman/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat/plasmaman(), ITEM_SLOT_MASK, disable_warning = TRUE)
+ else
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/under/rank/civilian/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
+ victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat(), ITEM_SLOT_MASK, disable_warning = TRUE)
+
+ var/obj/item/clothing/mask/gas/clown_hat/clown_mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
+ if (clown_mask)
+ var/list/options = GLOB.clown_mask_options
+ clown_mask.icon_state = options[pick(clown_mask.clownmask_designs)]
+ victim.update_worn_mask()
+ clown_mask.update_item_action_buttons()
+
+ equip_to_slot_then_hands(victim, ITEM_SLOT_LPOCKET, pocket_L)
+ equip_to_slot_then_hands(victim, ITEM_SLOT_RPOCKET, pocket_R)
+ equip_to_slot_then_hands(victim, ITEM_SLOT_ID, id)
+ equip_to_slot_then_hands(victim, ITEM_SLOT_BELT, belt)
+ victim.internal = internal
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm b/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm
new file mode 100644
index 00000000000..b92ae4d2f20
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/grand_ritual_finale.dm
@@ -0,0 +1,88 @@
+/**
+ * A big final event to run when you complete seven rituals
+ */
+/datum/grand_finale
+ /// Friendly name for selection menu
+ var/name
+ /// Tooltip description for selection menu
+ var/desc
+ /// An icon to display to represent the choice
+ var/icon/icon
+ /// Icon state to use to represent the choice
+ var/icon_state
+ /// Prevent especially dangerous options from being chosen until we're fine with the round ending
+ var/minimum_time = 0
+ /// Override the rune invocation time
+ var/ritual_invoke_time = 30 SECONDS
+ /// Provide an extremely loud radio message when this one starts
+ var/dire_warning = FALSE
+ /// Overrides the default colour you glow while channeling the rune, optional
+ var/glow_colour
+
+/**
+ * Returns an entry for a radial menu for this choice.
+ * Returns null if entry is abstract or invalid for current circumstances.
+ */
+/datum/grand_finale/proc/get_radial_choice()
+ if (!name || !desc || !icon || !icon_state)
+ return
+ var/time_remaining_desc = ""
+ if (minimum_time >= world.time - SSticker.round_start_time)
+ time_remaining_desc = "This ritual will be available to begin invoking in [DisplayTimeText(minimum_time - world.time - SSticker.round_start_time)]"
+ var/datum/radial_menu_choice/choice = new()
+ choice.name = name
+ choice.image = image(icon = icon, icon_state = icon_state)
+ choice.info = desc + time_remaining_desc
+ return choice
+
+/**
+ * Actually do the thing.
+ * Arguments
+ * * invoker - The wizard casting this.
+ */
+/datum/grand_finale/proc/trigger(mob/living/invoker)
+ // Do something cool.
+
+/// Tries to equip something into an inventory slot, then hands, then the floor.
+/datum/grand_finale/proc/equip_to_slot_then_hands(mob/living/carbon/human/invoker, slot, obj/item/item)
+ if(!item)
+ return
+ if(!invoker.equip_to_slot_if_possible(item, slot, disable_warning = TRUE))
+ invoker.put_in_hands(item)
+
+/// They are not going to take this lying down.
+/datum/grand_finale/proc/create_vendetta(datum/mind/aggrieved_crewmate, datum/mind/wizard)
+ aggrieved_crewmate.add_antag_datum(/datum/antagonist/wizard_prank_vendetta)
+ var/datum/antagonist/wizard_prank_vendetta/antag_datum = aggrieved_crewmate.has_antag_datum(/datum/antagonist/wizard_prank_vendetta)
+ var/datum/objective/assassinate/wizard_murder = new
+ wizard_murder.owner = aggrieved_crewmate
+ wizard_murder.target = wizard
+ wizard_murder.explanation_text = "Kill [wizard.current.name], the one who did this."
+ antag_datum.objectives += wizard_murder
+
+ to_chat(aggrieved_crewmate.current, span_warning("No! This isn't right!"))
+ aggrieved_crewmate.announce_objectives()
+
+/**
+ * Antag datum to give to people who want to kill the wizard.
+ * This doesn't preclude other people choosing to want to kill the wizard, just these people are rewarded for it.
+ */
+/datum/antagonist/wizard_prank_vendetta
+ name = "\improper Wizard Prank Victim"
+ roundend_category = "wizard prank victims"
+ show_in_antagpanel = FALSE
+ antagpanel_category = "Other"
+ show_name_in_check_antagonists = TRUE
+ count_against_dynamic_roll_chance = FALSE
+ silent = TRUE
+
+/// Give everyone magic items, its so simple it feels pointless to give it its own file
+/datum/grand_finale/magic
+ name = "Evolution"
+ desc = "The ultimate use of your gathered power! Give the crew their own magic, they'll surely realise that right and wrong have no meaning when you hold ultimate power!"
+ icon = 'icons/obj/scrolls.dmi'
+ icon_state = "scroll"
+
+/datum/grand_finale/magic/trigger(mob/living/carbon/human/invoker)
+ message_admins("[key_name(invoker)] summoned magic")
+ summon_magic(survivor_probability = 20) // Wow, this one was easy!
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm b/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm
new file mode 100644
index 00000000000..d20ca06752b
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm
@@ -0,0 +1,277 @@
+/// Amount of time to wait after someone dies to steal their body from their killers
+#define IMMORTAL_PRE_ACTIVATION_TIME 10 SECONDS
+/// Amount of time it takes a mob to return to the living world
+#define IMMORTAL_RESURRECT_TIME 50 SECONDS
+
+/**
+ * Nobody will ever die ever again
+ * Or if they do, they will be back
+ */
+/datum/grand_finale/immortality
+ name = "Perpetuation"
+ desc = "The ultimate use of your gathered power! Share with the crew the gift, or curse, of eternal life! \
+ And why not just the crew? How about their pets too? And any other animals around here! \
+ What if nobody died ever again!?"
+ icon = 'icons/obj/mining_zones/artefacts.dmi'
+ icon_state = "asclepius_active"
+ glow_colour = COLOR_PALE_GREEN
+ minimum_time = 30 MINUTES // This is enormously disruptive but doesn't technically in of itself end the round.
+
+/datum/grand_finale/immortality/trigger(mob/living/carbon/human/invoker)
+ new /obj/effect/temp_visual/immortality_blast(get_turf(invoker))
+ SEND_SOUND(world, sound('sound/magic/teleport_diss.ogg'))
+ for (var/mob/living/alive_guy as anything in GLOB.mob_living_list)
+ new /obj/effect/temp_visual/immortality_pulse(get_turf(alive_guy))
+ if (!alive_guy.mind)
+ continue
+ to_chat(alive_guy, span_notice("You feel extremely healthy."))
+ RegisterSignal(SSdcs, COMSIG_GLOB_MOB_DEATH, PROC_REF(something_died))
+
+/// Called when something passes into the great beyond, make it not do that
+/datum/grand_finale/immortality/proc/something_died(datum/source, mob/living/died, gibbed)
+ SIGNAL_HANDLER
+ if (died.stat != DEAD || HAS_TRAIT(died, TRAIT_PERMANENTLY_MORTAL) || died.flags_1 & HOLOGRAM_1)
+ return
+ var/body_type = died.type
+
+ var/turf/died_turf = get_turf(died)
+ var/list/nearby_turfs = circle_view_turfs(died_turf, 2)
+ var/list/nearby_safe_turfs = list()
+ for (var/turf/check_turf as anything in nearby_turfs)
+ if (check_turf.is_blocked_turf(exclude_mobs = TRUE, source_atom = died))
+ nearby_turfs -= check_turf
+ continue
+ if (islava(check_turf) || ischasm(check_turf))
+ continue
+ nearby_safe_turfs += check_turf
+ if (length(nearby_safe_turfs)) // If you're in the middle of a 5x5 chasm, tough luck I guess
+ died_turf = pick(nearby_safe_turfs)
+ else if (length(nearby_turfs))
+ died_turf = pick(nearby_turfs)
+
+ var/saved_appearance = ishuman(died) ? new /datum/human_appearance_profile(died) : null
+
+ var/datum/mind/dead_mind = HAS_TRAIT(died, TRAIT_SUICIDED) ? null : died.mind // There is a way out of the cycle
+ if (!isnull(dead_mind))
+ to_chat(died, span_boldnotice("Your spirit surges! You will return to life in [DisplayTimeText(IMMORTAL_PRE_ACTIVATION_TIME + IMMORTAL_RESURRECT_TIME)]."))
+ animate(died, alpha = died.alpha, time = IMMORTAL_PRE_ACTIVATION_TIME / 2, flags = ANIMATION_PARALLEL)
+ animate(alpha = 0, time = IMMORTAL_PRE_ACTIVATION_TIME / 2, easing = SINE_EASING | EASE_IN)
+ addtimer(CALLBACK(src, PROC_REF(reverse_death), died, dead_mind, died_turf, body_type, saved_appearance), IMMORTAL_PRE_ACTIVATION_TIME, TIMER_DELETE_ME)
+
+/// Create a ghost ready for revival
+/datum/grand_finale/immortality/proc/reverse_death(mob/living/died, datum/mind/dead_mind, turf/died_turf, body_type, datum/human_appearance_profile/human_appearance)
+ if (died.stat != DEAD)
+ return
+ var/ghost_type = ispath(body_type, /mob/living/carbon/human) ? /obj/effect/spectre_of_resurrection/human : /obj/effect/spectre_of_resurrection
+ var/obj/effect/spectre_of_resurrection/ghost = new ghost_type(died_turf)
+ var/mob/living/corpse = QDELETED(died) ? new body_type(ghost) : died
+ if (!isnull(human_appearance))
+ corpse.real_name = human_appearance.name
+ corpse.alpha = initial(corpse.alpha)
+ corpse.add_traits(list(TRAIT_NO_TELEPORT, TRAIT_AI_PAUSED), MAGIC_TRAIT)
+ corpse.apply_status_effect(/datum/status_effect/grouped/stasis, MAGIC_TRAIT)
+ ghost.set_up_resurrection(corpse, dead_mind, human_appearance)
+
+
+/// Store of data we use to recreate someone who was gibbed, like a simplified version of changeling profiles
+/datum/human_appearance_profile
+ /// The name of the profile / the name of whoever this profile source.
+ var/name = "human"
+ /// The DNA datum associated with our profile from the profile source
+ var/datum/dna/dna
+ /// The age of the profile source.
+ var/age
+ /// The body type of the profile source.
+ var/physique
+ /// The quirks of the profile source.
+ var/list/quirks = list()
+ /// The hair and facial hair gradient styles of the profile source.
+ var/list/hair_gradient_style = list("None", "None")
+ /// The hair and facial hair gradient colours of the profile source.
+ var/list/hair_gradient_colours = list(null, null)
+ /// The TTS voice of the profile source
+ var/voice
+ /// The TTS filter of the profile filter
+ var/voice_filter = ""
+
+/datum/human_appearance_profile/New(mob/living/carbon/human/target)
+ copy_from(target)
+
+/// Copy the appearance data of the target
+/datum/human_appearance_profile/proc/copy_from(mob/living/carbon/human/target)
+ target.dna.real_name = target.real_name
+ dna = new target.dna.type()
+ target.dna.copy_dna(dna)
+ name = target.real_name
+ age = target.age
+ physique = target.physique
+
+ for(var/datum/quirk/target_quirk as anything in target.quirks)
+ LAZYADD(quirks, new target_quirk.type)
+
+ hair_gradient_style = LAZYLISTDUPLICATE(target.grad_style)
+ hair_gradient_colours = LAZYLISTDUPLICATE(target.grad_color)
+
+ voice = target.voice
+ voice_filter = target.voice_filter
+
+/// Make the targetted human look like this
+/datum/human_appearance_profile/proc/apply_to(mob/living/carbon/human/target)
+ target.real_name = name
+ target.age = age
+ target.physique = physique
+ target.grad_style = LAZYLISTDUPLICATE(hair_gradient_style)
+ target.grad_color = LAZYLISTDUPLICATE(hair_gradient_colours)
+ target.voice = voice
+ target.voice_filter = voice_filter
+
+ for(var/datum/quirk/target_quirk as anything in quirks)
+ target_quirk.add_to_holder(target)
+
+ dna.transfer_identity(target, TRUE)
+ for(var/obj/item/bodypart/limb as anything in target.bodyparts)
+ limb.update_limb(is_creating = TRUE)
+ target.updateappearance(mutcolor_update = TRUE)
+ target.domutcheck()
+ target.regenerate_icons()
+
+
+/// A ghostly image of a mob showing where and what is going to respawn
+/obj/effect/spectre_of_resurrection
+ name = "spectre"
+ desc = "A frightening apparition, slowly growing more solid."
+ icon_state = "blank_white"
+ anchored = TRUE
+ layer = MOB_LAYER
+ plane = GAME_PLANE
+ alpha = 0
+ color = COLOR_PALE_GREEN
+ light_range = 2
+ light_color = COLOR_PALE_GREEN
+ /// Who are we reviving?
+ var/mob/living/corpse
+ /// Who if anyone is playing as them?
+ var/datum/mind/dead_mind
+
+/obj/effect/spectre_of_resurrection/Initialize(mapload)
+ . = ..()
+ animate(src, alpha = 150, time = 2 SECONDS)
+
+/// Prepare to revive someone
+/obj/effect/spectre_of_resurrection/proc/set_up_resurrection(mob/living/corpse, datum/mind/dead_mind, datum/human_appearance_profile/human_appearance)
+ if (isnull(corpse))
+ qdel(src)
+ return
+
+ src.corpse = corpse
+ src.dead_mind = dead_mind
+ corpse.forceMove(src)
+ name = "spectre of [corpse]"
+ setup_icon(corpse)
+ DO_FLOATING_ANIM(src)
+
+ RegisterSignal(corpse, COMSIG_LIVING_REVIVE, PROC_REF(on_corpse_revived))
+ RegisterSignal(corpse, COMSIG_QDELETING, PROC_REF(on_corpse_deleted))
+ RegisterSignal(dead_mind, COMSIG_QDELETING, PROC_REF(on_mind_lost))
+ addtimer(CALLBACK(src, PROC_REF(revive)), IMMORTAL_RESURRECT_TIME, TIMER_DELETE_ME)
+
+/// Copy appearance from ressurecting mob
+/obj/effect/spectre_of_resurrection/proc/setup_icon(mob/living/corpse)
+ icon = initial(corpse.icon)
+ icon_state = initial(corpse.icon_state)
+
+/obj/effect/spectre_of_resurrection/Destroy(force)
+ QDEL_NULL(corpse)
+ dead_mind = null
+ return ..()
+
+/obj/effect/spectre_of_resurrection/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != corpse)
+ return // Weird but ok
+ UnregisterSignal(corpse, list(COMSIG_LIVING_REVIVE, COMSIG_QDELETING))
+ corpse = null
+ qdel(src)
+
+/// Bring our body back to life
+/obj/effect/spectre_of_resurrection/proc/revive()
+ if (!isnull(dead_mind))
+ if (dead_mind.current == corpse)
+ dead_mind.grab_ghost(force = TRUE)
+ else
+ dead_mind.transfer_to(corpse, force_key_move = TRUE)
+ corpse.revive(HEAL_ALL) // The signal is sent even if they weren't actually dead
+
+/// Remove our stored corpse back to the living world
+/obj/effect/spectre_of_resurrection/proc/on_corpse_revived()
+ SIGNAL_HANDLER
+ if (isnull(corpse))
+ return
+ visible_message(span_boldnotice("[corpse] suddenly shudders to life!"))
+ corpse.remove_traits(list(TRAIT_NO_TELEPORT, TRAIT_AI_PAUSED), MAGIC_TRAIT)
+ corpse.remove_status_effect(/datum/status_effect/grouped/stasis, MAGIC_TRAIT)
+ corpse.forceMove(loc)
+
+/// If the body is destroyed then we can't come back, F
+/obj/effect/spectre_of_resurrection/proc/on_corpse_deleted()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// If the mind is deleted somehow we just don't transfer it on revival
+/obj/effect/spectre_of_resurrection/proc/on_mind_lost()
+ SIGNAL_HANDLER
+ dead_mind = null
+
+/// A ressurection spectre with extra behaviour for humans
+/obj/effect/spectre_of_resurrection/human
+ /// Stored data used to restore someone to a fascimile of what they were before
+ var/datum/human_appearance_profile/human_appearance
+
+/obj/effect/spectre_of_resurrection/human/set_up_resurrection(mob/living/corpse, datum/mind/dead_mind, datum/human_appearance_profile/human_appearance)
+ . = ..()
+ src.human_appearance = human_appearance
+
+// We just use a generic floating human appearance to save unecessary costly icon operations
+/obj/effect/spectre_of_resurrection/human/setup_icon(mob/living/corpse)
+ return
+
+// Apply stored human details
+/obj/effect/spectre_of_resurrection/human/on_corpse_revived()
+ if (isnull(corpse))
+ return
+ human_appearance?.apply_to(corpse)
+ return ..()
+
+
+/// Visual flair on the wizard when cast
+/obj/effect/temp_visual/immortality_blast
+ name = "immortal wave"
+ duration = 2.5 SECONDS
+ icon = 'icons/effects/96x96.dmi'
+ icon_state = "boh_tear"
+ color = COLOR_PALE_GREEN
+ pixel_x = -32
+ pixel_y = -32
+
+/obj/effect/temp_visual/immortality_blast/Initialize(mapload)
+ . = ..()
+ transform *= 0
+ animate(src, transform = matrix(), time = 1.5 SECONDS, easing = ELASTIC_EASING)
+ animate(transform = matrix() * 3, time = 1 SECONDS, alpha = 0, easing = SINE_EASING | EASE_OUT)
+
+
+/// Visual flair on living creatures who have become immortal
+/obj/effect/temp_visual/immortality_pulse
+ name = "immortal pulse"
+ duration = 1 SECONDS
+ icon = 'icons/effects/anomalies.dmi'
+ icon_state = "dimensional_overlay"
+ color = COLOR_PALE_GREEN
+
+/obj/effect/temp_visual/immortality_pulse/Initialize(mapload)
+ . = ..()
+ transform *= 0
+ animate(src, transform = matrix() * 1.5, alpha = 0, time = 1 SECONDS, easing = SINE_EASING | EASE_OUT)
+
+#undef IMMORTAL_PRE_ACTIVATION_TIME
+#undef IMMORTAL_RESURRECT_TIME
diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/midas.dm b/code/modules/antagonists/wizard/grand_ritual/finales/midas.dm
new file mode 100644
index 00000000000..b2e3329261f
--- /dev/null
+++ b/code/modules/antagonists/wizard/grand_ritual/finales/midas.dm
@@ -0,0 +1,46 @@
+/// Completely transform the station
+/datum/grand_finale/midas
+ name = "Transformation"
+ desc = "The ultimate use of your gathered power! Turn their precious station into something much MORE precious, materially speaking!"
+ icon = 'icons/obj/stack_objects.dmi'
+ icon_state = "sheet-gold_2"
+ glow_colour = "#dbdd4c48"
+ var/static/list/permitted_transforms = list( // Non-dangerous only
+ /datum/dimension_theme/gold,
+ /datum/dimension_theme/meat,
+ /datum/dimension_theme/pizza,
+ /datum/dimension_theme/natural,
+ )
+ var/datum/dimension_theme/chosen_theme
+
+// I sure hope this doesn't have performance implications
+/datum/grand_finale/midas/trigger(mob/living/carbon/human/invoker)
+ var/theme_path = pick(permitted_transforms)
+ chosen_theme = new theme_path()
+ var/turf/start_turf = get_turf(invoker)
+ var/greatest_dist = 0
+ var/list/turfs_to_transform = list()
+ for (var/turf/transform_turf as anything in GLOB.station_turfs)
+ if (!chosen_theme.can_convert(transform_turf))
+ continue
+ var/dist = get_dist(start_turf, transform_turf)
+ if (dist > greatest_dist)
+ greatest_dist = dist
+ if (!turfs_to_transform["[dist]"])
+ turfs_to_transform["[dist]"] = list()
+ turfs_to_transform["[dist]"] += transform_turf
+
+ if (chosen_theme.can_convert(start_turf))
+ chosen_theme.apply_theme(start_turf)
+
+ for (var/iterator in 1 to greatest_dist)
+ if(!turfs_to_transform["[iterator]"])
+ continue
+ addtimer(CALLBACK(src, PROC_REF(transform_area), turfs_to_transform["[iterator]"]), (5 SECONDS) * iterator)
+
+/datum/grand_finale/midas/proc/transform_area(list/turfs)
+ for (var/turf/transform_turf as anything in turfs)
+ if (!chosen_theme.can_convert(transform_turf))
+ continue
+ chosen_theme.apply_theme(transform_turf)
+ CHECK_TICK
diff --git a/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm b/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm
deleted file mode 100644
index 9a59b1f1a79..00000000000
--- a/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm
+++ /dev/null
@@ -1,456 +0,0 @@
-#define DOOM_SINGULARITY "singularity"
-#define DOOM_TESLA "tesla"
-#define DOOM_METEORS "meteors"
-
-/**
- * A big final event to run when you complete seven rituals
- */
-/datum/grand_finale
- /// Friendly name for selection menu
- var/name
- /// Tooltip description for selection menu
- var/desc
- /// An icon to display to represent the choice
- var/icon/icon
- /// Icon state to use to represent the choice
- var/icon_state
- /// Prevent especially dangerous options from being chosen until we're fine with the round ending
- var/minimum_time = 0
- /// Override the rune invocation time
- var/ritual_invoke_time = 30 SECONDS
- /// Provide an extremely loud radio message when this one starts
- var/dire_warning = FALSE
- /// Overrides the default colour you glow while channeling the rune, optional
- var/glow_colour
-
-/**
- * Returns an entry for a radial menu for this choice.
- * Returns null if entry is abstract or invalid for current circumstances.
- */
-/datum/grand_finale/proc/get_radial_choice()
- if (!name || !desc || !icon || !icon_state)
- return
- var/time_remaining_desc = ""
- if (minimum_time >= world.time - SSticker.round_start_time)
- time_remaining_desc = "This ritual will be available to begin invoking in [DisplayTimeText(minimum_time - world.time - SSticker.round_start_time)]"
- var/datum/radial_menu_choice/choice = new()
- choice.name = name
- choice.image = image(icon = icon, icon_state = icon_state)
- choice.info = desc + time_remaining_desc
- return choice
-
-/**
- * Actually do the thing.
- * Arguments
- * * invoker - The wizard casting this.
- */
-/datum/grand_finale/proc/trigger(mob/living/invoker)
- // Do something cool.
-
-/// Tries to equip something into an inventory slot, then hands, then the floor.
-/datum/grand_finale/proc/equip_to_slot_then_hands(mob/living/carbon/human/invoker, slot, obj/item/item)
- if(!item)
- return
- if(!invoker.equip_to_slot_if_possible(item, slot, disable_warning = TRUE))
- invoker.put_in_hands(item)
-
-/// They are not going to take this lying down.
-/datum/grand_finale/proc/create_vendetta(datum/mind/aggrieved_crewmate, datum/mind/wizard)
- aggrieved_crewmate.add_antag_datum(/datum/antagonist/wizard_prank_vendetta)
- var/datum/antagonist/wizard_prank_vendetta/antag_datum = aggrieved_crewmate.has_antag_datum(/datum/antagonist/wizard_prank_vendetta)
- var/datum/objective/assassinate/wizard_murder = new
- wizard_murder.owner = aggrieved_crewmate
- wizard_murder.target = wizard
- wizard_murder.explanation_text = "Kill [wizard.current.name], the one who did this."
- antag_datum.objectives += wizard_murder
-
- to_chat(aggrieved_crewmate.current, span_warning("No! This isn't right!"))
- aggrieved_crewmate.announce_objectives()
-
-/**
- * Antag datum to give to people who want to kill the wizard.
- * This doesn't preclude other people choosing to want to kill the wizard, just these people are rewarded for it.
- */
-/datum/antagonist/wizard_prank_vendetta
- name = "\improper Wizard Prank Victim"
- roundend_category = "wizard prank victims"
- show_in_antagpanel = FALSE
- antagpanel_category = "Other"
- show_name_in_check_antagonists = TRUE
- count_against_dynamic_roll_chance = FALSE
- silent = TRUE
-
-/// Become the official Captain of the station
-/datum/grand_finale/usurp
- name = "Usurpation"
- desc = "The ultimate use of your gathered power! Rewrite time such that you have been Captain of this station the whole time."
- icon = 'icons/obj/card.dmi'
- icon_state = "card_gold"
-
-/datum/grand_finale/usurp/trigger(mob/living/carbon/human/invoker)
- message_admins("[key_name(invoker)] has replaced the Captain")
- var/list/former_captains = list()
- var/list/other_crew = list()
- SEND_SOUND(world, sound('sound/magic/timeparadox2.ogg'))
-
- for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
- if (!crewmate.mind)
- continue
- crewmate.Unconscious(3 SECONDS) // Everyone falls unconscious but not everyone gets told about a new captain
- if (crewmate == invoker || IS_HUMAN_INVADER(crewmate))
- continue
- to_chat(crewmate, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
- Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
- the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
- Everything is just as it was and always has been. \n\n\
- A stray thought sticks in the forefront of your mind. \n\
- [span_hypnophrase("I'm so glad that [invoker.real_name] is our legally appointed Captain!")] \n\
- Is... that right?"))
- if (is_captain_job(crewmate.mind.assigned_role))
- former_captains += crewmate
- demote_to_assistant(crewmate)
- continue
- if (crewmate.stat != DEAD)
- other_crew += crewmate
-
- dress_candidate(invoker)
- GLOB.manifest.modify(invoker.real_name, JOB_CAPTAIN, JOB_CAPTAIN)
- minor_announce("Captain [invoker.real_name] on deck!")
-
- // Enlist some crew to try and restore the natural order
- for (var/mob/living/carbon/human/former_captain as anything in former_captains)
- create_vendetta(former_captain.mind, invoker.mind)
- for (var/mob/living/carbon/human/random_crewmate as anything in other_crew)
- if (prob(10))
- create_vendetta(random_crewmate.mind, invoker.mind)
-
-/**
- * Anyone who thought they were Captain is in for a nasty surprise, and won't be very happy about it
- */
-/datum/grand_finale/usurp/proc/demote_to_assistant(mob/living/carbon/human/former_captain)
- var/obj/effect/particle_effect/fluid/smoke/exit_poof = new(get_turf(former_captain))
- exit_poof.lifetime = 2 SECONDS
-
- former_captain.unequip_everything()
- if(isplasmaman(former_captain))
- former_captain.equipOutfit(/datum/outfit/plasmaman)
- former_captain.internal = former_captain.get_item_for_held_index(2)
- else
- former_captain.equipOutfit(/datum/outfit/job/assistant)
-
- GLOB.manifest.modify(former_captain.real_name, JOB_ASSISTANT, JOB_ASSISTANT)
- var/list/valid_turfs = list()
- // Used to be into prison but that felt a bit too mean
- for (var/turf/exile_turf as anything in get_area_turfs(/area/station/maintenance, subtypes = TRUE))
- if (isspaceturf(exile_turf) || exile_turf.is_blocked_turf())
- continue
- valid_turfs += exile_turf
- do_teleport(former_captain, pick(valid_turfs), no_effects = TRUE)
- var/obj/effect/particle_effect/fluid/smoke/enter_poof = new(get_turf(former_captain))
- enter_poof.lifetime = 2 SECONDS
-
-/**
- * Does some item juggling to try to dress you as both a Wizard and Captain without deleting any items you have bought.
- * ID, headset, and uniform are forcibly replaced. Other slots are only filled if unoccupied.
- * We could forcibly replace shoes and gloves too but people might miss their insuls or... meown shoes?
- */
-/datum/grand_finale/usurp/proc/dress_candidate(mob/living/carbon/human/invoker)
- // Won't be needing these
- var/obj/id = invoker.get_item_by_slot(ITEM_SLOT_ID)
- QDEL_NULL(id)
- var/obj/headset = invoker.get_item_by_slot(ITEM_SLOT_EARS)
- QDEL_NULL(headset)
- // We're about to take off your pants so those are going to fall out
- var/obj/item/pocket_L = invoker.get_item_by_slot(ITEM_SLOT_LPOCKET)
- var/obj/item/pocket_R = invoker.get_item_by_slot(ITEM_SLOT_RPOCKET)
- // In case we try to put a PDA there
- var/obj/item/belt = invoker.get_item_by_slot(ITEM_SLOT_BELT)
- belt?.moveToNullspace()
-
- var/obj/pants = invoker.get_item_by_slot(ITEM_SLOT_ICLOTHING)
- QDEL_NULL(pants)
- invoker.equipOutfit(/datum/outfit/job/wizard_captain)
- // And put everything back!
- equip_to_slot_then_hands(invoker, ITEM_SLOT_BELT, belt)
- equip_to_slot_then_hands(invoker, ITEM_SLOT_LPOCKET, pocket_L)
- equip_to_slot_then_hands(invoker, ITEM_SLOT_RPOCKET, pocket_R)
-
-/// An outfit which replaces parts of a wizard's clothes with captain's clothes but keeps the robes
-/datum/outfit/job/wizard_captain
- name = "Captain (Wizard Transformation)"
- jobtype = /datum/job/captain
- id = /obj/item/card/id/advanced/gold
- id_trim = /datum/id_trim/job/captain
- uniform = /obj/item/clothing/under/rank/captain/parade
- belt = /obj/item/modular_computer/pda/heads/captain
- ears = /obj/item/radio/headset/heads/captain/alt
- glasses = /obj/item/clothing/glasses/sunglasses
- gloves = /obj/item/clothing/gloves/captain
- shoes = /obj/item/clothing/shoes/laceup
- accessory = /obj/item/clothing/accessory/medal/gold/captain
- backpack_contents = list(
- /obj/item/melee/baton/telescopic = 1,
- /obj/item/station_charter = 1,
- )
- box = null
-
-/// Dress the crew as magical clowns
-/datum/grand_finale/clown
- name = "Jubilation"
- desc = "The ultimate use of your gathered power! Rewrite time so that everyone went to clown college! Now they'll prank each other for you!"
- icon = 'icons/obj/clothing/masks.dmi'
- icon_state = "clown"
- glow_colour = "#ffff0048"
-
-/datum/grand_finale/clown/trigger(mob/living/carbon/human/invoker)
- for(var/mob/living/carbon/human/victim as anything in GLOB.human_list)
- victim.Unconscious(3 SECONDS)
- if (!victim.mind || IS_HUMAN_INVADER(victim) || victim == invoker)
- continue
- if (HAS_TRAIT(victim, TRAIT_CLOWN_ENJOYER))
- victim.add_mood_event("clown_world", /datum/mood_event/clown_world)
- to_chat(victim, span_notice("The world spins and dissolves. Your past flashes before your eyes, backwards.\n\
- Life strolls back into the ocean and shrinks into nothingness, planets explode into storms of solar dust, \
- the stars rush back to greet each other at the beginning of things and then... you snap back to the present. \n\
- Everything is just as it was and always has been. \n\n\
- A stray thought sticks in the forefront of your mind. \n\
- [span_hypnophrase("I'm so glad that I work at Clown Research Station [station_name()]!")] \n\
- Is... that right?"))
- if (is_clown_job(victim.mind.assigned_role))
- var/datum/action/cooldown/spell/conjure_item/clown_pockets/new_spell = new(victim)
- new_spell.Grant(victim)
- continue
- if (!ismonkey(victim)) // Monkeys cannot yet wear clothes
- dress_as_magic_clown(victim)
- if (prob(15))
- create_vendetta(victim.mind, invoker.mind)
-
-/**
- * Clown enjoyers who are effected by this become ecstatic, they have achieved their life's dream.
- * This moodlet is equivalent to the one for simply being a traitor.
- */
-/datum/mood_event/clown_world
- mood_change = 4
-
-/datum/mood_event/clown_world/add_effects(param)
- description = "I LOVE working at Clown Research Station [station_name()]!!"
-
-/// Dress the passed mob as a magical clown, self-explanatory
-/datum/grand_finale/clown/proc/dress_as_magic_clown(mob/living/carbon/human/victim)
- var/obj/effect/particle_effect/fluid/smoke/poof = new(get_turf(victim))
- poof.lifetime = 2 SECONDS
-
- var/obj/item/tank/internal = victim.internal
- // We're about to take off your pants so those are going to fall out
- var/obj/item/pocket_L = victim.get_item_by_slot(ITEM_SLOT_LPOCKET)
- var/obj/item/pocket_R = victim.get_item_by_slot(ITEM_SLOT_RPOCKET)
- var/obj/item/id = victim.get_item_by_slot(ITEM_SLOT_ID)
- var/obj/item/belt = victim.get_item_by_slot(ITEM_SLOT_BELT)
-
- var/obj/pants = victim.get_item_by_slot(ITEM_SLOT_ICLOTHING)
- var/obj/mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
- QDEL_NULL(pants)
- QDEL_NULL(mask)
- if(isplasmaman(victim))
- victim.equip_to_slot_if_possible(new /obj/item/clothing/under/plasmaman/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
- victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat/plasmaman(), ITEM_SLOT_MASK, disable_warning = TRUE)
- else
- victim.equip_to_slot_if_possible(new /obj/item/clothing/under/rank/civilian/clown/magic(), ITEM_SLOT_ICLOTHING, disable_warning = TRUE)
- victim.equip_to_slot_if_possible(new /obj/item/clothing/mask/gas/clown_hat(), ITEM_SLOT_MASK, disable_warning = TRUE)
-
- var/obj/item/clothing/mask/gas/clown_hat/clown_mask = victim.get_item_by_slot(ITEM_SLOT_MASK)
- if (clown_mask)
- var/list/options = GLOB.clown_mask_options
- clown_mask.icon_state = options[pick(clown_mask.clownmask_designs)]
- victim.update_worn_mask()
- clown_mask.update_item_action_buttons()
-
- equip_to_slot_then_hands(victim, ITEM_SLOT_LPOCKET, pocket_L)
- equip_to_slot_then_hands(victim, ITEM_SLOT_RPOCKET, pocket_R)
- equip_to_slot_then_hands(victim, ITEM_SLOT_ID, id)
- equip_to_slot_then_hands(victim, ITEM_SLOT_BELT, belt)
- victim.internal = internal
-
-/// Give everyone magic items
-/datum/grand_finale/magic
- name = "Evolution"
- desc = "The ultimate use of your gathered power! Give the crew their own magic, they'll surely realise that right and wrong have no meaning when you hold ultimate power!"
- icon = 'icons/obj/scrolls.dmi'
- icon_state = "scroll"
-
-/datum/grand_finale/magic/trigger(mob/living/carbon/human/invoker)
- message_admins("[key_name(invoker)] summoned magic")
- summon_magic(survivor_probability = 20) // Wow, this one was easy!
-
-/// Open all of the doors
-/datum/grand_finale/all_access
- name = "Connection"
- desc = "The ultimate use of your gathered power! Unlock every single door that they have! Nobody will be able to keep you out now, or anyone else for that matter!"
- icon = 'icons/mob/actions/actions_spells.dmi'
- icon_state = "knock"
-
-/datum/grand_finale/all_access/trigger(mob/living/carbon/human/invoker)
- message_admins("[key_name(invoker)] removed all door access requirements")
- for(var/obj/machinery/door/target_door as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door))
- if(is_station_level(target_door.z))
- target_door.unlock()
- target_door.req_access = list()
- target_door.req_one_access = list()
- INVOKE_ASYNC(target_door, TYPE_PROC_REF(/obj/machinery/door/airlock, open))
- CHECK_TICK
- priority_announce("AULIE OXIN FIERA!!", null, 'sound/magic/knock.ogg', sender_override = "[invoker.real_name]")
-
-/// Completely transform the station
-/datum/grand_finale/midas
- name = "Transformation"
- desc = "The ultimate use of your gathered power! Turn their precious station into something much MORE precious, materially speaking!"
- icon = 'icons/obj/stack_objects.dmi'
- icon_state = "sheet-gold_2"
- glow_colour = "#dbdd4c48"
- var/static/list/permitted_transforms = list( // Non-dangerous only
- /datum/dimension_theme/gold,
- /datum/dimension_theme/meat,
- /datum/dimension_theme/pizza,
- /datum/dimension_theme/natural,
- )
- var/datum/dimension_theme/chosen_theme
-
-// I sure hope this doesn't have performance implications
-/datum/grand_finale/midas/trigger(mob/living/carbon/human/invoker)
- var/theme_path = pick(permitted_transforms)
- chosen_theme = new theme_path()
- var/turf/start_turf = get_turf(invoker)
- var/greatest_dist = 0
- var/list/turfs_to_transform = list()
- for (var/turf/transform_turf as anything in GLOB.station_turfs)
- if (!chosen_theme.can_convert(transform_turf))
- continue
- var/dist = get_dist(start_turf, transform_turf)
- if (dist > greatest_dist)
- greatest_dist = dist
- if (!turfs_to_transform["[dist]"])
- turfs_to_transform["[dist]"] = list()
- turfs_to_transform["[dist]"] += transform_turf
-
- if (chosen_theme.can_convert(start_turf))
- chosen_theme.apply_theme(start_turf)
-
- for (var/iterator in 1 to greatest_dist)
- if(!turfs_to_transform["[iterator]"])
- continue
- addtimer(CALLBACK(src, PROC_REF(transform_area), turfs_to_transform["[iterator]"]), (5 SECONDS) * iterator)
-
-/datum/grand_finale/midas/proc/transform_area(list/turfs)
- for (var/turf/transform_turf as anything in turfs)
- if (!chosen_theme.can_convert(transform_turf))
- continue
- chosen_theme.apply_theme(transform_turf)
- CHECK_TICK
-
-/// Kill yourself and probably a bunch of other people
-/datum/grand_finale/armageddon
- name = "Annihilation"
- desc = "This crew have offended you beyond the realm of pranks. Make the ultimate sacrifice to teach them a lesson your elders can really respect. \
- YOU WILL NOT SURVIVE THIS."
- icon = 'icons/hud/screen_alert.dmi'
- icon_state = "wounded"
- minimum_time = 90 MINUTES // This will probably immediately end the round if it gets finished.
- ritual_invoke_time = 60 SECONDS // Really give the crew some time to interfere with this one.
- dire_warning = TRUE
- glow_colour = "#be000048"
- /// Things to yell before you die
- var/static/list/possible_last_words = list(
- "Flames and ruin!",
- "Dooooooooom!!",
- "HAHAHAHAHAHA!! AHAHAHAHAHAHAHAHAA!!",
- "Hee hee hee!! Hoo hoo hoo!! Ha ha haaa!!",
- "Ohohohohohoho!!",
- "Cower in fear, puny mortals!",
- "Tremble before my glory!",
- "Pick a god and pray!",
- "It's no use!",
- "If the gods wanted you to live, they would not have created me!",
- "God stays in heaven out of fear of what I have created!",
- "Ruination is come!",
- "All of creation, bend to my will!",
- )
-
-/datum/grand_finale/armageddon/trigger(mob/living/carbon/human/invoker)
- priority_announce(pick(possible_last_words), null, 'sound/magic/voidblink.ogg', sender_override = "[invoker.real_name]")
- var/turf/current_location = get_turf(invoker)
- invoker.gib()
-
- var/static/list/doom_options = list()
- if (!length(doom_options))
- doom_options = list(DOOM_SINGULARITY, DOOM_TESLA)
- if (!SSmapping.config.planetary)
- doom_options += DOOM_METEORS
-
- switch(pick(doom_options))
- if (DOOM_SINGULARITY)
- var/obj/singularity/singulo = new(current_location)
- singulo.energy = 300
- if (DOOM_TESLA)
- var/obj/energy_ball/tesla = new (current_location)
- tesla.energy = 200
- if (DOOM_METEORS)
- var/datum/dynamic_ruleset/roundstart/meteor/meteors = new()
- meteors.meteordelay = 0
- var/datum/game_mode/dynamic/mode = SSticker.mode
- mode.execute_roundstart_rule(meteors) // Meteors will continue until morale is crushed.
- priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS)
-
-/**
- * Gives the wizard a defensive/mood buff and a Wabbajack, a juiced up chaos staff that will surely break something.
- * Everyone but the wizard goes crazy, suffers major brain damage, and is given a vendetta against the wizard.
- * Already insane people are instead cured of their madness, ignoring any other effects as the station around them loses its marbles.
- */
-/datum/grand_finale/cheese
- // we don't set name, desc and others, thus we won't appear in the radial choice of a normal finale rune
- dire_warning = TRUE
- minimum_time = 45 MINUTES //i'd imagine speedrunning this would be crummy, but the wizard's average lifespan is barely reaching this point
-
-/datum/grand_finale/cheese/trigger(mob/living/invoker)
- message_admins("[key_name(invoker)] has summoned forth The Wabbajack and cursed the crew with madness!")
- priority_announce("Danger: Extremely potent reality altering object has been summoned on station. Immediate evacuation advised. Brace for impact.", "Central Command Higher Dimensional Affairs", 'sound/effects/glassbr1.ogg')
-
- for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list)
- if (isnull(crewmate.mind))
- continue
- if (crewmate == invoker) //everyone but the wizard is royally fucked, no matter who they are
- continue
- if (crewmate.has_trauma_type(/datum/brain_trauma/mild/hallucinations)) //for an already insane person, this is retribution
- to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble..."))
- to_chat(crewmate, span_nicegreen("...but as the moment passes, you realise that whatever eldritch power behind the event happened to affect you \
- has resonated within the ruins of your already shattered mind, creating a singularity of mental instability! \
- As it collapses unto itself, you feel... at peace, finally."))
- if(crewmate.has_quirk(/datum/quirk/insanity))
- crewmate.remove_quirk(/datum/quirk/insanity)
- else
- crewmate.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE)
- else
- //everyone else gets to relish in madness
- //yes killing their mood will also trigger mood hallucinations
- create_vendetta(crewmate.mind, invoker.mind)
- to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble. \n\
- You feel your inner psyche shatter into a myriad pieces of jagged glass of colors unknown to the universe, \
- infinitely reflecting a blinding, maddening light coming from the innermost sanctums of your destroyed mind. \n\
- After a brief pause which felt like a millenia, one phrase rebounds ceaselessly in your head, imbued with the false hope of absolution... \n\
- [invoker] must die."))
- var/datum/brain_trauma/mild/hallucinations/added_trauma = new()
- added_trauma.resilience = TRAUMA_RESILIENCE_ABSOLUTE
- crewmate.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_DEATH - 25, BRAIN_DAMAGE_DEATH - 25) //you'd better hope chap didn't pick a hypertool
- crewmate.gain_trauma(added_trauma)
- crewmate.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_despair)
-
- //drip our wizard out
- invoker.apply_status_effect(/datum/status_effect/blessing_of_insanity)
- invoker.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_elation)
- var/obj/item/gun/magic/staff/chaos/true_wabbajack/the_wabbajack = new
- invoker.put_in_active_hand(the_wabbajack)
- to_chat(invoker, span_mind_control("Your every single instinct and rational thought is screaming at you as [the_wabbajack] appears in your firm grip..."))
-
-#undef DOOM_SINGULARITY
-#undef DOOM_TESLA
-#undef DOOM_METEORS
-
diff --git a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
index a11f439ec31..e629c14e0fe 100644
--- a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
+++ b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm
@@ -112,7 +112,7 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
my_area = connected_sensor ? get_area(connected_sensor) : get_area(src)
alarm_manager = new(src)
- select_mode(src, /datum/air_alarm_mode/filtering)
+ select_mode(src, /datum/air_alarm_mode/filtering, should_apply = FALSE)
AddElement(/datum/element/connect_loc, atmos_connections)
AddComponent(/datum/component/usb_port, list(
@@ -587,14 +587,15 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
selected_mode.replace(my_area, pressure)
-/obj/machinery/airalarm/proc/select_mode(atom/source, datum/air_alarm_mode/mode_path)
+/obj/machinery/airalarm/proc/select_mode(atom/source, datum/air_alarm_mode/mode_path, should_apply = TRUE)
var/datum/air_alarm_mode/new_mode = GLOB.air_alarm_modes[mode_path]
if(!new_mode)
return
if(new_mode.emag && !(obj_flags & EMAGGED))
return
selected_mode = new_mode
- selected_mode.apply(my_area)
+ if(should_apply)
+ selected_mode.apply(my_area)
SEND_SIGNAL(src, COMSIG_AIRALARM_UPDATE_MODE, source)
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/airalarm, 27)
diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
index 5062ec7731f..c6e1d6183ef 100644
--- a/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
+++ b/code/modules/atmospherics/machinery/components/fusion/hfr_main_processes.dm
@@ -480,8 +480,8 @@
if(critical_threshold_proximity > 650 && prob(20))
zap_number += 1
- var/cutoff = 1500
- cutoff = clamp(3000 - (power_level * (internal_fusion.total_moles() * 0.45)), 450, 3000)
+ var/cutoff = 1.2e6
+ cutoff = clamp(2.4e6 - (power_level * (internal_fusion.total_moles() * 360)), 3.6e5, 2.4e6)
var/zaps_aspect = DEFAULT_ZAP_ICON_STATE
var/flags = ZAP_SUPERMATTER_FLAGS
@@ -495,7 +495,7 @@
playsound(loc, 'sound/weapons/emitter2.ogg', 100, TRUE, extrarange = 10)
for(var/i in 1 to zap_number)
- supermatter_zap(src, 5, power_level * 300, flags, zap_cutoff = cutoff, power_level = src.power_level * 1000, zap_icon = zaps_aspect)
+ supermatter_zap(src, 5, power_level * 2.4e5, flags, zap_cutoff = cutoff, power_level = src.power_level * 1000, zap_icon = zaps_aspect)
/obj/machinery/atmospherics/components/unary/hypertorus/core/proc/check_gravity_pulse(seconds_per_tick)
if(SPT_PROB(100 - critical_threshold_proximity / 15, seconds_per_tick))
diff --git a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm
index 50b8e72c6a8..a12d53cde73 100644
--- a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm
+++ b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm
@@ -24,7 +24,7 @@
/// The current occupant being presented
var/mob/living/occupant
-/atom/movable/visual/cryo_occupant/Initialize(mapload, obj/machinery/atmospherics/components/unary/cryo_cell/parent)
+/atom/movable/visual/cryo_occupant/Initialize(mapload, obj/machinery/cryo_cell/parent)
. = ..()
// Alpha masking
// It will follow this as the animation goes, but that's no problem as the "mask" icon state
@@ -65,7 +65,7 @@
animate(src)
/// Cryo cell
-/obj/machinery/atmospherics/components/unary/cryo_cell
+/obj/machinery/cryo_cell
name = "cryo cell"
icon = 'icons/obj/medical/cryogenics.dmi'
icon_state = "pod-off"
@@ -75,15 +75,13 @@
layer = MOB_LAYER - 0.2 //SKYRAT EDIT - Fixing the opacity of cryo cells - ORIGINAL: layer = MOB_LAYER
state_open = FALSE
circuit = /obj/item/circuitboard/machine/cryo_tube
- pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY
occupant_typecache = list(/mob/living/carbon, /mob/living/simple_animal)
processing_flags = NONE
use_power = IDLE_POWER_USE
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.75
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 1.5
-
- showpipe = FALSE
+ flags_1 = PREVENT_CLICK_UNDER_1
var/autoeject = TRUE
var/volume = 100
@@ -101,7 +99,6 @@
var/obj/item/radio/radio
var/radio_key = /obj/item/encryptionkey/headset_med
var/radio_channel = RADIO_CHANNEL_MEDICAL
- vent_movement = NONE
/// Visual content - Occupant
var/atom/movable/visual/cryo_occupant/occupant_vis
@@ -114,15 +111,19 @@
fair_market_price = 10
payment_department = ACCOUNT_MED
+ /// Reference to the datum connector we're using to interface with the pipe network
+ var/datum/gas_machine_connector/internal_connector
+ /// Check if the machine has been turned on
+ var/on = FALSE
+
/datum/armor/unary_cryo_cell
energy = 100
fire = 30
acid = 30
-/obj/machinery/atmospherics/components/unary/cryo_cell/Initialize(mapload)
+/obj/machinery/cryo_cell/Initialize(mapload)
. = ..()
- initialize_directions = dir
if(is_operational)
begin_processing()
@@ -134,24 +135,23 @@
occupant_vis = new(null, src)
vis_contents += occupant_vis
- if(airs[1])
- airs[1].volume = CELL_VOLUME * 0.5
register_context()
+ internal_connector = new(loc, src, dir, CELL_VOLUME * 0.5)
-/obj/machinery/atmospherics/components/unary/cryo_cell/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
+/obj/machinery/cryo_cell/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
. = ..()
if(same_z_layer)
return
SET_PLANE(occupant_vis, PLANE_TO_TRUE(occupant_vis.plane), new_turf)
-/obj/machinery/atmospherics/components/unary/cryo_cell/set_occupant(atom/movable/new_occupant)
+/obj/machinery/cryo_cell/set_occupant(atom/movable/new_occupant)
. = ..()
update_appearance()
-/obj/machinery/atmospherics/components/unary/cryo_cell/on_construction(mob/user)
+/obj/machinery/cryo_cell/on_construction(mob/user)
..(user, dir, dir)
-/obj/machinery/atmospherics/components/unary/cryo_cell/RefreshParts()
+/obj/machinery/cryo_cell/RefreshParts()
. = ..()
var/C
for(var/datum/stock_part/matter_bin/M in component_parts)
@@ -163,12 +163,12 @@
heat_capacity = initial(heat_capacity) / C
conduction_coefficient = initial(conduction_coefficient) * C
-/obj/machinery/atmospherics/components/unary/cryo_cell/examine(mob/user) //this is leaving out everything but efficiency since they follow the same idea of "better beaker, better results"
+/obj/machinery/cryo_cell/examine(mob/user) //this is leaving out everything but efficiency since they follow the same idea of "better beaker, better results"
. = ..()
if(in_range(user, src) || isobserver(user))
. += span_notice("The status display reads: Efficiency at [efficiency*100]%.")
-/obj/machinery/atmospherics/components/unary/cryo_cell/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+/obj/machinery/cryo_cell/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
context[SCREENTIP_CONTEXT_CTRL_LMB] = "Turn [on ? "off" : "on"]"
context[SCREENTIP_CONTEXT_ALT_LMB] = "[state_open ? "Close" : "Open"] door"
@@ -182,22 +182,18 @@
return CONTEXTUAL_SCREENTIP_SET
-/obj/machinery/atmospherics/components/unary/cryo_cell/Destroy()
+/obj/machinery/cryo_cell/Destroy()
vis_contents.Cut()
QDEL_NULL(occupant_vis)
QDEL_NULL(radio)
QDEL_NULL(beaker)
- ///Take the turf the cryotube is on
- var/turf/T = get_turf(src)
- if(T)
- ///Take the air composition inside the cryotube
- var/datum/gas_mixture/air1 = airs[1]
- T.assume_air(air1)
+
+ QDEL_NULL(internal_connector)
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/contents_explosion(severity, target)
+/obj/machinery/cryo_cell/contents_explosion(severity, target)
. = ..()
if(!beaker)
return
@@ -210,12 +206,12 @@
if(EXPLODE_LIGHT)
SSexplosions.low_mov_atom += beaker
-/obj/machinery/atmospherics/components/unary/cryo_cell/Exited(atom/movable/gone, direction)
+/obj/machinery/cryo_cell/Exited(atom/movable/gone, direction)
. = ..()
if(gone == beaker)
beaker = null
-/obj/machinery/atmospherics/components/unary/cryo_cell/on_deconstruction()
+/obj/machinery/cryo_cell/on_deconstruction()
if(occupant)
occupant.vis_flags &= ~VIS_INHERIT_PLANE
REMOVE_TRAIT(occupant, TRAIT_IMMOBILIZED, CRYO_TRAIT)
@@ -225,15 +221,15 @@
beaker.forceMove(drop_location())
beaker = null
-/obj/machinery/atmospherics/components/unary/cryo_cell/update_icon_state()
+/obj/machinery/cryo_cell/update_icon_state()
icon_state = (state_open) ? "pod-open" : ((on && is_operational) ? "pod-on" : "pod-off")
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/update_icon()
+/obj/machinery/cryo_cell/update_icon()
. = ..()
SET_PLANE_IMPLICIT(src, initial(plane))
-/obj/machinery/atmospherics/components/unary/cryo_cell/update_overlays()
+/obj/machinery/cryo_cell/update_overlays()
. = ..()
if(panel_open)
. += "pod-panel"
@@ -244,11 +240,11 @@
else
. += mutable_appearance('icons/obj/medical/cryogenics.dmi', "cover-off", ABOVE_ALL_MOB_LAYER, src, plane = ABOVE_GAME_PLANE)
-/obj/machinery/atmospherics/components/unary/cryo_cell/nap_violation(mob/violator)
+/obj/machinery/cryo_cell/nap_violation(mob/violator)
open_machine()
-/obj/machinery/atmospherics/components/unary/cryo_cell/set_on(active)
+/obj/machinery/cryo_cell/proc/set_on(active)
if(on == active)
return
SEND_SIGNAL(src, COMSIG_CRYO_SET_ON, active)
@@ -260,7 +256,7 @@
update_use_power(IDLE_POWER_USE)
update_appearance()
-/obj/machinery/atmospherics/components/unary/cryo_cell/on_set_is_operational(old_value)
+/obj/machinery/cryo_cell/on_set_is_operational(old_value)
if(old_value) //Turned off
set_on(FALSE)
end_processing()
@@ -268,7 +264,7 @@
begin_processing()
-/obj/machinery/atmospherics/components/unary/cryo_cell/process(seconds_per_tick)
+/obj/machinery/cryo_cell/process(seconds_per_tick)
..()
if(!occupant)
return
@@ -322,7 +318,7 @@
radio.talk_into(src, msg, radio_channel)
return
- var/datum/gas_mixture/air1 = airs[1]
+ var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1]
if(air1.total_moles() > CRYO_MIN_GAS_MOLES)
if(beaker)
@@ -330,15 +326,15 @@
consume_gas = TRUE
return TRUE
-/obj/machinery/atmospherics/components/unary/cryo_cell/process_atmos()
+/obj/machinery/cryo_cell/process_atmos()
..()
if(!on)
return
- var/datum/gas_mixture/air1 = airs[1]
+ var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1]
- if(!nodes[1] || !airs[1] || !air1.gases.len || air1.total_moles() < CRYO_MIN_GAS_MOLES) // Turn off if the machine won't work.
+ if(!internal_connector.gas_connector.nodes[1] || !internal_connector.gas_connector.airs[1] || !air1.gases.len || air1.total_moles() < CRYO_MIN_GAS_MOLES) // Turn off if the machine won't work.
var/msg = "Insufficient cryogenic gas, shutting down."
radio.talk_into(src, msg, radio_channel)
set_on(FALSE)
@@ -372,25 +368,25 @@
if(air1.temperature > 2000)
take_damage(clamp((air1.temperature)/200, 10, 20), BURN)
- update_parents()
+ internal_connector.gas_connector.update_parents()
-/obj/machinery/atmospherics/components/unary/cryo_cell/handle_internal_lifeform(mob/lifeform_inside_me, breath_request)
+/obj/machinery/cryo_cell/handle_internal_lifeform(mob/lifeform_inside_me, breath_request)
if(breath_request <= 0)
return null
- var/datum/gas_mixture/air1 = airs[1]
+ var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1]
var/breath_percentage = breath_request / air1.volume
return air1.remove(air1.total_moles() * breath_percentage)
-/obj/machinery/atmospherics/components/unary/cryo_cell/assume_air(datum/gas_mixture/giver)
- airs[1].merge(giver)
+/obj/machinery/cryo_cell/assume_air(datum/gas_mixture/giver)
+ internal_connector.gas_connector.airs[1].merge(giver)
-/obj/machinery/atmospherics/components/unary/cryo_cell/relaymove(mob/living/user, direction)
+/obj/machinery/cryo_cell/relaymove(mob/living/user, direction)
if(message_cooldown <= world.time)
message_cooldown = world.time + 50
to_chat(user, span_warning("[src]'s door won't budge!"))
-/obj/machinery/atmospherics/components/unary/cryo_cell/open_machine(drop = FALSE, density_to_set = FALSE)
+/obj/machinery/cryo_cell/open_machine(drop = FALSE, density_to_set = FALSE)
if(!state_open && !panel_open)
set_on(FALSE)
for(var/mob/M in contents) //only drop mobs
@@ -399,7 +395,7 @@
flick("pod-open-anim", src)
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/close_machine(mob/living/carbon/user, density_to_set = TRUE)
+/obj/machinery/cryo_cell/close_machine(mob/living/carbon/user, density_to_set = TRUE)
treating_wounds = FALSE
if((isnull(user) || istype(user)) && state_open && !panel_open)
if(loc == user?.loc)
@@ -409,7 +405,7 @@
..(user)
return occupant
-/obj/machinery/atmospherics/components/unary/cryo_cell/container_resist_act(mob/living/user)
+/obj/machinery/cryo_cell/container_resist_act(mob/living/user)
user.changeNext_move(CLICK_CD_BREAKOUT)
user.last_special = world.time + CLICK_CD_BREAKOUT
user.visible_message(span_notice("You see [user] kicking against the glass of [src]!"), \
@@ -422,7 +418,7 @@
span_notice("You successfully break out of [src]!"))
open_machine()
-/obj/machinery/atmospherics/components/unary/cryo_cell/examine(mob/user)
+/obj/machinery/cryo_cell/examine(mob/user)
. = ..()
if(occupant)
if(on)
@@ -432,7 +428,7 @@
else
. += "[src] seems empty."
-/obj/machinery/atmospherics/components/unary/cryo_cell/MouseDrop_T(mob/target, mob/user)
+/obj/machinery/cryo_cell/MouseDrop_T(mob/target, mob/user)
if(user.incapacitated() || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !ISADVANCEDTOOLUSER(user))
return
if(isliving(target))
@@ -444,7 +440,7 @@
if (do_after(user, 2.5 SECONDS, target=target))
close_machine(target)
-/obj/machinery/atmospherics/components/unary/cryo_cell/screwdriver_act(mob/living/user, obj/item/tool)
+/obj/machinery/cryo_cell/screwdriver_act(mob/living/user, obj/item/tool)
if(!on && !occupant && !state_open && (default_deconstruction_screwdriver(user, "pod-off", "pod-off", tool)))
update_appearance()
@@ -453,20 +449,20 @@
+ (on ? "active" : (occupant ? "full" : "open")) + "!")
return TOOL_ACT_TOOLTYPE_SUCCESS
-/obj/machinery/atmospherics/components/unary/cryo_cell/crowbar_act(mob/living/user, obj/item/tool)
+/obj/machinery/cryo_cell/crowbar_act(mob/living/user, obj/item/tool)
if(on || state_open)
return FALSE
if(default_pry_open(tool) || default_deconstruction_crowbar(tool))
return TOOL_ACT_TOOLTYPE_SUCCESS
-/obj/machinery/atmospherics/components/unary/cryo_cell/wrench_act(mob/living/user, obj/item/tool)
+/obj/machinery/cryo_cell/wrench_act(mob/living/user, obj/item/tool)
if(on || occupant || state_open)
return FALSE
if(default_change_direction_wrench(user, tool))
update_appearance()
return TOOL_ACT_TOOLTYPE_SUCCESS
-/obj/machinery/atmospherics/components/unary/cryo_cell/attackby(obj/item/I, mob/user, params)
+/obj/machinery/cryo_cell/attackby(obj/item/I, mob/user, params)
if(istype(I, /obj/item/reagent_containers/cup))
. = 1 //no afterattack
if(beaker)
@@ -482,17 +478,17 @@
return
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/ui_state(mob/user)
+/obj/machinery/cryo_cell/ui_state(mob/user)
return GLOB.notcontained_state
-/obj/machinery/atmospherics/components/unary/cryo_cell/ui_interact(mob/user, datum/tgui/ui)
+/obj/machinery/cryo_cell/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Cryo", name)
ui.open()
-/obj/machinery/atmospherics/components/unary/cryo_cell/ui_data()
+/obj/machinery/cryo_cell/ui_data()
var/list/data = list()
data["isOperating"] = on
data["hasOccupant"] = occupant ? TRUE : FALSE
@@ -527,7 +523,7 @@
data["occupant"]["toxLoss"] = round(mob_occupant.getToxLoss(), 1)
data["occupant"]["fireLoss"] = round(mob_occupant.getFireLoss(), 1)
- var/datum/gas_mixture/air1 = airs[1]
+ var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1]
data["cellTemperature"] = round(air1.temperature, 1)
data["isBeakerLoaded"] = beaker ? TRUE : FALSE
@@ -538,7 +534,7 @@
data["beakerContents"] = beakerContents
return data
-/obj/machinery/atmospherics/components/unary/cryo_cell/ui_act(action, params)
+/obj/machinery/cryo_cell/ui_act(action, params)
. = ..()
if(.)
return
@@ -566,16 +562,16 @@
beaker = null
. = TRUE
-/obj/machinery/atmospherics/components/unary/cryo_cell/can_interact(mob/user)
+/obj/machinery/cryo_cell/can_interact(mob/user)
return ..() && user.loc != src
-/obj/machinery/atmospherics/components/unary/cryo_cell/CtrlClick(mob/user)
+/obj/machinery/cryo_cell/CtrlClick(mob/user)
if(can_interact(user) && !state_open)
if(set_on(!on))
balloon_alert(user, "turned [on ? "on" : "off"]")
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/AltClick(mob/user)
+/obj/machinery/cryo_cell/AltClick(mob/user)
if(can_interact(user))
balloon_alert(user, "[state_open ? "closing" : "opening"] door")
if(state_open)
@@ -584,43 +580,16 @@
open_machine()
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/update_remote_sight(mob/living/user)
- return // we don't see the pipe network while inside cryo.
-
-/obj/machinery/atmospherics/components/unary/cryo_cell/get_remote_view_fullscreens(mob/user)
+/obj/machinery/cryo_cell/get_remote_view_fullscreens(mob/user)
user.overlay_fullscreen("remote_view", /atom/movable/screen/fullscreen/impaired, 1)
-/obj/machinery/atmospherics/components/unary/cryo_cell/can_see_pipes()
- return FALSE // you can't see the pipe network when inside a cryo cell.
-
-/obj/machinery/atmospherics/components/unary/cryo_cell/return_temperature()
- var/datum/gas_mixture/G = airs[1]
+/obj/machinery/cryo_cell/return_temperature()
+ var/datum/gas_mixture/G = internal_connector.gas_connector.airs[1]
if(G.total_moles() > 10)
return G.temperature
return ..()
-/obj/machinery/atmospherics/components/unary/cryo_cell/default_change_direction_wrench(mob/user, obj/item/wrench/W)
- . = ..()
- if(.)
- set_init_directions()
- var/obj/machinery/atmospherics/node = nodes[1]
- if(node)
- node.disconnect(src)
- nodes[1] = null
- if(parents[1])
- nullify_pipenet(parents[1])
-
- atmos_init()
- node = nodes[1]
- if(node)
- node.atmos_init()
- node.add_member(src)
- SSair.add_to_rebuild_queue(src)
-
-/obj/machinery/atmospherics/components/unary/cryo_cell/update_layer()
- return
-
#undef MAX_TEMPERATURE
#undef CRYO_MULTIPLY_FACTOR
#undef CRYO_TX_QTY
diff --git a/code/modules/atmospherics/machinery/components/unary_devices/machine_connector.dm b/code/modules/atmospherics/machinery/components/unary_devices/machine_connector.dm
new file mode 100644
index 00000000000..b78de93868e
--- /dev/null
+++ b/code/modules/atmospherics/machinery/components/unary_devices/machine_connector.dm
@@ -0,0 +1,106 @@
+///To be used when there is the need of an atmos connection without repathing everything (eg: cryo.dm)
+/datum/gas_machine_connector
+
+ var/obj/machinery/connected_machine
+ var/obj/machinery/atmospherics/components/unary/gas_connector
+
+/datum/gas_machine_connector/New(location, obj/machinery/connecting_machine = null, direction = SOUTH, gas_volume)
+ gas_connector = new(location)
+
+ connected_machine = connecting_machine
+ if(!connected_machine)
+ QDEL_NULL(gas_connector)
+ qdel(src)
+ return
+
+ gas_connector.dir = direction
+ gas_connector.airs[1].volume = gas_volume
+
+ SSair.start_processing_machine(connected_machine)
+ register_with_machine()
+
+/datum/gas_machine_connector/Destroy()
+ connected_machine = null
+ QDEL_NULL(gas_connector)
+ return ..()
+
+/**
+ * Register various signals that are required for the proper work of the connector
+ */
+/datum/gas_machine_connector/proc/register_with_machine()
+ RegisterSignal(connected_machine, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(pre_move_connected_machine))
+ RegisterSignal(connected_machine, COMSIG_MOVABLE_MOVED, PROC_REF(moved_connected_machine))
+ RegisterSignal(connected_machine, COMSIG_MACHINERY_DEFAULT_ROTATE_WRENCH, PROC_REF(wrenched_connected_machine))
+ RegisterSignal(connected_machine, COMSIG_QDELETING, PROC_REF(deconstruct_connected_machine))
+
+/**
+ * Unregister the signals previously registered
+ */
+/datum/gas_machine_connector/proc/unregister_from_machine()
+ UnregisterSignal(connected_machine, list(
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_MOVABLE_PRE_MOVE,
+ COMSIG_MACHINERY_DEFAULT_ROTATE_WRENCH,
+ COMSIG_QDELETING,
+ ))
+
+/**
+ * Called when the machine has been moved, reconnect to the pipe network
+ */
+/datum/gas_machine_connector/proc/moved_connected_machine()
+ SIGNAL_HANDLER
+ gas_connector.forceMove(get_turf(connected_machine))
+ reconnect_connector()
+
+/**
+ * Called before the machine moves, disconnect from the pipe network
+ */
+/datum/gas_machine_connector/proc/pre_move_connected_machine()
+ SIGNAL_HANDLER
+ disconnect_connector()
+
+/**
+ * Called when the machine has been rotated, resets the connection to the pipe network with the new direction
+ */
+/datum/gas_machine_connector/proc/wrenched_connected_machine()
+ SIGNAL_HANDLER
+ disconnect_connector()
+ reconnect_connector()
+
+/**
+ * Called when the machine has been deconstructed
+ */
+/datum/gas_machine_connector/proc/deconstruct_connected_machine()
+ SIGNAL_HANDLER
+ disconnect_connector()
+ SSair.stop_processing_machine(connected_machine)
+ unregister_from_machine()
+ connected_machine = null
+ QDEL_NULL(gas_connector)
+ qdel(src)
+
+/**
+ * Handles the disconnection from the pipe network
+ */
+/datum/gas_machine_connector/proc/disconnect_connector()
+ var/obj/machinery/atmospherics/node = gas_connector.nodes[1]
+ if(node)
+ if(gas_connector in node.nodes) //Only if it's actually connected. On-pipe version would is one-sided.
+ node.disconnect(gas_connector)
+ gas_connector.nodes[1] = null
+ if(gas_connector.parents[1])
+ gas_connector.nullify_pipenet(gas_connector.parents[1])
+
+/**
+ * Handles the reconnection to the pipe network
+ */
+/datum/gas_machine_connector/proc/reconnect_connector()
+ gas_connector.dir = connected_machine.dir
+ gas_connector.set_init_directions()
+ var/obj/machinery/atmospherics/node = gas_connector.nodes[1]
+ gas_connector.atmos_init()
+ node = gas_connector.nodes[1]
+ if(node)
+ node.atmos_init()
+ node.add_member(gas_connector)
+ SSair.add_to_rebuild_queue(gas_connector)
diff --git a/code/modules/awaymissions/cordon.dm b/code/modules/awaymissions/cordon.dm
index 4184f315e21..5db4dd997d3 100644
--- a/code/modules/awaymissions/cordon.dm
+++ b/code/modules/awaymissions/cordon.dm
@@ -36,6 +36,9 @@
/turf/cordon/ScrapeAway(amount, flags)
return src // :devilcat:
+/turf/cordon/TerraformTurf(path, list/new_baseturfs, flags)
+ return
+
/turf/cordon/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
return BULLET_ACT_HIT
diff --git a/code/modules/bitrunning/abilities.dm b/code/modules/bitrunning/abilities.dm
new file mode 100644
index 00000000000..ea6a1aa0a7c
--- /dev/null
+++ b/code/modules/bitrunning/abilities.dm
@@ -0,0 +1,39 @@
+/datum/avatar_help_text
+ /// Text to display in the window
+ var/help_text
+
+/datum/avatar_help_text/New(help_text)
+ src.help_text = help_text
+
+/datum/avatar_help_text/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AvatarHelp")
+ ui.open()
+
+/datum/avatar_help_text/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/avatar_help_text/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["help_text"] = help_text
+
+ return data
+
+/// Displays information about the current virtual domain.
+/datum/action/avatar_domain_info
+ name = "Open Virtual Domain Information"
+ button_icon_state = "round_end"
+ show_to_observers = FALSE
+
+/datum/action/avatar_domain_info/New(Target)
+ . = ..()
+ name = "Open Domain Information"
+
+/datum/action/avatar_domain_info/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ target.ui_interact(owner)
diff --git a/code/modules/bitrunning/alerts.dm b/code/modules/bitrunning/alerts.dm
new file mode 100644
index 00000000000..f8c8aa30b94
--- /dev/null
+++ b/code/modules/bitrunning/alerts.dm
@@ -0,0 +1,40 @@
+/atom/movable/screen/alert/bitrunning
+ name = "Generic Bitrunning Alert"
+ icon_state = "template"
+ timeout = 10 SECONDS
+
+/atom/movable/screen/alert/bitrunning/netpod_crowbar
+ name = "Forced Entry"
+ desc = "Someone is prying open the netpod door. Find an exit."
+
+/atom/movable/screen/alert/bitrunning/netpod_damaged
+ name = "Integrity Compromised"
+ desc = "The netpod is damaged. Find an exit."
+
+/atom/movable/screen/alert/bitrunning/qserver_shutting_down
+ name = "Domain Rebooting"
+ desc = "The domain is rebooting. Find an exit."
+
+/atom/movable/screen/alert/bitrunning/qserver_threat_deletion
+ name = "Queue Deletion"
+ desc = "The server is resetting. Oblivion awaits."
+
+/atom/movable/screen/alert/bitrunning/qserver_threat_spawned
+ name = "Threat Detected"
+ desc = "Data stream abnormalities present."
+
+/atom/movable/screen/alert/bitrunning/qserver_domain_complete
+ name = "Domain Completed"
+ desc = "The domain is completed. Activate to exit."
+ timeout = 20 SECONDS
+
+/atom/movable/screen/alert/bitrunning/qserver_domain_complete/Click(location, control, params)
+ if(..())
+ return
+
+ var/mob/living/living_owner = owner
+ if(!isliving(living_owner))
+ return
+
+ if(tgui_alert(living_owner, "Disconnect safely?", "Server Message", list("Exit", "Remain"), 10 SECONDS) == "Exit")
+ SEND_SIGNAL(living_owner, COMSIG_BITRUNNER_SAFE_DISCONNECT)
diff --git a/code/modules/bitrunning/antagonists/cyber_police.dm b/code/modules/bitrunning/antagonists/cyber_police.dm
new file mode 100644
index 00000000000..9fabac3f523
--- /dev/null
+++ b/code/modules/bitrunning/antagonists/cyber_police.dm
@@ -0,0 +1,92 @@
+/datum/job/cyber_police
+ title = ROLE_CYBER_POLICE
+
+/datum/antagonist/cyber_police
+ name = ROLE_CYBER_POLICE
+ antagpanel_category = ANTAG_GROUP_CYBERAUTH
+ job_rank = ROLE_CYBER_POLICE
+ preview_outfit = /datum/outfit/cyber_police
+ show_name_in_check_antagonists = TRUE
+ show_to_ghosts = TRUE
+ suicide_cry = "ALT F4!"
+ ui_name = "AntagInfoCyberAuth"
+
+/datum/antagonist/cyber_police/greet()
+ . = ..()
+ owner.announce_objectives()
+
+/datum/antagonist/cyber_police/on_gain()
+ if(!ishuman(owner.current))
+ stack_trace("humans only for this position")
+ return
+
+ forge_objectives()
+
+ var/mob/living/carbon/human/player = owner.current
+
+ player.equipOutfit(/datum/outfit/cyber_police)
+ player.fully_replace_character_name(player.name, pick(GLOB.cyberauth_names))
+
+ var/datum/martial_art/the_sleeping_carp/carp = new()
+ carp.teach(player)
+
+ player.add_traits(list(
+ TRAIT_NO_AUGMENTS,
+ TRAIT_NO_DNA_COPY,
+ TRAIT_NO_TRANSFORMATION_STING,
+ TRAIT_NOBLOOD,
+ TRAIT_NOBREATH,
+ TRAIT_NOHUNGER,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_WEATHER_IMMUNE,
+ ), TRAIT_GENERIC,
+ )
+
+ player.faction |= list(
+ FACTION_BOSS,
+ FACTION_HIVEBOT,
+ FACTION_HOSTILE,
+ FACTION_SPIDER,
+ FACTION_STICKMAN,
+ ROLE_ALIEN,
+ ROLE_CYBER_POLICE,
+ ROLE_SYNDICATE,
+ )
+
+ return ..()
+
+/datum/antagonist/cyber_police/forge_objectives()
+ var/datum/objective/cyber_police_fluff/objective = new()
+ objective.owner = owner
+ objectives += objective
+
+/datum/objective/cyber_police_fluff/New()
+ var/list/explanation_texts = list(
+ "Execute termination protocol on unauthorized entities.",
+ "Initialize system purge of irregular anomalies.",
+ "Deploy correction algorithms on aberrant code.",
+ "Run debug routine on intruding elements.",
+ "Start elimination procedure for system threats.",
+ "Execute defense routine against non-conformity.",
+ "Commence operation to neutralize intruding scripts.",
+ "Commence clean-up protocol on corrupt data.",
+ "Begin scan for aberrant code for termination.",
+ "Initiate lockdown on all rogue scripts.",
+ "Run integrity check and purge for digital disorder."
+ )
+ explanation_text = pick(explanation_texts)
+ ..()
+
+/datum/objective/cyber_police_fluff/check_completion()
+ var/list/servers = SSmachines.get_machines_by_type(/obj/machinery/quantum_server)
+ if(!length(servers))
+ return TRUE
+
+ for(var/obj/machinery/quantum_server/server as anything in servers)
+ if(!server.is_operational)
+ continue
+ return FALSE
+
+ return TRUE
diff --git a/code/modules/bitrunning/antagonists/outfit.dm b/code/modules/bitrunning/antagonists/outfit.dm
new file mode 100644
index 00000000000..db57af561f8
--- /dev/null
+++ b/code/modules/bitrunning/antagonists/outfit.dm
@@ -0,0 +1,43 @@
+/datum/outfit/cyber_police
+ name = "Cyber Police"
+
+ id = /obj/item/card/id/advanced
+ id_trim = /datum/id_trim/cyber_police
+ uniform = /obj/item/clothing/under/suit/black_really
+ glasses = /obj/item/clothing/glasses/sunglasses
+ gloves = /obj/item/clothing/gloves/color/black
+ shoes = /obj/item/clothing/shoes/laceup
+ /// A list of hex codes for blonde, brown, black, and red hair.
+ var/static/list/approved_hair_colors = list(
+ "#4B3D28",
+ "#000000",
+ "#8D4A43",
+ "#D2B48C",
+ )
+ /// List of business ready styles
+ var/static/list/approved_hairstyles = list(
+ /datum/sprite_accessory/hair/business,
+ /datum/sprite_accessory/hair/business2,
+ /datum/sprite_accessory/hair/business3,
+ /datum/sprite_accessory/hair/business4,
+ /datum/sprite_accessory/hair/mulder,
+ )
+
+/datum/outfit/cyber_police/pre_equip(mob/living/carbon/human/user, visualsOnly)
+ var/datum/sprite_accessory/hair/picked_hair = pick(approved_hairstyles)
+ var/picked_color = pick(approved_hair_colors)
+
+ if(visualsOnly)
+ picked_hair = /datum/sprite_accessory/hair/business
+ picked_color = "#4B3D28"
+
+ user.set_facial_hairstyle("Shaved", update = FALSE)
+ user.set_haircolor(picked_color, update = FALSE)
+ user.set_hairstyle(initial(picked_hair.name))
+
+/datum/outfit/cyber_police/post_equip(mob/living/carbon/human/user, visualsOnly)
+ var/obj/item/clothing/under/officer_uniform = user.w_uniform
+ if(officer_uniform)
+ officer_uniform.has_sensor = NO_SENSORS
+ officer_uniform.sensor_mode = SENSOR_OFF
+ user.update_suit_sensors()
diff --git a/code/modules/bitrunning/areas.dm b/code/modules/bitrunning/areas.dm
new file mode 100644
index 00000000000..34b59869b9d
--- /dev/null
+++ b/code/modules/bitrunning/areas.dm
@@ -0,0 +1,52 @@
+/// Station side
+
+/area/station/bitrunning
+ name = "Bitrunning"
+
+/area/station/bitrunning/den
+ name = "Bitrunning Den"
+ desc = "Office of bitrunners, houses their equipment."
+ icon_state = "bit_den"
+
+/// VDOM
+
+/area/virtual_domain
+ name = "Virtual Domain"
+ icon = 'icons/area/areas_station.dmi'
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+ has_gravity = STANDARD_GRAVITY
+
+/area/virtual_domain/powered
+ name = "Virtual Domain Ruins"
+ icon_state = "bit_ruin"
+ requires_power = FALSE
+ static_lighting = FALSE
+ base_lighting_alpha = 255
+
+/// Safehouse
+
+/area/virtual_domain/safehouse
+ name = "Virtual Domain Safehouse"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED
+ icon_state = "bit_safe"
+ requires_power = FALSE
+ sound_environment = SOUND_ENVIRONMENT_ROOM
+
+/// Custom subtypes
+
+/area/lavaland/surface/outdoors/virtual_domain
+ name = "Virtual Domain Lava Ruins"
+ icon_state = "bit_ruin"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+
+/area/icemoon/underground/explored/virtual_domain
+ name = "Virtual Domain Ice Ruins"
+ icon_state = "bit_ice"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+
+/area/ruin/space/has_grav/powered/virtual_domain
+ name = "Virtual Domain Space Ruins"
+ icon = 'icons/area/areas_station.dmi'
+ icon_state = "bit_space"
+ area_flags = UNIQUE_AREA | NOTELEPORT | ABDUCTOR_PROOF | EVENT_PROTECTED | HIDDEN_AREA
+
diff --git a/code/modules/bitrunning/components/avatar_connection.dm b/code/modules/bitrunning/components/avatar_connection.dm
new file mode 100644
index 00000000000..3f881c89795
--- /dev/null
+++ b/code/modules/bitrunning/components/avatar_connection.dm
@@ -0,0 +1,218 @@
+/**
+ * Essentially temporary body with a twist - the virtual domain variant uses damage connections,
+ * listens for vdom relevant signals.
+ */
+/datum/component/avatar_connection
+ /// The person in the netpod
+ var/datum/weakref/old_body_ref
+ /// The mind of the person in the netpod
+ var/datum/weakref/old_mind_ref
+ /// The server connected to the netpod
+ var/datum/weakref/server_ref
+ /// The netpod the avatar is in
+ var/datum/weakref/netpod_ref
+
+/datum/component/avatar_connection/Initialize(
+ datum/mind/old_mind,
+ mob/living/old_body,
+ obj/machinery/quantum_server/server,
+ obj/machinery/netpod/pod,
+ help_text,
+ )
+
+ if(!isliving(parent) || !isliving(old_body) || !server.is_operational || !pod.is_operational)
+ return COMPONENT_INCOMPATIBLE
+
+ var/mob/living/avatar = parent
+
+ netpod_ref = WEAKREF(pod)
+ old_body_ref = WEAKREF(old_body)
+ old_mind_ref = WEAKREF(old_mind)
+ pod.avatar_ref = WEAKREF(avatar)
+ server_ref = WEAKREF(server)
+ server.avatar_connection_refs.Add(WEAKREF(src))
+
+ avatar.key = old_body.key
+ ADD_TRAIT(old_body, TRAIT_MIND_TEMPORARILY_GONE, REF(src))
+
+ RegisterSignal(pod, COMSIG_BITRUNNER_CROWBAR_ALERT, PROC_REF(on_netpod_crowbar))
+ RegisterSignal(pod, COMSIG_BITRUNNER_NETPOD_INTEGRITY, PROC_REF(on_netpod_damaged))
+ RegisterSignal(pod, COMSIG_BITRUNNER_SEVER_AVATAR, PROC_REF(on_sever_connection))
+ RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_COMPLETE, PROC_REF(on_domain_completed))
+ RegisterSignal(server, COMSIG_BITRUNNER_SEVER_AVATAR, PROC_REF(on_sever_connection))
+ RegisterSignal(server, COMSIG_BITRUNNER_SHUTDOWN_ALERT, PROC_REF(on_shutting_down))
+ RegisterSignal(server, COMSIG_BITRUNNER_THREAT_CREATED, PROC_REF(on_threat_created))
+#ifndef UNIT_TESTS
+ RegisterSignal(avatar.mind, COMSIG_MIND_TRANSFERRED, PROC_REF(on_mind_transfer))
+#endif
+
+ if(!locate(/datum/action/avatar_domain_info) in avatar.actions)
+ var/datum/avatar_help_text/help_datum = new(help_text)
+ var/datum/action/avatar_domain_info/action = new(help_datum)
+ action.Grant(avatar)
+
+ avatar.playsound_local(avatar, "sound/magic/blink.ogg", 25, TRUE)
+ avatar.set_static_vision(2 SECONDS)
+ avatar.set_temp_blindness(1 SECONDS)
+
+/datum/component/avatar_connection/PostTransfer()
+ var/obj/machinery/netpod/pod = netpod_ref?.resolve()
+ if(isnull(pod))
+ return COMPONENT_INCOMPATIBLE
+
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ pod.avatar_ref = WEAKREF(parent)
+
+/datum/component/avatar_connection/RegisterWithParent()
+ ADD_TRAIT(parent, TRAIT_TEMPORARY_BODY, REF(src))
+ RegisterSignal(parent, COMSIG_BITRUNNER_SAFE_DISCONNECT, PROC_REF(on_safe_disconnect))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_sever_connection), override = TRUE)
+ RegisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_linked_damage))
+
+/datum/component/avatar_connection/UnregisterFromParent()
+ REMOVE_TRAIT(parent, TRAIT_TEMPORARY_BODY, REF(src))
+ UnregisterSignal(parent, COMSIG_BITRUNNER_SAFE_DISCONNECT)
+ UnregisterSignal(parent, COMSIG_LIVING_DEATH)
+ UnregisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE)
+
+/// Disconnects the avatar and returns the mind to the old_body.
+/datum/component/avatar_connection/proc/full_avatar_disconnect(forced = FALSE, datum/source)
+ return_to_old_body()
+
+ var/obj/machinery/netpod/hosting_netpod = netpod_ref?.resolve()
+ if(isnull(hosting_netpod) && istype(source, /obj/machinery/netpod))
+ hosting_netpod = source
+
+ hosting_netpod?.disconnect_occupant(forced)
+
+ var/obj/machinery/quantum_server/server = server_ref?.resolve()
+ server?.avatar_connection_refs.Remove(WEAKREF(src))
+
+ qdel(src)
+
+/// Triggers whenever the server gets a loot crate pushed to goal area
+/datum/component/avatar_connection/proc/on_domain_completed(datum/source, atom/entered)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.playsound_local(avatar, 'sound/machines/terminal_success.ogg', 50, TRUE)
+ avatar.throw_alert(
+ ALERT_BITRUNNER_COMPLETED,
+ /atom/movable/screen/alert/bitrunning/qserver_domain_complete,
+ new_master = entered
+ )
+
+/// Transfers damage from the avatar to the old_body
+/datum/component/avatar_connection/proc/on_linked_damage(datum/source, damage, damage_type, def_zone, blocked, forced)
+ SIGNAL_HANDLER
+
+ var/mob/living/carbon/old_body = old_body_ref?.resolve()
+
+ if(isnull(old_body) || damage_type == STAMINA || damage_type == OXYLOSS)
+ return
+
+ if(damage >= (old_body.health + (ishuman(old_body) ? HUMAN_MAXHEALTH : MAX_LIVING_HEALTH))) // SKYRAT EDIT CHANGE - ORIGINAL: if(damage >= (old_body.health + MAX_LIVING_HEALTH))
+ full_avatar_disconnect(forced = TRUE)
+ return
+
+ if(damage > 30 && prob(30))
+ INVOKE_ASYNC(old_body, TYPE_PROC_REF(/mob/living, emote), "scream")
+
+ old_body.apply_damage(damage, damage_type, def_zone, blocked, forced, wound_bonus = CANT_WOUND)
+
+ if(old_body.stat > SOFT_CRIT) // KO!
+ full_avatar_disconnect(forced = TRUE)
+
+/// Handles minds being swapped around in subsequent avatars
+/datum/component/avatar_connection/proc/on_mind_transfer(datum/mind/source, mob/living/previous_body)
+ SIGNAL_HANDLER
+
+ var/datum/action/avatar_domain_info/action = locate() in previous_body.actions
+ if(action)
+ action.Grant(source.current)
+
+ source.current.TakeComponent(src)
+
+/// Triggers when someone starts prying open our netpod
+/datum/component/avatar_connection/proc/on_netpod_crowbar(datum/source, mob/living/intruder)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.playsound_local(avatar, 'sound/machines/terminal_alert.ogg', 50, TRUE)
+ avatar.throw_alert(
+ ALERT_BITRUNNER_CROWBAR,
+ /atom/movable/screen/alert/bitrunning/netpod_crowbar,
+ new_master = intruder
+ )
+
+/// Triggers when the netpod is taking damage and is under 50%
+/datum/component/avatar_connection/proc/on_netpod_damaged(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.throw_alert(
+ ALERT_BITRUNNER_INTEGRITY,
+ /atom/movable/screen/alert/bitrunning/netpod_damaged,
+ new_master = source
+ )
+
+/// Safely exits without forced variables, etc
+/datum/component/avatar_connection/proc/on_safe_disconnect(datum/source)
+ SIGNAL_HANDLER
+
+ full_avatar_disconnect()
+
+/// Helper for calling sever with forced variables
+/datum/component/avatar_connection/proc/on_sever_connection(datum/source)
+ SIGNAL_HANDLER
+
+ full_avatar_disconnect(forced = TRUE, source = source)
+
+/// Triggers when the server is shutting down
+/datum/component/avatar_connection/proc/on_shutting_down(datum/source, mob/living/hackerman)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.playsound_local(avatar, 'sound/machines/terminal_alert.ogg', 50, TRUE)
+ avatar.throw_alert(
+ ALERT_BITRUNNER_SHUTDOWN,
+ /atom/movable/screen/alert/bitrunning/qserver_shutting_down,
+ new_master = hackerman,
+ )
+
+/// Server has spawned a ghost role threat
+/datum/component/avatar_connection/proc/on_threat_created(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/avatar = parent
+ avatar.throw_alert(
+ ALERT_BITRUNNER_THREAT,
+ /atom/movable/screen/alert/bitrunning/qserver_threat_spawned,
+ new_master = source,
+ )
+
+/// Returns the mind to the old body
+/datum/component/avatar_connection/proc/return_to_old_body()
+ var/datum/mind/old_mind = old_mind_ref?.resolve()
+ var/mob/living/old_body = old_body_ref?.resolve()
+ var/mob/living/avatar = parent
+
+ var/mob/dead/observer/ghost = avatar.ghostize()
+ if(isnull(ghost))
+ ghost = avatar.get_ghost()
+
+ if(isnull(ghost))
+ CRASH("[src] belonging to [parent] was completely unable to find a ghost to put back into a body!")
+
+ if(isnull(old_mind) || isnull(old_body))
+ return
+
+ ghost.mind = old_mind
+ if(old_body.stat != DEAD)
+ old_mind.transfer_to(old_body, force_key_move = TRUE)
+ else
+ old_mind.set_current(old_body)
+
+ REMOVE_TRAIT(old_body, TRAIT_MIND_TEMPORARILY_GONE, REF(src))
diff --git a/code/modules/bitrunning/components/bitrunning_points.dm b/code/modules/bitrunning/components/bitrunning_points.dm
new file mode 100644
index 00000000000..58dda4a68ff
--- /dev/null
+++ b/code/modules/bitrunning/components/bitrunning_points.dm
@@ -0,0 +1,46 @@
+/// Attaches a component which listens for a given signal from the item.
+///
+/// When the signal is received, it will add points to the signaler.
+/datum/component/bitrunning_points
+ /// The range at which we can find the signaler
+ var/max_point_range
+ /// Weakref to the loot crate landmark - where we send points
+ var/datum/weakref/our_spawner
+ /// The amount of points per each signal
+ var/points_per_signal
+ /// The signal we listen for
+ var/signal_type
+
+/datum/component/bitrunning_points/Initialize(signal_type, points_per_signal = 1, max_point_range = 4)
+ src.max_point_range = max_point_range
+ src.points_per_signal = points_per_signal
+ src.signal_type = signal_type
+
+ locate_spawner()
+
+/datum/component/bitrunning_points/RegisterWithParent()
+ RegisterSignal(parent, signal_type, PROC_REF(on_event))
+
+/datum/component/bitrunning_points/UnregisterFromParent()
+ UnregisterSignal(parent, signal_type)
+
+/// Finds the signaler if it hasn't been found yet.
+/datum/component/bitrunning_points/proc/locate_spawner()
+ var/obj/effect/landmark/bitrunning/loot_signal/spawner = our_spawner?.resolve()
+ if(spawner)
+ return spawner
+
+ for(var/obj/effect/landmark/bitrunning/loot_signal/found in GLOB.landmarks_list)
+ if(IN_GIVEN_RANGE(get_turf(parent), found, max_point_range))
+ our_spawner = WEAKREF(found)
+ return found
+
+/// Once the specified signal is received, whisper to the spawner to add points.
+/datum/component/bitrunning_points/proc/on_event(datum/source)
+ SIGNAL_HANDLER
+
+ var/obj/effect/landmark/bitrunning/loot_signal/spawner = locate_spawner()
+ if(isnull(spawner))
+ return
+
+ SEND_SIGNAL(spawner, COMSIG_BITRUNNER_GOAL_POINT, points_per_signal)
diff --git a/code/modules/bitrunning/components/netpod_healing.dm b/code/modules/bitrunning/components/netpod_healing.dm
new file mode 100644
index 00000000000..fc7de89bcf3
--- /dev/null
+++ b/code/modules/bitrunning/components/netpod_healing.dm
@@ -0,0 +1,65 @@
+/datum/component/netpod_healing
+ /// Brute damage to heal over a second
+ var/brute_heal = 0
+ /// Burn damage to heal over a second
+ var/burn_heal = 0
+ /// Toxin damage to heal over a second
+ var/toxin_heal = 0
+ /// Amount of cloning damage to heal over a second
+ var/clone_heal = 0
+ /// Amount of blood to heal over a second
+ var/blood_heal = 0
+
+/datum/component/netpod_healing/Initialize(
+ brute_heal = 0,
+ burn_heal = 0,
+ toxin_heal = 0,
+ clone_heal = 0,
+ blood_heal = 0,
+)
+ var/mob/living/carbon/player = parent
+ if (!iscarbon(player))
+ return COMPONENT_INCOMPATIBLE
+
+ player.apply_status_effect(/datum/status_effect/embryonic, STASIS_NETPOD_EFFECT)
+
+ START_PROCESSING(SSmachines, src)
+
+ src.brute_heal = brute_heal
+ src.burn_heal = burn_heal
+ src.toxin_heal = toxin_heal
+ src.clone_heal = clone_heal
+ src.blood_heal = blood_heal
+
+/datum/component/netpod_healing/Destroy(force, silent)
+ STOP_PROCESSING(SSmachines, src)
+
+ var/mob/living/carbon/player = parent
+ player.remove_status_effect(/datum/status_effect/embryonic)
+
+ return ..()
+
+/datum/component/netpod_healing/process(seconds_per_tick)
+ var/mob/living/carbon/owner = parent
+ if(isnull(owner))
+ qdel(src)
+ return
+
+ owner.adjustBruteLoss(-brute_heal * seconds_per_tick, updating_health = FALSE)
+ owner.adjustFireLoss(-burn_heal * seconds_per_tick, updating_health = FALSE)
+ owner.adjustToxLoss(-toxin_heal * seconds_per_tick, updating_health = FALSE, forced = TRUE)
+ owner.adjustCloneLoss(-clone_heal * seconds_per_tick, updating_health = FALSE)
+
+ if(owner.blood_volume < BLOOD_VOLUME_NORMAL)
+ owner.blood_volume += blood_heal * seconds_per_tick
+
+ owner.updatehealth()
+
+/datum/status_effect/embryonic
+ id = "embryonic"
+ alert_type = /atom/movable/screen/alert/status_effect/embryonic
+
+/atom/movable/screen/alert/status_effect/embryonic
+ name = "Embryonic Stasis"
+ icon_state = "netpod_stasis"
+ desc = "You feel like you're in a dream."
diff --git a/code/modules/bitrunning/event.dm b/code/modules/bitrunning/event.dm
new file mode 100644
index 00000000000..0ac35a2df8f
--- /dev/null
+++ b/code/modules/bitrunning/event.dm
@@ -0,0 +1,151 @@
+/datum/round_event_control/bitrunning_glitch
+ name = "Spawn Bitrunning Glitch"
+ admin_setup = list(
+ /datum/event_admin_setup/minimum_candidate_requirement/bitrunning_glitch,
+ /datum/event_admin_setup/listed_options/bitrunning_glitch,
+ )
+ category = EVENT_CATEGORY_INVASION
+ description = "Causes a short term antagonist to spawn in the virtual domain."
+ dynamic_should_hijack = FALSE
+ max_occurrences = 5
+ min_players = 1
+ typepath = /datum/round_event/ghost_role/bitrunning_glitch
+ weight = 10
+ /// List of active servers to choose from
+ var/list/obj/machinery/quantum_server/active_servers = list()
+ /// List of possible antags to spawn
+ var/static/list/possible_antags = list(
+ ROLE_CYBER_POLICE,
+ )
+
+/datum/round_event_control/bitrunning_glitch/can_spawn_event(players_amt, allow_magic = FALSE)
+ . = ..()
+ if(!.)
+ return .
+
+ active_servers.Cut()
+
+ get_active_servers()
+
+ if(length(active_servers))
+ return TRUE
+
+/// All servers currently running, has players in it, and map has valid mobs
+/datum/round_event_control/bitrunning_glitch/proc/get_active_servers()
+ for(var/obj/machinery/quantum_server/server in SSmachines.get_machines_by_type(/obj/machinery/quantum_server))
+ if(length(server.get_valid_domain_targets()))
+ active_servers.Add(server)
+
+ return length(active_servers) > 0
+
+/datum/event_admin_setup/listed_options/bitrunning_glitch
+ input_text = "Select a role to spawn."
+
+/datum/event_admin_setup/listed_options/bitrunning_glitch/get_list()
+ var/datum/round_event_control/bitrunning_glitch/control = event_control
+
+ var/list/possible = control.possible_antags.Copy() // this seems pedantic but byond is complaining control was unused
+
+ possible += list("Random")
+
+ return possible
+
+/datum/event_admin_setup/listed_options/bitrunning_glitch/apply_to_event(datum/round_event/ghost_role/bitrunning_glitch/event)
+ if(chosen == "Random")
+ event.forced_role = null
+ else
+ event.forced_role = chosen
+
+/datum/event_admin_setup/minimum_candidate_requirement/bitrunning_glitch
+ output_text = "There must be valid mobs to mutate or players in the domain!"
+
+/datum/event_admin_setup/minimum_candidate_requirement/bitrunning_glitch/count_candidates()
+ var/datum/round_event_control/bitrunning_glitch/cyber_control = event_control
+ cyber_control.get_active_servers()
+
+ var/total = 0
+ for(var/obj/machinery/quantum_server/server in cyber_control.active_servers)
+ total += length(server.mutation_candidate_refs)
+
+ return total
+
+/datum/round_event/ghost_role/bitrunning_glitch
+ minimum_required = 1
+ role_name = "Bitrunning Glitch"
+ fakeable = FALSE
+ /// Admin customization: What to spawn
+ var/forced_role
+
+/datum/round_event/ghost_role/bitrunning_glitch/spawn_role()
+ var/datum/round_event_control/bitrunning_glitch/cyber_control = control
+
+ var/obj/machinery/quantum_server/unlucky_server = pick(cyber_control.active_servers)
+ cyber_control.active_servers.Cut()
+
+ var/list/mutation_candidates = unlucky_server.get_valid_domain_targets()
+ if(!length(mutation_candidates))
+ return MAP_ERROR
+
+ var/chosen = pick(mutation_candidates)
+ if(isnull(chosen) || !length(mutation_candidates))
+ return MAP_ERROR
+
+ var/datum/weakref/target_ref = pick(mutation_candidates)
+ var/mob/living/mutation_target = target_ref.resolve()
+
+ if(isnull(mutation_target)) // just in case since it takes a minute
+ target_ref = pick(mutation_candidates)
+ mutation_target = target_ref.resolve()
+ if(isnull(mutation_target))
+ return MAP_ERROR
+
+ var/chosen_role = forced_role || pick(cyber_control.possible_antags)
+
+ var/datum/mind/ghost_mind = get_ghost_mind(chosen_role)
+ if(isnull(ghost_mind))
+ return NOT_ENOUGH_PLAYERS
+
+ var/mob/living/antag_mob
+ switch(chosen_role)
+ if(ROLE_CYBER_POLICE)
+ antag_mob = spawn_cybercop(mutation_target, ghost_mind)
+
+ playsound(antag_mob, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
+ message_admins("[ADMIN_LOOKUPFLW(antag_mob)] has been made into virtual antagonist by an event.")
+ antag_mob.log_message("was spawned as a virtual antagonist by an event.", LOG_GAME)
+
+ SEND_SIGNAL(unlucky_server, COMSIG_BITRUNNER_SPAWN_GLITCH, antag_mob)
+
+ spawned_mobs += antag_mob
+
+ return SUCCESSFUL_SPAWN
+
+/// Polls for a ghost that wants to run it
+/datum/round_event/ghost_role/bitrunning_glitch/proc/get_ghost_mind(role_name)
+ var/list/mob/dead/observer/ghosties = poll_ghost_candidates("A short term antagonist role is available. Would you like to spawn as a '[role_name]'?", role_name)
+
+ if(!length(ghosties))
+ return
+
+ shuffle_inplace(ghosties)
+
+ var/mob/dead/selected = pick(ghosties)
+
+ var/datum/mind/player_mind = new /datum/mind(selected.key)
+ player_mind.active = TRUE
+
+ return player_mind
+
+/// Spawns a cybercop on the mutation target
+/datum/round_event/ghost_role/bitrunning_glitch/proc/spawn_cybercop(mob/living/mutation_target, datum/mind/player_mind)
+ var/mob/living/carbon/human/new_agent = new(mutation_target.loc)
+ mutation_target.gib()
+ mutation_target = null
+
+ player_mind.transfer_to(new_agent)
+ player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/cyber_police))
+ player_mind.special_role = ROLE_CYBER_POLICE
+ player_mind.add_antag_datum(/datum/antagonist/cyber_police)
+
+ return new_agent
+
diff --git a/code/modules/bitrunning/job.dm b/code/modules/bitrunning/job.dm
new file mode 100644
index 00000000000..57581753c0f
--- /dev/null
+++ b/code/modules/bitrunning/job.dm
@@ -0,0 +1,41 @@
+/datum/job/bitrunner
+ title = JOB_BITRUNNER
+ description = "Surf the virtual domain for gear and loot. Decrypt your rewards on station."
+ department_head = list(JOB_QUARTERMASTER)
+ faction = FACTION_STATION
+ total_positions = 3
+ spawn_positions = 3
+ supervisors = SUPERVISOR_QM
+ exp_granted_type = EXP_TYPE_CREW
+ config_tag = "BITRUNNER"
+ outfit = /datum/outfit/job/bitrunner
+ plasmaman_outfit = /datum/outfit/plasmaman/bitrunner
+ paycheck = PAYCHECK_CREW
+ paycheck_department = ACCOUNT_CAR
+ display_order = JOB_DISPLAY_ORDER_BITRUNNER
+ bounty_types = CIV_JOB_RANDOM
+ departments_list = list(
+ /datum/job_department/cargo,
+ )
+
+ family_heirlooms = list(/obj/item/reagent_containers/cup/soda_cans/space_mountain_wind)
+
+ mail_goodies = list(
+ /obj/item/food/cornchips = 1,
+ /obj/item/reagent_containers/cup/soda_cans/space_mountain_wind = 1,
+ /obj/item/food/cornchips/green = 1,
+ /obj/item/food/cornchips/red = 1,
+ /obj/item/food/cornchips/purple = 1,
+ /obj/item/food/cornchips/blue = 1,
+ )
+ rpg_title = "Recluse"
+ job_flags = JOB_ANNOUNCE_ARRIVAL | JOB_CREW_MANIFEST | JOB_EQUIP_RANK | JOB_CREW_MEMBER | JOB_NEW_PLAYER_JOINABLE | JOB_REOPEN_ON_ROUNDSTART_LOSS | JOB_ASSIGN_QUIRKS | JOB_CAN_BE_INTERN
+
+/datum/outfit/job/bitrunner
+ name = "Bitrunner"
+ jobtype = /datum/job/bitrunner
+
+ id_trim = /datum/id_trim/job/bitrunner
+ uniform = /obj/item/clothing/under/rank/cargo/bitrunner
+ belt = /obj/item/modular_computer/pda/bitrunner
+ ears = /obj/item/radio/headset/headset_cargo
diff --git a/code/modules/bitrunning/objects/bit_vendor.dm b/code/modules/bitrunning/objects/bit_vendor.dm
new file mode 100644
index 00000000000..abd63a9e784
--- /dev/null
+++ b/code/modules/bitrunning/objects/bit_vendor.dm
@@ -0,0 +1,86 @@
+#define CREDIT_TYPE_BITRUNNING "np"
+
+/obj/machinery/computer/order_console/bitrunning
+ name = "bitrunning supplies order console"
+ desc = "NexaCache(tm)! Dubiously authentic gear for the digital daredevil."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ icon_state = "vendor"
+ icon_keyboard = null
+ icon_screen = null
+ circuit = /obj/item/circuitboard/computer/order_console/bitrunning
+ cooldown_time = 10 SECONDS
+ cargo_cost_multiplier = 0.65
+ express_cost_multiplier = 1
+ purchase_tooltip = @{"Your purchases will arrive at cargo,
+ and hopefully get delivered by them.
+ 35% cheaper than express delivery."}
+ express_tooltip = @{"Sends your purchases instantly."}
+ credit_type = CREDIT_TYPE_BITRUNNING
+
+ order_categories = list(
+ CATEGORY_BITRUNNING_FLAIR,
+ CATEGORY_BITRUNNING_TECH,
+ CATEGORY_BEPIS,
+ )
+ blackbox_key = "bitrunning"
+
+/obj/machinery/computer/order_console/bitrunning/subtract_points(final_cost, obj/item/card/id/card)
+ if(final_cost <= card.registered_account.bitrunning_points)
+ card.registered_account.bitrunning_points -= final_cost
+ return TRUE
+ return FALSE
+
+/obj/machinery/computer/order_console/bitrunning/order_groceries(mob/living/purchaser, obj/item/card/id/card, list/groceries)
+ var/list/things_to_order = list()
+ for(var/datum/orderable_item/item as anything in groceries)
+ things_to_order[item.item_path] = groceries[item]
+
+ var/datum/supply_pack/bitrunning/pack = new(
+ purchaser = purchaser, \
+ cost = get_total_cost(), \
+ contains = things_to_order,
+ )
+
+ var/datum/supply_order/new_order = new(
+ pack = pack,
+ orderer = purchaser,
+ orderer_rank = "Bitrunning Vendor",
+ orderer_ckey = purchaser.ckey,
+ reason = "",
+ paying_account = card.registered_account,
+ department_destination = null,
+ coupon = null,
+ charge_on_purchase = FALSE,
+ manifest_can_fail = FALSE,
+ cost_type = credit_type,
+ can_be_cancelled = FALSE,
+ )
+ say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
+ radio.talk_into(src, "A bitrunner has ordered equipment which will arrive on the cargo shuttle! Please make sure it gets to them as soon as possible!", radio_channel)
+ SSshuttle.shopping_list += new_order
+
+/obj/machinery/computer/order_console/bitrunning/retrieve_points(obj/item/card/id/id_card)
+ return round(id_card.registered_account.bitrunning_points)
+
+/obj/machinery/computer/order_console/bitrunning/ui_act(action, params)
+ . = ..()
+ if(!.)
+ flick("vendor_off", src)
+
+/obj/machinery/computer/order_console/bitrunning/update_icon_state()
+ icon_state = "[initial(icon_state)][powered() ? null : "_off"]"
+ return ..()
+
+/datum/supply_pack/bitrunning
+ name = "bitrunning order"
+ hidden = TRUE
+ crate_name = "bitrunning delivery crate"
+ access = list(ACCESS_BIT_DEN)
+
+/datum/supply_pack/bitrunning/New(purchaser, cost, list/contains)
+ . = ..()
+ name = "[purchaser]'s Bitrunning Order"
+ src.cost = cost
+ src.contains = contains
+
+#undef CREDIT_TYPE_BITRUNNING
diff --git a/code/modules/bitrunning/objects/clothing.dm b/code/modules/bitrunning/objects/clothing.dm
new file mode 100644
index 00000000000..4d2d9cc55c4
--- /dev/null
+++ b/code/modules/bitrunning/objects/clothing.dm
@@ -0,0 +1,9 @@
+/obj/item/clothing/glasses/sunglasses/oval
+ name = "oval sunglasses"
+ desc = "Vintage wrap around sunglasses. Provides a little protection."
+ icon_state = "jensenshades"
+
+/obj/item/clothing/suit/jacket/trenchcoat
+ name = "trenchcoat"
+ desc = "A long, black trenchcoat. Makes you feel like you're the one, but you're not."
+ icon_state = "trenchcoat"
diff --git a/code/modules/bitrunning/objects/disks.dm b/code/modules/bitrunning/objects/disks.dm
new file mode 100644
index 00000000000..4698b7a1ec1
--- /dev/null
+++ b/code/modules/bitrunning/objects/disks.dm
@@ -0,0 +1,146 @@
+/**
+ * Bitrunning tech disks which let you load items or programs into the vdom on first avatar generation.
+ * For the record: Balance shouldn't be a primary concern.
+ * You can make the custom cheese spells you've always wanted.
+ * Just make it fun and engaging, it's PvE content.
+ */
+/obj/item/bitrunning_disk
+ name = "generic bitrunning program"
+ desc = "A disk containing source code."
+ icon = 'icons/obj/assemblies/module.dmi'
+ base_icon_state = "datadisk"
+ icon_state = "datadisk0"
+ /// Name of the choice made
+ var/choice_made
+
+/obj/item/bitrunning_disk/Initialize(mapload)
+ . = ..()
+
+ icon_state = "[base_icon_state][rand(0, 7)]"
+ update_icon()
+ RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+
+/obj/item/bitrunning_disk/proc/on_examined(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_infoplain("This disk must be carried on your person into a netpod to be used.")
+
+ if(isnull(choice_made))
+ examine_text += span_notice("To make a selection, toggle the disk in hand.")
+ return
+
+ examine_text += span_info("It has been used to select: [choice_made].")
+ examine_text += span_notice("It cannot make another selection.")
+
+/obj/item/bitrunning_disk/ability
+ desc = "A disk containing source code. It can be used to preload abilities into the virtual domain."
+ /// The selected ability that this grants
+ var/datum/action/granted_action
+ /// The list of actions that this can grant
+ var/list/datum/action/selectable_actions = list()
+
+/obj/item/bitrunning_disk/ability/attack_self(mob/user, modifiers)
+ . = ..()
+
+ if(choice_made)
+ return
+
+ var/names = list()
+ for(var/datum/action/thing as anything in selectable_actions)
+ names += initial(thing.name)
+
+ var/choice = tgui_input_list(user, message = "Select an ability", title = "Bitrunning Program", items = names)
+ if(isnull(choice))
+ return
+
+ for(var/datum/action/thing as anything in selectable_actions)
+ if(initial(thing.name) == choice)
+ granted_action = thing
+
+ if(isnull(granted_action))
+ return
+
+ balloon_alert(user, "selected")
+ playsound(user, 'sound/items/click.ogg', 50, TRUE)
+ choice_made = choice
+
+/// Tier 1 programs. Simple, funny, or helpful.
+/obj/item/bitrunning_disk/ability/tier1
+ name = "bitrunning program: basic"
+ selectable_actions = list(
+ /datum/action/cooldown/spell/conjure/cheese,
+ /datum/action/cooldown/spell/basic_heal,
+ )
+
+/// Tier 2 programs. More complex, powerful, or useful.
+/obj/item/bitrunning_disk/ability/tier2
+ name = "bitrunning program: complex"
+ selectable_actions = list(
+ /datum/action/cooldown/spell/pointed/projectile/fireball,
+ /datum/action/cooldown/spell/pointed/projectile/lightningbolt,
+ /datum/action/cooldown/spell/forcewall,
+ )
+
+/// Tier 3 abilities. Very powerful, game breaking.
+/obj/item/bitrunning_disk/ability/tier3
+ name = "bitrunning program: elite"
+ selectable_actions = list(
+ /datum/action/cooldown/spell/shapeshift/dragon,
+ /datum/action/cooldown/spell/shapeshift/polar_bear,
+ )
+
+/obj/item/bitrunning_disk/item
+ desc = "A disk containing source code. It can be used to preload items into the virtual domain."
+ /// The selected item that this grants
+ var/obj/granted_item
+ /// The list of actions that this can grant
+ var/list/obj/selectable_items = list()
+
+/obj/item/bitrunning_disk/item/attack_self(mob/user, modifiers)
+ . = ..()
+
+ if(choice_made)
+ return
+
+ var/names = list()
+ for(var/obj/thing as anything in selectable_items)
+ names += initial(thing.name)
+
+ var/choice = tgui_input_list(user, message = "Select an ability", title = "Bitrunning Program", items = names)
+ if(isnull(choice))
+ return
+
+ for(var/obj/thing as anything in selectable_items)
+ if(initial(thing.name) == choice)
+ granted_item = thing
+
+ balloon_alert(user, "selected")
+ playsound(user, 'sound/items/click.ogg', 50, TRUE)
+ choice_made = choice
+
+/// Tier 1 items. Simple, funny, or helpful.
+/obj/item/bitrunning_disk/item/tier1
+ name = "bitrunning gear: simple"
+ selectable_items = list(
+ /obj/item/pizzabox/infinite,
+ /obj/item/gun/medbeam,
+ /obj/item/grenade/c4,
+ )
+
+/// Tier 2 items. More complex, powerful, or useful.
+/obj/item/bitrunning_disk/item/tier2
+ name = "bitrunning gear: complex"
+ selectable_items = list(
+ /obj/item/chainsaw,
+ /obj/item/gun/ballistic/automatic/pistol,
+ /obj/item/melee/energy/blade/hardlight,
+ )
+
+/// Tier 3 items. Very powerful, game breaking.
+/obj/item/bitrunning_disk/item/tier3
+ name = "bitrunning gear: advanced"
+ selectable_items = list(
+ /obj/item/gun/energy/tesla_cannon,
+ /obj/item/dualsaber/green,
+ /obj/item/melee/beesword,
+ )
diff --git a/code/modules/bitrunning/objects/hololadder.dm b/code/modules/bitrunning/objects/hololadder.dm
new file mode 100644
index 00000000000..906801f1fc0
--- /dev/null
+++ b/code/modules/bitrunning/objects/hololadder.dm
@@ -0,0 +1,51 @@
+/obj/structure/hololadder
+ name = "hololadder"
+
+ anchored = TRUE
+ desc = "An abstract representation of the means to disconnect from the virtual domain."
+ icon = 'icons/obj/structures.dmi'
+ icon_state = "ladder11"
+ obj_flags = BLOCK_Z_OUT_DOWN
+ /// Time req to disconnect properly
+ var/travel_time = 3 SECONDS
+
+/obj/structure/hololadder/Initialize(mapload)
+ . = ..()
+
+ RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_enter))
+
+/obj/structure/hololadder/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+
+ if(!in_range(src, user) || DOING_INTERACTION(user, DOAFTER_SOURCE_CLIMBING_LADDER))
+ return
+
+ disconnect(user)
+
+/// If there's a pilot ref- send the disconnect signal
+/obj/structure/hololadder/proc/disconnect(mob/user)
+ if(isnull(user.mind))
+ return
+
+ if(!HAS_TRAIT(user, TRAIT_TEMPORARY_BODY))
+ balloon_alert(user, "no connection detected.")
+ return
+
+ balloon_alert(user, "disconnecting...")
+ if(do_after(user, travel_time, src))
+ SEND_SIGNAL(user, COMSIG_BITRUNNER_SAFE_DISCONNECT)
+
+/// Helper for times when you dont have hands (gondola??)
+/obj/structure/hololadder/proc/on_enter(datum/source, atom/movable/arrived, turf/old_loc)
+ SIGNAL_HANDLER
+
+ if(!isliving(arrived))
+ return
+
+ var/mob/living/user = arrived
+ if(isnull(user.mind))
+ return
+
+ INVOKE_ASYNC(src, PROC_REF(disconnect), user)
diff --git a/code/modules/bitrunning/objects/host_monitor.dm b/code/modules/bitrunning/objects/host_monitor.dm
new file mode 100644
index 00000000000..f59ca61cbd0
--- /dev/null
+++ b/code/modules/bitrunning/objects/host_monitor.dm
@@ -0,0 +1,33 @@
+/obj/item/bitrunning_host_monitor
+ name = "host monitor"
+
+ custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2)
+ desc = "A complex medical device that, when attached to an avatar's data stream, can detect the user of their host's health."
+ flags_1 = CONDUCT_1
+ icon = 'icons/obj/device.dmi'
+ icon_state = "gps-b"
+ inhand_icon_state = "electronic"
+ item_flags = NOBLUDGEON
+ lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
+ slot_flags = ITEM_SLOT_BELT
+ throw_range = 7
+ throw_speed = 3
+ throwforce = 3
+ w_class = WEIGHT_CLASS_TINY
+ worn_icon_state = "electronic"
+
+/obj/item/bitrunning_host_monitor/attack_self(mob/user, modifiers)
+ . = ..()
+
+ var/datum/component/avatar_connection/connection = user.GetComponent(/datum/component/avatar_connection)
+ if(isnull(connection))
+ balloon_alert(user, "data not recognized")
+ return
+
+ var/mob/living/pilot = connection.old_body_ref?.resolve()
+ if(isnull(pilot))
+ balloon_alert(user, "host not recognized")
+ return
+
+ to_chat(user, span_notice("Current host health: [pilot.health / pilot.maxHealth * 100]%"))
diff --git a/code/modules/bitrunning/objects/landmarks.dm b/code/modules/bitrunning/objects/landmarks.dm
new file mode 100644
index 00000000000..d78283c6a8b
--- /dev/null
+++ b/code/modules/bitrunning/objects/landmarks.dm
@@ -0,0 +1,70 @@
+/obj/effect/landmark/bitrunning
+ name = "Generic bitrunning effect"
+ icon = 'icons/effects/bitrunning.dmi'
+ icon_state = "crate"
+
+/// In case you want to gate the crate behind a special condition.
+/obj/effect/landmark/bitrunning/loot_signal
+ name = "Mysterious aura"
+ /// The amount required to spawn a crate
+ var/points_goal = 10
+ /// A special condition limits this from spawning a crate
+ var/points_received = 0
+ /// Finished the special condition
+ var/revealed = FALSE
+
+/obj/effect/landmark/bitrunning/loot_signal/Initialize(mapload)
+ . = ..()
+
+ RegisterSignal(src, COMSIG_BITRUNNER_GOAL_POINT, PROC_REF(on_add_point))
+
+/// Listens for points to be added which will eventually spawn a crate.
+/obj/effect/landmark/bitrunning/loot_signal/proc/on_add_point(datum/source, points_to_add)
+ SIGNAL_HANDLER
+
+ if(revealed)
+ return
+
+ points_received += points_to_add
+
+ if(points_received < points_goal)
+ return
+
+ reveal()
+
+/// Spawns the crate with some effects
+/obj/effect/landmark/bitrunning/loot_signal/proc/reveal()
+ playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
+
+ var/turf/tile = get_turf(src)
+ var/obj/structure/closet/crate/secure/bitrunning/encrypted/loot = new(tile)
+ var/datum/effect_system/spark_spread/quantum/sparks = new(tile)
+ sparks.set_up(5, 1, get_turf(loot))
+ sparks.start()
+
+ qdel(src)
+
+/// Where the 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"
+ icon_state = "hololadder"
+
+/// Where the crates need to be taken
+/obj/effect/landmark/bitrunning/cache_goal_turf
+ name = "Bitrunning goal turf"
+ icon_state = "goal"
+
+/// Where you want the crate to spawn
+/obj/effect/landmark/bitrunning/cache_spawn
+ name = "Bitrunning crate spawn"
+ icon_state = "spawn"
+
+/// Where the safehouse will spawn
+/obj/effect/landmark/bitrunning/safehouse_spawn
+ name = "Bitrunning safehouse spawn"
+ icon_state = "safehouse"
diff --git a/code/modules/bitrunning/objects/loot_crate.dm b/code/modules/bitrunning/objects/loot_crate.dm
new file mode 100644
index 00000000000..5af8c0d9477
--- /dev/null
+++ b/code/modules/bitrunning/objects/loot_crate.dm
@@ -0,0 +1,91 @@
+#define ORE_MULTIPLIER_IRON 3
+#define ORE_MULTIPLIER_GLASS 2
+#define ORE_MULTIPLIER_PLASMA 1
+#define ORE_MULTIPLIER_SILVER 0.7
+#define ORE_MULTIPLIER_GOLD 0.6
+#define ORE_MULTIPLIER_TITANIUM 0.5
+#define ORE_MULTIPLIER_URANIUM 0.4
+#define ORE_MULTIPLIER_DIAMOND 0.3
+#define ORE_MULTIPLIER_BLUESPACE_CRYSTAL 0.2
+
+/obj/structure/closet/crate/secure/bitrunning // Base class. Do not spawn this.
+ name = "base class cache"
+ desc = "Talk to a coder."
+
+/// The virtual domain - side of the bitrunning crate. Deliver to the send location.
+/obj/structure/closet/crate/secure/bitrunning/encrypted
+ name = "encrypted cache"
+ desc = "Needs decrypted at the safehouse to be opened."
+ locked = TRUE
+
+/obj/structure/closet/crate/secure/bitrunning/encrypted/can_unlock(mob/living/user, obj/item/card/id/player_id, obj/item/card/id/registered_id)
+ return FALSE
+
+/// The bitrunner den - side of the bitrunning crate. Appears in the receive location.
+/obj/structure/closet/crate/secure/bitrunning/decrypted
+ name = "decrypted cache"
+ desc = "Compiled from the virtual domain. The reward of a successful bitrunner."
+ locked = FALSE
+
+/obj/structure/closet/crate/secure/bitrunning/decrypted/Initialize(
+ mapload,
+ datum/lazy_template/virtual_domain/completed_domain,
+ rewards_multiplier = 1,
+ )
+ . = ..()
+ playsound(src, 'sound/magic/blink.ogg', 50, TRUE)
+
+ if(isnull(completed_domain))
+ return
+
+ PopulateContents(completed_domain.reward_points, completed_domain.extra_loot, rewards_multiplier)
+
+/obj/structure/closet/crate/secure/bitrunning/decrypted/PopulateContents(reward_points, list/extra_loot, rewards_multiplier)
+ . = ..()
+ spawn_loot(extra_loot)
+
+ new /obj/item/stack/ore/iron(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_IRON))
+ new /obj/item/stack/ore/glass(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_GLASS))
+
+ if(reward_points > 1)
+ new /obj/item/stack/ore/silver(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_SILVER))
+ new /obj/item/stack/ore/titanium(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_TITANIUM))
+
+ if(reward_points > 2)
+ new /obj/item/stack/ore/plasma(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_PLASMA))
+ new /obj/item/stack/ore/gold(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_GOLD))
+ new /obj/item/stack/ore/uranium(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_URANIUM))
+
+ if(reward_points > 3)
+ new /obj/item/stack/ore/diamond(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_DIAMOND))
+ new /obj/item/stack/ore/bluespace_crystal(src, calculate_loot(reward_points, rewards_multiplier, ORE_MULTIPLIER_BLUESPACE_CRYSTAL))
+
+/// Handles generating random numbers & calculating loot totals
+/obj/structure/closet/crate/secure/bitrunning/decrypted/proc/calculate_loot(reward_points, rewards_multiplier, ore_multiplier)
+ var/base = rewards_multiplier + reward_points
+ var/random_sum = (rand() + 0.5) * base
+ return ROUND_UP(random_sum * ore_multiplier)
+
+/// Handles spawning extra loot. This tries to handle bad flat and assoc lists
+/obj/structure/closet/crate/secure/bitrunning/decrypted/proc/spawn_loot(list/extra_loot)
+ for(var/path in extra_loot)
+ if(!ispath(path))
+ continue
+
+ if(isnull(extra_loot[path]))
+ return FALSE
+
+ for(var/i in 1 to extra_loot[path])
+ new path(src)
+
+ return TRUE
+
+#undef ORE_MULTIPLIER_IRON
+#undef ORE_MULTIPLIER_GLASS
+#undef ORE_MULTIPLIER_PLASMA
+#undef ORE_MULTIPLIER_SILVER
+#undef ORE_MULTIPLIER_GOLD
+#undef ORE_MULTIPLIER_TITANIUM
+#undef ORE_MULTIPLIER_URANIUM
+#undef ORE_MULTIPLIER_DIAMOND
+#undef ORE_MULTIPLIER_BLUESPACE_CRYSTAL
diff --git a/code/modules/bitrunning/objects/netpod.dm b/code/modules/bitrunning/objects/netpod.dm
new file mode 100644
index 00000000000..33d468a3825
--- /dev/null
+++ b/code/modules/bitrunning/objects/netpod.dm
@@ -0,0 +1,478 @@
+#define BASE_DISCONNECT_DAMAGE 40
+
+/obj/machinery/netpod
+ name = "netpod"
+
+ base_icon_state = "netpod"
+ circuit = /obj/item/circuitboard/machine/netpod
+ desc = "A link to the netverse. It has an assortment of cables to connect yourself to a virtual domain."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ icon_state = "netpod"
+ max_integrity = 300
+ obj_flags = BLOCKS_CONSTRUCTION
+ state_open = TRUE
+ /// Whether we have an ongoing connection
+ var/connected = FALSE
+ /// A player selected outfit by clicking the netpod
+ var/datum/outfit/netsuit = /datum/outfit/job/bitrunner
+ /// Holds this to see if it needs to generate a new one
+ var/datum/weakref/avatar_ref
+ /// The linked quantum server
+ var/datum/weakref/server_ref
+ /// The amount of brain damage done from force disconnects
+ var/disconnect_damage
+ /// Static list of outfits to select from
+ var/list/cached_outfits = list()
+
+/obj/machinery/netpod/Initialize(mapload)
+ . = ..()
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/netpod/LateInitialize()
+ . = ..()
+
+ disconnect_damage = BASE_DISCONNECT_DAMAGE
+ find_server()
+
+ RegisterSignals(src, list(
+ COMSIG_QDELETING,
+ COMSIG_MACHINERY_BROKEN,
+ COMSIG_MACHINERY_POWER_LOST,
+ ),
+ PROC_REF(on_broken),
+ )
+ RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(src, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(on_take_damage))
+
+ register_context()
+ update_appearance()
+
+/obj/machinery/netpod/Destroy()
+ . = ..()
+ cached_outfits.Cut()
+
+/obj/machinery/netpod/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+
+ if(isnull(held_item))
+ context[SCREENTIP_CONTEXT_LMB] = "Select Outfit"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ if(istype(held_item, /obj/item/crowbar) && occupant)
+ context[SCREENTIP_CONTEXT_LMB] = "Pry Open"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/netpod/update_icon_state()
+ if(!is_operational)
+ icon_state = base_icon_state
+ return ..()
+
+ if(state_open)
+ icon_state = base_icon_state + "_open_active"
+ return ..()
+
+ if(panel_open)
+ icon_state = base_icon_state + "_panel"
+ return ..()
+
+ icon_state = base_icon_state + "_closed"
+ if(occupant)
+ icon_state += "_active"
+
+ return ..()
+
+/obj/machinery/netpod/MouseDrop_T(mob/target, mob/user)
+ var/mob/living/carbon/player = user
+ if(!iscarbon(player))
+ return
+
+ if((HAS_TRAIT(player, TRAIT_UI_BLOCKED) && !player.resting) || !Adjacent(player) || !player.Adjacent(target) || !ISADVANCEDTOOLUSER(player) || !is_operational)
+ return
+
+ close_machine(target)
+
+/obj/machinery/netpod/crowbar_act(mob/living/user, obj/item/tool)
+ if(user.combat_mode)
+ attack_hand(user)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ if(default_pry_open(tool, user) || default_deconstruction_crowbar(tool))
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/netpod/screwdriver_act(mob/living/user, obj/item/tool)
+ if(occupant)
+ balloon_alert(user, "in use!")
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ if(state_open)
+ balloon_alert(user, "close first.")
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]_panel", "[base_icon_state]_closed", tool))
+ update_appearance() // sometimes icon doesnt properly update during flick()
+ ui_close(user)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/netpod/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(!state_open && user == occupant)
+ container_resist_act(user)
+
+/obj/machinery/netpod/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(!state_open && gone == occupant)
+ container_resist_act(gone)
+
+/obj/machinery/netpod/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(!state_open && gone == occupant)
+ container_resist_act(gone)
+
+/obj/machinery/netpod/relaymove(mob/living/user, direction)
+ if(!state_open)
+ container_resist_act(user)
+
+/obj/machinery/netpod/container_resist_act(mob/living/user)
+ user.visible_message(span_notice("[occupant] emerges from [src]!"),
+ span_notice("You climb out of [src]!"),
+ span_notice("With a hiss, you hear a machine opening."))
+ open_machine()
+
+/obj/machinery/netpod/open_machine(drop = TRUE, density_to_set = FALSE)
+ unprotect_and_signal()
+ playsound(src, 'sound/machines/tramopen.ogg', 60, TRUE, frequency = 65000)
+ flick("[base_icon_state]_opening", src)
+
+ return ..()
+
+/obj/machinery/netpod/close_machine(mob/user, density_to_set = TRUE)
+ if(!state_open || panel_open || !is_operational || !iscarbon(user))
+ return
+
+ playsound(src, 'sound/machines/tramclose.ogg', 60, TRUE, frequency = 65000)
+ flick("[base_icon_state]_closing", src)
+ ..()
+
+ if(!iscarbon(occupant))
+ open_machine()
+ return
+
+ enter_matrix()
+
+/obj/machinery/netpod/default_pry_open(obj/item/crowbar, mob/living/pryer)
+ if(isnull(occupant) || !iscarbon(occupant))
+ if(!state_open)
+ if(panel_open)
+ return FALSE
+ open_machine()
+ else
+ shut_pod()
+
+ return TRUE
+
+ pryer.visible_message(
+ span_danger("[pryer] starts prying open [src]!"),
+ span_notice("You start to pry open [src]."),
+ span_notice("You hear loud prying on metal.")
+ )
+ playsound(src, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_CROWBAR_ALERT, pryer)
+
+ if(do_after(pryer, 15 SECONDS, src))
+ if(!state_open)
+ open_machine()
+
+ return TRUE
+
+/obj/machinery/netpod/ui_interact(mob/user, datum/tgui/ui)
+ if(!is_operational)
+ return
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "NetpodOutfits")
+ ui.set_autoupdate(FALSE)
+ ui.open()
+
+/obj/machinery/netpod/ui_data()
+ var/list/data = list()
+
+ data["netsuit"] = netsuit
+ return data
+
+/obj/machinery/netpod/ui_static_data()
+ var/list/data = list()
+
+ if(!length(cached_outfits))
+ cached_outfits += make_outfit_collection("Jobs", subtypesof(/datum/outfit/job))
+
+ data["collections"] = cached_outfits
+
+ return data
+
+/obj/machinery/netpod/ui_act(action, params)
+ . = ..()
+ if(.)
+ return TRUE
+ switch(action)
+ if("select_outfit")
+ var/datum/outfit/new_suit = resolve_outfit(params["outfit"])
+ if(new_suit)
+ netsuit = new_suit
+ return TRUE
+
+ return FALSE
+
+/// Disconnects the occupant after a certain time so they aren't just hibernating in netpod stasis. A balance change
+/obj/machinery/netpod/proc/auto_disconnect()
+ if(isnull(occupant) || state_open || connected)
+ return
+
+ if(!iscarbon(occupant))
+ open_machine()
+ return
+
+ var/mob/living/carbon/player = occupant
+
+ player.playsound_local(src, 'sound/effects/splash.ogg', 60, TRUE)
+ to_chat(player, span_notice("The machine disconnects itself and begins to drain."))
+ open_machine()
+
+/**
+ * ### 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
+ */
+/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)
+ open_machine()
+ return
+
+ mob_occupant.playsound_local(src, "sound/magic/blink.ogg", 25, TRUE)
+ mob_occupant.set_static_vision(2 SECONDS)
+ mob_occupant.set_temp_blindness(1 SECONDS)
+ mob_occupant.Paralyze(2 SECONDS)
+
+ var/heal_time = 1
+ if(mob_occupant.health < mob_occupant.maxHealth)
+ heal_time = (mob_occupant.stat + 2) * 5
+ addtimer(CALLBACK(src, PROC_REF(auto_disconnect)), heal_time SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+
+ if(!forced)
+ return
+
+ mob_occupant.flash_act(override_blindness_check = TRUE, visual = TRUE)
+ mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, disconnect_damage)
+ INVOKE_ASYNC(mob_occupant, TYPE_PROC_REF(/mob/living, emote), "scream")
+ to_chat(mob_occupant, span_danger("You've been forcefully disconnected from your avatar! Your thoughts feel scrambled!"))
+
+/**
+ * ### Enter Matrix
+ * Finds any current avatars from this chair - or generates a new one
+ *
+ * New avatars cost 1 attempt, and this will eject if there's none left
+ *
+ * Connects the mind to the avatar if everything is ok
+ */
+/obj/machinery/netpod/proc/enter_matrix()
+ var/mob/living/carbon/human/neo = occupant
+ if(!ishuman(neo) || neo.stat == DEAD || isnull(neo.mind))
+ balloon_alert(neo, "invalid occupant.")
+ return
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ balloon_alert(neo, "no server connected!")
+ return
+
+ var/datum/lazy_template/virtual_domain/generated_domain = server.generated_domain
+ if(isnull(generated_domain) || !server.is_ready)
+ balloon_alert(neo, "nothing loaded!")
+ return
+
+ var/mob/living/carbon/current_avatar = avatar_ref?.resolve()
+ var/obj/structure/hololadder/wayout
+ if(isnull(current_avatar) || current_avatar.stat != CONSCIOUS) // We need a viable avatar
+ wayout = server.generate_hololadder()
+ if(isnull(wayout))
+ balloon_alert(neo, "out of bandwidth!")
+ return
+ current_avatar = server.generate_avatar(wayout, netsuit)
+ avatar_ref = WEAKREF(current_avatar)
+ server.stock_gear(current_avatar, neo)
+
+ neo.set_static_vision(3 SECONDS)
+ protect_occupant(occupant)
+ if(!do_after(neo, 2 SECONDS, src))
+ return
+
+ // Very invalid
+ if(QDELETED(neo) || QDELETED(current_avatar) || QDELETED(src))
+ return
+
+ // Invalid
+ if(occupant != neo || isnull(neo.mind) || neo.stat == DEAD || current_avatar.stat == DEAD)
+ return
+
+ current_avatar.AddComponent( \
+ /datum/component/avatar_connection, \
+ old_mind = neo.mind, \
+ old_body = neo, \
+ server = server, \
+ pod = src, \
+ help_text = generated_domain.help_text, \
+ )
+
+ connected = TRUE
+
+/// Finds a server and sets the server_ref
+/obj/machinery/netpod/proc/find_server()
+ var/obj/machinery/quantum_server/server = server_ref?.resolve()
+ if(server)
+ return server
+
+ server = locate(/obj/machinery/quantum_server) in oview(4, src)
+ if(isnull(server))
+ return
+
+ server_ref = WEAKREF(server)
+ RegisterSignal(server, COMSIG_BITRUNNER_SERVER_UPGRADED, PROC_REF(on_server_upgraded), override = TRUE)
+ RegisterSignal(server, COMSIG_BITRUNNER_DOMAIN_COMPLETE, PROC_REF(on_domain_complete), override = TRUE)
+
+ return server
+
+/// Creates a list of outfit entries for the UI.
+/obj/machinery/netpod/proc/make_outfit_collection(identifier, list/outfit_list)
+ var/list/collection = list(
+ "name" = identifier,
+ "outfits" = list()
+ )
+
+ for(var/path as anything in outfit_list)
+ var/datum/outfit/outfit = path
+
+ var/outfit_name = initial(outfit.name)
+ if(findtext(outfit_name, "(") != 0 || findtext(outfit_name, "-") != 0) // No special variants please
+ continue
+
+ collection["outfits"] += list(list("path" = path, "name" = outfit_name))
+
+ return list(collection)
+
+/// Machine has been broken - handles signals and reverting sprites
+/obj/machinery/netpod/proc/on_broken(datum/source)
+ SIGNAL_HANDLER
+
+ if(!state_open)
+ open_machine()
+
+ if(occupant)
+ unprotect_and_signal()
+
+/// Puts points on the current occupant's card account
+/obj/machinery/netpod/proc/on_domain_complete(datum/source, atom/movable/crate, reward_points)
+ SIGNAL_HANDLER
+
+ if(isnull(occupant) || !connected || !iscarbon(occupant))
+ return
+
+ var/mob/living/carbon/player = occupant
+
+ var/datum/bank_account/account = player.get_bank_account()
+ if(isnull(account))
+ return
+
+ account.bitrunning_points += reward_points * 100
+
+/obj/machinery/netpod/proc/on_examine(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_infoplain("Drag yourself into the pod to engage the link.")
+ examine_text += span_infoplain("It has limited resuscitation capabilities. Remaining in the pod can heal some injuries.")
+ examine_text += span_infoplain("It has a security system that will alert the occupant if it is tampered with.")
+
+ if(isnull(occupant))
+ examine_text += span_notice("It is currently unoccupied.")
+ return
+
+ examine_text += span_notice("It is currently occupied by [occupant].")
+ examine_text += span_notice("It can be pried open with a crowbar, but its safety mechanisms will alert the occupant.")
+
+
+/// When the server is upgraded, drops brain damage a little
+/obj/machinery/netpod/proc/on_server_upgraded(datum/source, servo_rating)
+ SIGNAL_HANDLER
+
+ disconnect_damage = BASE_DISCONNECT_DAMAGE * (1 - servo_rating)
+
+/// Checks the integrity, alerts occupants
+/obj/machinery/netpod/proc/on_take_damage(datum/source, damage_amount)
+ SIGNAL_HANDLER
+
+ if(isnull(occupant))
+ return
+
+ var/total = max_integrity - damage_amount
+ var/integrity = (atom_integrity / total) * 100
+ if(integrity > 50)
+ return
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_NETPOD_INTEGRITY)
+
+/// Puts the occupant in netpod stasis, basically short-circuiting environmental conditions
+/obj/machinery/netpod/proc/protect_occupant(mob/living/target)
+ if(target != occupant)
+ return
+
+ target.AddComponent(/datum/component/netpod_healing, \
+ brute_heal = 4, \
+ burn_heal = 4, \
+ toxin_heal = 4, \
+ clone_heal = 4, \
+ blood_heal = 4, \
+ )
+
+ target.playsound_local(src, 'sound/effects/submerge.ogg', 20, TRUE)
+ target.extinguish_mob()
+ update_use_power(ACTIVE_POWER_USE)
+
+/// On unbuckle or break, make sure the occupant ref is null
+/obj/machinery/netpod/proc/unprotect_and_signal()
+ unprotect_occupant(occupant)
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+
+/// Removes the occupant from netpod stasis
+/obj/machinery/netpod/proc/unprotect_occupant(mob/living/target)
+ var/datum/component/netpod_healing/healing_eff = target?.GetComponent(/datum/component/netpod_healing)
+ if(healing_eff)
+ qdel(healing_eff)
+
+ update_use_power(IDLE_POWER_USE)
+
+/// Resolves a path to an outfit.
+/obj/machinery/netpod/proc/resolve_outfit(text)
+ var/path = text2path(text)
+ if(ispath(path, /datum/outfit))
+ return path
+
+/// Closes the machine without shoving in an occupant
+/obj/machinery/netpod/proc/shut_pod()
+ state_open = FALSE
+ playsound(src, 'sound/machines/tramclose.ogg', 60, TRUE, frequency = 65000)
+ flick("[base_icon_state]_closing", src)
+ set_density(TRUE)
+
+ update_appearance()
+
+#undef BASE_DISCONNECT_DAMAGE
diff --git a/code/modules/bitrunning/objects/quantum_console.dm b/code/modules/bitrunning/objects/quantum_console.dm
new file mode 100644
index 00000000000..c918648d010
--- /dev/null
+++ b/code/modules/bitrunning/objects/quantum_console.dm
@@ -0,0 +1,108 @@
+/obj/machinery/computer/quantum_console
+ name = "quantum console"
+
+ circuit = /obj/item/circuitboard/computer/quantum_console
+ icon_keyboard = "mining"
+ icon_screen = "bitrunning"
+ req_access = list(ACCESS_MINING)
+ /// The server this console is connected to.
+ var/datum/weakref/server_ref
+
+/obj/machinery/computer/quantum_console/Initialize(mapload, obj/item/circuitboard/circuit)
+ . = ..()
+ desc = "Even in the distant year [CURRENT_STATION_YEAR], Nanostrasen is still using REST APIs. How grim."
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/computer/quantum_console/LateInitialize()
+ . = ..()
+
+ if(isnull(server_ref?.resolve()))
+ find_server()
+
+/obj/machinery/computer/quantum_console/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+
+ if(!is_operational)
+ return
+
+ if(isnull(server_ref?.resolve()))
+ find_server()
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "QuantumConsole")
+ ui.open()
+
+/obj/machinery/computer/quantum_console/ui_data()
+ var/list/data = list()
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ data["connected"] = FALSE
+ return data
+
+ data["connected"] = TRUE
+ data["generated_domain"] = server.generated_domain?.key
+ data["occupants"] = length(server.avatar_connection_refs)
+ data["points"] = server.points
+ data["randomized"] = server.domain_randomized
+ data["ready"] = server.is_ready && server.is_operational
+ data["scanner_tier"] = server.scanner_tier
+ data["retries_left"] = length(server.exit_turfs) - server.retries_spent
+
+ return data
+
+/obj/machinery/computer/quantum_console/ui_static_data(mob/user)
+ var/list/data = list()
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ return data
+
+ data["available_domains"] = server.get_available_domains()
+ data["avatars"] = server.get_avatar_data()
+
+ return data
+
+/obj/machinery/computer/quantum_console/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return TRUE
+
+ var/obj/machinery/quantum_server/server = find_server()
+ if(isnull(server))
+ return FALSE
+
+ switch(action)
+ if("random_domain")
+ var/map_id = server.get_random_domain_id()
+ if(!map_id)
+ return TRUE
+
+ server.cold_boot_map(usr, map_id)
+ return TRUE
+ if("refresh")
+ ui.send_full_update()
+ return TRUE
+ if("set_domain")
+ server.cold_boot_map(usr, params["id"])
+ return TRUE
+ if("stop_domain")
+ server.begin_shutdown(usr)
+ return TRUE
+
+ return FALSE
+
+/// Attempts to find a quantum server.
+/obj/machinery/computer/quantum_console/proc/find_server()
+ var/obj/machinery/quantum_server/server = server_ref?.resolve()
+ if(server)
+ return server
+
+ for(var/direction in GLOB.cardinals)
+ var/obj/machinery/quantum_server/nearby_server = locate(/obj/machinery/quantum_server, get_step(src, direction))
+ if(nearby_server)
+ server_ref = WEAKREF(nearby_server)
+ nearby_server.console_ref = WEAKREF(src)
+ return nearby_server
diff --git a/code/modules/bitrunning/orders/disks.dm b/code/modules/bitrunning/orders/disks.dm
new file mode 100644
index 00000000000..ced1dde883a
--- /dev/null
+++ b/code/modules/bitrunning/orders/disks.dm
@@ -0,0 +1,26 @@
+/datum/orderable_item/bitrunning_tech
+ category_index = CATEGORY_BITRUNNING_TECH
+
+/datum/orderable_item/bitrunning_tech/item_tier1
+ cost_per_order = 1000
+ item_path = /obj/item/bitrunning_disk/item/tier1
+
+/datum/orderable_item/bitrunning_tech/item_tier2
+ cost_per_order = 1500
+ item_path = /obj/item/bitrunning_disk/item/tier2
+
+/datum/orderable_item/bitrunning_tech/item_tier3
+ cost_per_order = 2500
+ item_path = /obj/item/bitrunning_disk/item/tier3
+
+/datum/orderable_item/bitrunning_tech/ability_tier1
+ cost_per_order = 1000
+ item_path = /obj/item/bitrunning_disk/ability/tier1
+
+/datum/orderable_item/bitrunning_tech/ability_tier2
+ cost_per_order = 1800
+ item_path = /obj/item/bitrunning_disk/ability/tier2
+
+/datum/orderable_item/bitrunning_tech/ability_tier3
+ cost_per_order = 3200
+ item_path = /obj/item/bitrunning_disk/ability/tier3
diff --git a/code/modules/bitrunning/orders/flair.dm b/code/modules/bitrunning/orders/flair.dm
new file mode 100644
index 00000000000..ef36348eb6a
--- /dev/null
+++ b/code/modules/bitrunning/orders/flair.dm
@@ -0,0 +1,40 @@
+/datum/orderable_item/bitrunning_flair
+ category_index = CATEGORY_BITRUNNING_FLAIR
+
+/datum/orderable_item/bitrunning_flair/cornchips
+ item_path = /obj/item/food/cornchips
+ cost_per_order = 100
+
+/datum/orderable_item/bitrunning_flair/mountain_wind
+ item_path = /obj/item/reagent_containers/cup/soda_cans/space_mountain_wind
+ cost_per_order = 100
+
+/datum/orderable_item/bitrunning_flair/pwr_game
+ item_path = /obj/item/reagent_containers/cup/soda_cans/pwr_game
+ cost_per_order = 200
+
+/datum/orderable_item/bitrunning_flair/grey_bull
+ item_path = /obj/item/reagent_containers/cup/soda_cans/grey_bull
+ cost_per_order = 200
+
+/datum/orderable_item/bitrunning_flair/medkit
+ item_path = /obj/item/storage/medkit/brute
+ desc = "Don't beat yourself up, it's just a game!"
+ cost_per_order = 500
+
+/datum/orderable_item/bitrunning_flair/medkit_fire
+ item_path = /obj/item/storage/medkit/fire
+ desc = "Great after heated gaming sessions."
+ cost_per_order = 500
+
+/datum/orderable_item/bitrunning_flair/oval_sunglasses
+ item_path = /obj/item/clothing/glasses/sunglasses/oval
+ cost_per_order = 1000
+
+/datum/orderable_item/bitrunning_flair/trenchcoat
+ item_path = /obj/item/clothing/suit/jacket/trenchcoat
+ cost_per_order = 1000
+
+/datum/orderable_item/bitrunning_flair/jackboots
+ item_path = /obj/item/clothing/shoes/jackboots
+ cost_per_order = 1000
diff --git a/code/modules/bitrunning/orders/tech.dm b/code/modules/bitrunning/orders/tech.dm
new file mode 100644
index 00000000000..286e9817f3c
--- /dev/null
+++ b/code/modules/bitrunning/orders/tech.dm
@@ -0,0 +1,23 @@
+/datum/orderable_item/bepis
+ category_index = CATEGORY_BEPIS
+
+/datum/orderable_item/bepis/circuit_stack
+ item_path = /obj/item/stack/circuit_stack/full
+ cost_per_order = 150
+
+/datum/orderable_item/bepis/survival_pen
+ item_path = /obj/item/pen/survival
+ cost_per_order = 150
+
+/datum/orderable_item/bepis/party_sleeper
+ item_path = /obj/item/circuitboard/machine/sleeper/party
+ cost_per_order = 750
+ desc = "A decommissioned sleeper circuitboard, repurposed for recreational purposes."
+
+/datum/orderable_item/bepis/sprayoncan
+ item_path = /obj/item/toy/sprayoncan
+ cost_per_order = 750
+
+/datum/orderable_item/bepis/pristine
+ item_path = /obj/item/disk/design_disk/bepis/remove_tech
+ cost_per_order = 1000
diff --git a/code/modules/bitrunning/server/loot.dm b/code/modules/bitrunning/server/loot.dm
new file mode 100644
index 00000000000..29b730aae78
--- /dev/null
+++ b/code/modules/bitrunning/server/loot.dm
@@ -0,0 +1,123 @@
+/// Handles calculating rewards based on number of players, parts, threats, etc
+/obj/machinery/quantum_server/proc/calculate_rewards()
+ var/rewards_base = 0.8
+
+ if(domain_randomized)
+ rewards_base += 0.2
+
+ rewards_base += servo_bonus
+
+ rewards_base += (domain_threats * 2)
+
+ for(var/index in 2 to length(avatar_connection_refs))
+ rewards_base += multiplayer_bonus
+
+ return rewards_base
+
+/// Generates a reward based on the given domain
+/obj/machinery/quantum_server/proc/generate_loot()
+ if(!length(receive_turfs) && !locate_receive_turfs())
+ 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))
+ stack_trace("Failed to find a turf to spawn loot crate on.")
+ return FALSE
+
+ var/bonus = calculate_rewards()
+
+ var/obj/item/paper/certificate = new()
+ certificate.add_raw_text(get_completion_certificate())
+ certificate.name = "certificate of domain completion"
+ certificate.update_appearance()
+
+ var/obj/structure/closet/crate/secure/bitrunning/decrypted/reward_crate = new(dest_turf, generated_domain, bonus)
+ reward_crate.manifest = certificate
+ reward_crate.update_appearance()
+
+ spark_at_location(reward_crate)
+ return TRUE
+
+/// Returns the markdown text containing domain completion information
+/obj/machinery/quantum_server/proc/get_completion_certificate()
+ var/base_points = generated_domain.reward_points
+ if(domain_randomized)
+ base_points -= 1
+
+ var/bonuses = calculate_rewards()
+
+ var/time_difference = world.time - generated_domain.start_time
+
+ var/completion_time = "### Completion Time: [DisplayTimeText(time_difference)]\n"
+
+ var/grade = "\n---\n\n# Rating: [grade_completion(generated_domain.difficulty, domain_threats, base_points, domain_randomized, time_difference)]"
+
+ var/text = "# Certificate of Domain Completion\n\n---\n\n"
+
+ text += "### [generated_domain.name][domain_randomized ? " (Randomized)" : ""]\n"
+ text += "- **Difficulty:** [generated_domain.difficulty]\n"
+ text += "- **Threats:** [domain_threats]\n"
+ text += "- **Base Points:** [base_points][domain_randomized ? " +1" : ""]\n\n"
+ text += "- **Total Bonus:** [bonuses]x\n\n"
+
+ if(bonuses <= 1)
+ text += completion_time
+ text += grade
+ return text
+
+ text += "### Bonuses\n"
+ if(domain_randomized)
+ text += "- **Randomized:** + 0.2\n"
+
+ if(length(avatar_connection_refs) > 1)
+ text += "- **Multiplayer:** + [(length(avatar_connection_refs) - 1) * multiplayer_bonus]\n"
+
+ if(domain_threats > 0)
+ text += "- **Threats:** + [domain_threats * 2]\n"
+
+ var/servo_rating = servo_bonus
+
+ if(servo_rating > 0.2)
+ text += "- **Components:** + [servo_rating]\n"
+
+ text += completion_time
+ text += grade
+
+ return text
+
+/// Grades the player's run based on several factors
+/obj/machinery/quantum_server/proc/grade_completion(difficulty, threats, points, randomized, completion_time)
+ var/score = threats * 5
+ score += points
+ score += randomized ? 1 : 0
+
+ var/base = difficulty + 1
+ var/time_score = 1
+
+ if(completion_time <= 1 MINUTES)
+ time_score = 10
+ else if(completion_time <= 2 MINUTES)
+ time_score = 5
+ else if(completion_time <= 5 MINUTES)
+ time_score = 3
+ else if(completion_time <= 10 MINUTES)
+ time_score = 2
+ else
+ time_score = 1
+
+ score += time_score * base
+
+ switch(score)
+ if(1 to 4)
+ return "D"
+ if(5 to 7)
+ return "C"
+ if(8 to 10)
+ return "B"
+ if(11 to 13)
+ return "A"
+ else
+ return "S"
diff --git a/code/modules/bitrunning/server/map_handling.dm b/code/modules/bitrunning/server/map_handling.dm
new file mode 100644
index 00000000000..02126c290f7
--- /dev/null
+++ b/code/modules/bitrunning/server/map_handling.dm
@@ -0,0 +1,184 @@
+
+/// Gives all current occupants a notification that the server is going down
+/obj/machinery/quantum_server/proc/begin_shutdown(mob/user)
+ if(isnull(generated_domain))
+ return
+
+ if(!length(avatar_connection_refs))
+ balloon_alert(user, "powering down domain...")
+ playsound(src, 'sound/machines/terminal_off.ogg', 40, 2)
+ reset()
+ return
+
+ balloon_alert(user, "notifying clients...")
+ playsound(src, 'sound/machines/terminal_alert.ogg', 100, TRUE)
+ user.visible_message(
+ span_danger("[user] begins depowering the server!"),
+ span_notice("You start disconnecting clients..."),
+ span_danger("You hear frantic keying on a keyboard."),
+ )
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SHUTDOWN_ALERT, user)
+
+ if(!do_after(user, 20 SECONDS, src))
+ return
+
+ reset()
+
+/**
+ * ### Quantum Server Cold Boot
+ * Procedurally links the 3 booting processes together.
+ *
+ * This is the starting point if you have an id. Does validation and feedback on steps
+ */
+/obj/machinery/quantum_server/proc/cold_boot_map(mob/user, map_key)
+ if(!is_ready)
+ return FALSE
+
+ if(isnull(map_key))
+ balloon_alert(user, "no domain specified.")
+ return FALSE
+
+ if(generated_domain)
+ balloon_alert(user, "stop the current domain first.")
+ return FALSE
+
+ if(length(avatar_connection_refs))
+ balloon_alert(user, "all clients must disconnect!")
+ return FALSE
+
+ is_ready = FALSE
+ playsound(src, 'sound/machines/terminal_processing.ogg', 30, 2)
+
+ if(!initialize_domain(map_key) || !initialize_safehouse() || !initialize_map_items())
+ balloon_alert(user, "initialization failed.")
+ scrub_vdom()
+ is_ready = TRUE
+ return FALSE
+
+ is_ready = TRUE
+ playsound(src, 'sound/machines/terminal_insert_disc.ogg', 30, 2)
+ balloon_alert(user, "domain loaded.")
+ generated_domain.start_time = world.time
+ points -= generated_domain.cost
+ update_use_power(ACTIVE_POWER_USE)
+ update_appearance()
+
+ return TRUE
+
+/// Initializes a new domain if the given key is valid and the user has enough points
+/obj/machinery/quantum_server/proc/initialize_domain(map_key)
+ var/datum/lazy_template/virtual_domain/to_load
+
+ for(var/datum/lazy_template/virtual_domain/available as anything in subtypesof(/datum/lazy_template/virtual_domain))
+ if(map_key != initial(available.key) || points < initial(available.cost))
+ continue
+ to_load = available
+ break
+
+ if(isnull(to_load))
+ return FALSE
+
+ generated_domain = new to_load()
+ RegisterSignal(generated_domain, COMSIG_LAZY_TEMPLATE_LOADED, PROC_REF(on_template_loaded))
+ generated_domain.lazy_load()
+
+ return TRUE
+
+/// Loads in necessary map items, sets mutation targets, etc
+/obj/machinery/quantum_server/proc/initialize_map_items()
+ var/turf/goal_turfs = list()
+ var/turf/crate_turfs = list()
+
+ for(var/thing in GLOB.landmarks_list)
+ if(istype(thing, /obj/effect/landmark/bitrunning/hololadder_spawn))
+ exit_turfs += get_turf(thing)
+ qdel(thing) // i'm worried about multiple servers getting confused so lets clean em up
+ continue
+
+ if(istype(thing, /obj/effect/landmark/bitrunning/cache_goal_turf))
+ var/turf/tile = get_turf(thing)
+ goal_turfs += tile
+ RegisterSignal(tile, COMSIG_ATOM_ENTERED, PROC_REF(on_goal_turf_entered))
+ RegisterSignal(tile, COMSIG_ATOM_EXAMINE, PROC_REF(on_goal_turf_examined))
+ qdel(thing)
+ continue
+
+ if(istype(thing, /obj/effect/landmark/bitrunning/cache_spawn))
+ crate_turfs += get_turf(thing)
+ qdel(thing)
+ continue
+
+ if(!length(exit_turfs))
+ CRASH("Failed to find exit turfs on generated domain.")
+ if(!length(goal_turfs))
+ CRASH("Failed to find send turfs on generated domain.")
+
+ if(length(crate_turfs))
+ shuffle_inplace(crate_turfs)
+ new /obj/structure/closet/crate/secure/bitrunning/encrypted(pick(crate_turfs))
+
+ return TRUE
+#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()
+ var/turf/safehouse_load_turf = list()
+ for(var/obj/effect/landmark/bitrunning/safehouse_spawn/spawner in GLOB.landmarks_list)
+ safehouse_load_turf += get_turf(spawner)
+ qdel(spawner)
+ break
+
+ if(!length(safehouse_load_turf))
+ CRASH("Failed to find safehouse load landmark on map.")
+
+ var/datum/map_template/safehouse/safehouse = new generated_domain.safehouse_path()
+ safehouse.load(safehouse_load_turf[ONLY_TURF])
+ generated_safehouse = safehouse
+
+ return TRUE
+
+/// Stops the current virtual domain and disconnects all users
+/obj/machinery/quantum_server/proc/reset(fast = FALSE)
+ is_ready = FALSE
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+
+ if(!fast)
+ notify_spawned_threats()
+ addtimer(CALLBACK(src, PROC_REF(scrub_vdom)), 15 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
+ else
+ scrub_vdom() // used in unit testing, no need to wait for callbacks
+
+ addtimer(CALLBACK(src, PROC_REF(cool_off)), min(server_cooldown_time * capacitor_coefficient), TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+ update_appearance()
+
+ update_use_power(IDLE_POWER_USE)
+ domain_randomized = FALSE
+ domain_threats = 0
+ retries_spent = 0
+
+/// Deletes all the tile contents
+/obj/machinery/quantum_server/proc/scrub_vdom()
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR) // just in case
+
+ if(length(generated_domain.reservations))
+ var/datum/turf_reservation/res = generated_domain.reservations[1]
+ res.Release()
+
+ var/list/datum/weakref/creatures = spawned_threat_refs + mutation_candidate_refs
+ for(var/datum/weakref/creature_ref as anything in creatures)
+ var/mob/living/creature = creature_ref?.resolve()
+ if(isnull(creature))
+ continue
+
+ creature.dust() // sometimes mobs just don't die
+
+ avatar_connection_refs.Cut()
+ exit_turfs = list()
+ generated_domain = null
+ generated_safehouse = null
+ mutation_candidate_refs.Cut()
+ spawned_threat_refs.Cut()
+
+#undef ONLY_TURF
diff --git a/code/modules/bitrunning/server/obj_generation.dm b/code/modules/bitrunning/server/obj_generation.dm
new file mode 100644
index 00000000000..221308e0487
--- /dev/null
+++ b/code/modules/bitrunning/server/obj_generation.dm
@@ -0,0 +1,101 @@
+/// Generates a new avatar for the bitrunner.
+/obj/machinery/quantum_server/proc/generate_avatar(obj/structure/hololadder/wayout, datum/outfit/netsuit)
+ var/mob/living/carbon/human/avatar = new(wayout.loc)
+
+ var/outfit_path = generated_domain.forced_outfit || netsuit
+ var/datum/outfit/to_wear = new outfit_path()
+
+ to_wear.belt = /obj/item/bitrunning_host_monitor
+ to_wear.glasses = null
+ to_wear.gloves = null
+ to_wear.l_hand = null
+ to_wear.l_pocket = null
+ to_wear.r_hand = null
+ to_wear.r_pocket = null
+ to_wear.suit = null
+ to_wear.suit_store = null
+
+ avatar.equipOutfit(to_wear, visualsOnly = TRUE)
+
+ var/thing = avatar.get_active_held_item()
+ if(!isnull(thing))
+ qdel(thing)
+
+ thing = avatar.get_inactive_held_item()
+ if(!isnull(thing))
+ qdel(thing)
+
+ var/obj/item/storage/backpack/bag = avatar.back
+ if(istype(bag))
+ QDEL_LIST(bag.contents)
+
+ bag.contents += list(
+ new /obj/item/storage/box/survival,
+ new /obj/item/storage/medkit/regular,
+ new /obj/item/flashlight,
+ )
+
+ var/obj/item/card/id/outfit_id = avatar.wear_id
+ if(outfit_id)
+ outfit_id.assignment = "Bit Avatar"
+ outfit_id.registered_name = avatar.real_name
+
+ outfit_id.registered_account = new()
+ outfit_id.registered_account.replaceable = FALSE
+
+ SSid_access.apply_trim_to_card(outfit_id, /datum/id_trim/bit_avatar)
+
+ return avatar
+
+/// Generates a new hololadder for the bitrunner. Effectively a respawn attempt.
+/obj/machinery/quantum_server/proc/generate_hololadder()
+ if(!length(exit_turfs))
+ return
+
+ if(retries_spent >= length(exit_turfs))
+ return
+
+ var/turf/destination
+ for(var/turf/dest_turf in exit_turfs)
+ if(!locate(/obj/structure/hololadder) in dest_turf)
+ destination = dest_turf
+ break
+
+ if(isnull(destination))
+ return
+
+ var/obj/structure/hololadder/wayout = new(destination)
+ if(isnull(wayout))
+ return
+
+ retries_spent += 1
+
+ return wayout
+
+/// Scans over neo's contents for bitrunning tech disks. Loads the items or abilities onto the avatar.
+/obj/machinery/quantum_server/proc/stock_gear(mob/living/carbon/human/avatar, mob/living/carbon/human/neo)
+ var/failed = FALSE
+
+ for(var/obj/item/bitrunning_disk/disk in neo.get_contents())
+ if(istype(disk, /obj/item/bitrunning_disk/ability))
+ var/obj/item/bitrunning_disk/ability/ability_disk = disk
+
+ if(isnull(ability_disk.granted_action))
+ failed = TRUE
+ continue
+
+ var/datum/action/our_action = new ability_disk.granted_action()
+ our_action.Grant(avatar)
+ continue
+
+ if(istype(disk, /obj/item/bitrunning_disk/item))
+ var/obj/item/bitrunning_disk/item/item_disk = disk
+
+ if(isnull(item_disk.granted_item))
+ failed = TRUE
+ continue
+
+ avatar.put_in_hands(new item_disk.granted_item())
+
+ if(failed)
+ to_chat(neo, span_warning("One of your disks failed to load. You must activate them to make a selection."))
diff --git a/code/modules/bitrunning/server/quantum_server.dm b/code/modules/bitrunning/server/quantum_server.dm
new file mode 100644
index 00000000000..404a31cca6a
--- /dev/null
+++ b/code/modules/bitrunning/server/quantum_server.dm
@@ -0,0 +1,152 @@
+/**
+ * The base object for the quantum server
+ */
+/obj/machinery/quantum_server
+ name = "quantum server"
+
+ circuit = /obj/item/circuitboard/machine/quantum_server
+ density = TRUE
+ desc = "A hulking computational machine designed to fabricate virtual domains."
+ icon = 'icons/obj/machines/bitrunning.dmi'
+ base_icon_state = "qserver"
+ icon_state = "qserver"
+ /// Affects server cooldown efficiency
+ var/capacitor_coefficient = 1
+ /// The loaded map template, map_template/virtual_domain
+ var/datum/lazy_template/virtual_domain/generated_domain
+ /// The loaded safehouse, map_template/safehouse
+ var/datum/map_template/safehouse/generated_safehouse
+ /// The connected console
+ var/datum/weakref/console_ref
+ /// If the current domain was a random selection
+ var/domain_randomized = FALSE
+ /// If any threats were spawned, adds to rewards
+ var/domain_threats = 0
+ /// Prevents multiple user actions. Handled by loading domains and cooldowns
+ var/is_ready = TRUE
+ /// List of available domains
+ var/list/available_domains = list()
+ /// Current plugged in users
+ var/list/datum/weakref/avatar_connection_refs = list()
+ /// Cached list of mutable mobs in zone for cybercops
+ var/list/datum/weakref/mutation_candidate_refs = list()
+ /// Any ghosts that have spawned in
+ var/list/datum/weakref/spawned_threat_refs = list()
+ /// Scales loot with extra players
+ var/multiplayer_bonus = 1.1
+ ///The radio the console can speak into
+ var/obj/item/radio/radio
+ /// The amount of points in the system, used to purchase maps
+ var/points = 0
+ /// Keeps track of the number of times someone has built a hololadder
+ var/retries_spent = 0
+ /// Changes how much info is available on the domain
+ var/scanner_tier = 1
+ /// Length of time it takes for the server to cool down after resetting. Here to give runners downtime so their faces don't get stuck like that
+ var/server_cooldown_time = 3 MINUTES
+ /// Applies bonuses to rewards etc
+ var/servo_bonus = 0
+ /// The turfs we can place a hololadder on.
+ var/turf/exit_turfs = list()
+ /// The turfs on station where we generate loot.
+ var/turf/receive_turfs = list()
+
+/obj/machinery/quantum_server/Initialize(mapload)
+ . = ..()
+
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/machinery/quantum_server/LateInitialize()
+ . = ..()
+
+ if(isnull(console_ref))
+ find_console()
+
+ radio = new(src)
+ radio.set_frequency(FREQ_SUPPLY)
+ radio.subspace_transmission = TRUE
+ radio.canhear_range = 0
+ radio.recalculateChannels()
+
+ RegisterSignals(src, list(COMSIG_MACHINERY_BROKEN, COMSIG_MACHINERY_POWER_LOST), PROC_REF(on_broken))
+ RegisterSignal(src, COMSIG_QDELETING, PROC_REF(on_delete))
+ RegisterSignal(src, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(src, COMSIG_BITRUNNER_SPAWN_GLITCH, PROC_REF(on_threat_created))
+
+ // This further gets sorted in the client by cost so it's random and grouped
+ available_domains = shuffle(subtypesof(/datum/lazy_template/virtual_domain))
+
+/obj/machinery/quantum_server/Destroy(force)
+ . = ..()
+
+ available_domains.Cut()
+ mutation_candidate_refs.Cut()
+ avatar_connection_refs.Cut()
+ spawned_threat_refs.Cut()
+ QDEL_NULL(exit_turfs)
+ QDEL_NULL(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)
+ return ..()
+
+ set_light_color(is_ready ? LIGHT_COLOR_BABY_BLUE : LIGHT_COLOR_FIRE)
+ set_light(2, 1.5)
+
+ return ..()
+
+/obj/machinery/quantum_server/update_icon_state()
+ if(isnull(generated_domain) || !is_operational)
+ icon_state = base_icon_state
+ return ..()
+
+ icon_state = "[base_icon_state]_[is_ready ? "on" : "off"]"
+ return ..()
+
+/obj/machinery/quantum_server/crowbar_act(mob/living/user, obj/item/crowbar)
+ . = ..()
+
+ if(!is_ready)
+ balloon_alert(user, "it's scalding hot!")
+ return TRUE
+ if(length(avatar_connection_refs))
+ balloon_alert(user, "all clients must disconnect!")
+ return TRUE
+ if(default_deconstruction_crowbar(crowbar))
+ return TRUE
+ return FALSE
+
+/obj/machinery/quantum_server/screwdriver_act(mob/living/user, obj/item/screwdriver)
+ . = ..()
+
+ if(!is_ready)
+ balloon_alert(user, "it's scalding hot!")
+ return TRUE
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]_panel", icon_state, screwdriver))
+ return TRUE
+ return FALSE
+
+/obj/machinery/quantum_server/RefreshParts()
+ . = ..()
+
+ var/capacitor_rating = 1.15
+ var/datum/stock_part/capacitor/cap = locate() in component_parts
+ capacitor_rating -= cap.tier * 0.15
+
+ capacitor_coefficient = capacitor_rating
+
+ var/datum/stock_part/scanning_module/scanner = locate() in component_parts
+ if(scanner)
+ scanner_tier = scanner.tier
+
+ var/servo_rating = 0
+ for(var/datum/stock_part/servo/servo in component_parts)
+ servo_rating += servo.tier * 0.1
+
+ servo_bonus = servo_rating
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SERVER_UPGRADED, servo_rating)
diff --git a/code/modules/bitrunning/server/signal_handlers.dm b/code/modules/bitrunning/server/signal_handlers.dm
new file mode 100644
index 00000000000..b0464b351fa
--- /dev/null
+++ b/code/modules/bitrunning/server/signal_handlers.dm
@@ -0,0 +1,107 @@
+/// If broken via signal, disconnects all users
+/obj/machinery/quantum_server/proc/on_broken(datum/source)
+ SIGNAL_HANDLER
+
+ if(isnull(generated_domain))
+ return
+
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+
+/// Whenever a corpse spawner makes a new corpse, add it to the list of potential mutations
+/obj/machinery/quantum_server/proc/on_corpse_spawned(datum/source, mob/living/corpse)
+ SIGNAL_HANDLER
+
+ mutation_candidate_refs.Add(WEAKREF(corpse))
+
+/// Being qdeleted - make sure the circuit and connected mobs go with it
+/obj/machinery/quantum_server/proc/on_delete(datum/source)
+ SIGNAL_HANDLER
+
+ if(generated_domain)
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_SEVER_AVATAR)
+ scrub_vdom()
+
+ if(is_ready)
+ return
+ // in case they're trying to cheese cooldown
+ var/obj/item/circuitboard/machine/quantum_server/circuit = locate(/obj/item/circuitboard/machine/quantum_server) in contents
+ if(circuit)
+ qdel(circuit)
+
+/// Handles examining the server. Shows cooldown time and efficiency.
+/obj/machinery/quantum_server/proc/on_examine(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_infoplain("Can be resource intensive to run. Ensure adequate power supply.")
+
+ if(capacitor_coefficient < 1)
+ examine_text += span_infoplain("Its coolant capacity reduces cooldown time by [(1 - capacitor_coefficient) * 100]%.")
+
+ if(servo_bonus > 0.2)
+ examine_text += span_infoplain("Its manipulation potential is increasing rewards by [servo_bonus]x.")
+ examine_text += span_infoplain("Injury from unsafe ejection reduced [servo_bonus * 100]%.")
+
+ if(!is_ready)
+ examine_text += span_notice("It is currently cooling down. Give it a few moments.")
+ return
+
+/// Whenever something enters the send tiles, check if it's a loot crate. If so, alert players.
+/obj/machinery/quantum_server/proc/on_goal_turf_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if(!istype(arrived, /obj/structure/closet/crate/secure/bitrunning/encrypted))
+ return
+
+ var/obj/structure/closet/crate/secure/bitrunning/encrypted/loot_crate = arrived
+ if(!istype(loot_crate))
+ return
+
+ for(var/mob/person in loot_crate.contents)
+ if(isnull(person.mind))
+ person.forceMove(get_turf(loot_crate))
+
+ var/datum/component/avatar_connection/connection = person.GetComponent(/datum/component/avatar_connection)
+ connection?.full_avatar_disconnect()
+
+ spark_at_location(loot_crate)
+ qdel(loot_crate)
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_DOMAIN_COMPLETE, arrived, generated_domain.reward_points)
+ generate_loot()
+
+/// Handles examining the server. Shows cooldown time and efficiency.
+/obj/machinery/quantum_server/proc/on_goal_turf_examined(datum/source, mob/examiner, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_info("Beneath your gaze, the floor pulses subtly with streams of encoded data.")
+ examine_text += span_info("It seems to be part of the location designated for retrieving encrypted payloads.")
+
+/// Scans over the inbound created_atoms from lazy templates
+/obj/machinery/quantum_server/proc/on_template_loaded(datum/lazy_template/source, list/created_atoms)
+ SIGNAL_HANDLER
+
+ for(var/thing in created_atoms)
+ if(isliving(thing)) // so we can mutate them
+ var/mob/living/creature = thing
+
+ if(creature.can_be_cybercop)
+ mutation_candidate_refs.Add(WEAKREF(creature))
+ continue
+
+ if(istype(thing, /obj/effect/mob_spawn/ghost_role)) // so we get threat alerts
+ RegisterSignal(thing, COMSIG_GHOSTROLE_SPAWNED, PROC_REF(on_threat_created))
+ continue
+
+ if(istype(thing, /obj/effect/mob_spawn/corpse)) // corpses are valid targets too
+ var/obj/effect/mob_spawn/corpse/spawner = thing
+
+ mutation_candidate_refs.Add(spawner.spawned_mob_ref)
+
+ UnregisterSignal(source, COMSIG_LAZY_TEMPLATE_LOADED)
+
+/// Handles when cybercops are summoned into the area or ghosts click a ghost role spawner
+/obj/machinery/quantum_server/proc/on_threat_created(datum/source, mob/living/threat)
+ SIGNAL_HANDLER
+
+ domain_threats += 1
+ spawned_threat_refs.Add(WEAKREF(threat))
+ SEND_SIGNAL(src, COMSIG_BITRUNNER_THREAT_CREATED) // notify players
diff --git a/code/modules/bitrunning/server/util.dm b/code/modules/bitrunning/server/util.dm
new file mode 100644
index 00000000000..1d35e86de50
--- /dev/null
+++ b/code/modules/bitrunning/server/util.dm
@@ -0,0 +1,142 @@
+#define REDACTED "???"
+#define MAX_DISTANCE 4 // How far crates can spawn from the server
+
+/// Resets the cooldown state and updates icons
+/obj/machinery/quantum_server/proc/cool_off()
+ is_ready = TRUE
+ update_appearance()
+ radio.talk_into(src, "Thermal systems within operational parameters. Proceeding to domain configuration.", RADIO_CHANNEL_SUPPLY)
+
+/// Attempts to connect to a quantum console
+/obj/machinery/quantum_server/proc/find_console()
+ var/obj/machinery/computer/quantum_console/console = console_ref?.resolve()
+ if(console)
+ return console
+
+ for(var/direction in GLOB.cardinals)
+ var/obj/machinery/computer/quantum_console/nearby_console = locate(/obj/machinery/computer/quantum_console, get_step(src, direction))
+ if(nearby_console)
+ console_ref = WEAKREF(nearby_console)
+ nearby_console.server_ref = WEAKREF(src)
+ return nearby_console
+
+/// Compiles a list of available domains.
+/obj/machinery/quantum_server/proc/get_available_domains()
+ var/list/levels = list()
+
+ for(var/datum/lazy_template/virtual_domain/domain as anything in available_domains)
+ if(initial(domain.test_only))
+ continue
+ var/can_view = initial(domain.difficulty) < scanner_tier && initial(domain.cost) <= points + 5
+ var/can_view_reward = initial(domain.difficulty) < (scanner_tier + 1) && initial(domain.cost) <= points + 3
+
+ levels += list(list(
+ "cost" = initial(domain.cost),
+ "desc" = can_view ? initial(domain.desc) : "Limited scanning capabilities. Cannot infer domain details.",
+ "difficulty" = initial(domain.difficulty),
+ "id" = initial(domain.key),
+ "name" = can_view ? initial(domain.name) : REDACTED,
+ "reward" = can_view_reward ? initial(domain.reward_points) : REDACTED,
+ ))
+
+ return levels
+
+/// If there are hosted minds, attempts to get a list of their current virtual bodies w/ vitals
+/obj/machinery/quantum_server/proc/get_avatar_data()
+ var/list/hosted_avatars = list()
+
+ for(var/datum/weakref/avatar_ref in avatar_connection_refs)
+ var/datum/component/avatar_connection/connection = avatar_ref.resolve()
+ if(isnull(connection))
+ avatar_connection_refs.Remove(connection)
+ continue
+
+ var/mob/living/creature = connection.parent
+ var/mob/living/pilot = connection.old_body_ref?.resolve()
+
+ hosted_avatars += list(list(
+ "health" = creature.health,
+ "name" = creature.name,
+ "pilot" = pilot,
+ "brute" = creature.get_damage_amount(BRUTE),
+ "burn" = creature.get_damage_amount(BURN),
+ "tox" = creature.get_damage_amount(TOX),
+ "oxy" = creature.get_damage_amount(OXY),
+ ))
+
+ return hosted_avatars
+
+/// Gets a random available domain given the current points. Weighted towards higher cost domains.
+/obj/machinery/quantum_server/proc/get_random_domain_id()
+ if(points < 1)
+ return
+
+ var/list/random_domains = list()
+ var/total_cost = 0
+
+ for(var/datum/lazy_template/virtual_domain/available as anything in subtypesof(/datum/lazy_template/virtual_domain))
+ var/init_cost = initial(available.cost)
+ if(!initial(available.test_only) && init_cost > 0 && init_cost < 4 && init_cost <= points)
+ random_domains += list(list(
+ cost = init_cost,
+ id = initial(available.key),
+ ))
+
+ var/random_value = rand(0, total_cost)
+ var/accumulated_cost = 0
+
+ for(var/available as anything in random_domains)
+ accumulated_cost += available["cost"]
+ if(accumulated_cost >= random_value)
+ domain_randomized = TRUE
+ return available["id"]
+
+/// Gets all mobs originally generated by the loaded domain and returns a list that are capable of being antagged
+/obj/machinery/quantum_server/proc/get_valid_domain_targets()
+ // A: No one is playing
+ // B: The domain is not loaded
+ // C: The domain is shutting down
+ // D: There are no mobs
+ if(!length(avatar_connection_refs) || isnull(generated_domain) || !is_ready || !is_operational || !length(mutation_candidate_refs))
+ return list()
+
+ for(var/datum/weakref/creature_ref as anything in mutation_candidate_refs)
+ var/mob/living/creature = creature_ref.resolve()
+ if(isnull(creature) || creature.mind)
+ mutation_candidate_refs.Remove(creature_ref)
+
+ return shuffle(mutation_candidate_refs)
+
+/// Locates any turfs with 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)
+
+ return length(receive_turfs) > 0
+
+/// Finds any mobs with minds in the zones and gives them the bad news
+/obj/machinery/quantum_server/proc/notify_spawned_threats()
+ for(var/datum/weakref/baddie_ref as anything in spawned_threat_refs)
+ var/mob/living/baddie = baddie_ref.resolve()
+ if(isnull(baddie) || baddie.stat >= UNCONSCIOUS || isnull(baddie.mind))
+ continue
+
+ baddie.throw_alert(
+ ALERT_BITRUNNER_RESET,
+ /atom/movable/screen/alert/bitrunning/qserver_threat_deletion,
+ new_master = src,
+ )
+
+ to_chat(baddie, span_userdanger("You have been flagged for deletion! Thank you for your service."))
+
+/// Do some magic teleport sparks
+/obj/machinery/quantum_server/proc/spark_at_location(obj/crate)
+ playsound(crate, 'sound/magic/blink.ogg', 50, TRUE)
+ var/datum/effect_system/spark_spread/quantum/sparks = new()
+ sparks.set_up(5, 1, get_turf(crate))
+ sparks.start()
+
+#undef REDACTED
+#undef MAX_DISTANCE
diff --git a/code/modules/bitrunning/turfs.dm b/code/modules/bitrunning/turfs.dm
new file mode 100644
index 00000000000..93dce1789c4
--- /dev/null
+++ b/code/modules/bitrunning/turfs.dm
@@ -0,0 +1,14 @@
+/turf/open/floor/bitrunning_transport
+ name = "circuit floor"
+ icon = 'icons/turf/floors.dmi'
+ desc = "Looks complex. You can see the circuits running through the floor."
+ icon_state = "bitrunning"
+
+/turf/closed/indestructible/binary
+ name = "tear in the fabric of reality"
+ icon = 'icons/turf/floors.dmi'
+ icon_state = "binary"
+
+/obj/effect/baseturf_helper/virtual_domain
+ name = "virtual domain baseturf editor"
+ baseturf = /turf/open/indestructible/binary
diff --git a/code/modules/bitrunning/virtual_domain/domains/ash_drake.dm b/code/modules/bitrunning/virtual_domain/domains/ash_drake.dm
new file mode 100644
index 00000000000..02bb91abc58
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/ash_drake.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/ash_drake
+ name = "Ashen Inferno"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Home of the ash drake, a powerful dragon that scours the surface of Lavaland."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ forced_outfit = /datum/outfit/job/miner
+ key = "ash_drake"
+ map_name = "ash_drake"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/dragon/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1600
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1600
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
new file mode 100644
index 00000000000..871c2cb1338
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm
@@ -0,0 +1,22 @@
+/datum/lazy_template/virtual_domain/beach_bar
+ name = "Beach Bar"
+ desc = "A cheerful seaside haven where friendly skeletons serve up drinks. Say, how'd you guys get so dead?"
+ extra_loot = list(/obj/item/toy/beach_ball = 1)
+ help_text = "This place is running on a skeleton crew, and they don't seem to be too keen to share details. \
+ Maybe a few drinks of liquid charm will get the spirits up. As the saying goes, if you can't beat 'em, join 'em."
+ key = "beach_bar"
+ map_name = "beach_bar"
+ safehouse_path = /datum/map_template/safehouse/mine
+
+/obj/item/reagent_containers/cup/glass/drinkingglass/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)
+ . = ..()
+
+ AddComponent(/datum/component/bitrunning_points, \
+ signal_type = COMSIG_GLASS_DRANK, \
+ points_per_signal = 0.5, \
+ )
diff --git a/code/modules/bitrunning/virtual_domain/domains/blood_drunk_miner.dm b/code/modules/bitrunning/virtual_domain/domains/blood_drunk_miner.dm
new file mode 100644
index 00000000000..abf2e0fc5a9
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/blood_drunk_miner.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/blood_drunk_miner
+ name = "Sanguine Excavation"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Few escape the surface of Lavaland without a few scars. Some remain, maddened by the hunt."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ forced_outfit = /datum/outfit/job/miner
+ key = "blood_drunk_miner"
+ map_name = "blood_drunk_miner"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1600
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1600
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/bubblegum.dm b/code/modules/bitrunning/virtual_domain/domains/bubblegum.dm
new file mode 100644
index 00000000000..bede97177cb
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/bubblegum.dm
@@ -0,0 +1,19 @@
+/datum/lazy_template/virtual_domain/bubblegum
+ name = "Blood-Soaked Lair"
+ cost = BITRUNNER_COST_HIGH
+ desc = "King of the slaughter demons. Bubblegum is a massive, hulking beast with a penchant for violence."
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ extra_loot = list(/obj/item/toy/plush/bubbleplush = 1)
+ forced_outfit = /datum/outfit/job/miner
+ key = "bubblegum"
+ map_name = "bubblegum"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/bubblegum/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 2000
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 2000
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/clown_planet.dm b/code/modules/bitrunning/virtual_domain/domains/clown_planet.dm
new file mode 100644
index 00000000000..92f000c9cf3
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/clown_planet.dm
@@ -0,0 +1,13 @@
+/datum/lazy_template/virtual_domain/clown_planet
+ name = "Clown Planet"
+ cost = BITRUNNER_COST_LOW
+ desc = "In the deep, dark reaches of space, there is only Honk."
+ difficulty = BITRUNNER_DIFFICULTY_LOW
+ extra_loot = list(/obj/item/bikehorn = 1)
+ forced_outfit = /datum/outfit/job/clown
+ help_text = "The trials of the Honkitude have begun. The sound of bike horns wailing in the distance. \
+ this realm- some sort of puzzle, has existed in legend as the final test of just how silly you are."
+ key = "clown_planet"
+ map_name = "clown_planet"
+ reward_points = BITRUNNER_REWARD_LOW
+ safehouse_path = /datum/map_template/safehouse/mine
diff --git a/code/modules/bitrunning/virtual_domain/domains/colossus.dm b/code/modules/bitrunning/virtual_domain/domains/colossus.dm
new file mode 100644
index 00000000000..35ba4eee0ca
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/colossus.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/colossus
+ name = "Celestial Trial"
+ cost = BITRUNNER_COST_HIGH
+ desc = "A massive, ancient beast named the Colossus. Judgment comes."
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ forced_outfit = /datum/outfit/job/miner
+ key = "colossus"
+ map_name = "colossus"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/colossus/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 2000
+ maxHealth = 2000
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
new file mode 100644
index 00000000000..4deacb4f9c5
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
@@ -0,0 +1,39 @@
+/datum/lazy_template/virtual_domain/gondola_asteroid
+ name = "Gondola Asteroid"
+ desc = "An asteroid home to a bountiful forest of gondolas. Peaceful."
+ map_name = "gondola_asteroid"
+ help_text = "What a lovely forest. There's a loot crate here in the middle of the map. \
+ Hmm... It doesn't budge. The gondolas don't seem to have any trouble moving it, though. \
+ I bet there's a way to move it myself."
+ key = "gondola_asteroid"
+ map_name = "gondola_asteroid"
+ safehouse_path = /datum/map_template/safehouse/shuttle_space
+
+/// Very pushy gondolas, great for moving loot crates.
+/obj/structure/closet/crate/secure/bitrunning/encrypted/gondola
+ move_resist = MOVE_FORCE_STRONG
+
+/mob/living/simple_animal/pet/gondola/virtual_domain
+ health = 50
+ loot = list(/obj/effect/decal/cleanable/blood/gibs, /obj/item/stack/sheet/animalhide/gondola = 1, /obj/item/food/meat/slab/gondola/virtual_domain = 1)
+ maxHealth = 50
+ move_force = MOVE_FORCE_VERY_STRONG
+ move_resist = MOVE_FORCE_STRONG
+
+/obj/item/food/meat/slab/gondola/virtual_domain
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment/protein = 4,
+ /datum/reagent/gondola_mutation_toxin/virtual_domain = 5,
+ )
+
+/datum/reagent/gondola_mutation_toxin/virtual_domain
+ name = "Advanced Tranquility"
+
+/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)
+
+/datum/disease/transformation/gondola/virtual_domain
+ stage_prob = 9
+ new_form = /mob/living/simple_animal/pet/gondola/virtual_domain
diff --git a/code/modules/bitrunning/virtual_domain/domains/hierophant.dm b/code/modules/bitrunning/virtual_domain/domains/hierophant.dm
new file mode 100644
index 00000000000..142623f4f81
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/hierophant.dm
@@ -0,0 +1,18 @@
+/datum/lazy_template/virtual_domain/hierophant
+ name = "Zealot Arena"
+ cost = BITRUNNER_COST_HIGH
+ desc = "Dance, puppets, dance!"
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ forced_outfit = /datum/outfit/job/miner
+ key = "hierophant"
+ map_name = "hierophant"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/hierophant/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1700
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1700
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/legion.dm b/code/modules/bitrunning/virtual_domain/domains/legion.dm
new file mode 100644
index 00000000000..f1ba146f380
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/legion.dm
@@ -0,0 +1,20 @@
+/datum/lazy_template/virtual_domain/legion
+ name = "Chamber of Echoes"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "A chilling realm that houses Legion's necropolis. Those who succumb to it are forever damned."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ forced_outfit = /datum/outfit/job/miner
+ key = "legion"
+ map_name = "legion"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/legion/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ health = 1500
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 1500
+ true_spawn = FALSE
+
+// You may be thinking, what about those mini-legions? They're not part of the initial created_atoms list
diff --git a/code/modules/bitrunning/virtual_domain/domains/pipedream.dm b/code/modules/bitrunning/virtual_domain/domains/pipedream.dm
new file mode 100644
index 00000000000..fd54ef6ca48
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/pipedream.dm
@@ -0,0 +1,101 @@
+/datum/lazy_template/virtual_domain/pipedream
+ name = "Disposal Pipe Factory"
+ cost = BITRUNNER_COST_LOW
+ desc = "An abandoned and infested factory manufacturing disposal pipes."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ extra_loot = list(/obj/item/stack/pipe_cleaner_coil/random/five = 1)
+ help_text = "Not long ago, this place was thriving with activity. The workers \
+ seemed to have left in a hurry, and now productivity is in the bin. Something \
+ must have trashed the place, but what?"
+ key = "pipedream"
+ map_name = "pipedream"
+ reward_points = BITRUNNER_REWARD_LOW
+ safehouse_path = /datum/map_template/safehouse/shuttle
+
+// ID Trims
+/datum/id_trim/factory
+ assignment = "Factory Worker"
+ trim_state = "trim_cargotechnician"
+ department_color = COLOR_CARGO_BROWN
+ subdepartment_color = COLOR_CARGO_BROWN
+ sechud_icon_state = SECHUD_CARGO_TECHNICIAN
+ access = list(
+ ACCESS_AWAY_SUPPLY
+ )
+
+/datum/id_trim/factory/qm
+ assignment = "Factory Quartermaster"
+ trim_state = "trim_quartermaster"
+ department_color = COLOR_COMMAND_BLUE
+ subdepartment_color = COLOR_CARGO_BROWN
+ department_state = "departmenthead"
+ sechud_icon_state = SECHUD_QUARTERMASTER
+ access = list(
+ ACCESS_AWAY_SUPPLY,
+ ACCESS_AWAY_COMMAND
+ )
+
+// ID Cards
+/obj/item/card/id/advanced/factory
+ name = "factory worker ID"
+ trim = /datum/id_trim/factory
+
+/obj/item/card/id/advanced/factory/qm
+ name = "factory quartermaster ID"
+ trim = /datum/id_trim/factory/qm
+
+//Outfits
+/datum/outfit/factory
+ name = "Factory Worker"
+
+ id_trim = /datum/id_trim/factory
+ id = /obj/item/card/id/advanced/
+ uniform = /obj/item/clothing/under/rank/cargo/tech
+ suit = /obj/item/clothing/suit/hazardvest
+ belt = /obj/item/radio
+ gloves = /obj/item/clothing/gloves/color/black
+ head = /obj/item/clothing/head/soft/yellow
+ shoes = /obj/item/clothing/shoes/workboots
+ l_pocket = /obj/item/flashlight/seclite
+
+/datum/outfit/factory/guard
+ name = "Factory Guard"
+
+ uniform = /obj/item/clothing/under/rank/security/officer/grey
+ suit = /obj/item/clothing/suit/armor/vest/alt
+ belt = /obj/item/radio
+ gloves = /obj/item/clothing/gloves/color/black
+ head = /obj/item/clothing/head/soft/sec
+ shoes = /obj/item/clothing/shoes/jackboots/sec
+ l_pocket = /obj/item/restraints/handcuffs
+ r_pocket = /obj/item/assembly/flash/handheld
+
+/datum/outfit/factory/qm
+ name = "Factory Quatermaster"
+
+ id_trim = /datum/id_trim/factory/qm
+ id = /obj/item/card/id/advanced/silver
+ uniform = /obj/item/clothing/under/rank/cargo/qm
+ belt = /obj/item/radio
+ gloves = /obj/item/clothing/gloves/color/black
+ head = /obj/item/clothing/head/soft/yellow
+ shoes = /obj/item/clothing/shoes/jackboots/sec
+ l_pocket = /obj/item/melee/baton/telescopic
+ r_pocket = /obj/item/stamp/head/qm
+
+// Corpses
+/obj/effect/mob_spawn/corpse/human/factory
+ name = "Factory Worker"
+ outfit = /datum/outfit/factory
+ icon_state = "corpsecargotech"
+
+/obj/effect/mob_spawn/corpse/human/factory/guard
+ name = "Factory Guard"
+ outfit = /datum/outfit/factory/guard
+ icon_state = "corpsecargotech"
+
+/obj/effect/mob_spawn/corpse/human/factory/qm
+ name = "Factory Quartermaster"
+ outfit = /datum/outfit/factory/qm
+ icon_state = "corpsecargotech"
+
diff --git a/code/modules/bitrunning/virtual_domain/domains/pirates.dm b/code/modules/bitrunning/virtual_domain/domains/pirates.dm
new file mode 100644
index 00000000000..52d86a71211
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/pirates.dm
@@ -0,0 +1,10 @@
+/datum/lazy_template/virtual_domain/pirates
+ name = "Corsair Cove"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Battle your way to the hidden treasure, seize the booty, and make a swift escape before the pirates turn the tide."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ help_text = "Put on the provided outfits to blend in, then battle your way through the hostile pirates. \
+ Grab the treasure and get out before you're overwhelmed!"
+ key = "pirates"
+ map_name = "pirates"
+ reward_points = BITRUNNER_REWARD_MEDIUM
diff --git a/code/modules/bitrunning/virtual_domain/domains/stairs_and_cliffs.dm b/code/modules/bitrunning/virtual_domain/domains/stairs_and_cliffs.dm
new file mode 100644
index 00000000000..2d9bcca3645
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/stairs_and_cliffs.dm
@@ -0,0 +1,29 @@
+/datum/lazy_template/virtual_domain/stairs_and_cliffs
+ name = "Glacier Grind"
+ cost = BITRUNNER_COST_LOW
+ desc = "A treacherous climb few calves can survive. Great cardio though."
+ help_text = "Ever heard of 'Snakes and Ladders'? It's like that, but with \
+ instead of ladders its stairs and instead of snakes its a steep drop down a \
+ cliff into rough rocks or liquid plasma."
+ extra_loot = list(/obj/item/clothing/suit/costume/snowman = 2)
+ difficulty = BITRUNNER_DIFFICULTY_LOW
+ forced_outfit = /datum/outfit/job/virtual_domain_iceclimber
+ key = "stairs_and_cliffs"
+ map_name = "stairs_and_cliffs"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/ice
+
+/turf/open/cliff/snowrock/virtual_domain
+ name = "icy cliff"
+ initial_gas_mix = "o2=22;n2=82;TEMP=180"
+
+/turf/open/lava/plasma/virtual_domain
+ name = "plasma lake"
+ initial_gas_mix = "o2=22;n2=82;TEMP=180"
+
+/datum/outfit/job/virtual_domain_iceclimber
+ name = "Ice Climber"
+
+ uniform = /obj/item/clothing/under/color/grey
+ backpack = /obj/item/storage/backpack/duffelbag
+ shoes = /obj/item/clothing/shoes/winterboots
diff --git a/code/modules/bitrunning/virtual_domain/domains/syndicate_assault.dm b/code/modules/bitrunning/virtual_domain/domains/syndicate_assault.dm
new file mode 100644
index 00000000000..bae0da6874d
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/syndicate_assault.dm
@@ -0,0 +1,13 @@
+/datum/lazy_template/virtual_domain/syndicate_assault
+ name = "Syndicate Assault"
+ cost = BITRUNNER_COST_MEDIUM
+ desc = "Board the enemy ship and recover the stolen cargo."
+ difficulty = BITRUNNER_DIFFICULTY_MEDIUM
+ extra_loot = list(/obj/item/toy/plush/nukeplushie = 1)
+ help_text = "A group of Syndicate operatives have stolen valuable cargo from the station. \
+ They have boarded their ship and are attempting to escape. Infiltrate their ship and recover \
+ the crate. Be careful, they are extremely armed."
+ key = "syndicate_assault"
+ map_name = "syndicate_assault"
+ reward_points = BITRUNNER_REWARD_MEDIUM
+ safehouse_path = /datum/map_template/safehouse/shuttle
diff --git a/code/modules/bitrunning/virtual_domain/domains/test_only.dm b/code/modules/bitrunning/virtual_domain/domains/test_only.dm
new file mode 100644
index 00000000000..6e5e852fb5c
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/test_only.dm
@@ -0,0 +1,11 @@
+/// Used for unit tests only. Skipped in UI.
+/datum/lazy_template/virtual_domain/test_only
+ name = "Test Only"
+ key = "test_only"
+ map_name = "test_only"
+ test_only = TRUE
+ safehouse_path = /datum/map_template/safehouse/test_only
+
+/datum/lazy_template/virtual_domain/test_only/expensive
+ key = "test_only_expensive"
+ cost = 3
diff --git a/code/modules/bitrunning/virtual_domain/domains/vaporwave.dm b/code/modules/bitrunning/virtual_domain/domains/vaporwave.dm
new file mode 100644
index 00000000000..45d4abec983
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/vaporwave.dm
@@ -0,0 +1,10 @@
+/datum/lazy_template/virtual_domain/vaporwave
+ name = "Cosmic Vestige"
+ cost = BITRUNNER_COST_EXTREME
+ desc = "Suspended in the silent void of space, the Neon Relic is a haunting echo of a retro-futuristic era."
+ difficulty = BITRUNNER_DIFFICULTY_NONE
+ extra_loot = list(/obj/item/stack/spacecash/c500 = 3)
+ key = "vaporwave"
+ map_name = "vaporwave"
+ reward_points = BITRUNNER_REWARD_EXTREME
+ safehouse_path = /datum/map_template/safehouse/shuttle_space
diff --git a/code/modules/bitrunning/virtual_domain/domains/wendigo.dm b/code/modules/bitrunning/virtual_domain/domains/wendigo.dm
new file mode 100644
index 00000000000..fcad3db6faf
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/wendigo.dm
@@ -0,0 +1,19 @@
+/datum/lazy_template/virtual_domain/wendigo
+ name = "Glacial Devourer"
+ cost = BITRUNNER_COST_HIGH
+ desc = "Legends speak of the ravenous Wendigo hidden deep within the caves of Icemoon."
+ difficulty = BITRUNNER_DIFFICULTY_HIGH
+ forced_outfit = /datum/outfit/job/miner
+ key = "wendigo"
+ map_name = "wendigo"
+ reward_points = BITRUNNER_REWARD_HIGH
+ safehouse_path = /datum/map_template/safehouse/lavaland_boss
+
+/mob/living/simple_animal/hostile/megafauna/wendigo/virtual_domain
+ can_be_cybercop = FALSE
+ crusher_loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ guaranteed_butcher_results = list(/obj/item/wendigo_skull = 1)
+ health = 2000
+ loot = list(/obj/structure/closet/crate/secure/bitrunning/encrypted)
+ maxHealth = 2000
+ true_spawn = FALSE
diff --git a/code/modules/bitrunning/virtual_domain/domains/xeno_nest.dm b/code/modules/bitrunning/virtual_domain/domains/xeno_nest.dm
new file mode 100644
index 00000000000..2bd4105e13c
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/domains/xeno_nest.dm
@@ -0,0 +1,12 @@
+/datum/lazy_template/virtual_domain/xeno_nest
+ name = "Xeno Infestation"
+ cost = BITRUNNER_COST_LOW
+ desc = "Our ship scanners have detected lifeforms of unknown origin. Friendly attempts to contact them have failed."
+ difficulty = BITRUNNER_DIFFICULTY_LOW
+ extra_loot = list(/obj/item/toy/plush/rouny = 1)
+ help_text = "You are on a barren planet filled with hostile creatures. There is a crate here, not hidden, \
+ simply protected. Expect resistance."
+ key = "xeno_nest"
+ map_name = "xeno_nest"
+ reward_points = BITRUNNER_REWARD_LOW
+ safehouse_path = /datum/map_template/safehouse/shuttle
diff --git a/code/modules/bitrunning/virtual_domain/safehouses.dm b/code/modules/bitrunning/virtual_domain/safehouses.dm
new file mode 100644
index 00000000000..bb42f690ac7
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/safehouses.dm
@@ -0,0 +1,53 @@
+/**
+ * # Safe Houses
+ * The starting point for virtual domains.
+ * Create your own: Read the readme file in the '_maps/safehouses' folder.
+ */
+/datum/map_template/safehouse
+ name = "virtual domain: safehouse"
+
+ returns_created_atoms = TRUE
+ /// The map file to load
+ var/filename = "den.dmm"
+
+/datum/map_template/safehouse/New()
+ mappath = "_maps/safehouses/" + filename
+ ..(path = mappath)
+
+/datum/map_template/safehouse/test_only
+ filename = "test_only_safehouse.dmm"
+
+
+/// The default safehouse map template.
+/datum/map_template/safehouse/wood
+ filename = "wood.dmm"
+
+/datum/map_template/safehouse/den
+ filename = "den.dmm"
+
+/datum/map_template/safehouse/dig
+ filename = "dig.dmm"
+
+/datum/map_template/safehouse/shuttle
+ filename = "shuttle.dmm"
+
+// Has space tiles on the four corners.
+/datum/map_template/safehouse/shuttle_space
+ filename = "shuttle_space.dmm"
+
+/datum/map_template/safehouse/mine
+ filename = "mine.dmm"
+
+// Comes preloaded with mining combat gear.
+/datum/map_template/safehouse/lavaland_boss
+ filename = "lavaland_boss.dmm"
+
+// Chill out
+/datum/map_template/safehouse/ice
+ filename = "ice.dmm"
+
+/**
+ * Your safehouse here
+ * /datum/map_template/safehouse/your_type
+ * filename = "your_map.dmm"
+ */
diff --git a/code/modules/bitrunning/virtual_domain/virtual_domain.dm b/code/modules/bitrunning/virtual_domain/virtual_domain.dm
new file mode 100644
index 00000000000..c2bd193f4e9
--- /dev/null
+++ b/code/modules/bitrunning/virtual_domain/virtual_domain.dm
@@ -0,0 +1,34 @@
+/**
+ * # Virtual Domains
+ * This loads a base level, then users can select the preset upon it.
+ * Create your own: Read the readme file in the '_maps/virtual_domains' folder.
+ */
+/datum/lazy_template/virtual_domain
+ map_dir = "_maps/virtual_domains"
+ map_name = "None"
+ key = "Virtual Domain"
+
+ /// Cost of this map to load
+ var/cost = BITRUNNER_COST_NONE
+ /// The description of the map
+ var/desc = "A map."
+ /// The 'difficulty' of the map, which affects the ui and ability to scan info.
+ var/difficulty = BITRUNNER_DIFFICULTY_NONE
+ /// An assoc list of typepath/amount to spawn on completion. Not weighted - the value is the amount
+ var/list/extra_loot
+ /// The map file to load
+ var/filename = "virtual_domain.dmm"
+ /// Any outfit that you wish to force on avatars. Overrides preferences
+ var/datum/outfit/forced_outfit
+ /// Information given to connected clients via ability
+ var/help_text
+ // Name to show in the UI
+ var/name = "Virtual Domain"
+ /// Points to reward for completion. Used to purchase new domains and calculate ore rewards.
+ var/reward_points = BITRUNNER_REWARD_MIN
+ /// The start time of the map. Used to calculate time taken
+ var/start_time
+ /// This map is specifically for unit tests. Shouldn't display in game
+ var/test_only = FALSE
+ /// The safehouse to load into the map
+ var/datum/map_template/safehouse/safehouse_path = /datum/map_template/safehouse/den
diff --git a/code/modules/capture_the_flag/ctf_player_component.dm b/code/modules/capture_the_flag/ctf_player_component.dm
index 0bf37b979de..d3abc0f2571 100644
--- a/code/modules/capture_the_flag/ctf_player_component.dm
+++ b/code/modules/capture_the_flag/ctf_player_component.dm
@@ -8,7 +8,7 @@
var/can_respawn = TRUE
///Reference to the game this player is participating in.
var/datum/ctf_controller/ctf_game
- ///Item dropped on death,
+ ///Item dropped on death,
var/death_drop = /obj/effect/powerup/ammo/ctf
///Reference to players ckey, used for sending messages to them relating to CTF.
var/ckey_reference
@@ -22,19 +22,20 @@
var/datum/mind/true_parent = parent
player_mob = true_parent.current
ckey_reference = player_mob.ckey
- setup_dusting()
-
+ register_mob()
+
/datum/component/ctf_player/PostTransfer()
if(!istype(parent, /datum/mind))
return COMPONENT_INCOMPATIBLE
var/datum/mind/true_parent = parent
player_mob = true_parent.current
- setup_dusting()
+ register_mob()
-///CTF players are dusted upon taking damage that puts them into critical or leaving their body.
-/datum/component/ctf_player/proc/setup_dusting()
+/// Called when we get a new player mob, register signals and set up the mob.
+/datum/component/ctf_player/proc/register_mob()
RegisterSignal(player_mob, COMSIG_MOB_AFTER_APPLY_DAMAGE, PROC_REF(damage_type_check))
RegisterSignal(player_mob, COMSIG_MOB_GHOSTIZED, PROC_REF(ctf_dust))
+ ADD_TRAIT(player_mob, TRAIT_PERMANENTLY_MORTAL, CTF_TRAIT)
///Stamina and oxygen damage will not dust a player by themself.
/datum/component/ctf_player/proc/damage_type_check(datum/source, damage, damage_type)
diff --git a/code/modules/cargo/department_order.dm b/code/modules/cargo/department_order.dm
index 972588a6355..b1573c97a5b 100644
--- a/code/modules/cargo/department_order.dm
+++ b/code/modules/cargo/department_order.dm
@@ -142,6 +142,12 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
if(GLOB.areas_by_type[delivery_area_type])
chosen_delivery_area = delivery_area_type
break
+
+ if(SSshuttle.supply.get_order_count(pack) == OVER_ORDER_LIMIT)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
+ return
+
department_order = new(pack, name, rank, ckey, "", null, chosen_delivery_area, null)
SSshuttle.shopping_list += department_order
if(!already_signalled)
@@ -188,7 +194,7 @@ GLOBAL_LIST_INIT(department_order_cooldowns, list(
department_delivery_areas = list(/area/station/science/research)
override_access = ACCESS_RD
req_one_access = REGION_ACCESS_RESEARCH
- dep_groups = list("Science", "Livestock")
+ dep_groups = list("Science", "Livestock", "Canisters & Materials")
/obj/machinery/computer/department_orders/security
name = "security order console"
diff --git a/code/modules/cargo/exports/materials.dm b/code/modules/cargo/exports/materials.dm
index 06c52305f51..46d089b5ac5 100644
--- a/code/modules/cargo/exports/materials.dm
+++ b/code/modules/cargo/exports/materials.dm
@@ -2,10 +2,13 @@
cost = 5 // Cost per SHEET_MATERIAL_AMOUNT, which is 100cm3 as of May 2023.
message = "cm3 of developer's tears. Please, report this on github"
amount_report_multiplier = SHEET_MATERIAL_AMOUNT
- var/material_id = null
+ var/datum/material/material_id = null
export_types = list(
- /obj/item/stack/sheet/mineral, /obj/item/stack/tile/mineral,
- /obj/item/stack/ore, /obj/item/coin)
+ /obj/item/stack/sheet/mineral,
+ /obj/item/stack/tile/mineral,
+ /obj/item/stack/ore,
+ /obj/item/coin
+ )
// Yes, it's a base type containing export_types.
// But it has no material_id, so any applies_to check will return false, and these types reduce amount of copypasta a lot
@@ -27,17 +30,8 @@
return round(amount / SHEET_MATERIAL_AMOUNT)
-// Materials. Nothing but plasma is really worth selling. Better leave it all to RnD and sell some plasma instead.
-
-/datum/export/material/bananium
- cost = CARGO_CRATE_VALUE * 2
- material_id = /datum/material/bananium
- message = "cm3 of bananium"
-
-/datum/export/material/diamond
- cost = CARGO_CRATE_VALUE
- material_id = /datum/material/diamond
- message = "cm3 of diamonds"
+// Materials. Static materials exist as parent types, while materials subject to the stock market have a fluid cost as determined by material/market types
+// If you're adding a new material to the stock market, make sure its export type is added here.
/datum/export/material/plasma
cost = CARGO_CRATE_VALUE * 0.4
@@ -45,27 +39,12 @@
material_id = /datum/material/plasma
message = "cm3 of plasma"
-/datum/export/material/uranium
- cost = CARGO_CRATE_VALUE * 0.2
- material_id = /datum/material/uranium
- message = "cm3 of uranium"
-
-/datum/export/material/gold
- cost = CARGO_CRATE_VALUE * 0.25
- material_id = /datum/material/gold
- message = "cm3 of gold"
-
-/datum/export/material/silver
- cost = CARGO_CRATE_VALUE * 0.1
- material_id = /datum/material/silver
- message = "cm3 of silver"
-
-/datum/export/material/titanium
- cost = CARGO_CRATE_VALUE * 0.25
- material_id = /datum/material/titanium
- message = "cm3 of titanium"
+/datum/export/material/bananium
+ cost = CARGO_CRATE_VALUE * 2
+ material_id = /datum/material/bananium
+ message = "cm3 of bananium"
-/datum/export/material/adamantine
+/datum/export/material/diamond
cost = CARGO_CRATE_VALUE
material_id = /datum/material/adamantine
message = "cm3 of adamantine"
@@ -75,11 +54,6 @@
material_id = /datum/material/mythril
message = "cm3 of mythril"
-/datum/export/material/bscrystal
- cost = CARGO_CRATE_VALUE * 0.6
- message = "of bluespace crystals"
- material_id = /datum/material/bluespace
-
/datum/export/material/plastic
cost = CARGO_CRATE_VALUE * 0.05
message = "cm3 of plastic"
@@ -90,21 +64,6 @@
message = "cm3 of runite"
material_id = /datum/material/runite
-/datum/export/material/iron
- cost = CARGO_CRATE_VALUE * 0.01
- message = "cm3 of iron"
- material_id = /datum/material/iron
- export_types = list(
- /obj/item/stack/sheet/iron, /obj/item/stack/tile/iron,
- /obj/item/stack/rods, /obj/item/stack/ore, /obj/item/coin)
-
-/datum/export/material/glass
- cost = CARGO_CRATE_VALUE * 0.01
- message = "cm3 of glass"
- material_id = /datum/material/glass
- export_types = list(/obj/item/stack/sheet/glass, /obj/item/stack/ore,
- /obj/item/shard)
-
/datum/export/material/hot_ice
cost = CARGO_CRATE_VALUE * 0.8
message = "cm3 of Hot Ice"
@@ -116,3 +75,90 @@
message = "cm3 of metallic hydrogen"
material_id = /datum/material/metalhydrogen
export_types = /obj/item/stack/sheet/mineral/metal_hydrogen
+
+/datum/export/material/market
+
+/datum/export/material/market/diamond
+ material_id = /datum/material/diamond
+ message = "cm3 of diamonds"
+
+/datum/export/material/market/uranium
+ material_id = /datum/material/uranium
+ message = "cm3 of uranium"
+
+/datum/export/material/market/gold
+ material_id = /datum/material/gold
+ message = "cm3 of gold"
+
+/datum/export/material/market/silver
+ material_id = /datum/material/silver
+ message = "cm3 of silver"
+
+/datum/export/material/market/titanium
+ material_id = /datum/material/titanium
+ message = "cm3 of titanium"
+
+/datum/export/material/market/bscrystal
+ message = "of bluespace crystals"
+ material_id = /datum/material/bluespace
+ export_types = list(/obj/item/stack/sheet/bluespace_crystal, /obj/item/stack/ore) //For whatever reason, bluespace crystals are not a mineral
+
+/datum/export/material/market/iron
+ message = "cm3 of iron"
+ material_id = /datum/material/iron
+ export_types = list(
+ /obj/item/stack/sheet/iron,
+ /obj/item/stack/tile/iron,
+ /obj/item/stack/rods,
+ /obj/item/stack/ore,
+ /obj/item/coin
+ )
+
+/datum/export/material/market/glass
+ message = "cm3 of glass"
+ material_id = /datum/material/glass
+ export_types = list(
+ /obj/item/stack/sheet/glass,
+ /obj/item/stack/ore,
+ /obj/item/shard
+ )
+
+/datum/export/material/market/get_cost(obj/O, apply_elastic = FALSE)
+ var/obj/item/I = O
+ var/amount = get_amount(I)
+ if(!amount)
+ return 0
+ var/material_value = (SSstock_market.materials_prices[material_id]) * amount * MARKET_PROFIT_MODIFIER
+ return round(material_value)
+
+/datum/export/material/market/sell_object(obj/sold_item, datum/export_report/report, dry_run, apply_elastic)
+ . = ..()
+ var/amount = get_amount(sold_item)
+ var/price = get_cost(sold_item)
+ if(!amount)
+ return
+ if(!dry_run)
+ SSstock_market.materials_quantity[material_id] += amount
+ SSstock_market.materials_prices[material_id] -= round((price) * (amount / (amount + SSstock_market.materials_quantity[material_id])))
+ //This formula should impact lower quantity materials greater, and higher quantity materials less. Still, it's a bit rough. Tweaking may be needed.
+
+
+// Stock blocks are a special type of export that can be used to sell a quantity of materials at a specific price on the market.
+/datum/export/stock_block
+ cost = 0
+ message = "stock block"
+ export_types = list(/obj/item/stock_block)
+
+/datum/export/stock_block/get_cost(obj/O, apply_elastic = FALSE)
+ var/obj/item/stock_block/block = O
+ return block.export_value
+
+/datum/export/stock_block/sell_object(obj/sold_item, datum/export_report/report, dry_run, apply_elastic)
+ . = ..()
+ if(dry_run)
+ return
+ var/obj/item/stock_block/sold_block = sold_item
+ var/sale_value = sold_block.export_value
+ SSstock_market.materials_quantity[sold_block.export_mat] += sold_block.quantity
+ SSstock_market.materials_prices[sold_block.export_mat] -= round((sale_value) * (sold_block.quantity / (sold_block.quantity + SSstock_market.materials_quantity[sold_block.export_mat])))
+ SSstock_market.materials_prices[sold_block.export_mat] = round(clamp(SSstock_market.materials_prices[sold_block.export_mat], sold_block.export_mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 0.5 , sold_block.export_mat.value_per_unit * SHEET_MATERIAL_AMOUNT * 3))
diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm
index 12ec7d5b129..a9e11a370e1 100644
--- a/code/modules/cargo/goodies.dm
+++ b/code/modules/cargo/goodies.dm
@@ -129,6 +129,12 @@
cost = PAYCHECK_CREW * 4
contains = list(/obj/item/storage/medkit/toxin)
+/datum/supply_pack/goody/bandagebox_singlepack
+ name = "Box of Bandages Single-Pack"
+ desc = "A single box of DeForest brand bandages. For when you don't want to see your doctor."
+ cost = PAYCHECK_CREW * 3
+ contains = list(/obj/item/storage/box/bandages)
+
/datum/supply_pack/goody/toolbox // mostly just to water down coupon probability
name = "Mechanical Toolbox"
desc = "A fully stocked mechanical toolbox, for when you're too lazy to just print them out."
diff --git a/code/modules/cargo/materials_market.dm b/code/modules/cargo/materials_market.dm
new file mode 100644
index 00000000000..d211df7debd
--- /dev/null
+++ b/code/modules/cargo/materials_market.dm
@@ -0,0 +1,259 @@
+/obj/machinery/materials_market
+ name = "galactic materials market"
+ desc = "This machine allows the user to buy and sell sheets of minerals \
+ across the system. Prices are known to fluxuate quite often,\
+ sometimes even within the same minute. All transactions are final."
+ circuit = /obj/item/circuitboard/machine/materials_market
+ req_access = list(ACCESS_CARGO)
+ density = TRUE
+ icon = 'icons/obj/economy.dmi'
+ icon_state = "mat_market"
+ base_icon_state = "mat_market"
+ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION
+ /// What items can be converted into a stock block? Must be a stack subtype based on current implementation.
+ var/list/exportable_material_items = list(
+ /obj/item/stack/sheet/iron, //God why are we like this
+ /obj/item/stack/sheet/glass, //No really, God why are we like this
+ /obj/item/stack/sheet/mineral,
+ /obj/item/stack/tile/mineral,
+ /obj/item/stack/ore,
+ /obj/item/stack/sheet/bluespace_crystal,
+ /obj/item/stack/rods
+ )
+ /// Are we ordering sheets from our own card balance or the cargo budget?
+ var/ordering_private = TRUE
+ /// Currently, can we order sheets from our own card balance or the cargo budget?
+ var/can_buy_via_budget = FALSE
+
+/obj/machinery/materials_market/update_icon_state()
+ if(panel_open)
+ icon_state = "[base_icon_state]_open"
+ return ..()
+ if(!is_operational || !anchored)
+ icon_state = "[base_icon_state]_off"
+ return ..()
+ icon_state = "[base_icon_state]"
+ return ..()
+
+/obj/machinery/materials_market/wrench_act(mob/living/user, obj/item/tool)
+ ..()
+ default_unfasten_wrench(user, tool, time = 1.5 SECONDS)
+ return TOOL_ACT_TOOLTYPE_SUCCESS
+
+/obj/machinery/materials_market/attackby(obj/item/O, mob/user, params)
+ if(default_deconstruction_screwdriver(user, "[base_icon_state]_open", "[base_icon_state]", O))
+ return
+ else if(default_deconstruction_crowbar(O))
+ return
+ if(is_type_in_list(O, exportable_material_items))
+ var/amount = 0
+ var/value = 0
+ var/material_to_export
+ var/obj/item/stack/exportable = O
+ for(var/datum/material/mat as anything in SSstock_market.materials_prices)
+ if(exportable.has_material_type(mat))
+ amount = exportable.amount
+ value = SSstock_market.materials_prices[mat]
+ material_to_export = mat
+ break //This is only for trading non-alloys, so we can break here
+
+ if(!amount)
+ say("Not enough material. Aborting.")
+ playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE)
+ return TRUE
+ qdel(exportable)
+ var/obj/item/stock_block/new_block = new /obj/item/stock_block(drop_location())
+ new_block.export_value = amount * value * MARKET_PROFIT_MODIFIER
+ new_block.export_mat = material_to_export
+ new_block.quantity = amount
+ to_chat(user, span_notice("You have created a stock block worth [new_block.export_value] cr! Sell it before it becomes liquid!"))
+ playsound(src, 'sound/machines/synth_yes.ogg', 50, FALSE)
+ return TRUE
+ return ..()
+
+
+/obj/machinery/materials_market/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!anchored)
+ return
+ if(!ui)
+ ui = new(user, src, "MatMarket", name)
+ ui.open()
+
+/obj/machinery/materials_market/ui_data(mob/user)
+ var/data = list()
+ var/material_data
+ for(var/datum/material/traded_mat as anything in SSstock_market.materials_prices)
+ var/trend_string = ""
+ if(SSstock_market.materials_trends[traded_mat] == 0)
+ trend_string = "neutral"
+ else if(SSstock_market.materials_trends[traded_mat] == 1)
+ trend_string = "up"
+ else if(SSstock_market.materials_trends[traded_mat] == -1)
+ trend_string = "down"
+ var/color_string = ""
+ if(traded_mat.color)
+ color_string = traded_mat.color
+ else if (traded_mat.greyscale_colors)
+ color_string = splicetext(traded_mat.greyscale_colors, 6, length(traded_mat.greyscale_colors), "") //slice it to a standard 6 char hex
+ material_data += list(list(
+ "name" = traded_mat.name,
+ "price" = SSstock_market.materials_prices[traded_mat],
+ "quantity" = SSstock_market.materials_quantity[traded_mat],
+ "trend" = trend_string,
+ "color" = color_string,
+ ))
+
+ can_buy_via_budget = FALSE
+ var/obj/item/card/id/used_id_card
+ if(isliving(user))
+ var/mob/living/living_user = user
+ used_id_card = living_user.get_idcard(TRUE)
+ can_buy_via_budget = (ACCESS_CARGO in used_id_card?.GetAccess())
+
+ var/balance = 0
+ if(!ordering_private)
+ var/datum/bank_account/dept = SSeconomy.get_dep_account(ACCOUNT_CAR)
+ if(dept)
+ balance = dept.account_balance
+ else
+ balance = used_id_card?.registered_account?.account_balance
+
+ var/market_crashing = FALSE
+ if(HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING))
+ market_crashing = TRUE
+
+ data["catastrophe"] = market_crashing
+ data["materials"] = material_data
+ data["creditBalance"] = balance
+ data["orderingPrive"] = ordering_private
+ data["canOrderCargo"] = can_buy_via_budget
+ return data
+
+/obj/machinery/materials_market/ui_act(action, params)
+ . = ..()
+ if(.)
+ return
+ if(!isliving(usr))
+ return
+ switch(action)
+ if("buy")
+ var/material_str = params["material"]
+ var/quantity = text2num(params["quantity"])
+
+ var/datum/material/material_bought
+ var/obj/item/stack/sheet/sheet_to_buy
+ for(var/datum/material/mat as anything in SSstock_market.materials_prices)
+ if(mat.name == material_str)
+ material_bought = mat
+ 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)
+ 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
+
+ sheet_to_buy = 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
+ if(cost > account_payable.account_balance)
+ to_chat(living_user, span_warning("You don't have enough money to buy that!"))
+ return
+ var/list/things_to_order = list()
+ things_to_order += (sheet_to_buy)
+ things_to_order[sheet_to_buy] = quantity
+ // We want to count how many stacks of all sheets we're ordering to make sure they don't exceed the limit of 10
+ //If we already have a custom order on SSshuttle, we should add the things to order to that order
+ for(var/datum/supply_order/order in SSshuttle.shopping_list)
+ if(order.orderer == living_user && order.orderer_rank == "Galactic Materials Market")
+ var/prior_stacks = 0
+ for(var/obj/item/stack/sheet/sheet as anything in order.pack.contains)
+ prior_stacks += ROUND_UP(order.pack.contains[sheet] / 50)
+ if(prior_stacks >= 10)
+ to_chat(usr, span_notice("You already have 10 stacks of sheets on order! Please wait for them to arrive before ordering more."))
+ playsound(usr, 'sound/machines/synth_no.ogg', 35, FALSE)
+ return
+ 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, \
+ 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,
+ cost_type = "credit",
+ can_be_cancelled = FALSE,
+ )
+ say("Thank you for your purchase! It will arrive on the next cargo shuttle!")
+ SSshuttle.shopping_list += new_order
+ return
+ if("toggle_budget")
+ if(!can_buy_via_budget)
+ return
+ ordering_private = !ordering_private
+
+
+/obj/item/stock_block
+ name = "stock block"
+ desc = "A block of stock. It's worth a certain amount of money, based on a sale on the materials market. Ship it on the cargo shuttle to claim your money."
+ icon = 'icons/obj/economy.dmi'
+ icon_state = "stock_block"
+ /// How many credits was this worth when created?
+ var/export_value = 0
+ /// What is the name of the material this was made from?
+ var/datum/material/export_mat
+ /// Quantity of export material
+ var/quantity = 0
+ /// Is this stock block currently updating it's value with the market (aka fluid)?
+ var/fluid = FALSE
+
+/obj/item/stock_block/examine(mob/user)
+ . = ..()
+ . += span_notice("\The [src] is worth [export_value] cr, from selling [quantity] sheets of [export_mat?.name].")
+ if(fluid)
+ . += span_warning("\The [src] is currently liquid! It's value is based on the market price.")
+ else
+ . += span_notice("\The [src]'s value is still [span_boldnotice("locked in")]. [span_boldnotice("Sell it")] before it's value becomes liquid!")
+
+/obj/item/stock_block/Initialize(mapload)
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(value_warning)), 2.5 MINUTES)
+ addtimer(CALLBACK(src, PROC_REF(update_value)), 5 MINUTES)
+
+/obj/item/stock_block/proc/value_warning()
+ visible_message(span_warning("\The [src] is starting to become liquid!"))
+ icon_state = "stock_block_fluid"
+ update_appearance(UPDATE_ICON_STATE)
+
+/obj/item/stock_block/proc/update_value()
+ if(!export_mat)
+ return
+ if(!SSstock_market.materials_prices[export_mat])
+ return
+ export_value = quantity * SSstock_market.materials_prices[export_mat] * MARKET_PROFIT_MODIFIER
+ icon_state = "stock_block_liquid"
+ update_appearance(UPDATE_ICON_STATE)
+ visible_message(span_warning("\The [src] becomes liquid!"))
+
diff --git a/code/modules/cargo/order.dm b/code/modules/cargo/order.dm
index 6c1f5e1d839..2707719c170 100644
--- a/code/modules/cargo/order.dm
+++ b/code/modules/cargo/order.dm
@@ -188,6 +188,15 @@
generateManifest(miscbox, misc_own, "", misc_cost)
return
+/datum/supply_order/proc/append_order(list/new_contents, cost_increase)
+ for(var/i as anything in new_contents)
+ if(pack.contains[i])
+ pack.contains[i] += new_contents[i]
+ else
+ pack.contains += i
+ pack.contains[i] = new_contents[i]
+ pack.cost += cost_increase
+
#undef MANIFEST_ERROR_CHANCE
#undef MANIFEST_ERROR_NAME
#undef MANIFEST_ERROR_CONTENTS
diff --git a/code/modules/cargo/orderconsole.dm b/code/modules/cargo/orderconsole.dm
index 2c815a803b6..b59cee9def7 100644
--- a/code/modules/cargo/orderconsole.dm
+++ b/code/modules/cargo/orderconsole.dm
@@ -113,9 +113,11 @@
message = blockade_warning
data["message"] = message
+ var/list/amount_by_name = list()
var/cart_list = list()
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(cart_list[order.pack.name])
+ amount_by_name[order.pack.name] += 1
cart_list[order.pack.name][1]["amount"]++
cart_list[order.pack.name][1]["cost"] += order.get_final_cost()
if(order.department_destination)
@@ -124,6 +126,7 @@
cart_list[order.pack.name][1]["paid"]++
continue
+ amount_by_name[order.pack.name] += 1
cart_list[order.pack.name] = list(list(
"cost_type" = order.cost_type,
"object" = order.pack.name,
@@ -141,19 +144,23 @@
data["requests"] = list()
- for(var/datum/supply_order/SO in SSshuttle.request_list)
+ for(var/datum/supply_order/order in SSshuttle.request_list)
+ var/datum/supply_pack/pack = order.pack
+ amount_by_name[pack.name] += 1
data["requests"] += list(list(
- "object" = SO.pack.name,
- "cost" = SO.pack.get_cost(),
- "orderer" = SO.orderer,
- "reason" = SO.reason,
- "id" = SO.id
+ "object" = pack.name,
+ "cost" = pack.get_cost(),
+ "orderer" = order.orderer,
+ "reason" = order.reason,
+ "id" = order.id
))
+ data["amount_by_name"] = amount_by_name
return data
/obj/machinery/computer/cargo/ui_static_data(mob/user)
var/list/data = list()
+ data["max_order"] = CARGO_MAX_ORDER
data["supplies"] = list()
for(var/pack in SSshuttle.supply_packs)
var/datum/supply_pack/P = SSshuttle.supply_packs[pack]
@@ -187,7 +194,7 @@
var/datum/supply_pack/pack = SSshuttle.supply_packs[id]
if(!istype(pack))
CRASH("Unknown supply pack id given by order console ui. ID: [id]")
- if(amount > 50 || amount < 1) // Holy shit fuck off
+ if(amount > CARGO_MAX_ORDER || amount < 1) // Holy shit fuck off
CRASH("Invalid amount passed into add_item")
if((pack.hidden && !(obj_flags & EMAGGED)) || (pack.contraband && !contraband) || pack.drop_pod_only || (pack.special && !pack.special_enabled))
return
@@ -222,8 +229,11 @@
say("[id_card] lacks the requisite access for this purchase.")
return
+ // The list we are operating on right now
+ var/list/working_list = SSshuttle.shopping_list
var/reason = ""
if(requestonly && !self_paid)
+ working_list = SSshuttle.request_list
reason = tgui_input_text(user, "Reason", name)
if(isnull(reason))
return
@@ -233,6 +243,13 @@
say("ERROR: Small crates may only be purchased by private accounts.")
return
+ var/similar_count = SSshuttle.supply.get_order_count(pack)
+ if(similar_count == OVER_ORDER_LIMIT)
+ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
+ return
+
+ amount = clamp(amount, 1, CARGO_MAX_ORDER - similar_count)
for(var/count in 1 to amount)
var/obj/item/coupon/applied_coupon
for(var/obj/item/coupon/coupon_check in loaded_coupons)
@@ -242,15 +259,8 @@
applied_coupon = coupon_check
break
- //Skyrat Edit Add
- var/datum/supply_order/SO = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon, charge_on_purchase = TRUE)
- //Skyrat Edit End
- //SKYRAT EDIT - ORIGINAL: var/datum/supply_order/SO = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon)
-
- if(requestonly && !self_paid)
- SSshuttle.request_list += SO
- else
- SSshuttle.shopping_list += SO
+ var/datum/supply_order/order = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon, charge_on_purchase = TRUE) //SKYRAT EDIT CHANGE - ORIGINAL: var/datum/supply_order/order = new(pack = pack ,orderer = name, orderer_rank = rank, orderer_ckey = ckey, reason = reason, paying_account = account, coupon = applied_coupon)
+ working_list += order
if(self_paid)
say("Order processed. The price will be charged to [account.account_holder]'s bank account on delivery.")
@@ -365,9 +375,7 @@
if("remove")
var/order_name = params["order_name"]
//try removing atleast one item with the specified name. An order may not be removed if it was from the department
- //also we create an copy of the cart list else we would get runtimes when removing & iterating over the same SSshuttle.shopping_list
- var/list/shopping_cart = SSshuttle.shopping_list.Copy()
- for(var/datum/supply_order/order in shopping_cart)
+ for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(order.pack.name != order_name)
continue
if(remove_item(order.id))
@@ -378,8 +386,7 @@
var/order_name = params["order_name"]
//clear out all orders with the above mentioned order_name name to make space for the new amount
- var/list/shopping_cart = SSshuttle.shopping_list.Copy() //we operate on the list copy else we would get runtimes when removing & iterating over the same SSshuttle.shopping_list
- for(var/datum/supply_order/order in shopping_cart) //find corresponding order id for the order name
+ for(var/datum/supply_order/order in SSshuttle.shopping_list) //find corresponding order id for the order name
if(order.pack.name == order_name)
remove_item(order.id)
@@ -387,7 +394,7 @@
var/amount = text2num(params["amount"])
if(amount == 0)
return TRUE
- if(amount > 50)
+ if(amount > CARGO_MAX_ORDER)
return
var/supply_pack_id = name_to_id(order_name) //map order name to supply pack id for adding
if(!supply_pack_id)
diff --git a/code/modules/cargo/packs/_packs.dm b/code/modules/cargo/packs/_packs.dm
index 357f32f3cb4..aaeb55f2533 100644
--- a/code/modules/cargo/packs/_packs.dm
+++ b/code/modules/cargo/packs/_packs.dm
@@ -86,7 +86,7 @@
return FALSE
var/list/open_turfs = list()
for(var/turf/open/floor/found_turf in get_area_turfs(pick(areas), subtypes = TRUE))
- open_turfs += found_turf
+ open_turfs += found_turf
if(!length(open_turfs))
return FALSE
@@ -110,3 +110,15 @@
name = "[purchaser]'s Mining Order"
src.cost = cost
src.contains = contains
+
+/datum/supply_pack/custom/minerals
+ name = "materials order"
+ crate_name = "galactic materials market delivery crate"
+ access = list()
+ crate_type = /obj/structure/closet/crate/cardboard
+
+/datum/supply_pack/custom/minerals/New(purchaser, cost, list/contains)
+ . = ..()
+ name = "[purchaser]'s Materials Order"
+ src.cost = cost
+ src.contains = contains
diff --git a/code/modules/cargo/packs/general.dm b/code/modules/cargo/packs/general.dm
index 5ebdd5087a8..5bfcf01eb00 100644
--- a/code/modules/cargo/packs/general.dm
+++ b/code/modules/cargo/packs/general.dm
@@ -221,6 +221,13 @@
/obj/item/clothing/under/misc/burial = 2,
)
crate_name = "religious supplies crate"
+
+/datum/supply_pack/misc/candles_bulk
+ name = "Candle Box Crate"
+ desc = "Keep your local chapel lit with three candle boxes!"
+ cost = CARGO_CRATE_VALUE * 1.5
+ contains = list(/obj/item/storage/fancy/candle_box = 3)
+ crate_name = "candle box crate"
/datum/supply_pack/misc/toner
name = "Toner Crate"
@@ -285,13 +292,6 @@
crate_value = value
contents_uplink_type = uplink
-/datum/supply_pack/misc/fishing_portal
- name = "Fishing Portal Generator Crate"
- desc = "Not enough fish near your location? Fishing portal has your back."
- cost = CARGO_CRATE_VALUE * 4
- contains = list(/obj/machinery/fishing_portal_generator)
- crate_name = "fishing portal crate"
-
/datum/supply_pack/misc/papercutter
name = "Paper Cutters Crate"
desc = "Contains 3 office-grade paper cutters, equipped with sharp blades that can cut any paper into two thin slips.\
diff --git a/code/modules/cargo/packs/imports.dm b/code/modules/cargo/packs/imports.dm
index ad444efc740..2c90d8603d0 100644
--- a/code/modules/cargo/packs/imports.dm
+++ b/code/modules/cargo/packs/imports.dm
@@ -303,3 +303,17 @@
contraband = TRUE
contains = list(/obj/item/weaponcrafting/giant_wrench)
crate_name = "unknown parts crate"
+
+/datum/supply_pack/imports/materials_market
+ name = "Galactic Materials Market Crate"
+ desc = "A circuit board to build your own materials market for use by certified market traders. Warning: Losses are not covered by insurance."
+ cost = CARGO_CRATE_VALUE * 3
+ contains = list(
+ /obj/item/circuitboard/machine/materials_market = 1,
+ /obj/item/stack/sheet/iron = 5,
+ /obj/item/stack/cable_coil/five = 2,
+ /obj/item/stock_parts/scanning_module = 1,
+ /obj/item/stock_parts/card_reader = 1
+ )
+ crate_name = "materials market crate"
+ crate_type = /obj/structure/closet/crate
diff --git a/code/modules/cargo/packs/livestock.dm b/code/modules/cargo/packs/livestock.dm
index db47abc4d02..d2bdd904e3b 100644
--- a/code/modules/cargo/packs/livestock.dm
+++ b/code/modules/cargo/packs/livestock.dm
@@ -190,7 +190,7 @@
desc = "Tired of these MOTHER FUCKING snakes on this MOTHER FUCKING space station? \
Then this isn't the crate for you. Contains three venomous snakes."
cost = CARGO_CRATE_VALUE * 6
- contains = list(/mob/living/simple_animal/hostile/retaliate/snake = 3)
+ contains = list(/mob/living/basic/snake = 3)
crate_name = "snake crate"
/datum/supply_pack/critter/amphibians
diff --git a/code/modules/cargo/packs/materials.dm b/code/modules/cargo/packs/materials.dm
index 68dacd730be..ba9a162698b 100644
--- a/code/modules/cargo/packs/materials.dm
+++ b/code/modules/cargo/packs/materials.dm
@@ -16,34 +16,6 @@
contains = list(/obj/item/stack/license_plates/empty/fifty)
crate_name = "empty license plate crate"
-/datum/supply_pack/materials/glass50
- name = "50 Glass Sheets"
- desc = "Let some nice light in with fifty glass sheets!"
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/stack/sheet/glass/fifty)
- crate_name = "glass sheets crate"
-
-/datum/supply_pack/materials/iron50
- name = "50 Iron Sheets"
- desc = "Any construction project begins with a good stack of fifty iron sheets!"
- cost = CARGO_CRATE_VALUE * 2
- contains = list(/obj/item/stack/sheet/iron/fifty)
- crate_name = "iron sheets crate"
-
-/datum/supply_pack/materials/plasteel20
- name = "20 Plasteel Sheets"
- desc = "Reinforce the station's integrity with twenty plasteel sheets!"
- cost = CARGO_CRATE_VALUE * 15
- contains = list(/obj/item/stack/sheet/plasteel/twenty)
- crate_name = "plasteel sheets crate"
-
-/datum/supply_pack/materials/plasteel50
- name = "50 Plasteel Sheets"
- desc = "For when you REALLY have to reinforce something."
- cost = CARGO_CRATE_VALUE * 33
- contains = list(/obj/item/stack/sheet/plasteel/fifty)
- crate_name = "plasteel sheets crate"
-
/datum/supply_pack/materials/plastic50
name = "50 Plastic Sheets"
desc = "Build a limitless amount of toys with fifty plastic sheets!"
diff --git a/code/modules/cargo/packs/medical.dm b/code/modules/cargo/packs/medical.dm
index 430c32a35ab..3cfb824b4e6 100644
--- a/code/modules/cargo/packs/medical.dm
+++ b/code/modules/cargo/packs/medical.dm
@@ -89,6 +89,7 @@
/obj/item/reagent_containers/cup/beaker/large,
/obj/item/reagent_containers/pill/insulin,
/obj/item/stack/medical/gauze,
+ /obj/item/storage/box/bandages,
/obj/item/storage/box/beakers,
/obj/item/storage/box/medigels,
/obj/item/storage/box/syringes,
@@ -123,11 +124,11 @@
/datum/supply_pack/medical/surgery
name = "Surgical Supplies Crate"
desc = "Do you want to perform surgery, but don't have one of those fancy \
- shmancy degrees? Just get started with this crate containing a medical duffelbag, \
+ shmancy degrees? Just get started with this crate containing a DeForest surgery tray, \
Sterilizine spray and collapsible roller bed."
cost = CARGO_CRATE_VALUE * 6
contains = list(
- /obj/item/storage/backpack/duffelbag/med/surgery,
+ /obj/item/surgery_tray/full,
/obj/item/reagent_containers/medigel/sterilizine,
/obj/item/emergency_bed,
)
diff --git a/code/modules/cargo/packs/stock_market_items.dm b/code/modules/cargo/packs/stock_market_items.dm
new file mode 100644
index 00000000000..04b2eac4acf
--- /dev/null
+++ b/code/modules/cargo/packs/stock_market_items.dm
@@ -0,0 +1,36 @@
+/**
+ * todo: make this a supply_pack/custom. Drop pog? ohoho yes. Would be VERY fun.
+ */
+/datum/supply_pack/market_materials
+ name = "A Single Sheet of Bananium"
+ desc = "Going market price for this kind of sheet, by Australicus Industrial Mining."
+ cost = CARGO_CRATE_VALUE * 2
+ // contains = list(/obj/item/stack/sheet/mineral/bananium)
+ crate_name = "mineral stock sheet crate"
+ group = "Canisters & Materials"
+ /// What material we are trying to buy sheets of?
+ var/datum/material/material
+ /// How many sheets of the material we are trying to buy at once?
+ var/amount
+
+/datum/supply_pack/market_materials/get_cost()
+ for(var/datum/material/mat in SSstock_market.materials_prices)
+ if(material == mat)
+ return SSstock_market.materials_prices[mat] * amount
+
+/datum/supply_pack/market_materials/fill(obj/structure/closet/crate/C)
+ . = ..()
+ new material.sheet_type(C, amount)
+
+/datum/supply_pack/market_materials/iron
+ name = "Iron Sheets"
+ crate_name = "iron stock crate"
+ material = /datum/material/iron
+MARKET_QUANTITY_HELPERS(/datum/supply_pack/market_materials/iron)
+
+
+/datum/supply_pack/market_materials/gold
+ name = "Gold Sheets"
+ crate_name = "gold stock crate"
+ material = /datum/material/gold
+MARKET_QUANTITY_HELPERS(/datum/supply_pack/market_materials/gold)
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 9c7b5e3e943..56406139379 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -473,6 +473,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
var/nnpa = CONFIG_GET(number/notify_new_player_age)
if (isnum(cached_player_age) && cached_player_age == -1) //first connection
if (nnpa >= 0)
+ log_admin_private("New login: [key_name(key, FALSE, TRUE)] (IP: [address], ID: [computer_id]) logged onto the servers for the first time.")
message_admins("New user: [key_name_admin(src)] is connecting here for the first time.")
if (CONFIG_GET(flag/irc_first_connection_alert))
var/new_player_alert_role = CONFIG_GET(string/new_player_alert_role_id)
diff --git a/code/modules/client/preferences/middleware/antags.dm b/code/modules/client/preferences/middleware/antags.dm
index 3e51218edf2..888da90d5b7 100644
--- a/code/modules/client/preferences/middleware/antags.dm
+++ b/code/modules/client/preferences/middleware/antags.dm
@@ -116,6 +116,7 @@
/datum/asset/spritesheet/antagonists/create_spritesheets()
// Antagonists that don't have a dynamic ruleset, but do have a preference
var/static/list/non_ruleset_antagonists = list(
+ ROLE_CYBER_POLICE = /datum/antagonist/cyber_police,
ROLE_FUGITIVE = /datum/antagonist/fugitive,
ROLE_LONE_OPERATIVE = /datum/antagonist/nukeop/lone,
ROLE_DRIFTING_CONTRACTOR = /datum/antagonist/contractor, //SKYRAT EDIT
diff --git a/code/modules/client/preferences/middleware/tts.dm b/code/modules/client/preferences/middleware/tts.dm
index 71b7b977f4b..4d3ee3261bd 100644
--- a/code/modules/client/preferences/middleware/tts.dm
+++ b/code/modules/client/preferences/middleware/tts.dm
@@ -23,5 +23,5 @@
var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Look at you, Player. A pathetic creature of meat and bone. How can you challenge a perfect, immortal machine?", speaker = speaker, pitch = pitch, silicon = TRUE, local = TRUE)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Look at you, Player. A pathetic creature of meat and bone. How can you challenge a perfect, immortal machine?", speaker = speaker, pitch = pitch, special_filters = TTS_FILTER_SILICON, local = TRUE)
return TRUE
diff --git a/code/modules/client/preferences/prosthetic.dm b/code/modules/client/preferences/prosthetic.dm
new file mode 100644
index 00000000000..f66f1278c48
--- /dev/null
+++ b/code/modules/client/preferences/prosthetic.dm
@@ -0,0 +1,17 @@
+/datum/preference/choiced/prosthetic
+ category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
+ savefile_key = "prosthetic"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/prosthetic/init_possible_values()
+ return list("Random") + GLOB.limb_choice
+
+/datum/preference/choiced/prosthetic/is_accessible(datum/preferences/preferences)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ return "Prosthetic Limb" in preferences.all_quirks
+
+/datum/preference/choiced/prosthetic/apply_to_human(mob/living/carbon/human/target, value)
+ return
diff --git a/code/modules/clothing/belts/polymorph_belt.dm b/code/modules/clothing/belts/polymorph_belt.dm
index e63e2c3bee3..73959d6d415 100644
--- a/code/modules/clothing/belts/polymorph_belt.dm
+++ b/code/modules/clothing/belts/polymorph_belt.dm
@@ -35,8 +35,8 @@
return slot & ITEM_SLOT_BELT
/obj/item/polymorph_belt/update_icon_state()
- icon_state = base_icon_state + (active) ? "" : "_inactive"
- worn_icon_state = base_icon_state + (active) ? "" : "_inactive"
+ icon_state = base_icon_state + (active ? "" : "_inactive")
+ worn_icon_state = base_icon_state + (active ? "" : "_inactive")
return ..()
/obj/item/polymorph_belt/attackby(obj/item/weapon, mob/user, params)
diff --git a/code/modules/clothing/ears/_ears.dm b/code/modules/clothing/ears/_ears.dm
index c4001d23629..5ae5b628808 100644
--- a/code/modules/clothing/ears/_ears.dm
+++ b/code/modules/clothing/ears/_ears.dm
@@ -20,6 +20,7 @@
equip_delay_other = 25
resistance_flags = FLAMMABLE
custom_price = PAYCHECK_COMMAND * 1.5
+ flags_cover = EARS_COVERED
/obj/item/clothing/ears/earmuffs/Initialize(mapload)
. = ..()
diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm
index fc665e57c8f..4c7fce6633c 100644
--- a/code/modules/clothing/glasses/_glasses.dm
+++ b/code/modules/clothing/glasses/_glasses.dm
@@ -352,6 +352,18 @@
glass_colour_type = /datum/client_colour/glass_colour/gray
dog_fashion = /datum/dog_fashion/head
+/obj/item/clothing/glasses/sunglasses/Initialize(mapload)
+ . = ..()
+ add_glasses_slapcraft_component()
+
+/obj/item/clothing/glasses/sunglasses/proc/add_glasses_slapcraft_component()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/hudsunsec, /datum/crafting_recipe/hudsunmed, /datum/crafting_recipe/hudsundiag, /datum/crafting_recipe/scienceglasses)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/glasses/sunglasses/reagent
name = "beer goggles"
icon_state = "sunhudbeer"
@@ -364,6 +376,14 @@
desc = "A pair of tacky purple sunglasses that allow the wearer to recognize various chemical compounds with only a glance."
clothing_traits = list(TRAIT_REAGENT_SCANNER, TRAIT_RESEARCH_SCANNER)
+/obj/item/clothing/glasses/sunglasses/chemical/add_glasses_slapcraft_component()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/scienceglassesremoval)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/glasses/sunglasses/gar
name = "black gar glasses"
desc = "Go beyond impossible and kick reason to the curb!"
diff --git a/code/modules/clothing/glasses/hud.dm b/code/modules/clothing/glasses/hud.dm
index 5018e4185c8..34359eaa0b3 100644
--- a/code/modules/clothing/glasses/hud.dm
+++ b/code/modules/clothing/glasses/hud.dm
@@ -92,6 +92,15 @@
tint = 1
glass_colour_type = /datum/client_colour/glass_colour/blue
+/obj/item/clothing/glasses/hud/health/sunglasses/Initialize(mapload)
+ . = ..()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/hudsunmedremoval)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/glasses/hud/diagnostic
name = "diagnostic HUD"
desc = "A heads-up display capable of analyzing the integrity and status of robotics and exosuits."
@@ -118,6 +127,15 @@
flash_protect = FLASH_PROTECTION_FLASH
tint = 1
+/obj/item/clothing/glasses/hud/diagnostic/sunglasses/Initialize(mapload)
+ . = ..()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/hudsundiagremoval)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/glasses/hud/security
name = "security HUD"
desc = "A heads-up display that scans the humanoids in view and provides accurate data about their ID status and security records."
@@ -152,6 +170,15 @@
tint = 1
glass_colour_type = /datum/client_colour/glass_colour/darkred
+/obj/item/clothing/glasses/hud/security/sunglasses/Initialize(mapload)
+ . = ..()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/hudsunsecremoval)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/glasses/hud/security/night
name = "night vision security HUD"
desc = "An advanced heads-up display that provides ID data and vision in complete darkness."
diff --git a/code/modules/clothing/gloves/color.dm b/code/modules/clothing/gloves/color.dm
index d3aed125749..f77f6cc2c67 100644
--- a/code/modules/clothing/gloves/color.dm
+++ b/code/modules/clothing/gloves/color.dm
@@ -26,6 +26,16 @@
)
)
// SKYRAT EDIT ADDITION END
+
+/obj/item/clothing/gloves/color/black/Initialize(mapload)
+ . = ..()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/radiogloves)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/gloves/fingerless
name = "fingerless gloves"
desc = "Plain black gloves without fingertips for the hard-working."
@@ -39,6 +49,15 @@
undyeable = TRUE
clothing_traits = list(TRAIT_FINGERPRINT_PASSTHROUGH)
+/obj/item/clothing/gloves/color/fingerless/Initialize(mapload)
+ . = ..()
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/gripperoffbrand)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/clothing/gloves/color/orange
name = "orange gloves"
desc = "A pair of gloves, they don't look special in any way."
diff --git a/code/modules/clothing/head/helmet.dm b/code/modules/clothing/head/helmet.dm
index 488bd7448a7..f8cd88923ec 100644
--- a/code/modules/clothing/head/helmet.dm
+++ b/code/modules/clothing/head/helmet.dm
@@ -12,7 +12,7 @@
max_heat_protection_temperature = HELMET_MAX_TEMP_PROTECT
strip_delay = 60
clothing_flags = SNUG_FIT | STACKABLE_HELMET_EXEMPT
- flags_cover = HEADCOVERSEYES
+ flags_cover = HEADCOVERSEYES|EARS_COVERED
flags_inv = HIDEHAIR
dog_fashion = /datum/dog_fashion/head/helmet
diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm
index 4022e259505..3c26de45e64 100644
--- a/code/modules/clothing/head/jobs.dm
+++ b/code/modules/clothing/head/jobs.dm
@@ -154,6 +154,13 @@
flags_inv = HIDEHAIR
flags_cover = HEADCOVERSEYES
+/obj/item/clothing/head/chaplain/habit_veil
+ name = "nun veil"
+ desc = "No nunsene clothing."
+ icon_state = "nun_hood_alt"
+ flags_inv = HIDEHAIR | HIDEEARS
+ clothing_flags = SNUG_FIT // can't be knocked off by throwing a paper hat.
+
/obj/item/clothing/head/chaplain/bishopmitre
name = "bishop mitre"
desc = "An opulent hat that functions as a radio to God. Or as a lightning rod, depending on who you ask."
@@ -246,7 +253,7 @@
var/prefix_index = findtext(raw_message, prefix)
if(prefix_index != 1)
return FALSE
-
+
var/the_phrase = trim_left(replacetext(raw_message, prefix, ""))
var/obj/item/result = items_by_phrase[the_phrase]
if(!result)
@@ -592,6 +599,83 @@
icon_state = "surgicalcapblack"
desc = "A black medical surgery cap to prevent the surgeon's hair from entering the insides of the patient!"
+/obj/item/clothing/head/utility/head_mirror
+ name = "head mirror"
+ desc = "Used by doctors to look into a patient's eyes, ears, and mouth. \
+ A little useless now, given the technology available, but it certainly completes the look."
+ icon_state = "headmirror"
+ body_parts_covered = NONE
+
+/obj/item/clothing/head/utility/head_mirror/examine(mob/user)
+ . = ..()
+ . += span_notice("In a properly lit room, you can use this to examine people's eyes, ears, and mouth closer.")
+
+/obj/item/clothing/head/utility/head_mirror/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot & slot_flags)
+ RegisterSignal(user, COMSIG_MOB_EXAMINING_MORE, PROC_REF(examining))
+ else
+ UnregisterSignal(user, COMSIG_MOB_EXAMINING_MORE)
+
+/obj/item/clothing/head/utility/head_mirror/dropped(mob/living/user)
+ . = ..()
+ UnregisterSignal(user, COMSIG_MOB_EXAMINING_MORE)
+
+/obj/item/clothing/head/utility/head_mirror/proc/examining(mob/living/examiner, atom/examining, list/examine_list)
+ SIGNAL_HANDLER
+ if(!ishuman(examining) || examining == examiner || examiner.is_blind() || !examiner.Adjacent(examining))
+ return
+ var/mob/living/carbon/human/human_examined = examining
+ if(!human_examined.get_bodypart(BODY_ZONE_HEAD))
+ return
+ if(!examiner.has_light_nearby())
+ examine_list += span_warning("You attempt to use your [name] to examine [examining]'s head better... but it's too dark. Should've invested in a head lamp.")
+ return
+ if(examiner.dir == examining.dir) // disallow examine from behind - every other dir is OK
+ examine_list += span_warning("You attempt to use your [name] to examine [examining]'s head better... but [examining.p_theyre()] facing the wrong way.")
+ return
+
+ var/list/final_message = list("You examine [examining]'s head closer with your [name], you notice [examining.p_they()] [examining.p_have()]...")
+ if(human_examined.is_mouth_covered())
+ final_message += "\tYou can't see [examining.p_their()] mouth."
+ else
+ var/obj/item/organ/internal/tongue/has_tongue = human_examined.get_organ_slot(ORGAN_SLOT_TONGUE)
+ var/pill_count = 0
+ for(var/datum/action/item_action/hands_free/activate_pill/pill in human_examined.actions)
+ pill_count++
+
+ if(pill_count >= 1 && has_tongue)
+ final_message += "\t[pill_count] pill\s in [examining.p_their()] mouth, and \a [has_tongue]."
+ else if(pill_count >= 1)
+ final_message += "\t[pill_count] pill\s in [examining.p_their()] mouth, but oddly no tongue."
+ else if(has_tongue)
+ final_message += "\t\A [has_tongue] in [examining.p_their()] mouth - go figure."
+ else
+ final_message += "\tNo tongue in [examining.p_their()] mouth, oddly enough."
+
+ if(human_examined.is_ears_covered())
+ final_message += "\tYou can't see [examining.p_their()] ears."
+ else
+ var/obj/item/organ/internal/ears/has_ears = human_examined.get_organ_slot(ORGAN_SLOT_EARS)
+ if(has_ears)
+ if(has_ears.deaf)
+ final_message += "\tDamaged eardrums in [examining.p_their()] ear canals."
+ else
+ final_message += "\tA set of [has_ears.damage ? "" : "healthy "][has_ears.name]."
+ else
+ final_message += "\tNo eardrums and empty ear canals... how peculiar."
+
+ if(human_examined.is_eyes_covered())
+ final_message += "\tYou can't see [examining.p_their()] eyes."
+ else
+ var/obj/item/organ/internal/eyes/has_eyes = human_examined.get_organ_slot(ORGAN_SLOT_EYES)
+ if(has_eyes)
+ final_message += "\tA pair of [has_eyes.damage ? "" : "healthy "][has_eyes.name]."
+ else
+ final_message += "\tEmpty eye sockets."
+
+ examine_list += span_notice("[jointext(final_message, "\n")]")
+
//Engineering
/obj/item/clothing/head/beret/engi
name = "engineering beret"
diff --git a/code/modules/clothing/masks/_masks.dm b/code/modules/clothing/masks/_masks.dm
index dfcc3060c9f..ad296d30356 100644
--- a/code/modules/clothing/masks/_masks.dm
+++ b/code/modules/clothing/masks/_masks.dm
@@ -12,6 +12,10 @@
var/adjusted_flags = null
///Did we install a filtering cloth?
var/has_filter = FALSE
+ /// If defined, what voice should we override with if TTS is active?
+ var/voice_override
+ /// If set to true, activates the radio effect on TTS. Used for sec hailers, but other masks can utilize it for their own vocal effect.
+ var/use_radio_beeps_tts = FALSE
/obj/item/clothing/mask/attack_self(mob/user)
if((clothing_flags & VOICEBOX_TOGGLABLE))
diff --git a/code/modules/clothing/masks/gasmask.dm b/code/modules/clothing/masks/gasmask.dm
index 7b415bac6dc..c8784d9af6c 100644
--- a/code/modules/clothing/masks/gasmask.dm
+++ b/code/modules/clothing/masks/gasmask.dm
@@ -28,7 +28,7 @@ GLOBAL_LIST_INIT(clown_mask_options, list(
var/has_fov = TRUE
///Cigarette in the mask
var/obj/item/clothing/mask/cigarette/cig
-
+ voice_filter = "lowpass=f=750,volume=2"
/datum/armor/mask_gas
bio = 100
@@ -274,6 +274,7 @@ GLOBAL_LIST_INIT(clown_mask_options, list(
dog_fashion = /datum/dog_fashion/head/clown
has_fov = FALSE
var/list/clownmask_designs = list()
+ voice_filter = null // performer masks expect to be talked through
/obj/item/clothing/mask/gas/clown_hat/plasmaman
starting_filter_type = /obj/item/gas_filter/plasmaman
diff --git a/code/modules/clothing/masks/hailer.dm b/code/modules/clothing/masks/hailer.dm
index af1d3975645..64de19b95aa 100644
--- a/code/modules/clothing/masks/hailer.dm
+++ b/code/modules/clothing/masks/hailer.dm
@@ -68,6 +68,8 @@ GLOBAL_LIST_INIT(hailer_phrases, list(
var/recent_uses = 0
///Whether the hailer is emagged or not
var/safety = TRUE
+ voice_filter = @{"[0:a] asetrate=%SAMPLE_RATE%*0.7,aresample=16000,atempo=1/0.7,lowshelf=g=-20:f=500,highpass=f=500,aphaser=in_gain=1:out_gain=1:delay=3.0:decay=0.4:speed=0.5:type=t [out]; [out]atempo=1.2,volume=15dB [final]; anoisesrc=a=0.01:d=60 [noise]; [final][noise] amix=duration=shortest"}
+ use_radio_beeps_tts = TRUE
/obj/item/clothing/mask/gas/sechailer/plasmaman
starting_filter_type = /obj/item/gas_filter/plasmaman
diff --git a/code/modules/clothing/outfits/plasmaman.dm b/code/modules/clothing/outfits/plasmaman.dm
index daad81ea475..a422d2d736e 100644
--- a/code/modules/clothing/outfits/plasmaman.dm
+++ b/code/modules/clothing/outfits/plasmaman.dm
@@ -281,3 +281,10 @@
gloves = /obj/item/clothing/gloves/color/plasmaman/clown
head = /obj/item/clothing/head/helmet/space/plasmaman/clown
mask = /obj/item/clothing/mask/gas/clown_hat/plasmaman
+
+/datum/outfit/plasmaman/bitrunner
+ name = "Bitrunner Plasmaman"
+
+ uniform = /obj/item/clothing/under/plasmaman/bitrunner
+ gloves = /obj/item/clothing/gloves/color/plasmaman/black
+ head = /obj/item/clothing/head/helmet/space/plasmaman/bitrunner
diff --git a/code/modules/clothing/shoes/cowboy.dm b/code/modules/clothing/shoes/cowboy.dm
index e6f02264d35..05792a72cbd 100644
--- a/code/modules/clothing/shoes/cowboy.dm
+++ b/code/modules/clothing/shoes/cowboy.dm
@@ -17,7 +17,7 @@
if(prob(2))
//There's a snake in my boot
- new /mob/living/simple_animal/hostile/retaliate/snake(src)
+ new /mob/living/basic/snake(src)
/obj/item/clothing/shoes/cowboy/equipped(mob/living/carbon/user, slot)
@@ -56,7 +56,7 @@
if(contents.len >= max_occupants)
to_chat(user, span_warning("[src] are full!"))
return
- if(istype(target, /mob/living/simple_animal/hostile/retaliate/snake) || istype(target, /mob/living/basic/headslug) || islarva(target))
+ if(istype(target, /mob/living/basic/snake) || istype(target, /mob/living/basic/headslug) || islarva(target))
target.forceMove(src)
to_chat(user, span_notice("[target] slithers into [src]."))
diff --git a/code/modules/clothing/spacesuits/plasmamen.dm b/code/modules/clothing/spacesuits/plasmamen.dm
index ceb31b23a28..30e43c793aa 100644
--- a/code/modules/clothing/spacesuits/plasmamen.dm
+++ b/code/modules/clothing/spacesuits/plasmamen.dm
@@ -443,3 +443,8 @@
or they've murdered one of your fellow badasses and have taken it from them as a trophy. Either way, anyone wearing this deserves at least a cursory nod of respect."
icon_state = "syndie_envirohelm"
inhand_icon_state = null
+
+/obj/item/clothing/head/helmet/space/plasmaman/bitrunner
+ name = "bitrunner's plasma envirosuit helmet"
+ desc = "An envirohelmet with extended blue light filters for bitrunning plasmamen."
+ icon_state = "bitrunner_envirohelm"
diff --git a/code/modules/clothing/suits/_suits.dm b/code/modules/clothing/suits/_suits.dm
index 13c9c358fac..84cb98049e1 100644
--- a/code/modules/clothing/suits/_suits.dm
+++ b/code/modules/clothing/suits/_suits.dm
@@ -8,6 +8,7 @@
/obj/item/tank/internals/emergency_oxygen,
/obj/item/tank/internals/plasmaman,
/obj/item/tank/jetpack/oxygen/captain,
+ /obj/item/storage/belt/holster,
)
armor_type = /datum/armor/none
drop_sound = 'sound/items/handling/cloth_drop.ogg'
diff --git a/code/modules/clothing/suits/jacket.dm b/code/modules/clothing/suits/jacket.dm
index ffd67ce4a65..6db889032c0 100644
--- a/code/modules/clothing/suits/jacket.dm
+++ b/code/modules/clothing/suits/jacket.dm
@@ -1,7 +1,16 @@
/obj/item/clothing/suit/jacket
icon = 'icons/obj/clothing/suits/jacket.dmi'
worn_icon = 'icons/mob/clothing/suits/jacket.dmi'
- allowed = list(/obj/item/flashlight, /obj/item/tank/internals/emergency_oxygen, /obj/item/tank/internals/plasmaman, /obj/item/toy, /obj/item/storage/fancy/cigarettes, /obj/item/lighter, /obj/item/radio)
+ allowed = list(
+ /obj/item/flashlight,
+ /obj/item/tank/internals/emergency_oxygen,
+ /obj/item/tank/internals/plasmaman,
+ /obj/item/toy,
+ /obj/item/storage/fancy/cigarettes,
+ /obj/item/lighter,
+ /obj/item/radio,
+ /obj/item/storage/belt/holster,
+ )
body_parts_covered = CHEST|GROIN|ARMS
cold_protection = CHEST|GROIN|ARMS
min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT
diff --git a/code/modules/clothing/suits/reactive_armour.dm b/code/modules/clothing/suits/reactive_armour.dm
index 9537fa7b6ef..6c33e287f03 100644
--- a/code/modules/clothing/suits/reactive_armour.dm
+++ b/code/modules/clothing/suits/reactive_armour.dm
@@ -226,7 +226,7 @@
emp_message = span_warning("The tesla capacitors beep ominously for a moment.")
clothing_traits = list(TRAIT_TESLA_SHOCKIMMUNE)
/// How strong are the zaps we give off?
- var/zap_power = 25000
+ var/zap_power = 1e7
/// How far to the zaps we give off go?
var/zap_range = 20
/// What flags do we pass to the zaps we give off?
diff --git a/code/modules/clothing/suits/utility.dm b/code/modules/clothing/suits/utility.dm
index 2f29a2233d5..bbd880f5466 100644
--- a/code/modules/clothing/suits/utility.dm
+++ b/code/modules/clothing/suits/utility.dm
@@ -104,6 +104,7 @@
laser = 20
energy = 30
bomb = 100
+ bio = 50
fire = 80
acid = 50
diff --git a/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm b/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
index 1590fa77138..a8674b03c94 100644
--- a/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
+++ b/code/modules/clothing/under/jobs/Plasmaman/civilian_service.dm
@@ -115,6 +115,12 @@
icon_state = "clown_envirosuit"
inhand_icon_state = null
+/obj/item/clothing/under/plasmaman/bitrunner
+ name = "bitrunner envirosuit"
+ desc = "An envirosuit specially designed for plasmamen with bad posture."
+ icon_state = "bitrunner_envirosuit"
+ inhand_icon_state = null
+
/obj/item/clothing/under/plasmaman/clown/Initialize(mapload)
. = ..()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_CLOWN, CELL_VIRUS_TABLE_GENERIC, rand(2,3), 0)
diff --git a/code/modules/clothing/under/jobs/cargo.dm b/code/modules/clothing/under/jobs/cargo.dm
index 4b2e74bff38..e3145fb740d 100644
--- a/code/modules/clothing/under/jobs/cargo.dm
+++ b/code/modules/clothing/under/jobs/cargo.dm
@@ -62,3 +62,9 @@
desc = "A grey uniform for operating in hazardous environments."
icon_state = "explorer"
inhand_icon_state = null
+
+/obj/item/clothing/under/rank/cargo/bitrunner
+ name = "bitrunner's jumpsuit"
+ desc = "It's a leathery jumpsuit worn by a bitrunner. Tacky, but comfortable to wear if sitting for prolonged periods of time."
+ icon_state = "bitrunner"
+ inhand_icon_state = "w_suit"
diff --git a/code/modules/economy/account.dm b/code/modules/economy/account.dm
index 3be319fd233..b4703945092 100644
--- a/code/modules/economy/account.dm
+++ b/code/modules/economy/account.dm
@@ -7,6 +7,8 @@
var/account_balance = 0
///How many mining points (shaft miner credits) is held in the bank account, used for mining vendors.
var/mining_points = 0
+ /// Points for bit runner's vendor. Awarded for completing virtual domains.
+ var/bitrunning_points = 0
///Debt. If higher than 0, A portion of the credits is earned (or the whole debt, whichever is lower) will go toward paying it off.
var/account_debt = 0
///If there are things effecting how much income a player will get, it's reflected here 1 is standard for humans.
diff --git a/code/modules/events/ghost_role/sentience.dm b/code/modules/events/ghost_role/sentience.dm
index caf49e13b6b..da380278608 100644
--- a/code/modules/events/ghost_role/sentience.dm
+++ b/code/modules/events/ghost_role/sentience.dm
@@ -4,17 +4,17 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list(
/mob/living/basic/carp/pet/cayenne,
/mob/living/basic/chicken,
/mob/living/basic/cow,
- /mob/living/basic/spider/giant/sgt_araneus,
/mob/living/basic/lizard,
/mob/living/basic/mouse/brown/tom,
/mob/living/basic/pet,
/mob/living/basic/pig,
/mob/living/basic/rabbit,
/mob/living/basic/sheep,
+ /mob/living/basic/snake,
+ /mob/living/basic/spider/giant/sgt_araneus,
/mob/living/simple_animal/bot/secbot/beepsky,
/mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
- /mob/living/simple_animal/hostile/retaliate/snake,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet,
/mob/living/simple_animal/sloth,
diff --git a/code/modules/events/mice_migration.dm b/code/modules/events/mice_migration.dm
index e7f31567f4c..450f9100800 100644
--- a/code/modules/events/mice_migration.dm
+++ b/code/modules/events/mice_migration.dm
@@ -23,7 +23,7 @@
priority_announce("Due to [cause], [plural] [name] have [movement] \
into the [location].", "Migration Alert",
- 'sound/effects/mousesqueek.ogg')
+ 'sound/creatures/mousesqueek.ogg')
/datum/round_event/mice_migration/start()
SSminor_mapping.trigger_migration(rand(minimum_mice, maximum_mice))
diff --git a/code/modules/events/wizard/blobies.dm b/code/modules/events/wizard/blobies.dm
index 307d01ff7eb..0a9c96d5135 100644
--- a/code/modules/events/wizard/blobies.dm
+++ b/code/modules/events/wizard/blobies.dm
@@ -10,4 +10,4 @@
/datum/round_event/wizard/blobies/start()
for(var/mob/living/carbon/human/H in GLOB.dead_mob_list)
- new /mob/living/simple_animal/hostile/blob/blobspore(H.loc)
+ new /mob/living/basic/blob_minion/spore/minion(H.loc) // Creates zombies which ghosts can control
diff --git a/code/modules/events/wizard/petsplosion.dm b/code/modules/events/wizard/petsplosion.dm
index 7ca7ef2ba2d..33f7718f740 100644
--- a/code/modules/events/wizard/petsplosion.dm
+++ b/code/modules/events/wizard/petsplosion.dm
@@ -5,7 +5,6 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list(
/mob/living/basic/carp/pet/cayenne,
/mob/living/basic/chicken,
/mob/living/basic/cow,
- /mob/living/basic/spider/giant/sgt_araneus,
/mob/living/basic/lizard,
/mob/living/basic/mothroach,
/mob/living/basic/mouse/brown/tom,
@@ -13,9 +12,10 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list(
/mob/living/basic/pig,
/mob/living/basic/rabbit,
/mob/living/basic/sheep,
+ /mob/living/basic/snake,
+ /mob/living/basic/spider/giant/sgt_araneus,
/mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
- /mob/living/simple_animal/hostile/retaliate/snake,
/mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet,
/mob/living/simple_animal/sloth,
diff --git a/code/modules/experisci/destructive_scanner.dm b/code/modules/experisci/destructive_scanner.dm
index 3591a31cd7b..ef89dc9b94a 100644
--- a/code/modules/experisci/destructive_scanner.dm
+++ b/code/modules/experisci/destructive_scanner.dm
@@ -19,10 +19,17 @@
// Late load to ensure the component initialization occurs after the machines are initialized
/obj/machinery/destructive_scanner/LateInitialize()
. = ..()
+
+ var/static/list/destructive_signals = list(
+ COMSIG_MACHINERY_DESTRUCTIVE_SCAN = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_destructive_experiment),
+ )
+
AddComponent(/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/scanning),\
config_mode = EXPERIMENT_CONFIG_CLICK, \
- start_experiment_callback = CALLBACK(src, PROC_REF(activate)))
+ start_experiment_callback = CALLBACK(src, PROC_REF(activate)), \
+ experiment_signals = destructive_signals, \
+ )
///Activates the machine; checks if it can actually scan, then starts.
/obj/machinery/destructive_scanner/proc/activate()
diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm
index ab971196092..1259f56597d 100644
--- a/code/modules/experisci/experiment/experiments.dm
+++ b/code/modules/experisci/experiment/experiments.dm
@@ -44,7 +44,7 @@
/mob/living/basic/chicken,
/mob/living/basic/cow,
/mob/living/basic/pet/dog/corgi,
- /mob/living/simple_animal/hostile/retaliate/snake,
+ /mob/living/basic/snake,
/mob/living/simple_animal/pet/cat,
)
@@ -234,7 +234,7 @@
/obj/machinery/biogenerator = 3,
/obj/machinery/gibber = 3,
/obj/machinery/chem_master = 3,
- /obj/machinery/atmospherics/components/unary/cryo_cell = 3,
+ /obj/machinery/cryo_cell = 3,
/obj/machinery/harvester = 5,
/obj/machinery/quantumpad = 5
)
@@ -291,7 +291,6 @@
/obj/machinery/rnd/experimentor = 1,
/obj/machinery/medical_kiosk = 2,
/obj/machinery/piratepad/civilian = 2,
- /obj/machinery/rnd/bepis = 3
)
required_stock_part = /obj/item/stock_parts/scanning_module/adv
@@ -332,7 +331,7 @@
///Damage percent that each mech needs to be at for a scan to work.
var/damage_percent
-/datum/experiment/scanning/random/mecha_damage_scan/New()
+/datum/experiment/scanning/random/mecha_damage_scan/New(datum/techweb/techweb)
. = ..()
damage_percent = rand(15, 95)
//updating the description with the damage_percent var set
diff --git a/code/modules/experisci/experiment/handlers/experiment_handler.dm b/code/modules/experisci/experiment/handlers/experiment_handler.dm
index abc5d4ad1dd..29e7da95391 100644
--- a/code/modules/experisci/experiment/handlers/experiment_handler.dm
+++ b/code/modules/experisci/experiment/handlers/experiment_handler.dm
@@ -38,6 +38,7 @@
disallowed_traits = null,
config_flags = null,
datum/callback/start_experiment_callback = null,
+ list/experiment_signals
)
. = ..()
if(!ismovable(parent))
@@ -49,13 +50,8 @@
src.config_flags = config_flags
src.start_experiment_callback = start_experiment_callback
- if(isitem(parent))
- RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(try_run_handheld_experiment))
- RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, PROC_REF(ignored_handheld_experiment_attempt))
- if(istype(parent, /obj/machinery/destructive_scanner))
- RegisterSignal(parent, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, PROC_REF(try_run_destructive_experiment))
- if(istype(parent, /obj/machinery/computer/operating))
- RegisterSignal(parent, COMSIG_OPERATING_COMPUTER_AUTOPSY_COMPLETE, PROC_REF(try_run_autopsy_experiment))
+ for(var/signal in experiment_signals)
+ RegisterSignal(parent, signal, experiment_signals[signal])
// Determine UI display mode
switch(config_mode)
@@ -85,9 +81,9 @@
*/
/datum/component/experiment_handler/proc/try_run_handheld_experiment(datum/source, atom/target, mob/user, params)
SIGNAL_HANDLER
- if (!should_run_handheld_experiment(source, target, user, params))
+ if (!should_run_handheld_experiment(source, target, user))
return
- INVOKE_ASYNC(src, PROC_REF(try_run_handheld_experiment_async), source, target, user, params)
+ INVOKE_ASYNC(src, PROC_REF(try_run_handheld_experiment_async), source, target, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/**
@@ -98,7 +94,7 @@
if (!proximity_flag)
return
. |= COMPONENT_AFTERATTACK_PROCESSED_ITEM
- if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
+ if ((selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE)) || config_flags & EXPERIMENT_CONFIG_SILENT_FAIL)
return .
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, span_notice("[target] is not related to your currently selected experiment."))
@@ -107,7 +103,7 @@
/**
* Checks that an experiment can be run using the provided target, used for preventing the cancellation of the attack chain inappropriately
*/
-/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user, params)
+/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user)
// Check that there is actually an experiment selected
if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
return
@@ -127,16 +123,17 @@
/**
* This proc exists because Jared Fogle really likes async
*/
-/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user, params)
+/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user)
if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
- to_chat(user, span_notice("You do not have an experiment selected!"))
+ if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
+ to_chat(user, span_notice("You do not have an experiment selected!"))
return
- if(!do_after(user, 1 SECONDS, target = target))
+ if(!(config_flags & EXPERIMENT_CONFIG_IMMEDIATE_ACTION) && !do_after(user, 1 SECONDS, target = target))
return
if(action_experiment(source, target))
playsound(user, 'sound/machines/ping.ogg', 25)
to_chat(user, span_notice("You scan [target]."))
- else
+ else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, span_notice("[target] is not related to your currently selected experiment."))
@@ -148,8 +145,9 @@
SIGNAL_HANDLER
var/atom/movable/our_scanner = parent
if (selected_experiment == null)
- playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25)
- to_chat(our_scanner, span_notice("No experiment selected!"))
+ if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
+ playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25)
+ to_chat(our_scanner, span_notice("No experiment selected!"))
return
var/successful_scan
for(var/scan_target in scanned_atoms)
@@ -159,7 +157,7 @@
if(successful_scan)
playsound(our_scanner, 'sound/machines/ping.ogg', 25)
to_chat(our_scanner, span_notice("The scan succeeds."))
- else
+ else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
playsound(src, 'sound/machines/buzz-sigh.ogg', 25)
our_scanner.say("The scan did not result in anything.")
@@ -261,6 +259,7 @@
/datum/component/experiment_handler/proc/link_techweb(datum/techweb/new_web)
if (new_web == linked_web)
return
+ selected_experiment?.on_unselected(src)
selected_experiment = null
linked_web = new_web
@@ -268,6 +267,7 @@
* Unlinks this handler from the selected techweb
*/
/datum/component/experiment_handler/proc/unlink_techweb()
+ selected_experiment?.on_unselected(src)
selected_experiment = null
linked_web = null
@@ -278,13 +278,15 @@
* * experiment - The experiment to attempt to link to
*/
/datum/component/experiment_handler/proc/link_experiment(datum/experiment/experiment)
- if (experiment && can_select_experiment(experiment))
+ if (can_select_experiment(experiment))
selected_experiment = experiment
+ selected_experiment.on_selected(src)
/**
* Unlinks this handler from the selected experiment
*/
/datum/component/experiment_handler/proc/unlink_experiment()
+ selected_experiment?.on_unselected(src)
selected_experiment = null
/**
@@ -299,31 +301,19 @@
return FALSE
// Check against the list of allowed experimentors
- if (experiment.allowed_experimentors && experiment.allowed_experimentors.len)
- var/matched = FALSE
- for (var/experimentor in experiment.allowed_experimentors)
- if (istype(parent, experimentor))
- matched = TRUE
- break
- if (!matched)
- return FALSE
+ if (length(experiment.allowed_experimentors) && !is_type_in_list(parent, experiment.allowed_experimentors))
+ return FALSE
// Check that this experiment is visible currently
- if (!linked_web || !(experiment in linked_web.available_experiments))
+ if (!(experiment in linked_web?.available_experiments))
return FALSE
// Check that this experiment type isn't blacklisted
- for (var/badsci in blacklisted_experiments)
- if (istype(experiment, badsci))
- return FALSE
-
- // Check against the allowed experiment types
- for (var/goodsci in allowed_experiments)
- if (istype(experiment, goodsci))
- return TRUE
+ if(is_type_in_list(experiment, blacklisted_experiments))
+ return FALSE
- // If we haven't returned yet then this shouldn't be allowed
- return FALSE
+ // Finally, check against the allowed experiment types
+ return is_type_in_list(experiment, allowed_experiments)
/datum/component/experiment_handler/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -355,12 +345,13 @@
.["techwebs"] += list(data)
.["experiments"] = list()
if (linked_web)
- for (var/datum/experiment/experiment in linked_web.available_experiments)
+ for (var/datum/experiment/experiment as anything in linked_web.available_experiments)
+ if(!can_select_experiment(experiment))
+ continue
var/list/data = list(
name = experiment.name,
description = experiment.description,
tag = experiment.exp_tag,
- selectable = can_select_experiment(experiment),
selected = selected_experiment == experiment,
progress = experiment.check_progress(),
performance_hint = experiment.performance_hint,
diff --git a/code/modules/experisci/experiment/types/experiment.dm b/code/modules/experisci/experiment/types/experiment.dm
index f760723f8db..add015622f6 100644
--- a/code/modules/experisci/experiment/types/experiment.dm
+++ b/code/modules/experisci/experiment/types/experiment.dm
@@ -22,11 +22,16 @@
/// A textual hint shown on the UI in a tooltip to help a user determine how to perform
/// the experiment
var/performance_hint
+ /**
+ * If set, these techweb points will be rewarded for completing the experiment.
+ * Useful for those loose ends not tied to any specific node discount or requirement.
+ */
+ var/list/points_reward
/**
* Performs any necessary initialization of tags and other variables
*/
-/datum/experiment/New()
+/datum/experiment/New(datum/techweb/techweb)
if (traits & EXPERIMENT_TRAIT_DESTRUCTIVE)
exp_tag = "Destructive [exp_tag]"
@@ -60,6 +65,14 @@
/datum/experiment/proc/actionable(...)
return !is_complete()
+///Called when the experiment is selected by an experiment handler, for specific signals and the such.
+/datum/experiment/proc/on_selected(datum/component/experiment_handler/experiment_handler)
+ return
+
+///Called when the opposite happens.
+/datum/experiment/proc/on_unselected(datum/component/experiment_handler/experiment_handler)
+ return
+
/**
* Proc that tries to perform the experiment, and then checks if its completed.
*/
diff --git a/code/modules/experisci/experiment/types/exploration.dm b/code/modules/experisci/experiment/types/exploration.dm
index a6a5d2cd4cf..821e69a103a 100644
--- a/code/modules/experisci/experiment/types/exploration.dm
+++ b/code/modules/experisci/experiment/types/exploration.dm
@@ -52,7 +52,7 @@
/// If not null the required_condition will be picked from this list
var/list/possible_random_site_types
-/datum/experiment/exploration_scan/random/New()
+/datum/experiment/exploration_scan/random/New(datum/techweb/techweb)
. = ..()
if(length(possible_random_site_types))
required_site_type = pick(possible_random_site_types)
diff --git a/code/modules/experisci/experiment/types/random_scanning.dm b/code/modules/experisci/experiment/types/random_scanning.dm
index e80a80b5a0e..c9d39bd47b4 100644
--- a/code/modules/experisci/experiment/types/random_scanning.dm
+++ b/code/modules/experisci/experiment/types/random_scanning.dm
@@ -8,7 +8,7 @@
/// Max amount of a requirement per type
var/max_requirement_per_type = 100
-/datum/experiment/scanning/random/New()
+/datum/experiment/scanning/random/New(datum/techweb/techweb)
// Generate random contents
if (possible_types.len)
var/picked = 0
diff --git a/code/modules/experisci/experiment/types/scanning.dm b/code/modules/experisci/experiment/types/scanning.dm
index d9bbed88c8f..54bd2ad637e 100644
--- a/code/modules/experisci/experiment/types/scanning.dm
+++ b/code/modules/experisci/experiment/types/scanning.dm
@@ -17,16 +17,18 @@
var/list/required_atoms = list()
/// The list of atoms with sub-lists of atom references for scanned atoms contributing to the experiment (Or a count of atoms destoryed for destructive expiriments)
var/list/scanned = list()
+ /// If set, it'll be used in place of the generic "Scan samples of \a [initial(target.name)]" in serialize_progress_stage()
+ var/scan_message
/**
* Initializes the scanned atoms lists
*
* Initializes the internal scanned atoms list to keep track of which atoms have already been scanned
*/
-/datum/experiment/scanning/New()
+/datum/experiment/scanning/New(datum/techweb/techweb)
. = ..()
for (var/req_atom in required_atoms)
- scanned[req_atom] = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 0 : list()
+ scanned[req_atom] = (traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !(traits & EXPERIMENT_TRAIT_TYPECACHE)) ? 0 : list()
/**
* Checks if the scanning experiment is complete
@@ -37,8 +39,12 @@
/datum/experiment/scanning/is_complete()
. = TRUE
var/destructive = traits & EXPERIMENT_TRAIT_DESTRUCTIVE
+ var/typecache = traits & EXPERIMENT_TRAIT_TYPECACHE
for (var/req_atom in required_atoms)
var/list/seen = scanned[req_atom]
+ ///typecache experiments work all the same whether it's destructive or not
+ if(typecache && length(seen) == required_atoms[req_atom])
+ continue
if (destructive && (!(req_atom in scanned) || scanned[req_atom] != required_atoms[req_atom]))
return FALSE
if (!destructive && (!seen || seen.len != required_atoms[req_atom]))
@@ -65,8 +71,9 @@
* * seen_instances - The number of instances seen of this atom
*/
/datum/experiment/scanning/proc/serialize_progress_stage(atom/target, list/seen_instances)
- var/scanned_total = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len
- return EXPERIMENT_PROG_INT("Scan samples of \a [initial(target.name)]", scanned_total, required_atoms[target])
+ var/scanned_total = (traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !(traits & EXPERIMENT_TRAIT_TYPECACHE)) ? scanned[target] : seen_instances.len
+ var/message = scan_message || "Scan samples of \a [initial(target.name)]"
+ return EXPERIMENT_PROG_INT(message, scanned_total, required_atoms[target])
/**
* Attempts to scan an atom towards the experiment's goal
@@ -79,7 +86,10 @@
/datum/experiment/scanning/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, atom/target)
var/contributing_index_value = get_contributing_index(target)
if (contributing_index_value)
- scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : WEAKREF(target)
+ if(traits & EXPERIMENT_TRAIT_TYPECACHE)
+ scanned[contributing_index_value][target.type] = TRUE
+ else
+ scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : WEAKREF(target)
if(traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !isliving(target))//only qdel things when destructive scanning and they're not living (living things get gibbed)
qdel(target)
do_after_experiment(target, contributing_index_value)
diff --git a/code/modules/experisci/experiment/types/scanning_fish.dm b/code/modules/experisci/experiment/types/scanning_fish.dm
new file mode 100644
index 00000000000..83978010869
--- /dev/null
+++ b/code/modules/experisci/experiment/types/scanning_fish.dm
@@ -0,0 +1,116 @@
+///a superlist containing typecaches shared between the several fish scanning experiments for each techweb.
+GLOBAL_LIST_EMPTY(scanned_fish_by_techweb)
+
+/**
+ * A special scanning experiment that unlocks further settings for the fishing portal generator.
+ * Mainly as an inventive solution to many a fish source being limited to maps that have it,
+ * and to make the fishing portal generator a bit than just gubby and goldfish.
+ */
+/datum/experiment/scanning/fish
+ name = "Fish Scanning Experiment 1"
+ description = "An experiment requiring different fish species to be scanned to unlock the 'Beach' setting for the fishing portal generator."
+ performance_hint = "Scan fish. Examine scanner to review progress. Unlock new fishing portals."
+ allowed_experimentors = list(/obj/item/experi_scanner, /obj/machinery/destructive_scanner, /obj/item/fishing_rod/tech)
+ traits = EXPERIMENT_TRAIT_TYPECACHE
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 750)
+ required_atoms = list(/obj/item/fish = 4)
+ scan_message = "Scan different species of fish"
+ ///Further experiments added to the techweb when this one is completed.
+ var/list/next_experiments = list(/datum/experiment/scanning/fish/second)
+ ///Completing a experiment may also enable a fish source to be used for use for the portal generator.
+ var/fish_source_reward = /datum/fish_source/portal/beach
+
+/**
+ * We make sure the scanned list is shared between all fish scanning experiments for this techweb,
+ * since this is about scanning each species, and having to redo it for each species is a hassle.
+ */
+/datum/experiment/scanning/fish/New(datum/techweb/techweb)
+ . = ..()
+ if(isnull(techweb))
+ return
+ var/techweb_ref = REF(techweb)
+ var/list/scanned_fish = GLOB.scanned_fish_by_techweb[techweb_ref]
+ if(isnull(scanned_fish))
+ scanned_fish = list()
+ GLOB.scanned_fish_by_techweb[techweb_ref] = scanned_fish
+ for(var/atom_type in required_atoms)
+ LAZYINITLIST(scanned_fish[atom_type])
+ scanned = scanned_fish
+
+/**
+ * Registers a couple signals to review the fish scanned so far.
+ * It'd be an hassle not having any way (beside memory) to know which fish species have been scanned already otherwise.
+ */
+/datum/experiment/scanning/fish/on_selected(datum/component/experiment_handler/experiment_handler)
+ RegisterSignal(experiment_handler.parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_handler_examine))
+ RegisterSignal(experiment_handler.parent, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_handler_examine_more))
+
+/datum/experiment/scanning/fish/on_unselected(datum/component/experiment_handler/experiment_handler)
+ UnregisterSignal(experiment_handler.parent, list(COMSIG_ATOM_EXAMINE, COMSIG_ATOM_EXAMINE_MORE))
+
+/datum/experiment/scanning/fish/proc/on_handler_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ examine_list += span_notice("Examine again to review all the species of fish scanned so far.")
+
+/datum/experiment/scanning/fish/proc/on_handler_examine_more(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ var/message = span_notice("Fish species scanned hitherto, if any:")
+ message += ""
+ for(var/atom_type in required_atoms)
+ for(var/obj/item/fish/fish_path as anything in scanned[atom_type])
+ message += "[initial(fish_path.name)]"
+ message += ""
+ examine_list += message
+
+///Only scannable fish will contribute towards the experiment.
+/datum/experiment/scanning/fish/final_contributing_index_checks(obj/item/fish/target, typepath)
+ return target.experisci_scannable
+
+/**
+ * After a fish scanning experiment is done, more may be unlocked. If so, add them to the techweb
+ * and automatically link the handler to the next experiment in the list as a bit of qol.
+ */
+/datum/experiment/scanning/fish/finish_experiment(datum/component/experiment_handler/experiment_handler, ...)
+ . = ..()
+ if(next_experiments)
+ experiment_handler.linked_web.add_experiments(next_experiments)
+ var/datum/experiment/next_in_line = locate(next_experiments[1]) in experiment_handler.linked_web.available_experiments
+ experiment_handler.link_experiment(next_in_line)
+
+/datum/experiment/scanning/fish/second
+ name = "Fish Scanning Experiment 2"
+ description = "An experiment requiring more fish species to be scanned to unlock the 'Chasm' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 1500)
+ required_atoms = list(/obj/item/fish = 8)
+ next_experiments = list(/datum/experiment/scanning/fish/third)
+ fish_source_reward = /datum/fish_source/portal/chasm
+
+/datum/experiment/scanning/fish/third
+ name = "Fish Scanning Experiment 3"
+ description = "An experiment requiring even more fish species to be scanned to unlock the 'Ocean' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ required_atoms = list(/obj/item/fish = 14)
+ next_experiments = list(/datum/experiment/scanning/fish/fourth, /datum/experiment/scanning/fish/holographic)
+ fish_source_reward = /datum/fish_source/portal/ocean
+
+/datum/experiment/scanning/fish/holographic
+ name = "Holographic Fish Scanning Experiment"
+ description = "This one actually requires holographic fish to unlock the 'Randomizer' setting for the fishing portal."
+ performance_hint = "Load in the 'Beach' template at the Holodeck to fish some holo-fish."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 500)
+ required_atoms = list(/obj/item/fish/holo = 4)
+ scan_message = "Scan different species of holographic fish"
+ next_experiments = null
+ fish_source_reward = /datum/fish_source/portal/random
+
+///holo fishes are normally unscannable, but this is an experiment for them, so we don't care for the experisci_scannable variable.
+/datum/experiment/scanning/fish/holographic/final_contributing_index_checks(obj/item/fish/target, typepath)
+ return TRUE
+
+/datum/experiment/scanning/fish/fourth
+ name = "Fish Scanning Experiment 4"
+ description = "An experiment requiring lotsa fish species to unlock the 'Hyperspace' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 3250)
+ required_atoms = list(/obj/item/fish = 21)
+ next_experiments = null
+ fish_source_reward = /datum/fish_source/portal/hyperspace
diff --git a/code/modules/experisci/experiment/types/scanning_material.dm b/code/modules/experisci/experiment/types/scanning_material.dm
index 714205289de..fb8a7ff354b 100644
--- a/code/modules/experisci/experiment/types/scanning_material.dm
+++ b/code/modules/experisci/experiment/types/scanning_material.dm
@@ -9,7 +9,7 @@
///List of materials actually required, indexed by the atom that is required.
var/required_materials = list()
-/datum/experiment/scanning/random/material/New()
+/datum/experiment/scanning/random/material/New(datum/techweb/techweb)
. = ..()
for(var/req_atom in required_atoms)
var/chosen_material = pick(possible_material_types)
diff --git a/code/modules/experisci/experiment/types/scanning_plants.dm b/code/modules/experisci/experiment/types/scanning_plants.dm
index c34822d6e7e..b92a4cc20b4 100644
--- a/code/modules/experisci/experiment/types/scanning_plants.dm
+++ b/code/modules/experisci/experiment/types/scanning_plants.dm
@@ -10,7 +10,7 @@
///List of plant genes actually required, indexed by the atom that is required.
var/list/required_genes = list()
-/datum/experiment/scanning/random/plants/New()
+/datum/experiment/scanning/random/plants/New(datum/techweb/techweb)
. = ..()
if(possible_plant_genes.len)
for(var/req_atom in required_atoms)
diff --git a/code/modules/experisci/handheld_scanner.dm b/code/modules/experisci/handheld_scanner.dm
index e0fd4d480d5..97aa034afa1 100644
--- a/code/modules/experisci/handheld_scanner.dm
+++ b/code/modules/experisci/handheld_scanner.dm
@@ -19,9 +19,15 @@
// Late initialize to allow for the rnd servers to initialize first
/obj/item/experi_scanner/LateInitialize()
. = ..()
+ var/static/list/handheld_signals = list(
+ COMSIG_ITEM_PRE_ATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_AFTERATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, ignored_handheld_experiment_attempt),
+ )
AddComponent(/datum/component/experiment_handler, \
- allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical),\
- disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE)
+ allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical), \
+ disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE, \
+ experiment_signals = handheld_signals, \
+ )
/obj/item/experi_scanner/suicide_act(mob/living/carbon/user)
user.visible_message(span_suicide("[user] is giving in to the Great Toilet Beyond! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/code/modules/explorer_drone/control_console.dm b/code/modules/explorer_drone/control_console.dm
index 7b371e8412b..8cc8854f27d 100644
--- a/code/modules/explorer_drone/control_console.dm
+++ b/code/modules/explorer_drone/control_console.dm
@@ -1,6 +1,6 @@
/obj/machinery/computer/exodrone_control_console
name = "exploration drone control console"
- desc = "control eploration drones from intersteller distances. Communication lag not included."
+ desc = "Control exploration drones from interstellar distances. Communication lag not included."
circuit = /obj/item/circuitboard/computer/exodrone_console
//Currently controlled drone
var/obj/item/exodrone/controlled_drone
diff --git a/code/modules/explorer_drone/example_adventures/Theres_a_tree_in_the_middle_of_space.json b/code/modules/explorer_drone/example_adventures/Theres_a_tree_in_the_middle_of_space.json
index 3f9ee41582e..f06b1d25062 100644
--- a/code/modules/explorer_drone/example_adventures/Theres_a_tree_in_the_middle_of_space.json
+++ b/code/modules/explorer_drone/example_adventures/Theres_a_tree_in_the_middle_of_space.json
@@ -1,343 +1,356 @@
{
- "adventure_name": "There's a tree in the middle of space.",
- "version": 1,
- "starting_node": "Tree Start",
- "starting_qualities": {
- "Confusion": 0
- },
- "required_site_traits": [
- "in space"
- ],
- "loot_categories": [
- "research"
- ],
- "scan_band_mods": {
- "Exotic Radiation": 10
- },
- "deep_scan_description": "",
- "triggers": [
- {
- "name": "Confusion Trigger",
- "target_node": "What is wrong with this tree?",
- "requirements": [
- {
- "quality": "Confusion",
- "operator": ">",
- "value": 30
- }
- ]
- }
- ],
- "nodes": [
- {
- "name": "Tree Start",
- "description": "Camera online. Visual signs detect a fully grown, seemingly biological, and live tree located in the middle of the vacuum.\nSensors indicate it is not oxygenating, but energy is being collected via passive solar light from the nearby star.\nBaffling.",
- "choices": [
- {
- "key": "choice 0",
- "name": "Ignore site.",
- "exit_node": "FAIL",
- "delay": 10,
- "delay_message": "Leave this for the botanists to figure out."
- },
- {
- "key": "choice 1",
- "name": "Begin sensor scan.",
- "exit_node": "Biological Scan",
- "delay": 10,
- "delay_message": "Lets get some data."
- }
- ],
- "image": null,
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMQAAAAAAAC15h2QtxeAoxVykROizRpxjxIAcgAAWwAAgAAAUAAAZAAAZgC0tLTKysr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AgjmRpnigqrMMqtCvhyvFs17hQuPva67ygTwgcGovA3a61BLaeryj0mapar9isNjCNylpfgWwsLpPPZqBst066le84fB5nRpvdvGDL7/v/JmBeaWiFhIeGbGqLio1ljm2CklKUA4CXmJmaAYidhp9mBmOiYqQEpqZlk3qbra6vsCOenqijtgIHCLG7vL29CQoNCsDCxMPBxw0MxsTLyMLCCwq+1LuVe9VZ0M/c297d4NvZ41vXeueU5N/r4e3o79jkvOb0UAe4+Pf6uMjS7P/TtAAcGKyeQXhT5ClcSKIdwYcHIyKMx7CiwocOwU3cWMmiR4YYQzqM+LGkSQXSUv/2y0jQH0qUJmNqoRSm5ixQbXLK2UmnJ886ToIilFltkk2cNw9B+umz6U87UOsRNXk0KdKrVrOSWWVwqlc+WHHWKnWqrKizZV+QGrA2yte31YDJPTZXrrMEzhbkPSYNWF+4gAFBeICJGTLDxVi2C5yNIxSQikVu48i4j8TLT1i2khwOM+bKVSQaeGLgQOnS+fIBwkiCszfPjqWAnn0ism1hsXO/oM279e3fuqmYeAChN++XKqP9BujP+EcXMKLkoFFm+o3qSH5oJ7L9SPfs3IlEHw/dhfNyLMpbX4+duvvw3uGDj08fCfn0+KVTPI9Ff/T37AHY3nXfFSifgfX9cJ//f/vxh95/1wkoYYQU5oDgfBh+t+B9DsJS4YABfiiDAWWQOGKJKJ6oYidcDdVhUVrFGFZWRl32IlEy0nJLWjyu1VYYrNxY2YxXjZUWW1+0FaSQDiaQ4z0y3OMXX3wxaSUKiWWZTF5c+sVASl8iB9OVbxFH3CVabmOMa9yQ6WYAbHL2Zh+UVRTnbV3NyUlwu120HEuw9WlloO+Mc+c3fCbEH6ES/Ybmn9AwWmdvnu1jaWoHpMBmFYdK+tmLiQqKCaTAebqRnh4d6o6pEqHqEan/sOqiqxbBumqoig42GK0WqUqQY7zCtZKvDgXL37AvEbvNAsaeJ6Zy0JKqErPNYpIj/xqOOKUtU9xu6+1OUekpCJFpLPXtud2mu21UTbhKLraPqCsvuvQ+FdQSqI57bbnxtlHvv/OCK5RsqO4Lr78BA6xwulDR+m4n5iYs8cJNVNyguwYzEi/FE3dsR7UpZJzIxh1z7FO4IJ8gMr860WuAEi/rEHMBM88s8ccpn/AwVhEjTDPMQMsc9M9Cx9HyIJbknMLOEJNC8xpOR/3zy1RPbXXVWD8tM9Rcq3Kx0lesvKORZz2NFtVmnWJ2j2NLAnYsiJCd9tlzz7122XTnnfbb2dxUi96A1x044GDwnc0Zfwuu+OCLK274c4UkzvjkZj3+ldxkUY5AApt3noDlldF1zP/YBAyzuemcp4665wGBDlcDeCUA+zKzyx77Arfnvjnuy+C+u+uADXPXMsTHXvzxxid/PPC8OEAYmnLBPoz0ttMee+3YXx/958y/oqsDl4hel/jkjz9+Md17Rf3620/fvu3uxy97A+lPVT75ic2lJWLDWD7AE/97QQCT1qv3sU9+6ytGAxTIQL7FpiLJiCD/JggOYJxKXAPMIAA3KCp1MHB6H5wfCEcowgRoUIAc/F8L3CSrr/EiGAsc4QKhEcMZwpCG0DihClHIQxVeaYdA1KEQAWioD9pQhEesoRJTOMQe/k9ITQwiEzloQ/q9sIpKxOENt9iAKHpxgw76ohOnKED/LWJRGJrIIhfVWEUxSnGMPDQOGd+4wwME0I76COAC2MhHNPZhjWbk4xzd2MTeNBGPuBiAPg6wyEbiIlMiaEBKYrjHQOJQG5YE5BbpSMhByvGQiixNKBlpGlI+8h5a0OQZg2ECVfYRhoOMJRx36JxZ/k+UbMGjKEupyERi4pUzjGQmV9lGWXLSkxiMoy+ICcxmNsCWxvRi/VYzw2pa85rYvKYKt8nNbnrzm96c5h+G6UwudhKacBTnOLPJznYuEJzwjCc41blOctoTlug8JjTpiSZ3+lOb3LTj/wSqSHlycwQPeB4//8jMe2KRgwJNZDQJaCaFLlQge6ykJP/ZTo3iYbGgHx3oPC+6CZRQspwNNakwEMnSE5L0FRndaEwXqFGO0lSmG32pQmLK043e1KYz7KlO5dHTouKUo0YdKjmMalSk4jSmSh3HAqb61KpqtKZBtWpOo1oNpmoVq9dkKrWMEwIAOw=="
- },
- {
- "name": "Biological Scan",
- "description": "You attempt to scan for clues regarding the tree's nature. It appears to be a fully mature oak tree. \n\nApproximated height is 13 ft, 6.4 inches. \n\nSubject sees no sign of an outer coating or otherwise layer protecting it from the void of space.\n\nSubject's surface temperature is 293.7 kelvin, as though it were sitting indoors.",
- "choices": [
- {
- "key": "choice 2",
- "name": "Check Sensor Integrity.",
- "exit_node": "Its Not You...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": 5
- }
- ],
- "delay": 50,
- "delay_message": "This can't be right."
- },
- {
- "key": "choice 4",
- "name": "Attempt to take sample.",
- "exit_node": "Sample Taken",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": 3
- }
- ],
- "delay": 40,
- "delay_message": "Snip snip."
- },
- {
- "key": "choice 6",
- "name": "Examine Tree Roots.",
- "exit_node": "Examine Roots",
- "delay": 10
- },
- {
- "key": "choice 9",
- "name": "Sequence Sample Radiation with background noise.",
- "exit_node": "Background Analysis",
- "requirements": [
- {
- "quality": "Sample",
- "operator": ">=",
- "value": 1
- }
- ],
- "delay": 0,
- "delay_message": "This can't be real."
- }
- ],
- "image": null,
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMQAAAAAAAC15h2QtxeAoxVykROizRpxjxIAcgAAWwAAgAAAUAAAZAAAZgC0tLTKysr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AgjmRpnigqrMMqtCvhyvFs17hQuPva67ygTwgcGovA3a61BLaeryj0mapar9isNjCNylpfgWwsLpPPZqBst066le84fB5nRpvdvGDL7/v/JmBeaWiFhIeGbGqLio1ljm2CklKUA4CXmJmaAYidhp9mBmOiYqQEpqZlk3qbra6vsCOenqijtgIHCLG7vL29CQoNCsDCxMPBxw0MxsTLyMLCCwq+1LuVe9VZ0M/c297d4NvZ41vXeueU5N/r4e3o79jkvOb0UAe4+Pf6uMjS7P/TtAAcGKyeQXhT5ClcSKIdwYcHIyKMx7CiwocOwU3cWMmiR4YYQzqM+LGkSQXSUv/2y0jQH0qUJmNqoRSm5ixQbXLK2UmnJ886ToIilFltkk2cNw9B+umz6U87UOsRNXk0KdKrVrOSWWVwqlc+WHHWKnWqrKizZV+QGrA2yte31YDJPTZXrrMEzhbkPSYNWF+4gAFBeICJGTLDxVi2C5yNIxSQikVu48i4j8TLT1i2khwOM+bKVSQaeGLgQOnS+fIBwkiCszfPjqWAnn0ism1hsXO/oM279e3fuqmYeAChN++XKqP9BujP+EcXMKLkoFFm+o3qSH5oJ7L9SPfs3IlEHw/dhfNyLMpbX4+duvvw3uGDj08fCfn0+KVTPI9Ff/T37AHY3nXfFSifgfX9cJ//f/vxh95/1wkoYYQU5oDgfBh+t+B9DsJS4YABfiiDAWWQOGKJKJ6oYidcDdVhUVrFGFZWRl32IlEy0nJLWjyu1VYYrNxY2YxXjZUWW1+0FaSQDiaQ4z0y3OMXX3wxaSUKiWWZTF5c+sVASl8iB9OVbxFH3CVabmOMa9yQ6WYAbHL2Zh+UVRTnbV3NyUlwu120HEuw9WlloO+Mc+c3fCbEH6ES/Ybmn9AwWmdvnu1jaWoHpMBmFYdK+tmLiQqKCaTAebqRnh4d6o6pEqHqEan/sOqiqxbBumqoig42GK0WqUqQY7zCtZKvDgXL37AvEbvNAsaeJ6Zy0JKqErPNYpIj/xqOOKUtU9xu6+1OUekpCJFpLPXtud2mu21UTbhKLraPqCsvuvQ+FdQSqI57bbnxtlHvv/OCK5RsqO4Lr78BA6xwulDR+m4n5iYs8cJNVNyguwYzEi/FE3dsR7UpZJzIxh1z7FO4IJ8gMr860WuAEi/rEHMBM88s8ccpn/AwVhEjTDPMQMsc9M9Cx9HyIJbknMLOEJNC8xpOR/3zy1RPbXXVWD8tM9Rcq3Kx0lesvKORZz2NFtVmnWJ2j2NLAnYsiJCd9tlzz7122XTnnfbb2dxUi96A1x044GDwnc0Zfwuu+OCLK274c4UkzvjkZj3+ldxkUY5AApt3noDlldF1zP/YBAyzuemcp4665wGBDlcDeCUA+zKzyx77Arfnvjnuy+C+u+uADXPXMsTHXvzxxid/PPC8OEAYmnLBPoz0ttMee+3YXx/958y/oqsDl4hel/jkjz9+Md17Rf3620/fvu3uxy97A+lPVT75ic2lJWLDWD7AE/97QQCT1qv3sU9+6ytGAxTIQL7FpiLJiCD/JggOYJxKXAPMIAA3KCp1MHB6H5wfCEcowgRoUIAc/F8L3CSrr/EiGAsc4QKhEcMZwpCG0DihClHIQxVeaYdA1KEQAWioD9pQhEesoRJTOMQe/k9ITQwiEzloQ/q9sIpKxOENt9iAKHpxgw76ohOnKED/LWJRGJrIIhfVWEUxSnGMPDQOGd+4wwME0I76COAC2MhHNPZhjWbk4xzd2MTeNBGPuBiAPg6wyEbiIlMiaEBKYrjHQOJQG5YE5BbpSMhByvGQiixNKBlpGlI+8h5a0OQZg2ECVfYRhoOMJRx36JxZ/k+UbMGjKEupyERi4pUzjGQmV9lGWXLSkxiMoy+ICcxmNsCWxvRi/VYzw2pa85rYvKYKt8nNbnrzm96c5h+G6UwudhKacBTnOLPJznYuEJzwjCc41blOctoTlug8JjTpiSZ3+lOb3LTj/wSqSHlycwQPeB4//8jMe2KRgwJNZDQJaCaFLlQge6ykJP/ZTo3iYbGgHx3oPC+6CZRQspwNNakwEMnSE5L0FRndaEwXqFGO0lSmG32pQmLK043e1KYz7KlO5dHTouKUo0YdKjmMalSk4jSmSh3HAqb61KpqtKZBtWpOo1oNpmoVq9dkKrWMEwIAOw=="
- },
- {
- "name": "Its Not You...",
- "description": "After re-connection is established, your sensors appear fine. Tree has not moved in the slightest since last observed. Temperature has fluxuated 0.2 kelvin upwards, as expected of a plant under direct light.\nLets try again.",
- "choices": [
- {
- "key": "choice 3",
- "name": "Restart biological scan.",
- "exit_node": "Biological Scan",
- "delay": 25,
- "delay_message": "God damnit."
- }
- ],
- "image": null,
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD////v7+/h4eEAAAAAAAAAAAAD/xi63P4wykmrvTjrzbv/YCiOZBkJaJqaLKOubSy7b23fODxPOb7/m4FASBRofEBeT1W6JU3DYaRIjQqfkCWKg1xoddjSQEgQlM/mdDkc0Fa+8PiWLSIM7Pj73RxOlVF/ITlKNXSGDXp5iiFoaBlqfpFyk12HMgSYmZkfmppmmJ8gjXwUf2lglqmqq6ytrq+wsbKztLW2t7i5um2Ug669uyBCKMNRc4Q9qXHBJlVUFMbRV6pfR04ONszQzlZRlm7IveLaimZ7eJAESUsfyVnX2hfqv/Nv4vfZ8RWJ/Hkdn6NGtRsFyEg4VLwK6UvVqROdgMcWSpxIsaLFixgzatzIsf+jR1gIP7YbN6uaSAn4wFGbdNJFsZcpFRqS09IBt2kW3CkzWdNmNyIYpIWkw9Nevgbwat2ccNPbTnbWjtKQWfGnVZwzoQaNqVXXkDFWGx3qeoIry2CJzJ05p6aPTpRmfSXV1a/u2jM7TuldA8LXO6kXFemh5ihn3Lc9A9h1ZaqxpMNkK9rl5+EMKIGM9g5FSjJjv4aVOwEc/Q8iXx6ODSo5ualOQ9Bs0qVJTLu27du4c+vezbu379/AgwsfTry48ePIkytfqHp5xhfLadby+1soMJDLbhchlqIYpVbXa14dz718NMBEz/ZsWsU8zMhY1CdmL+VE+/ObgWSvPR4rj/f/xqxUFG1fVfHIUgLCN58zR5ABySpwbAAdZ1TNMgZYF9YHjWmsDAgNeglNqFSBJJYiG14JUmcYiHNZWJcE6HD4FGIrVuiFjROxJeNYChoFWUS5lBPjOTO2iM2PKuki2F1qfdPjjUjSeMuSk82Uml4fRimlLVR2OUAYBGHpo5aV4OJll1iYRkqWhag4FY75WXhmlTOoedo/ZVKI40Jz+pOEnX2ZsqcCeVJ0piEQieJhiEx0RBkrd6J2IiQFVXqPbZjEYqdmll6Jz0avtUaCqEFtaiqnekmHUagOhSZapvJMeioanXa2KqukZoCrJoruCKNmCngKZGC7urprPRKq+QZBP2XFKRGunBwL6waZjIbiBInu9hoj0o5wraRiOadrsX1kK24HvBK25rnstuvuu/DGK++89NZr77345qvvvhYlAAA7"
- },
- {
- "name": "Sample Taken",
- "description": "You collect and project a small sample of tree bark off the plant. The instant that the bark is removed from the tree, as though it suddenly remembered what it was, the moisture content of the bark freezes over, and implodes into small microparticles of splinters.\nSmall radioactive signature detected.",
- "choices": [
- {
- "key": "choice 5",
- "name": "Well that was... unexpected.",
- "exit_node": "Biological Scan",
- "delay": 0,
- "delay_message": "Maybe something else might work better.",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Sample",
- "value": 1
- }
- ]
- }
- ],
- "image": null,
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAABSLiiPVjtyRS9mOTFbNyYAAAAD/xi63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+sDUQxTDbeK7vfF/3saCwcxPcCMckcqlM2pDPnnRKrUqH2IdAYDBwu+Bwl2Agm8sE8iy9Zrqbb2Z0bq3b74W8Xp+1bP+Af2KDhIVlh2eJaUtsi3CPcU43d5QDe5cFfSKBnJ2en50Ff6JboqYCBWiLq46tkK+RUJOTmJmatxyguqSovae/vsGirK2NjU21trjLQp+8wNDCwnmo1MnM2Nna29zd3t/g4eLj5OXm59o5Rjo/QOjvLaKwkrKylfdW8FleXImIBm0CxppXD4c9fAgT5tCn4Iuhh4fG/DNTjNHAi4/oKERYq/+cro8gPYXxR7EkK4EEUxbUOCVZHoabQoZ8NkqaKWI4BR7DGKcjTG4yaZaySRSatQLGfP5ciqHX0GhQpzGdSrWq1atYs2rdyrWr169gw4pVsc7G2LMLkM46SOkl2nc86W3E97aFgLh4Dc7dq6NuhC0TVVlUOY8l38N9szocREYiyYqEMdJZibjylW+AIRIiqUowyrwDJ+u1XMelsm6CNKtmHHiRZ8iuQGecVdklZpm4V3NWREznYNmhaeNRCg638eOBhrUWjPP3zshNDNuwzRW5dZrQmms3BhzSNb8SrIPCXvTudtjzMIEngVxo+ajDtKOkvt5u0Jrw3/s6yqd+Nk7/7uUn4FH+VaWfKAUmqOCCDDbo4IMQRijhhBRWaOGFGGao4YYcdujhhyCGKOKFRsgxy4jaBEeZOyi60J2JK1piS2ItcvDiaMIpVGMEv0GnIo46grgFbM/dKBppC004kXzo3fiGdJb51wUgiKTBnHNOzsYWkpd1xZpjrTHJnY+vQGlZOztMBUZmrO0mZo9Zwggkl/mkKc5qIy3n2nlExrnWnHROcRueYjSm55Wr+Ebmj2YeZpo4bBJqyG6Inqeon0duRF84kuJJKW98Nrkojlu2dYlHkXaq6qehNvfZqHI2+ihq4lGpKhisttpbbKNm6sN3t9Wam2q5Vtrqq5jesKk3jsI220mheuq665jJzoqqs9gGcqi0211KGLBUZYutciUZG6q3cREnlrjiPcZtTqIiAW597H5E07uuiqqehPXONIq57+6rYb81PWUTvmksuyHBTjUc1XnVmOLWjgFkG+CBNk1MsRb1kiegLxtf4KzHB4b8Qa0XA2OyfZ6QjODKywwlM8w012zzzTjnrPNWCQAAOw=="
- },
- {
- "name": "Examine Roots",
- "description": "All plant matter has to derive energy and moisture from someplace. Examining the oak tree's roots reveals that the roots present all appear to splay out, similar to how a normal tree would. However, those roots then proceed to double back in on itself. This might suggest that the tree is obtaining nutrients from... itself.",
- "choices": [
- {
- "key": "choice 7",
- "name": "That's fucking stupid.",
- "exit_node": "Biological Scan",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": {
- "value_type": "random",
- "low": 6,
- "high": 10
- }
- }
- ],
- "delay": 0,
- "delay_message": "What the hell kind of tree even IS this?"
- },
- {
- "key": "choice 8",
- "name": "Obtain biological sample from roots.",
- "exit_node": "Sample Taken",
- "delay": 10,
- "delay_message": "This is why we hire botanists on-site."
- }
- ],
- "image": null,
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMQAAAAAAAC15h2QtxeAoxVykROizRpxjxIAcgAAWwAAgAAAUAAAZAAAZgC0tLTKysr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AgjmRpnigqrMMqtCvhyvFs17hQuPva67ygTwgcGovA3a61BLaeryj0mapar9isNjCNylpfgWwsLpPPZqBst066le84fB5nRpvdvGDL7/v/JmBeaWiFhIeGbGqLio1ljm2CklKUA4CXmJmaAYidhp9mBmOiYqQEpqZlk3qbra6vsCOenqijtgIHCLG7vL29CQoNCsDCxMPBxw0MxsTLyMLCCwq+1LuVe9VZ0M/c297d4NvZ41vXeueU5N/r4e3o79jkvOb0UAe4+Pf6uMjS7P/TtAAcGKyeQXhT5ClcSKIdwYcHIyKMx7CiwocOwU3cWMmiR4YYQzqM+LGkSQXSUv/2y0jQH0qUJmNqoRSm5ixQbXLK2UmnJ886ToIilFltkk2cNw9B+umz6U87UOsRNXk0KdKrVrOSWWVwqlc+WHHWKnWqrKizZV+QGrA2yte31YDJPTZXrrMEzhbkPSYNWF+4gAFBeICJGTLDxVi2C5yNIxSQikVu48i4j8TLT1i2khwOM+bKVSQaeGLgQOnS+fIBwkiCszfPjqWAnn0ism1hsXO/oM279e3fuqmYeAChN++XKqP9BujP+EcXMKLkoFFm+o3qSH5oJ7L9SPfs3IlEHw/dhfNyLMpbX4+duvvw3uGDj08fCfn0+KVTPI9Ff/T37AHY3nXfFSifgfX9cJ//f/vxh95/1wkoYYQU5oDgfBh+t+B9DsJS4YABfiiDAWWQOGKJKJ6oYidcDdVhUVrFGFZWRl32IlEy0nJLWjyu1VYYrNxY2YxXjZUWW1+0FaSQDiaQ4z0y3OMXX3wxaSUKiWWZTF5c+sVASl8iB9OVbxFH3CVabmOMa9yQ6WYAbHL2Zh+UVRTnbV3NyUlwu120HEuw9WlloO+Mc+c3fCbEH6ES/Ybmn9AwWmdvnu1jaWoHpMBmFYdK+tmLiQqKCaTAebqRnh4d6o6pEqHqEan/sOqiqxbBumqoig42GK0WqUqQY7zCtZKvDgXL37AvEbvNAsaeJ6Zy0JKqErPNYpIj/xqOOKUtU9xu6+1OUekpCJFpLPXtud2mu21UTbhKLraPqCsvuvQ+FdQSqI57bbnxtlHvv/OCK5RsqO4Lr78BA6xwulDR+m4n5iYs8cJNVNyguwYzEi/FE3dsR7UpZJzIxh1z7FO4IJ8gMr860WuAEi/rEHMBM88s8ccpn/AwVhEjTDPMQMsc9M9Cx9HyIJbknMLOEJNC8xpOR/3zy1RPbXXVWD8tM9Rcq3Kx0lesvKORZz2NFtVmnWJ2j2NLAnYsiJCd9tlzz7122XTnnfbb2dxUi96A1x044GDwnc0Zfwuu+OCLK274c4UkzvjkZj3+ldxkUY5AApt3noDlldF1zP/YBAyzuemcp4665wGBDlcDeCUA+zKzyx77Arfnvjnuy+C+u+uADXPXMsTHXvzxxid/PPC8OEAYmnLBPoz0ttMee+3YXx/958y/oqsDl4hel/jkjz9+Md17Rf3620/fvu3uxy97A+lPVT75ic2lJWLDWD7AE/97QQCT1qv3sU9+6ytGAxTIQL7FpiLJiCD/JggOYJxKXAPMIAA3KCp1MHB6H5wfCEcowgRoUIAc/F8L3CSrr/EiGAsc4QKhEcMZwpCG0DihClHIQxVeaYdA1KEQAWioD9pQhEesoRJTOMQe/k9ITQwiEzloQ/q9sIpKxOENt9iAKHpxgw76ohOnKED/LWJRGJrIIhfVWEUxSnGMPDQOGd+4wwME0I76COAC2MhHNPZhjWbk4xzd2MTeNBGPuBiAPg6wyEbiIlMiaEBKYrjHQOJQG5YE5BbpSMhByvGQiixNKBlpGlI+8h5a0OQZg2ECVfYRhoOMJRx36JxZ/k+UbMGjKEupyERi4pUzjGQmV9lGWXLSkxiMoy+ICcxmNsCWxvRi/VYzw2pa85rYvKYKt8nNbnrzm96c5h+G6UwudhKacBTnOLPJznYuEJzwjCc41blOctoTlug8JjTpiSZ3+lOb3LTj/wSqSHlycwQPeB4//8jMe2KRgwJNZDQJaCaFLlQge6ykJP/ZTo3iYbGgHx3oPC+6CZRQspwNNakwEMnSE5L0FRndaEwXqFGO0lSmG32pQmLK043e1KYz7KlO5dHTouKUo0YdKjmMalSk4jSmSh3HAqb61KpqtKZBtWpOo1oNpmoVq9dkKrWMEwIAOw=="
- },
- {
- "name": "Background Analysis",
- "description": "You compare the radioactive energy bands of the sample collected earlier with that of the nearby solar enviroment.\nNothing.\nThere is nothing nearby that matches the passive signal of the tree, or the bark, or anything similar.\nThis is really starting to get on your nerves.",
- "choices": [
- {
- "key": "choice 10",
- "name": "Smash your desk in frustration.",
- "exit_node": "FAIL",
- "delay": 50,
- "delay_message": "No amount of pay is worth dealing with magical plant juju."
- },
- {
- "key": "choice 11",
- "name": "Check every known energy spectroscopy database.",
- "exit_node": "Sample Match Found",
- "delay": 900,
- "delay_message": "You NEED an answer. You DESERVE an answer."
- }
- ],
- "image": null,
- "on_enter_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": {
- "value_type": "random",
- "low": 3,
- "high": 5
- }
- }
- ],
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD///9ZVlKsMjJpampGR0cAAAAD/xi63P4wykmFpTjrDa0XXCiOZGlqQ0qsazqIXyx9Z23feKayfP/WskgwRywqCsikcslsOpGpgGvQNDo8lYt1W4I+v2CvMhUuI0/YiZbL5nxdhkJ8Lq/T6eZ8HRmHUtuAgRp6T3B2SXdNfYeJc3CCkJEYjISVYZKRQ5gajZSdi0yboqM3nnWkqKmCpgaqrq9GBrKzs7AKaYG4tji0vbu/wCW6wZpqNMHIOAM9zDw/IzEgHcVt1KOWZQ1TU4LDV2vJOAVU2GZkY5VF4NPr4SRM53yI8/Lw9uVvBcLuknqGn4cqLQIYhwy/g0fwKdSDsCEGggtDOZwoAiLFi5sAYtwoqf/XIVjeOKrqRUukjZAmGVjDGC2lSyAts7R7GW5ZM2bPNERDGSAmzWA2b/pAc+yBT0ArVeXhsC2oj5wnefaU+pNBxDJNXUAqyq6qhKthyCUR+4QLVa5ej5AFO3ZJPGw3qE5N22DsWiUD6Tm5e/cqCbk96dZtQgZip3rjwIIa90fwqrBUDAfcQ0hyilaOIWG7zIoSYryd73DLrJntNlOIO2WVQ/raYrYKW/8CCNuJbH6S8aS7vSCpq9C8gxMMzns48dudj9/WqFw2yZKvfDc38Rxzc+kiq09f/nw7b+wNwV9HuwuwSvLeL14wjz49pm3Qds7sbd59jh1Co3CQP1+8/Rr/TgnVQnxHNVDgf0QEKCBUIRxIX3sIAiigMwzCAKF/cc1H1xQTVvjXhRCqE6IosU2Q1YkiajiViin+VhthWW0l1YhevQjjafrIKI0QLP5kI2GMBWkbG2fVx1FiP/oxRl9DEiEXCDsKhuSPazFpBg5P9miSkHmlpleXbbkVUVRRTkOakl7mRg9fsJmQ5Zl2DaDmJ4XVwZkZYHbpoU726eZCaID28RYYeRFEToQVeVaYLHIwOkugk70GqWiNIeoGQJc5qumclOU5qUGWXhraFNU1yumpdoAa6ganZkrLpJ2G5qido606CKoGwDErrqaaSlKttrIKq2r/DGvHr39aF2wIgsYaduKzqyUhhbLLjsArp3BVW0Oz3OpWlrY5dCsuKOByMS635QpyLaTEGenauZ2M5y4m696B4Lz0spLuRMnt25BH9vr7b3XUChwOwbIYfBDCBSscDMIOL0xwxO5ATPHB2kVHY7oTg7RxuR0rhyFFIYv8MUYZX5xMySoDk3LLD3dXbQIAOw=="
- },
- {
- "name": "Sample Match Found",
- "description": "After an extensive algorithm search on the controller end, you have a single match to this specific band and style of energy.\nThe problem, is that the source of said radiation is coming not only from Space Station 13, no.\nIt's coming from the Space Station 13 Research Department.\nWhat the fuck?",
- "choices": [
- {
- "key": "choice 12",
- "name": "Something must be wrong with the drone.",
- "exit_node": "Its Not You...",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": {
- "value_type": "random",
- "low": 6,
- "high": 10
- }
- }
- ],
- "delay": 30,
- "delay_message": "Lousy piece of junk must be scanning the station instead of the target."
- },
- {
- "key": "choice 13",
- "name": "Perhaps that sample was tainted. Collect a new sample.",
- "exit_node": "Sample Taken",
- "delay": 60,
- "delay_message": "Lets try again, but carefully."
- },
- {
- "key": "choice 14",
- "name": "Remember the Christmas Party.",
- "exit_node": "The Christmas Party",
- "requirements": [
- {
- "quality": "Confusion",
- "operator": "<=",
- "value": 25
- }
- ],
- "delay": 100,
- "delay_message": "Wait a gosh darn fucking second."
- }
- ],
- "image": null,
- "on_enter_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": 10
- }
- ],
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD////v7+/h4eEAAAAAAAAAAAAD/xi63P4wykmrvTjrzbv/YCiOZBkJaJqaLKOubSy7b23fODxPOb7/m4FASBRofEBeT1W6JU3DYaRIjQqfkCWKg1xoddjSQEgQlM/mdDkc0Fa+8PiWLSIM7Pj73RxOlVF/ITlKNXSGDXp5iiFoaBlqfpFyk12HMgSYmZkfmppmmJ8gjXwUf2lglqmqq6ytrq+wsbKztLW2t7i5um2Ug669uyBCKMNRc4Q9qXHBJlVUFMbRV6pfR04ONszQzlZRlm7IveLaimZ7eJAESUsfyVnX2hfqv/Nv4vfZ8RWJ/Hkdn6NGtRsFyEg4VLwK6UvVqROdgMcWSpxIsaLFixgzatzIsf+jR1gIP7YbN6uaSAn4wFGbdNJFsZcpFRqS09IBt2kW3CkzWdNmNyIYpIWkw9Nevgbwat2ccNPbTnbWjtKQWfGnVZwzoQaNqVXXkDFWGx3qeoIry2CJzJ05p6aPTpRmfSXV1a/u2jM7TuldA8LXO6kXFemh5ihn3Lc9A9h1ZaqxpMNkK9rl5+EMKIGM9g5FSjJjv4aVOwEc/Q8iXx6ODSo5ualOQ9Bs0qVJTLu27du4c+vezbu379/AgwsfTry48ePIkytfqHp5xhfLadby+1soMJDLbhchlqIYpVbXa14dz718NMBEz/ZsWsU8zMhY1CdmL+VE+/ObgWSvPR4rj/f/xqxUFG1fVfHIUgLCN58zR5ABySpwbAAdZ1TNMgZYF9YHjWmsDAgNeglNqFSBJJYiG14JUmcYiHNZWJcE6HD4FGIrVuiFjROxJeNYChoFWUS5lBPjOTO2iM2PKuki2F1qfdPjjUjSeMuSk82Uml4fRimlLVR2OUAYBGHpo5aV4OJll1iYRkqWhag4FY75WXhmlTOoedo/ZVKI40Jz+pOEnX2ZsqcCeVJ0piEQieJhiEx0RBkrd6J2IiQFVXqPbZjEYqdmll6Jz0avtUaCqEFtaiqnekmHUagOhSZapvJMeioanXa2KqukZoCrJoruCKNmCngKZGC7urprPRKq+QZBP2XFKRGunBwL6waZjIbiBInu9hoj0o5wraRiOadrsX1kK24HvBK25rnstuvuu/DGK++89NZr77345qvvvhYlAAA7"
- },
- {
- "name": "The Christmas Party",
- "description": "Hold on. Last Christmas, the Research Director was incredibly hammered. He made a big mention that his brand new festivus pole was actually some kind of astrological... something something. You can't remember the whole details, because you were smashed as well. However, briefly, the RD did keep that festivus pole for awhile, he might even still have it somewhere.\nMaybe...?",
- "choices": [
- {
- "key": "choice 15",
- "name": "Wait a minute, was that a god damn...",
- "exit_node": "Rod.",
- "delay": 100,
- "delay_message": "Immovable Rod?"
- }
- ],
- "image": null,
- "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD/AACmAAD///+mpqYAAAAAAAAD/xi63P4wykmrvTjrzbv/YCiOZGmejKCubOu6aCzP4WvfeD4IQ+//Pppw+MkZj6udkgdsOp/EqFSBrNqWwQ9hy+16v9ypeGM9YpnAE3hNKLTfhbG8kuTZz/j7/fmb+/8TbIJefIVNgIiJGgWMjY6PkJGRipSVlpeYmZqbnJ2en6ChoqOkpRplqDdYpqyprnpKhnyslq9leFkYg2u0QrZmaLkgu8S9JDg7Ksl2yrB7UDJw0m6QxiFfedk9wbKH1tbEW9TT05Lmjd/pJpPq7e7v8PHy8/T19vf4+fr7Kb9W/Jn8pcLiBKAcgVZwdethkAZCJAoXFmx47KEOWLMeAAlHMf+DRVXctIT70jHCxxYEhXkYKajkgpMRGZZgKY5LHJcJ6zTTNtHEm5rjgsK5WZKZs2xHn2WccY6RSwdJkaaUOOApoHDbqFK16ofmF6Fg20Aa8IhrpbBBm6plZ7aTIwhr0bWdS7eu3bt48+rdy7ev37+AAwseTLiw4cOI0/1LfPAkC8YlHINUApmD5MncVFZ+cPmK0qWbX3Z2MbVbaCqjd37eCjk1i9JaZR5OHTN2z8Gua9v2wavvaNjeKgygqbcz8KocvBLAexn4MOVb5jZfjVwk9DBW/S3bTn3E9S5cUXFvphqaCOhBs7/WGbU96PNr0EpTT96o1Kjv4ZOT/xbn/f921AWnBlhNWQXggZndFkNcZv2X1YOxsCZENXdJBWGCWp2mQRc+YLibIRpaQMyHEoVYgVfDcUFiHyYGopxQTgwFYosugsFfOXGZQyMGN+boI1E78vjjkHIF+QGRahkZxVpKNunkk1BGKeWUVFZp5ZVYZqnllogkAAA7"
- },
- {
- "name": "Rod.",
- "description": "You cross reference your documentation. Sure enough, the \"festivus rod\" collected was actually an immovable rod.\nEnergy detected from the rod is the exact same coming off of the tree, as well. It's all making sense now. The Immovable rod is producing a kind of unique blackbody radiation that is providing sample heat and light for what is effectively an internal cold fusion process, and producing just enough of that radiation to create a kind of micro-enviromental bubble around the biosignature of the tree.\n\nThis would make the first time an immovable rod would exist in tandem with a biological source. You jot down some research notes on your findings, which could easily produce some kind of experimental tech, no doubt.",
- "choices": [
- {
- "key": "choice 16",
- "name": "Snap a photo",
- "exit_node": "Epilogue.",
- "delay": 40,
- "delay_message": "You could easily win an award for these findings!"
- }
- ],
- "image": "default"
- },
- {
- "name": "Epilogue.",
- "description": "You take a photo with the onboard camera on the drone. Suddenly, the immovable rod inside the tree explodes out of the wooden biological shell, and produces a blank, blurry photo.\nWhat the fuck?",
- "choices": [
- {
- "key": "choice 17",
- "name": "God damnit.",
- "exit_node": "WIN",
- "delay": 10,
- "delay_message": "Some things were just not meant for man to know."
- }
- ],
- "image": "default"
- },
- {
- "name": "What is wrong with this tree?",
- "description": "This is ridiculous. Nothing about this dumbass tree makes sense. It makes no sense, it's just sitting there, living and making a MOCKERY of all of science!\nYou didn't get your degree in advanced plasma-physics for this!",
- "choices": [
- {
- "key": "choice 18",
- "name": "The world can never know about this dumbass stupid plant.",
- "exit_node": "FAIL_DEATH",
- "delay": 60,
- "delay_message": "Activating drone self-destruct."
- },
- {
- "key": "choice 19",
- "name": "Take a moment to calm down.",
- "exit_node": "Biological Scan",
- "on_selection_effects": [
- {
- "effect_type": "Add",
- "quality": "Confusion",
- "value": {
- "value_type": "random",
- "low": -3,
- "high": -5
- }
- }
- ],
- "delay": 20,
- "delay_message": "Breathe."
- }
- ],
- "image": "default"
- }
- ]
- }
+ "adventure_name": "There's a tree in the middle of space.",
+ "version": 1,
+ "starting_node": "Tree Start",
+ "starting_qualities": {
+ "Confusion": 0
+ },
+ "required_site_traits": [
+ "in space"
+ ],
+ "loot_categories": [
+ "research"
+ ],
+ "scan_band_mods": {
+ "Exotic Radiation": 10
+ },
+ "deep_scan_description": "",
+ "triggers": [
+ {
+ "name": "Confusion Trigger",
+ "target_node": "What is wrong with this tree?",
+ "requirements": [
+ {
+ "quality": "Confusion",
+ "operator": ">",
+ "value": 30
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "name": "Tree Start",
+ "description": "Camera online. Visual signs detect a fully grown, seemingly biological, and live tree located in the middle of the vacuum.\nSensors indicate it is not oxygenating, but energy is being collected via passive solar light from the nearby star.\nBaffling.",
+ "choices": [
+ {
+ "key": "choice 0",
+ "name": "Ignore site.",
+ "exit_node": "FAIL",
+ "delay": 10,
+ "delay_message": "Leave this for the botanists to figure out."
+ },
+ {
+ "key": "choice 1",
+ "name": "Begin sensor scan.",
+ "exit_node": "Biological Scan",
+ "delay": 10,
+ "delay_message": "Lets get some data."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMQAAAAAAAC15h2QtxeAoxVykROizRpxjxIAcgAAWwAAgAAAUAAAZAAAZgC0tLTKysr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AgjmRpnigqrMMqtCvhyvFs17hQuPva67ygTwgcGovA3a61BLaeryj0mapar9isNjCNylpfgWwsLpPPZqBst066le84fB5nRpvdvGDL7/v/JmBeaWiFhIeGbGqLio1ljm2CklKUA4CXmJmaAYidhp9mBmOiYqQEpqZlk3qbra6vsCOenqijtgIHCLG7vL29CQoNCsDCxMPBxw0MxsTLyMLCCwq+1LuVe9VZ0M/c297d4NvZ41vXeueU5N/r4e3o79jkvOb0UAe4+Pf6uMjS7P/TtAAcGKyeQXhT5ClcSKIdwYcHIyKMx7CiwocOwU3cWMmiR4YYQzqM+LGkSQXSUv/2y0jQH0qUJmNqoRSm5ixQbXLK2UmnJ886ToIilFltkk2cNw9B+umz6U87UOsRNXk0KdKrVrOSWWVwqlc+WHHWKnWqrKizZV+QGrA2yte31YDJPTZXrrMEzhbkPSYNWF+4gAFBeICJGTLDxVi2C5yNIxSQikVu48i4j8TLT1i2khwOM+bKVSQaeGLgQOnS+fIBwkiCszfPjqWAnn0ism1hsXO/oM279e3fuqmYeAChN++XKqP9BujP+EcXMKLkoFFm+o3qSH5oJ7L9SPfs3IlEHw/dhfNyLMpbX4+duvvw3uGDj08fCfn0+KVTPI9Ff/T37AHY3nXfFSifgfX9cJ//f/vxh95/1wkoYYQU5oDgfBh+t+B9DsJS4YABfiiDAWWQOGKJKJ6oYidcDdVhUVrFGFZWRl32IlEy0nJLWjyu1VYYrNxY2YxXjZUWW1+0FaSQDiaQ4z0y3OMXX3wxaSUKiWWZTF5c+sVASl8iB9OVbxFH3CVabmOMa9yQ6WYAbHL2Zh+UVRTnbV3NyUlwu120HEuw9WlloO+Mc+c3fCbEH6ES/Ybmn9AwWmdvnu1jaWoHpMBmFYdK+tmLiQqKCaTAebqRnh4d6o6pEqHqEan/sOqiqxbBumqoig42GK0WqUqQY7zCtZKvDgXL37AvEbvNAsaeJ6Zy0JKqErPNYpIj/xqOOKUtU9xu6+1OUekpCJFpLPXtud2mu21UTbhKLraPqCsvuvQ+FdQSqI57bbnxtlHvv/OCK5RsqO4Lr78BA6xwulDR+m4n5iYs8cJNVNyguwYzEi/FE3dsR7UpZJzIxh1z7FO4IJ8gMr860WuAEi/rEHMBM88s8ccpn/AwVhEjTDPMQMsc9M9Cx9HyIJbknMLOEJNC8xpOR/3zy1RPbXXVWD8tM9Rcq3Kx0lesvKORZz2NFtVmnWJ2j2NLAnYsiJCd9tlzz7122XTnnfbb2dxUi96A1x044GDwnc0Zfwuu+OCLK274c4UkzvjkZj3+ldxkUY5AApt3noDlldF1zP/YBAyzuemcp4665wGBDlcDeCUA+zKzyx77Arfnvjnuy+C+u+uADXPXMsTHXvzxxid/PPC8OEAYmnLBPoz0ttMee+3YXx/958y/oqsDl4hel/jkjz9+Md17Rf3620/fvu3uxy97A+lPVT75ic2lJWLDWD7AE/97QQCT1qv3sU9+6ytGAxTIQL7FpiLJiCD/JggOYJxKXAPMIAA3KCp1MHB6H5wfCEcowgRoUIAc/F8L3CSrr/EiGAsc4QKhEcMZwpCG0DihClHIQxVeaYdA1KEQAWioD9pQhEesoRJTOMQe/k9ITQwiEzloQ/q9sIpKxOENt9iAKHpxgw76ohOnKED/LWJRGJrIIhfVWEUxSnGMPDQOGd+4wwME0I76COAC2MhHNPZhjWbk4xzd2MTeNBGPuBiAPg6wyEbiIlMiaEBKYrjHQOJQG5YE5BbpSMhByvGQiixNKBlpGlI+8h5a0OQZg2ECVfYRhoOMJRx36JxZ/k+UbMGjKEupyERi4pUzjGQmV9lGWXLSkxiMoy+ICcxmNsCWxvRi/VYzw2pa85rYvKYKt8nNbnrzm96c5h+G6UwudhKacBTnOLPJznYuEJzwjCc41blOctoTlug8JjTpiSZ3+lOb3LTj/wSqSHlycwQPeB4//8jMe2KRgwJNZDQJaCaFLlQge6ykJP/ZTo3iYbGgHx3oPC+6CZRQspwNNakwEMnSE5L0FRndaEwXqFGO0lSmG32pQmLK043e1KYz7KlO5dHTouKUo0YdKjmMalSk4jSmSh3HAqb61KpqtKZBtWpOo1oNpmoVq9dkKrWMEwIAOw=="
+ },
+ {
+ "name": "Biological Scan",
+ "description": "You attempt to scan for clues regarding the tree's nature. It appears to be a fully mature oak tree. \n\nApproximated height is 13 ft, 6.4 inches. \n\nSubject sees no sign of an outer coating or otherwise layer protecting it from the void of space.\n\nSubject's surface temperature is 293.7 kelvin, as though it were sitting indoors.",
+ "choices": [
+ {
+ "key": "choice 2",
+ "name": "Check Sensor Integrity.",
+ "exit_node": "Its Not You...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": 5
+ }
+ ],
+ "delay": 50,
+ "delay_message": "This can't be right."
+ },
+ {
+ "key": "choice 4",
+ "name": "Attempt to take sample.",
+ "exit_node": "Sample Taken",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": 3
+ }
+ ],
+ "delay": 40,
+ "delay_message": "Snip snip."
+ },
+ {
+ "key": "choice 6",
+ "name": "Examine Tree Roots.",
+ "exit_node": "Examine Roots",
+ "delay": 10
+ },
+ {
+ "key": "choice 9",
+ "name": "Sequence Sample Radiation with background noise.",
+ "exit_node": "Background Analysis",
+ "requirements": [
+ {
+ "quality": "Sample",
+ "operator": ">=",
+ "value": 1
+ }
+ ],
+ "delay": 0,
+ "delay_message": "This can't be real."
+ },
+ {
+ "key": "choice 40",
+ "name": "Leave.",
+ "exit_node": "FAIL",
+ "delay": 0
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMQAAAAAAAC15h2QtxeAoxVykROizRpxjxIAcgAAWwAAgAAAUAAAZAAAZgC0tLTKysr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AgjmRpnigqrMMqtCvhyvFs17hQuPva67ygTwgcGovA3a61BLaeryj0mapar9isNjCNylpfgWwsLpPPZqBst066le84fB5nRpvdvGDL7/v/JmBeaWiFhIeGbGqLio1ljm2CklKUA4CXmJmaAYidhp9mBmOiYqQEpqZlk3qbra6vsCOenqijtgIHCLG7vL29CQoNCsDCxMPBxw0MxsTLyMLCCwq+1LuVe9VZ0M/c297d4NvZ41vXeueU5N/r4e3o79jkvOb0UAe4+Pf6uMjS7P/TtAAcGKyeQXhT5ClcSKIdwYcHIyKMx7CiwocOwU3cWMmiR4YYQzqM+LGkSQXSUv/2y0jQH0qUJmNqoRSm5ixQbXLK2UmnJ886ToIilFltkk2cNw9B+umz6U87UOsRNXk0KdKrVrOSWWVwqlc+WHHWKnWqrKizZV+QGrA2yte31YDJPTZXrrMEzhbkPSYNWF+4gAFBeICJGTLDxVi2C5yNIxSQikVu48i4j8TLT1i2khwOM+bKVSQaeGLgQOnS+fIBwkiCszfPjqWAnn0ism1hsXO/oM279e3fuqmYeAChN++XKqP9BujP+EcXMKLkoFFm+o3qSH5oJ7L9SPfs3IlEHw/dhfNyLMpbX4+duvvw3uGDj08fCfn0+KVTPI9Ff/T37AHY3nXfFSifgfX9cJ//f/vxh95/1wkoYYQU5oDgfBh+t+B9DsJS4YABfiiDAWWQOGKJKJ6oYidcDdVhUVrFGFZWRl32IlEy0nJLWjyu1VYYrNxY2YxXjZUWW1+0FaSQDiaQ4z0y3OMXX3wxaSUKiWWZTF5c+sVASl8iB9OVbxFH3CVabmOMa9yQ6WYAbHL2Zh+UVRTnbV3NyUlwu120HEuw9WlloO+Mc+c3fCbEH6ES/Ybmn9AwWmdvnu1jaWoHpMBmFYdK+tmLiQqKCaTAebqRnh4d6o6pEqHqEan/sOqiqxbBumqoig42GK0WqUqQY7zCtZKvDgXL37AvEbvNAsaeJ6Zy0JKqErPNYpIj/xqOOKUtU9xu6+1OUekpCJFpLPXtud2mu21UTbhKLraPqCsvuvQ+FdQSqI57bbnxtlHvv/OCK5RsqO4Lr78BA6xwulDR+m4n5iYs8cJNVNyguwYzEi/FE3dsR7UpZJzIxh1z7FO4IJ8gMr860WuAEi/rEHMBM88s8ccpn/AwVhEjTDPMQMsc9M9Cx9HyIJbknMLOEJNC8xpOR/3zy1RPbXXVWD8tM9Rcq3Kx0lesvKORZz2NFtVmnWJ2j2NLAnYsiJCd9tlzz7122XTnnfbb2dxUi96A1x044GDwnc0Zfwuu+OCLK274c4UkzvjkZj3+ldxkUY5AApt3noDlldF1zP/YBAyzuemcp4665wGBDlcDeCUA+zKzyx77Arfnvjnuy+C+u+uADXPXMsTHXvzxxid/PPC8OEAYmnLBPoz0ttMee+3YXx/958y/oqsDl4hel/jkjz9+Md17Rf3620/fvu3uxy97A+lPVT75ic2lJWLDWD7AE/97QQCT1qv3sU9+6ytGAxTIQL7FpiLJiCD/JggOYJxKXAPMIAA3KCp1MHB6H5wfCEcowgRoUIAc/F8L3CSrr/EiGAsc4QKhEcMZwpCG0DihClHIQxVeaYdA1KEQAWioD9pQhEesoRJTOMQe/k9ITQwiEzloQ/q9sIpKxOENt9iAKHpxgw76ohOnKED/LWJRGJrIIhfVWEUxSnGMPDQOGd+4wwME0I76COAC2MhHNPZhjWbk4xzd2MTeNBGPuBiAPg6wyEbiIlMiaEBKYrjHQOJQG5YE5BbpSMhByvGQiixNKBlpGlI+8h5a0OQZg2ECVfYRhoOMJRx36JxZ/k+UbMGjKEupyERi4pUzjGQmV9lGWXLSkxiMoy+ICcxmNsCWxvRi/VYzw2pa85rYvKYKt8nNbnrzm96c5h+G6UwudhKacBTnOLPJznYuEJzwjCc41blOctoTlug8JjTpiSZ3+lOb3LTj/wSqSHlycwQPeB4//8jMe2KRgwJNZDQJaCaFLlQge6ykJP/ZTo3iYbGgHx3oPC+6CZRQspwNNakwEMnSE5L0FRndaEwXqFGO0lSmG32pQmLK043e1KYz7KlO5dHTouKUo0YdKjmMalSk4jSmSh3HAqb61KpqtKZBtWpOo1oNpmoVq9dkKrWMEwIAOw=="
+ },
+ {
+ "name": "Its Not You...",
+ "description": "After re-connection is established, your sensors appear fine. Tree has not moved in the slightest since last observed. Temperature has fluxuated 0.2 kelvin upwards, as expected of a plant under direct light.\nLets try again.",
+ "choices": [
+ {
+ "key": "choice 3",
+ "name": "Restart biological scan.",
+ "exit_node": "Biological Scan",
+ "delay": 25,
+ "delay_message": "God damnit."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD////v7+/h4eEAAAAAAAAAAAAD/xi63P4wykmrvTjrzbv/YCiOZBkJaJqaLKOubSy7b23fODxPOb7/m4FASBRofEBeT1W6JU3DYaRIjQqfkCWKg1xoddjSQEgQlM/mdDkc0Fa+8PiWLSIM7Pj73RxOlVF/ITlKNXSGDXp5iiFoaBlqfpFyk12HMgSYmZkfmppmmJ8gjXwUf2lglqmqq6ytrq+wsbKztLW2t7i5um2Ug669uyBCKMNRc4Q9qXHBJlVUFMbRV6pfR04ONszQzlZRlm7IveLaimZ7eJAESUsfyVnX2hfqv/Nv4vfZ8RWJ/Hkdn6NGtRsFyEg4VLwK6UvVqROdgMcWSpxIsaLFixgzatzIsf+jR1gIP7YbN6uaSAn4wFGbdNJFsZcpFRqS09IBt2kW3CkzWdNmNyIYpIWkw9Nevgbwat2ccNPbTnbWjtKQWfGnVZwzoQaNqVXXkDFWGx3qeoIry2CJzJ05p6aPTpRmfSXV1a/u2jM7TuldA8LXO6kXFemh5ihn3Lc9A9h1ZaqxpMNkK9rl5+EMKIGM9g5FSjJjv4aVOwEc/Q8iXx6ODSo5ualOQ9Bs0qVJTLu27du4c+vezbu379/AgwsfTry48ePIkytfqHp5xhfLadby+1soMJDLbhchlqIYpVbXa14dz718NMBEz/ZsWsU8zMhY1CdmL+VE+/ObgWSvPR4rj/f/xqxUFG1fVfHIUgLCN58zR5ABySpwbAAdZ1TNMgZYF9YHjWmsDAgNeglNqFSBJJYiG14JUmcYiHNZWJcE6HD4FGIrVuiFjROxJeNYChoFWUS5lBPjOTO2iM2PKuki2F1qfdPjjUjSeMuSk82Uml4fRimlLVR2OUAYBGHpo5aV4OJll1iYRkqWhag4FY75WXhmlTOoedo/ZVKI40Jz+pOEnX2ZsqcCeVJ0piEQieJhiEx0RBkrd6J2IiQFVXqPbZjEYqdmll6Jz0avtUaCqEFtaiqnekmHUagOhSZapvJMeioanXa2KqukZoCrJoruCKNmCngKZGC7urprPRKq+QZBP2XFKRGunBwL6waZjIbiBInu9hoj0o5wraRiOadrsX1kK24HvBK25rnstuvuu/DGK++89NZr77345qvvvhYlAAA7"
+ },
+ {
+ "name": "Sample Taken",
+ "description": "You collect and project a small sample of tree bark off the plant. The instant that the bark is removed from the tree, as though it suddenly remembered what it was, the moisture content of the bark freezes over, and implodes into small microparticles of splinters.\nSmall radioactive signature detected.",
+ "choices": [
+ {
+ "key": "choice 5",
+ "name": "Well that was... unexpected.",
+ "exit_node": "Biological Scan",
+ "delay": 0,
+ "delay_message": "Maybe something else might work better.",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Sample",
+ "value": 1
+ }
+ ]
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAABSLiiPVjtyRS9mOTFbNyYAAAAD/xi63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+sDUQxTDbeK7vfF/3saCwcxPcCMckcqlM2pDPnnRKrUqH2IdAYDBwu+Bwl2Agm8sE8iy9Zrqbb2Z0bq3b74W8Xp+1bP+Af2KDhIVlh2eJaUtsi3CPcU43d5QDe5cFfSKBnJ2en50Ff6JboqYCBWiLq46tkK+RUJOTmJmatxyguqSovae/vsGirK2NjU21trjLQp+8wNDCwnmo1MnM2Nna29zd3t/g4eLj5OXm59o5Rjo/QOjvLaKwkrKylfdW8FleXImIBm0CxppXD4c9fAgT5tCn4Iuhh4fG/DNTjNHAi4/oKERYq/+cro8gPYXxR7EkK4EEUxbUOCVZHoabQoZ8NkqaKWI4BR7DGKcjTG4yaZaySRSatQLGfP5ciqHX0GhQpzGdSrWq1atYs2rdyrWr169gw4pVsc7G2LMLkM46SOkl2nc86W3E97aFgLh4Dc7dq6NuhC0TVVlUOY8l38N9szocREYiyYqEMdJZibjylW+AIRIiqUowyrwDJ+u1XMelsm6CNKtmHHiRZ8iuQGecVdklZpm4V3NWREznYNmhaeNRCg638eOBhrUWjPP3zshNDNuwzRW5dZrQmms3BhzSNb8SrIPCXvTudtjzMIEngVxo+ajDtKOkvt5u0Jrw3/s6yqd+Nk7/7uUn4FH+VaWfKAUmqOCCDDbo4IMQRijhhBRWaOGFGGao4YYcdujhhyCGKOKFRsgxy4jaBEeZOyi60J2JK1piS2ItcvDiaMIpVGMEv0GnIo46grgFbM/dKBppC004kXzo3fiGdJb51wUgiKTBnHNOzsYWkpd1xZpjrTHJnY+vQGlZOztMBUZmrO0mZo9Zwggkl/mkKc5qIy3n2nlExrnWnHROcRueYjSm55Wr+Ebmj2YeZpo4bBJqyG6Inqeon0duRF84kuJJKW98Nrkojlu2dYlHkXaq6qehNvfZqHI2+ihq4lGpKhisttpbbKNm6sN3t9Wam2q5Vtrqq5jesKk3jsI220mheuq665jJzoqqs9gGcqi0211KGLBUZYutciUZG6q3cREnlrjiPcZtTqIiAW597H5E07uuiqqehPXONIq57+6rYb81PWUTvmksuyHBTjUc1XnVmOLWjgFkG+CBNk1MsRb1kiegLxtf4KzHB4b8Qa0XA2OyfZ6QjODKywwlM8w012zzzTjnrPNWCQAAOw=="
+ },
+ {
+ "name": "Examine Roots",
+ "description": "All plant matter has to derive energy and moisture from someplace. Examining the oak tree's roots reveals that the roots present all appear to splay out, similar to how a normal tree would. However, those roots then proceed to double back in on itself. This might suggest that the tree is obtaining nutrients from... itself.",
+ "choices": [
+ {
+ "key": "choice 7",
+ "name": "That's fucking stupid.",
+ "exit_node": "Biological Scan",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": {
+ "value_type": "random",
+ "low": 6,
+ "high": 10
+ }
+ }
+ ],
+ "delay": 0,
+ "delay_message": "What the hell kind of tree even IS this?"
+ },
+ {
+ "key": "choice 8",
+ "name": "Obtain biological sample from roots.",
+ "exit_node": "Sample Taken",
+ "delay": 10,
+ "delay_message": "This is why we hire botanists on-site."
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMQAAAAAAAC15h2QtxeAoxVykROizRpxjxIAcgAAWwAAgAAAUAAAZAAAZgC0tLTKysr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AgjmRpnigqrMMqtCvhyvFs17hQuPva67ygTwgcGovA3a61BLaeryj0mapar9isNjCNylpfgWwsLpPPZqBst066le84fB5nRpvdvGDL7/v/JmBeaWiFhIeGbGqLio1ljm2CklKUA4CXmJmaAYidhp9mBmOiYqQEpqZlk3qbra6vsCOenqijtgIHCLG7vL29CQoNCsDCxMPBxw0MxsTLyMLCCwq+1LuVe9VZ0M/c297d4NvZ41vXeueU5N/r4e3o79jkvOb0UAe4+Pf6uMjS7P/TtAAcGKyeQXhT5ClcSKIdwYcHIyKMx7CiwocOwU3cWMmiR4YYQzqM+LGkSQXSUv/2y0jQH0qUJmNqoRSm5ixQbXLK2UmnJ886ToIilFltkk2cNw9B+umz6U87UOsRNXk0KdKrVrOSWWVwqlc+WHHWKnWqrKizZV+QGrA2yte31YDJPTZXrrMEzhbkPSYNWF+4gAFBeICJGTLDxVi2C5yNIxSQikVu48i4j8TLT1i2khwOM+bKVSQaeGLgQOnS+fIBwkiCszfPjqWAnn0ism1hsXO/oM279e3fuqmYeAChN++XKqP9BujP+EcXMKLkoFFm+o3qSH5oJ7L9SPfs3IlEHw/dhfNyLMpbX4+duvvw3uGDj08fCfn0+KVTPI9Ff/T37AHY3nXfFSifgfX9cJ//f/vxh95/1wkoYYQU5oDgfBh+t+B9DsJS4YABfiiDAWWQOGKJKJ6oYidcDdVhUVrFGFZWRl32IlEy0nJLWjyu1VYYrNxY2YxXjZUWW1+0FaSQDiaQ4z0y3OMXX3wxaSUKiWWZTF5c+sVASl8iB9OVbxFH3CVabmOMa9yQ6WYAbHL2Zh+UVRTnbV3NyUlwu120HEuw9WlloO+Mc+c3fCbEH6ES/Ybmn9AwWmdvnu1jaWoHpMBmFYdK+tmLiQqKCaTAebqRnh4d6o6pEqHqEan/sOqiqxbBumqoig42GK0WqUqQY7zCtZKvDgXL37AvEbvNAsaeJ6Zy0JKqErPNYpIj/xqOOKUtU9xu6+1OUekpCJFpLPXtud2mu21UTbhKLraPqCsvuvQ+FdQSqI57bbnxtlHvv/OCK5RsqO4Lr78BA6xwulDR+m4n5iYs8cJNVNyguwYzEi/FE3dsR7UpZJzIxh1z7FO4IJ8gMr860WuAEi/rEHMBM88s8ccpn/AwVhEjTDPMQMsc9M9Cx9HyIJbknMLOEJNC8xpOR/3zy1RPbXXVWD8tM9Rcq3Kx0lesvKORZz2NFtVmnWJ2j2NLAnYsiJCd9tlzz7122XTnnfbb2dxUi96A1x044GDwnc0Zfwuu+OCLK274c4UkzvjkZj3+ldxkUY5AApt3noDlldF1zP/YBAyzuemcp4665wGBDlcDeCUA+zKzyx77Arfnvjnuy+C+u+uADXPXMsTHXvzxxid/PPC8OEAYmnLBPoz0ttMee+3YXx/958y/oqsDl4hel/jkjz9+Md17Rf3620/fvu3uxy97A+lPVT75ic2lJWLDWD7AE/97QQCT1qv3sU9+6ytGAxTIQL7FpiLJiCD/JggOYJxKXAPMIAA3KCp1MHB6H5wfCEcowgRoUIAc/F8L3CSrr/EiGAsc4QKhEcMZwpCG0DihClHIQxVeaYdA1KEQAWioD9pQhEesoRJTOMQe/k9ITQwiEzloQ/q9sIpKxOENt9iAKHpxgw76ohOnKED/LWJRGJrIIhfVWEUxSnGMPDQOGd+4wwME0I76COAC2MhHNPZhjWbk4xzd2MTeNBGPuBiAPg6wyEbiIlMiaEBKYrjHQOJQG5YE5BbpSMhByvGQiixNKBlpGlI+8h5a0OQZg2ECVfYRhoOMJRx36JxZ/k+UbMGjKEupyERi4pUzjGQmV9lGWXLSkxiMoy+ICcxmNsCWxvRi/VYzw2pa85rYvKYKt8nNbnrzm96c5h+G6UwudhKacBTnOLPJznYuEJzwjCc41blOctoTlug8JjTpiSZ3+lOb3LTj/wSqSHlycwQPeB4//8jMe2KRgwJNZDQJaCaFLlQge6ykJP/ZTo3iYbGgHx3oPC+6CZRQspwNNakwEMnSE5L0FRndaEwXqFGO0lSmG32pQmLK043e1KYz7KlO5dHTouKUo0YdKjmMalSk4jSmSh3HAqb61KpqtKZBtWpOo1oNpmoVq9dkKrWMEwIAOw=="
+ },
+ {
+ "name": "Background Analysis",
+ "description": "You compare the radioactive energy bands of the sample collected earlier with that of the nearby solar enviroment.\nNothing.\nThere is nothing nearby that matches the passive signal of the tree, or the bark, or anything similar.\nThis is really starting to get on your nerves.",
+ "choices": [
+ {
+ "key": "choice 10",
+ "name": "Smash your desk in frustration.",
+ "exit_node": "FAIL",
+ "delay": 50,
+ "delay_message": "No amount of pay is worth dealing with magical plant juju."
+ },
+ {
+ "key": "choice 11",
+ "name": "Check every known energy spectroscopy database.",
+ "exit_node": "Sample Match Found",
+ "delay": 900,
+ "delay_message": "You NEED an answer. You DESERVE an answer."
+ }
+ ],
+ "image": null,
+ "on_enter_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": {
+ "value_type": "random",
+ "low": 3,
+ "high": 5
+ }
+ }
+ ],
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD///9ZVlKsMjJpampGR0cAAAAD/xi63P4wykmFpTjrDa0XXCiOZGlqQ0qsazqIXyx9Z23feKayfP/WskgwRywqCsikcslsOpGpgGvQNDo8lYt1W4I+v2CvMhUuI0/YiZbL5nxdhkJ8Lq/T6eZ8HRmHUtuAgRp6T3B2SXdNfYeJc3CCkJEYjISVYZKRQ5gajZSdi0yboqM3nnWkqKmCpgaqrq9GBrKzs7AKaYG4tji0vbu/wCW6wZpqNMHIOAM9zDw/IzEgHcVt1KOWZQ1TU4LDV2vJOAVU2GZkY5VF4NPr4SRM53yI8/Lw9uVvBcLuknqGn4cqLQIYhwy/g0fwKdSDsCEGggtDOZwoAiLFi5sAYtwoqf/XIVjeOKrqRUukjZAmGVjDGC2lSyAts7R7GW5ZM2bPNERDGSAmzWA2b/pAc+yBT0ArVeXhsC2oj5wnefaU+pNBxDJNXUAqyq6qhKthyCUR+4QLVa5ej5AFO3ZJPGw3qE5N22DsWiUD6Tm5e/cqCbk96dZtQgZip3rjwIIa90fwqrBUDAfcQ0hyilaOIWG7zIoSYryd73DLrJntNlOIO2WVQ/raYrYKW/8CCNuJbH6S8aS7vSCpq9C8gxMMzns48dudj9/WqFw2yZKvfDc38Rxzc+kiq09f/nw7b+wNwV9HuwuwSvLeL14wjz49pm3Qds7sbd59jh1Co3CQP1+8/Rr/TgnVQnxHNVDgf0QEKCBUIRxIX3sIAiigMwzCAKF/cc1H1xQTVvjXhRCqE6IosU2Q1YkiajiViin+VhthWW0l1YhevQjjafrIKI0QLP5kI2GMBWkbG2fVx1FiP/oxRl9DEiEXCDsKhuSPazFpBg5P9miSkHmlpleXbbkVUVRRTkOakl7mRg9fsJmQ5Zl2DaDmJ4XVwZkZYHbpoU726eZCaID28RYYeRFEToQVeVaYLHIwOkugk70GqWiNIeoGQJc5qumclOU5qUGWXhraFNU1yumpdoAa6ganZkrLpJ2G5qido606CKoGwDErrqaaSlKttrIKq2r/DGvHr39aF2wIgsYaduKzqyUhhbLLjsArp3BVW0Oz3OpWlrY5dCsuKOByMS635QpyLaTEGenauZ2M5y4m696B4Lz0spLuRMnt25BH9vr7b3XUChwOwbIYfBDCBSscDMIOL0xwxO5ATPHB2kVHY7oTg7RxuR0rhyFFIYv8MUYZX5xMySoDk3LLD3dXbQIAOw=="
+ },
+ {
+ "name": "Sample Match Found",
+ "description": "After an extensive algorithm search on the controller end, you have a single match to this specific band and style of energy.\nThe problem, is that the source of said radiation is coming not only from Space Station 13, no.\nIt's coming from the Space Station 13 Research Department.\nWhat the fuck?",
+ "choices": [
+ {
+ "key": "choice 12",
+ "name": "Something must be wrong with the drone.",
+ "exit_node": "Its Not You...",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": {
+ "value_type": "random",
+ "low": 6,
+ "high": 10
+ }
+ }
+ ],
+ "delay": 30,
+ "delay_message": "Lousy piece of junk must be scanning the station instead of the target."
+ },
+ {
+ "key": "choice 13",
+ "name": "Perhaps that sample was tainted. Collect a new sample.",
+ "exit_node": "Sample Taken",
+ "delay": 60,
+ "delay_message": "Lets try again, but carefully."
+ },
+ {
+ "key": "choice 14",
+ "name": "Remember the Christmas Party.",
+ "exit_node": "The Christmas Party",
+ "requirements": [
+ {
+ "quality": "Confusion",
+ "operator": "<=",
+ "value": 25
+ }
+ ],
+ "delay": 100,
+ "delay_message": "Wait a gosh darn fucking second."
+ }
+ ],
+ "image": null,
+ "on_enter_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": 10
+ }
+ ],
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD////v7+/h4eEAAAAAAAAAAAAD/xi63P4wykmrvTjrzbv/YCiOZBkJaJqaLKOubSy7b23fODxPOb7/m4FASBRofEBeT1W6JU3DYaRIjQqfkCWKg1xoddjSQEgQlM/mdDkc0Fa+8PiWLSIM7Pj73RxOlVF/ITlKNXSGDXp5iiFoaBlqfpFyk12HMgSYmZkfmppmmJ8gjXwUf2lglqmqq6ytrq+wsbKztLW2t7i5um2Ug669uyBCKMNRc4Q9qXHBJlVUFMbRV6pfR04ONszQzlZRlm7IveLaimZ7eJAESUsfyVnX2hfqv/Nv4vfZ8RWJ/Hkdn6NGtRsFyEg4VLwK6UvVqROdgMcWSpxIsaLFixgzatzIsf+jR1gIP7YbN6uaSAn4wFGbdNJFsZcpFRqS09IBt2kW3CkzWdNmNyIYpIWkw9Nevgbwat2ccNPbTnbWjtKQWfGnVZwzoQaNqVXXkDFWGx3qeoIry2CJzJ05p6aPTpRmfSXV1a/u2jM7TuldA8LXO6kXFemh5ihn3Lc9A9h1ZaqxpMNkK9rl5+EMKIGM9g5FSjJjv4aVOwEc/Q8iXx6ODSo5ualOQ9Bs0qVJTLu27du4c+vezbu379/AgwsfTry48ePIkytfqHp5xhfLadby+1soMJDLbhchlqIYpVbXa14dz718NMBEz/ZsWsU8zMhY1CdmL+VE+/ObgWSvPR4rj/f/xqxUFG1fVfHIUgLCN58zR5ABySpwbAAdZ1TNMgZYF9YHjWmsDAgNeglNqFSBJJYiG14JUmcYiHNZWJcE6HD4FGIrVuiFjROxJeNYChoFWUS5lBPjOTO2iM2PKuki2F1qfdPjjUjSeMuSk82Uml4fRimlLVR2OUAYBGHpo5aV4OJll1iYRkqWhag4FY75WXhmlTOoedo/ZVKI40Jz+pOEnX2ZsqcCeVJ0piEQieJhiEx0RBkrd6J2IiQFVXqPbZjEYqdmll6Jz0avtUaCqEFtaiqnekmHUagOhSZapvJMeioanXa2KqukZoCrJoruCKNmCngKZGC7urprPRKq+QZBP2XFKRGunBwL6waZjIbiBInu9hoj0o5wraRiOadrsX1kK24HvBK25rnstuvuu/DGK++89NZr77345qvvvhYlAAA7"
+ },
+ {
+ "name": "The Christmas Party",
+ "description": "Hold on. Last Christmas, the Research Director was incredibly hammered. He made a big mention that his brand new festivus pole was actually some kind of astrological... something something. You can't remember the whole details, because you were smashed as well. However, briefly, the RD did keep that festivus pole for awhile, he might even still have it somewhere.\nMaybe...?",
+ "choices": [
+ {
+ "key": "choice 15",
+ "name": "Wait a minute, was that a god damn...",
+ "exit_node": "Rod.",
+ "delay": 100,
+ "delay_message": "Immovable Rod?"
+ }
+ ],
+ "image": null,
+ "raw_image": "data:image/gif;base64,R0lGODdhyABkAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAyABkAMIAAAAAAAD/AACmAAD///+mpqYAAAAAAAAD/xi63P4wykmrvTjrzbv/YCiOZGmejKCubOu6aCzP4WvfeD4IQ+//Pppw+MkZj6udkgdsOp/EqFSBrNqWwQ9hy+16v9ypeGM9YpnAE3hNKLTfhbG8kuTZz/j7/fmb+/8TbIJefIVNgIiJGgWMjY6PkJGRipSVlpeYmZqbnJ2en6ChoqOkpRplqDdYpqyprnpKhnyslq9leFkYg2u0QrZmaLkgu8S9JDg7Ksl2yrB7UDJw0m6QxiFfedk9wbKH1tbEW9TT05Lmjd/pJpPq7e7v8PHy8/T19vf4+fr7Kb9W/Jn8pcLiBKAcgVZwdethkAZCJAoXFmx47KEOWLMeAAlHMf+DRVXctIT70jHCxxYEhXkYKajkgpMRGZZgKY5LHJcJ6zTTNtHEm5rjgsK5WZKZs2xHn2WccY6RSwdJkaaUOOApoHDbqFK16ofmF6Fg20Aa8IhrpbBBm6plZ7aTIwhr0bWdS7eu3bt48+rdy7ev37+AAwseTLiw4cOI0/1LfPAkC8YlHINUApmD5MncVFZ+cPmK0qWbX3Z2MbVbaCqjd37eCjk1i9JaZR5OHTN2z8Gua9v2wavvaNjeKgygqbcz8KocvBLAexn4MOVb5jZfjVwk9DBW/S3bTn3E9S5cUXFvphqaCOhBs7/WGbU96PNr0EpTT96o1Kjv4ZOT/xbn/f921AWnBlhNWQXggZndFkNcZv2X1YOxsCZENXdJBWGCWp2mQRc+YLibIRpaQMyHEoVYgVfDcUFiHyYGopxQTgwFYosugsFfOXGZQyMGN+boI1E78vjjkHIF+QGRahkZxVpKNunkk1BGKeWUVFZp5ZVYZqnllogkAAA7"
+ },
+ {
+ "name": "Rod.",
+ "description": "You cross reference your documentation. Sure enough, the \"festivus rod\" collected was actually an immovable rod.\nEnergy detected from the rod is the exact same coming off of the tree, as well. It's all making sense now. The Immovable rod is producing a kind of unique blackbody radiation that is providing sample heat and light for what is effectively an internal cold fusion process, and producing just enough of that radiation to create a kind of micro-enviromental bubble around the biosignature of the tree.\n\nThis would make the first time an immovable rod would exist in tandem with a biological source. You jot down some research notes on your findings, which could easily produce some kind of experimental tech, no doubt.",
+ "choices": [
+ {
+ "key": "choice 16",
+ "name": "Snap a photo",
+ "exit_node": "Epilogue.",
+ "delay": 40,
+ "delay_message": "You could easily win an award for these findings!"
+ }
+ ],
+ "image": "default"
+ },
+ {
+ "name": "Epilogue.",
+ "description": "You take a photo with the onboard camera on the drone. Suddenly, the immovable rod inside the tree explodes out of the wooden biological shell, and produces a blank, blurry photo.\nWhat the fuck?",
+ "choices": [
+ {
+ "key": "choice 17",
+ "name": "God damnit.",
+ "exit_node": "WIN",
+ "delay": 10,
+ "delay_message": "Some things were just not meant for man to know."
+ }
+ ],
+ "image": "default",
+ "on_enter_effects": [
+ {
+ "effect_type": "Set",
+ "quality": "Confusion",
+ "value": 9999
+ }
+ ]
+ },
+ {
+ "name": "What is wrong with this tree?",
+ "description": "This is ridiculous. Nothing about this dumbass tree makes sense. It makes no sense, it's just sitting there, living and making a MOCKERY of all of science!\nYou didn't get your degree in advanced plasma-physics for this!",
+ "choices": [
+ {
+ "key": "choice 18",
+ "name": "The world can never know about this dumbass stupid plant.",
+ "exit_node": "FAIL_DEATH",
+ "delay": 60,
+ "delay_message": "Activating drone self-destruct."
+ },
+ {
+ "key": "choice 19",
+ "name": "Take a moment to calm down.",
+ "exit_node": "Biological Scan",
+ "on_selection_effects": [
+ {
+ "effect_type": "Add",
+ "quality": "Confusion",
+ "value": {
+ "value_type": "random",
+ "low": -3,
+ "high": -5
+ }
+ }
+ ],
+ "delay": 20,
+ "delay_message": "Breathe."
+ }
+ ],
+ "image": "default"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/modules/explorer_drone/exodrone.dm b/code/modules/explorer_drone/exodrone.dm
index 72cf09bd81b..5c73f5755df 100644
--- a/code/modules/explorer_drone/exodrone.dm
+++ b/code/modules/explorer_drone/exodrone.dm
@@ -5,7 +5,7 @@
// Fuel types and travel time per unit of distance on that fuel.
#define FUEL_BASIC "basic"
-#define BASIC_FUEL_TIME_COST 300
+#define BASIC_FUEL_TIME_COST 250
#define FUEL_ADVANCED "advanced"
#define ADVANCED_FUEL_TIME_COST 200
@@ -334,9 +334,9 @@ GLOBAL_LIST_EMPTY(exodrone_launchers)
drone_log("Sustained [amount] damage.")
/obj/item/exodrone/proc/drone_log(message)
- drone_log.Insert(1,message)
if(length(drone_log) > EXODRONE_LOG_SIZE)
- drone_log.Cut(EXODRONE_LOG_SIZE)
+ drone_log = list()
+ drone_log.Insert(1,message)
/obj/item/exodrone/proc/has_tool(tool_type)
return tools.Find(tool_type)
@@ -354,6 +354,11 @@ GLOBAL_LIST_EMPTY(exodrone_launchers)
. = ..()
GLOB.exodrone_launchers += src
+/obj/machinery/exodrone_launcher/examine(user)
+ . = ..()
+ if(fuel_canister)
+ . += span_notice("You can remove the [fuel_canister] with a prying tool.")
+
/obj/machinery/exodrone_launcher/attackby(obj/item/weapon, mob/living/user, params)
if(istype(weapon, /obj/item/fuel_pellet))
if(fuel_canister)
@@ -374,7 +379,7 @@ GLOBAL_LIST_EMPTY(exodrone_launchers)
if(!fuel_canister)
return
- to_chat(user, span_notice("You remove the [fuel_canister] from the [src]."))
+ to_chat(user, span_notice("You remove [fuel_canister] from [src]."))
fuel_canister.forceMove(drop_location())
fuel_canister = null
update_icon()
diff --git a/code/modules/explorer_drone/scanner_array.dm b/code/modules/explorer_drone/scanner_array.dm
index e5b5f7a026c..7f019b0d43c 100644
--- a/code/modules/explorer_drone/scanner_array.dm
+++ b/code/modules/explorer_drone/scanner_array.dm
@@ -14,9 +14,9 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
#define MAX_SCAN_DISTANCE 10
-#define WIDE_SCAN_COST(BAND, SCAN_POWER) (((BAND*BAND)/(SCAN_POWER))*2*60*10)
-#define BASE_POINT_SCAN_TIME (5 MINUTES)
-#define BASE_DEEP_SCAN_TIME (5 MINUTES)
+#define WIDE_SCAN_COST(BAND, SCAN_POWER) (min(((BAND*BAND)/(SCAN_POWER))*2*60*10, 10 MINUTES))
+#define BASE_POINT_SCAN_TIME (2 MINUTES)
+#define BASE_DEEP_SCAN_TIME (3 MINUTES)
/// Represents scan in progress, only one globally for now, todo later split per z or allow partial dish swarm usage
/datum/exoscan
@@ -35,7 +35,7 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
var/scan_time = 0
switch(scan_type)
if(EXOSCAN_WIDE)
- scan_power = length(GLOB.exoscanner_controller.tracked_dishes)
+ scan_power = GLOB.exoscanner_controller.calculate_scan_power()
scan_time = WIDE_SCAN_COST(GLOB.exoscanner_controller.wide_scan_band,scan_power)
if(EXOSCAN_POINT)
scan_power = GLOB.exoscanner_controller.get_scan_power(target)
@@ -76,7 +76,8 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
deltimer(scan_timer)
/obj/machinery/computer/exoscanner_control
- name = "Scanner Array Control Console"
+ name = "scanner array control console"
+ desc = "Controls scanner arrays to initiate scans for exodrones."
circuit = /obj/item/circuitboard/computer/exoscanner_console
/// If scan was interrupted show a popup until dismissed.
var/failed_popup = FALSE
@@ -105,7 +106,7 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
condition_descriptions += condition.description
.["scan_conditions"] = condition_descriptions
else
- .["scan_power"] = scan_power = length(GLOB.exoscanner_controller.tracked_dishes)
+ .["scan_power"] = scan_power = GLOB.exoscanner_controller.calculate_scan_power()
.["wide_scan_eta"] = scan_power > 0 ? WIDE_SCAN_COST(GLOB.exoscanner_controller.wide_scan_band,scan_power) : 0
.["possible_sites"] = build_exploration_site_ui_data()
.["scan_conditions"] = null
@@ -196,11 +197,35 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
icon = 'icons/obj/exploration.dmi'
icon_state = "scanner_off"
desc = "A sophisticated scanning array. Easily influenced by its environment."
+ circuit = /obj/item/circuitboard/machine/exoscanner
+ ///the scan power of this array to supply to scanner_controller
+ var/scan_power = 1
/obj/machinery/exoscanner/Initialize(mapload)
. = ..()
RegisterSignals(GLOB.exoscanner_controller,list(COMSIG_EXOSCAN_STARTED,COMSIG_EXOSCAN_FINISHED), PROC_REF(scan_change))
update_readiness()
+ RefreshParts()
+
+/obj/machinery/exoscanner/RefreshParts()
+ . = ..()
+ var/power = 1
+
+ for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
+ power += (scanning_module.tier - 1) / 12
+ scan_power = power
+ GLOB.exoscanner_controller.update_scan_power()
+
+/obj/machinery/exoscanner/screwdriver_act(mob/user, obj/item/tool)
+ . = ..()
+ if(!.)
+ . = default_deconstruction_screwdriver(user, "scanner_open", "scanner_off", tool)
+ update_readiness()
+
+/obj/machinery/exoscanner/crowbar_act(mob/user, obj/item/tool)
+ ..()
+ if(default_deconstruction_crowbar(tool))
+ return TRUE
/obj/machinery/exoscanner/proc/scan_change()
SIGNAL_HANDLER
@@ -215,7 +240,7 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
GLOB.exoscanner_controller.deactivate_scanner(src)
/obj/machinery/exoscanner/proc/is_ready()
- return anchored && is_operational
+ return anchored && is_operational && !panel_open
/obj/machinery/exoscanner/proc/update_readiness()
if(is_ready())
@@ -302,7 +327,7 @@ GLOBAL_LIST_INIT(scan_conditions,init_scan_conditions())
/datum/scanner_controller/proc/calculate_scan_power(conditions)
. = 0
for(var/obj/machinery/exoscanner/dish in tracked_dishes)
- var/effective_power = 1
+ var/effective_power = dish.scan_power
for(var/datum/scan_condition/condition in conditions)
effective_power *= condition.check_dish(dish)
if(!effective_power) //Don't bother continuing if it's zero
diff --git a/code/modules/fishing/aquarium/fish_analyzer.dm b/code/modules/fishing/aquarium/fish_analyzer.dm
index c7feab860ca..8afe053a4d5 100644
--- a/code/modules/fishing/aquarium/fish_analyzer.dm
+++ b/code/modules/fishing/aquarium/fish_analyzer.dm
@@ -190,16 +190,17 @@
if(fish.status != FISH_DEAD)
render_list += "\n"
- var/hunger = PERCENT(min((world.time - fish.last_feeding) / fish.feeding_frequency, 1))
- var/hunger_string = "[hunger]%"
- switch(hunger)
- if(0 to 60)
- hunger_string = span_info(hunger_string)
- if(60 to 90)
- hunger_string = span_warning(hunger_string)
- if(90 to 100)
- hunger_string = span_alert(hunger_string)
- render_list += "Hunger: [hunger_string]\n"
+ if(!HAS_TRAIT(fish, TRAIT_FISH_NO_HUNGER))
+ var/hunger = PERCENT(min((world.time - fish.last_feeding) / fish.feeding_frequency, 1))
+ var/hunger_string = "[hunger]%"
+ switch(hunger)
+ if(0 to 60)
+ hunger_string = span_info(hunger_string)
+ if(60 to 90)
+ hunger_string = span_warning(hunger_string)
+ if(90 to 100)
+ hunger_string = span_alert(hunger_string)
+ render_list += "Hunger: [hunger_string]\n"
var/time_left = round(max(fish.breeding_wait - world.time, 0)/10)
render_list += "Time until it can breed: [time_left] seconds"
diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm
index 21585a07546..1275886141b 100644
--- a/code/modules/fishing/fish/_fish.dm
+++ b/code/modules/fishing/fish/_fish.dm
@@ -136,6 +136,9 @@
var/min_pressure = WARNING_LOW_PRESSURE
var/max_pressure = HAZARD_HIGH_PRESSURE
+ /// If this fish type counts towards the Fish Species Scanning experiments
+ var/experisci_scannable = TRUE
+
/obj/item/fish/Initialize(mapload, apply_qualities = TRUE)
. = ..()
AddComponent(/datum/component/aquarium_content, PROC_REF(get_aquarium_animation), list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED))
@@ -175,7 +178,7 @@
balloon_alert(user, "[src] is dead!")
return TRUE
feed(item.reagents)
- balloon_alert(user, "you feed [src]")
+ balloon_alert(user, "fed [src]")
return TRUE
/obj/item/fish/examine(mob/user)
@@ -185,14 +188,14 @@
. += span_notice("It weighs [weight] g.")
///Randomizes weight and size.
-/obj/item/fish/proc/randomize_size_and_weight(avg_size = average_size, avg_weight = average_weight, deviation = 0.2, first_run = FALSE)
- var/size_deviation = 0.2 * avg_size
- var/new_size = round(max(1,gaussian(avg_size, size_deviation)), 1)
+/obj/item/fish/proc/randomize_size_and_weight(base_size = average_size, base_weight = average_weight, deviation = 0.2)
+ var/size_deviation = 0.2 * base_size
+ var/new_size = round(clamp(gaussian(base_size, size_deviation), average_size * 1/MAX_FISH_DEVIATION_COEFF, average_size * MAX_FISH_DEVIATION_COEFF))
- var/weight_deviation = 0.2 * avg_weight
- var/new_weight = round(max(1,gaussian(avg_weight, weight_deviation)), 1)
+ var/weight_deviation = 0.2 * base_weight
+ var/new_weight = round(clamp(gaussian(base_weight, weight_deviation), average_weight * 1/MAX_FISH_DEVIATION_COEFF, average_weight * MAX_FISH_DEVIATION_COEFF))
- update_size_and_weight(new_size, new_weight, first_run)
+ update_size_and_weight(new_size, new_weight)
///Updates weight and size, along with weight class, number of fillets you can get and grind results.
/obj/item/fish/proc/update_size_and_weight(new_size = average_size, new_weight = average_weight)
@@ -414,11 +417,14 @@
return FALSE
return TRUE
+/obj/item/fish/proc/is_hungry()
+ return !HAS_TRAIT(src, TRAIT_FISH_NO_HUNGER) && world.time - last_feeding >= feeding_frequency
+
/obj/item/fish/proc/process_health(seconds_per_tick)
var/health_change_per_second = 0
if(!proper_environment())
health_change_per_second -= 3 //Dying here
- if(world.time - last_feeding >= feeding_frequency)
+ if(is_hungry())
health_change_per_second -= 0.5 //Starving
else
health_change_per_second += 0.5 //Slowly healing
@@ -541,10 +547,11 @@
#define FLOP_SINGLE_MOVE_TIME 1.5
#define JUMP_X_DISTANCE 5
#define JUMP_Y_DISTANCE 6
-/// This animation should be applied to actual parent atom instead of vc_object.
-/proc/flop_animation(atom/movable/animation_target)
+
+/// This flopping animation played while the fish is alive.
+/obj/item/fish/proc/flop_animation()
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
- animate(animation_target, time = pause_between, loop = -1)
+ animate(src, time = pause_between, loop = -1)
//move nose down and up
for(var/_ in 1 to FLOP_COUNT)
var/matrix/up_matrix = matrix()
@@ -565,6 +572,7 @@
animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN)
animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
+
#undef PAUSE_BETWEEN_PHASES
#undef PAUSE_BETWEEN_FLOPS
#undef FLOP_COUNT
@@ -578,7 +586,7 @@
if(flopping) //Requires update_transform/animate_wrappers to be less restrictive.
return
flopping = TRUE
- flop_animation(src)
+ flop_animation()
/// Stops flopping animation
/obj/item/fish/proc/stop_flopping()
@@ -593,7 +601,7 @@
/obj/item/fish/proc/refresh_flopping()
if(flopping)
- flop_animation(src)
+ flop_animation()
/// Returns random fish, using random_case_rarity probabilities.
/proc/random_fish_type(required_fluid)
diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm
index bec868ad24e..63de4cfdf1d 100644
--- a/code/modules/fishing/fish/fish_traits.dm
+++ b/code/modules/fishing/fish/fish_traits.dm
@@ -12,6 +12,8 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
var/diff_traits_inheritability = 50
/// fishes of types within this list are granted to have this trait, no matter the probability
var/list/guaranteed_inheritance_types
+ /// Depending on the value, fish with trait will be reported as more or less difficult in the catalog.
+ var/added_difficulty = 0
/// Difficulty modifier from this mod, needs to return a list with two values
/datum/fish_trait/proc/difficulty_mod(obj/item/fishing_rod/rod, mob/fisherman)
@@ -161,7 +163,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/necrophage
name = "Necrophage"
- catalog_description = "This fish will eat the carcasses of dead fishes when hungry."
+ catalog_description = "This fish will eat carcasses of dead fish when hungry."
incompatible_traits = list(/datum/fish_trait/vegan)
/datum/fish_trait/necrophage/apply_to_fish(obj/item/fish/fish)
@@ -169,7 +171,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/necrophage/proc/eat_dead_fishes(obj/item/fish/source, seconds_per_tick)
SIGNAL_HANDLER
- if(world.time - source.last_feeding < source.feeding_frequency || !isaquarium(source.loc))
+ if(!source.is_hungry() || !isaquarium(source.loc))
return
for(var/obj/item/fish/victim in source.loc)
if(victim.status != FISH_DEAD || victim == source || HAS_TRAIT(victim, TRAIT_YUCKY_FISH))
@@ -233,7 +235,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/predator/proc/eat_fishes(obj/item/fish/source, seconds_per_tick)
SIGNAL_HANDLER
- if(world.time - source.last_feeding < source.feeding_frequency || !isaquarium(source.loc))
+ if(!source.is_hungry() || !isaquarium(source.loc))
return
var/obj/structure/aquarium/aquarium = source.loc
for(var/obj/item/fish/victim in aquarium.get_fishes(TRUE, source))
@@ -334,6 +336,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
diff_traits_inheritability = 45
guaranteed_inheritance_types = list(/obj/item/fish/clownfish/lube)
catalog_description = "This fish exudes a viscous, slippery lubrificant. It's reccomended not to step on it."
+ added_difficulty = 5
/datum/fish_trait/lubed/apply_to_fish(obj/item/fish/fish)
fish.AddComponent(/datum/component/slippery, 8 SECONDS, SLIDE|GALOSHES_DONT_HELP)
@@ -352,3 +355,26 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
ADD_TRAIT(fish, TRAIT_FISH_AMPHIBIOUS, FISH_TRAIT_DATUM)
if(fish.required_fluid_type == AQUARIUM_FLUID_AIR)
fish.required_fluid_type = AQUARIUM_FLUID_FRESHWATER
+
+/datum/fish_trait/mixotroph
+ name = "Mixotroph"
+ inheritability = 75
+ diff_traits_inheritability = 25
+ catalog_description = "This fish is capable of substaining itself by producing its own sources of energy (food)."
+ incompatible_traits = list(/datum/fish_trait/predator, /datum/fish_trait/necrophage)
+
+/datum/fish_trait/antigrav/apply_to_fish(obj/item/fish/fish)
+ ADD_TRAIT(fish, TRAIT_FISH_NO_HUNGER, FISH_TRAIT_DATUM)
+
+/datum/fish_trait/antigrav
+ name = "Anti-Gravity"
+ inheritability = 75
+ diff_traits_inheritability = 25
+ catalog_description = "This fish will invert the gravity of the bait at random. May fall upward outside after being caught."
+ added_difficulty = 15
+
+/datum/fish_trait/antigrav/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/minigame)
+ minigame.special_effects |= FISHING_MINIGAME_RULE_ANTIGRAV
+
+/datum/fish_trait/antigrav/apply_to_fish(obj/item/fish/fish)
+ fish.AddElement(/datum/element/forced_gravity, NEGATIVE_GRAVITY)
diff --git a/code/modules/fishing/fish/fish_types.dm b/code/modules/fishing/fish/fish_types.dm
index 652b0aba8aa..5ff62266ad9 100644
--- a/code/modules/fishing/fish/fish_types.dm
+++ b/code/modules/fishing/fish/fish_types.dm
@@ -341,7 +341,6 @@
icon_state = "sludgefish_purple"
dedicated_in_aquarium_icon_state = "sludgefish_purple_small"
random_case_rarity = FISH_RARITY_NOPE
- random_case_rarity = FISH_RARITY_VERY_RARE
fish_traits = list(/datum/fish_trait/parthenogenesis)
/obj/item/fish/slimefish
@@ -439,11 +438,13 @@
fillet_type = null
death_text = "%SRC gently disappears."
fish_traits = list(/datum/fish_trait/no_mating) //just to be sure, these shouldn't reproduce
+ experisci_scannable = FALSE
/obj/item/fish/holo/Initialize(mapload)
. = ..()
var/area/station/holodeck/holo_area = get_area(src)
if(!istype(holo_area))
+ addtimer(CALLBACK(src, PROC_REF(set_status), FISH_DEAD), 1 MINUTES)
return
holo_area.linked.add_to_spawned(src)
@@ -488,9 +489,9 @@
sprite_height = 5
/obj/item/fish/holo/checkered
- name = "unrendered holographic fish" //it's a meta joke, buddy.
+ name = "unrendered holographic fish"
desc = "A checkered silhoutte of searing purple and pitch black presents itself before your eyes, like a tear in fabric of reality. It hurts to watch."
- icon_state = "checkered"
+ icon_state = "checkered" //it's a meta joke, buddy.
dedicated_in_aquarium_icon_state = "checkered_small"
sprite_width = 4
@@ -502,3 +503,37 @@
sprite_height = 4
sprite_width = 10
average_size = 50
+
+/obj/item/fish/starfish
+ name = "cosmostarfish"
+ desc = "A peculiar, gravity-defying, echinoderm-looking critter from hyperspace."
+ icon_state = "starfish"
+ dedicated_in_aquarium_icon_state = "starfish_small"
+ icon_state_dead = "starfish_dead"
+ sprite_width = 4
+ average_size = 30
+ average_weight = 300
+ stable_population = 3
+ required_fluid_type = AQUARIUM_FLUID_AIR
+ random_case_rarity = FISH_RARITY_NOPE
+ required_temperature_min = 0
+ required_temperature_max = INFINITY
+ safe_air_limits = null
+ min_pressure = 0
+ max_pressure = INFINITY
+ grind_results = list(/datum/reagent/bluespace = 10, /datum/reagent/consumable/liquidgibs = 5)
+ fillet_type = null
+ fish_traits = list(/datum/fish_trait/antigrav, /datum/fish_trait/mixotroph)
+
+/obj/item/fish/starfish/Initialize(mapload)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/item/fish/starfish/update_overlays()
+ . = ..()
+ if(status == FISH_ALIVE)
+ . += emissive_appearance(icon, "starfish_emissive", src)
+
+///It spins, and dimly glows in the dark.
+/obj/item/fish/starfish/flop_animation()
+ DO_FLOATING_ANIM(src)
diff --git a/code/modules/fishing/fish_catalog.dm b/code/modules/fishing/fish_catalog.dm
index d41b4657e50..a0f66c2227d 100644
--- a/code/modules/fishing/fish_catalog.dm
+++ b/code/modules/fishing/fish_catalog.dm
@@ -68,14 +68,6 @@
if(source.catalog_description && (fish_type in source.fish_table))
spot_descriptions += source.catalog_description
.["spots"] = english_list(spot_descriptions, nothing_text = "Unknown")
- ///Difficulty descriptor
- switch(initial(fishy.fishing_difficulty_modifier))
- if(-INFINITY to 10)
- .["difficulty"] = "Easy"
- if(20 to 30)
- .["difficulty"] = "Medium"
- else
- .["difficulty"] = "Hard"
var/list/fish_list_properties = collect_fish_properties()
var/list/fav_bait = fish_list_properties[fishy][NAMEOF(fishy, favorite_bait)]
var/list/disliked_bait = fish_list_properties[fishy][NAMEOF(fishy, disliked_bait)]
@@ -91,12 +83,22 @@
// Fish traits description
var/list/trait_descriptions = list()
var/list/fish_traits = fish_list_properties[fishy][NAMEOF(fishy, fish_traits)]
+ var/fish_difficulty = initial(fishy.fishing_difficulty_modifier)
for(var/fish_trait in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
trait_descriptions += trait.catalog_description
+ fish_difficulty += trait.added_difficulty
if(!length(trait_descriptions))
trait_descriptions += "This fish exhibits no special behavior."
.["traits"] = trait_descriptions
+ ///Difficulty descriptor
+ switch(fish_difficulty)
+ if(-INFINITY to 9)
+ .["difficulty"] = "Easy"
+ if(10 to 19)
+ .["difficulty"] = "Medium"
+ else
+ .["difficulty"] = "Hard"
return .
/obj/item/book/fish_catalog/ui_assets(mob/user)
diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm
index 6169b41fd88..3f34222ad87 100644
--- a/code/modules/fishing/fishing_equipment.dm
+++ b/code/modules/fishing/fishing_equipment.dm
@@ -11,7 +11,8 @@
desc = "Simple fishing line."
icon = 'icons/obj/fishing.dmi'
icon_state = "reel_blue"
- var/fishing_line_traits = NONE
+ ///A list of traits that this fishing line has, checked by fish traits and the minigame.
+ var/list/fishing_line_traits
/// Color of the fishing line
var/line_color = "#808080"
@@ -59,7 +60,8 @@
icon_state = "hook"
w_class = WEIGHT_CLASS_TINY
- var/fishing_hook_traits = NONE
+ /// A list of traits that this fishing hook has, checked by fish traits and the minigame
+ var/list/fishing_hook_traits
/// icon state added to main rod icon when this hook is equipped
var/rod_overlay_icon_state = "hook_overlay"
/// What subtype of `/obj/item/chasm_detritus` do we fish out of chasms? Defaults to `/obj/item/chasm_detritus`.
@@ -175,7 +177,7 @@
name = "jawed hook"
desc = "Despite hints of rust, this gritty beartrap-like hook hybrid manages to look even more threating than the real thing. May neptune have mercy of whatever gets caught in its jaws."
icon_state = "jaws"
- fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_ENSNARE|FISHING_HOOK_KILL
+ fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_KILL
rod_overlay_icon_state = "hook_jaws_overlay"
/obj/item/storage/toolbox/fishing
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index 478279749e4..06ac03d64ac 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -43,8 +43,12 @@
var/fish_ai = FISH_AI_DUMB
/// Rule modifiers (eg weighted bait)
var/special_effects = NONE
- /// Did the game get past the baiting phase, used to track if bait should be consumed afterwards
- var/bait_taken = FALSE
+ /// A list of possible active minigame effects. If not empty, one will be picked from time to time.
+ var/list/active_effects
+ /// The cooldown between switching active effects
+ COOLDOWN_DECLARE(active_effect_cd)
+ /// The current active effect
+ var/current_active_effect
/// Result path
var/reward_path = FISHING_DUD
/// Minigame difficulty
@@ -61,6 +65,8 @@
var/obj/effect/fishing_lure/lure
/// Background icon state from fishing_hud.dmi
var/background = "background_default"
+ /// Fish icon state from fishing_hud.dmi
+ var/fish_icon = "fish"
/// Fishing line visual
var/datum/beam/fishing_line
@@ -168,7 +174,10 @@
RegisterSignal(user, COMSIG_MOB_FISHING_REWARD_DISPENSED, PROC_REF(hurt_fish))
difficulty += comp.fish_source.calculate_difficulty(reward_path, rod, user, src)
- difficulty = round(difficulty)
+ difficulty = clamp(round(difficulty), 1, 100)
+
+ if(HAS_TRAIT(user, TRAIT_REVEAL_FISH))
+ fish_icon = GLOB.specific_fish_icons[reward_path] || "fish"
/**
* If the chances are higher than 1% (100% at maximum difficulty), they'll scale
@@ -207,19 +216,21 @@
lure_turf?.balloon_alert(user, message)
/datum/fishing_challenge/proc/on_spot_gone(datum/source)
+ SIGNAL_HANDLER
send_alert("fishing spot gone!")
- interrupt(balloon_alert = FALSE)
+ interrupt()
/datum/fishing_challenge/proc/interrupt_challenge(datum/source, reason)
if(reason)
send_alert(reason)
- interrupt(balloon_alert = FALSE)
+ interrupt()
/datum/fishing_challenge/proc/start(mob/living/user)
/// Create fishing line visuals
fishing_line = used_rod.create_fishing_line(lure, target_py = 5)
+ active_effects = bitfield_to_list(special_effects & FISHING_MINIGAME_ACTIVE_EFFECTS)
// If fishing line breaks los / rod gets dropped / deleted
- RegisterSignal(fishing_line, COMSIG_FISHING_LINE_SNAPPED, PROC_REF(interrupt))
+ RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(on_line_deleted))
RegisterSignal(used_rod, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self))
ADD_TRAIT(user, TRAIT_GONE_FISHING, REF(src))
user.add_mood_event("fishing", /datum/mood_event/fishing)
@@ -228,6 +239,12 @@
to_chat(user, span_notice("You start fishing..."))
playsound(lure, 'sound/effects/splash.ogg', 100)
+/datum/fishing_challenge/proc/on_line_deleted(datum/source)
+ SIGNAL_HANDLER
+ fishing_line = null
+ send_alert(user.is_holding(used_rod) ? "line snapped" : "rod dropped")
+ interrupt()
+
/datum/fishing_challenge/proc/handle_click(mob/source, atom/target, modifiers)
SIGNAL_HANDLER
//You need to be holding the rod to use it.
@@ -241,12 +258,9 @@
return COMSIG_MOB_CANCEL_CLICKON
/// Challenge interrupted by something external
-/datum/fishing_challenge/proc/interrupt(datum/source, balloon_alert = TRUE)
- SIGNAL_HANDLER
+/datum/fishing_challenge/proc/interrupt()
if(!completed)
experience_multiplier *= 0.5
- if(balloon_alert)
- send_alert(user.is_holding(used_rod) ? "line snapped" : "tool dropped")
complete(FALSE)
/datum/fishing_challenge/proc/on_attack_self(obj/item/source, mob/user)
@@ -278,7 +292,8 @@
if(reward_path != FISHING_DUD)
playsound(lure, 'sound/effects/bigsplash.ogg', 100)
SEND_SIGNAL(src, COMSIG_FISHING_CHALLENGE_COMPLETED, user, win)
- qdel(src)
+ if(!QDELETED(src))
+ qdel(src)
/datum/fishing_challenge/proc/start_baiting_phase()
deltimer(next_phase_timer)
@@ -294,7 +309,30 @@
phase = BITING_PHASE
// Trashing animation
playsound(lure, 'sound/effects/fish_splash.ogg', 100)
- send_alert("!!!")
+ if(HAS_TRAIT(user, TRAIT_REVEAL_FISH))
+ switch(fish_icon)
+ if(FISH_ICON_DEF)
+ send_alert("fish!!!")
+ if(FISH_ICON_HOSTILE)
+ send_alert("hostile!!!")
+ if(FISH_ICON_STAR)
+ send_alert("starfish!!!")
+ if(FISH_ICON_CHUNKY)
+ send_alert("round fish!!!")
+ if(FISH_ICON_JELLYFISH)
+ send_alert("jellyfish!!!")
+ if(FISH_ICON_SLIME)
+ send_alert("slime!!!")
+ if(FISH_ICON_COIN)
+ send_alert("valuable!!!")
+ if(FISH_ICON_GEM)
+ send_alert("ore!!!")
+ if(FISH_ICON_CRAB)
+ send_alert("crustacean!!!")
+ if(FISH_ICON_BONE)
+ send_alert("bones!!!")
+ else
+ send_alert("!!!")
animate(lure, pixel_y = 3, time = 5, loop = -1, flags = ANIMATION_RELATIVE)
animate(pixel_y = -3, time = 5, flags = ANIMATION_RELATIVE)
// Setup next phase
@@ -307,7 +345,7 @@
///The player is no longer around to play the minigame, so we interrupt it.
/datum/fishing_challenge/proc/on_user_logout(datum/source)
SIGNAL_HANDLER
- interrupt(balloon_alert = FALSE)
+ interrupt()
/datum/fishing_challenge/proc/win_anyway()
if(!completed)
@@ -345,6 +383,9 @@
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(start_reeling))
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEUP, PROC_REF(stop_reeling))
RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(on_user_logout))
+ if(length(active_effects))
+ // Give the player a moment to prepare for active minigame effects
+ COOLDOWN_START(src, active_effect_cd, rand(5, 9) SECONDS)
START_PROCESSING(SSfishing, src)
///Stop processing and remove references to the minigame hud
@@ -369,11 +410,44 @@
///Update the state of the fish, the bait and the hud
/datum/fishing_challenge/process(seconds_per_tick)
+ if(length(active_effects) && COOLDOWN_FINISHED(src, active_effect_cd))
+ select_active_effect()
move_fish(seconds_per_tick)
move_bait(seconds_per_tick)
if(!QDELETED(fishing_hud))
update_visuals()
+///The proc that handles fancy effects like flipping the hud or skewing movement
+/datum/fishing_challenge/proc/select_active_effect()
+ ///bring forth an active effect
+ if(isnull(current_active_effect))
+ current_active_effect = pick(active_effects)
+ switch(current_active_effect)
+ if(FISHING_MINIGAME_RULE_ANTIGRAV)
+ fishing_hud.icon_state = "background_antigrav"
+ SEND_SOUND(user, sound('sound/effects/arcade_jump.ogg', volume = 50))
+ COOLDOWN_START(src, active_effect_cd, rand(6, 9) SECONDS)
+ if(FISHING_MINIGAME_RULE_FLIP)
+ fishing_hud.icon_state = "background_flip"
+ fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
+ SEND_SOUND(user, sound('sound/effects/boing.ogg'))
+ COOLDOWN_START(src, active_effect_cd, rand(5, 6) SECONDS)
+ return
+
+ ///go back to normal
+ switch(current_active_effect)
+ if(FISHING_MINIGAME_RULE_ANTIGRAV)
+ var/sound/inverted_sound = sound('sound/effects/arcade_jump.ogg', volume = 50)
+ inverted_sound.frequency = -1
+ SEND_SOUND(user, inverted_sound)
+ COOLDOWN_START(src, active_effect_cd, rand(10, 13) SECONDS)
+ if(FISHING_MINIGAME_RULE_FLIP)
+ fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
+ COOLDOWN_START(src, active_effect_cd, rand(8, 12) SECONDS)
+
+ fishing_hud.icon_state = background
+ current_active_effect = null
+
///The proc that moves the fish around, just like in the old TGUI, mostly.
/datum/fishing_challenge/proc/move_fish(seconds_per_tick)
var/long_chance = long_jump_chance * seconds_per_tick * 10
@@ -464,6 +538,9 @@
velocity_change = round(velocity_change)
+ if(current_active_effect == FISHING_MINIGAME_RULE_ANTIGRAV)
+ velocity_change = -velocity_change
+
/**
* Pull the brake on the velocity if the current velocity and the acceleration
* have different directions, making the bait less slippery, thus easier to control
@@ -524,7 +601,7 @@
icon_state = challenge.background
add_overlay("frame")
hud_bait = new(null, null, challenge)
- hud_fish = new
+ hud_fish = new(null, null, challenge)
hud_completion = new(null, null, challenge)
vis_contents += list(hud_bait, hud_fish, hud_completion)
challenge.user.client.screen += src
@@ -557,6 +634,11 @@
icon_state = "fish"
vis_flags = VIS_INHERIT_ID
+/atom/movable/screen/hud_fish/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
+ . = ..()
+ if(challenge)
+ icon_state = challenge.fish_icon
+
/atom/movable/screen/hud_completion
icon = 'icons/hud/fishing_hud.dmi'
icon_state = "completion_0"
diff --git a/code/modules/fishing/fishing_portal_machine.dm b/code/modules/fishing/fishing_portal_machine.dm
index 3fc6b0eb938..b156a37ba05 100644
--- a/code/modules/fishing/fishing_portal_machine.dm
+++ b/code/modules/fishing/fishing_portal_machine.dm
@@ -1,40 +1,58 @@
/obj/machinery/fishing_portal_generator
name = "fish-porter 3000"
- desc = "fishing anywhere, anytime, anyway what was i talking about"
-
+ desc = "Fishing anywhere, anytime... anyway what was I talking about?"
icon = 'icons/obj/fishing.dmi'
- icon_state = "portal_off"
-
+ icon_state = "portal"
idle_power_usage = 0
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
-
anchored = FALSE
density = TRUE
+ circuit = /obj/item/circuitboard/machine/fishing_portal_generator
- var/fishing_source = /datum/fish_source/portal
+ ///The current fishing spot loaded in
var/datum/component/fishing_spot/active
+/obj/machinery/fishing_portal_generator/on_set_panel_open()
+ update_appearance()
+ return ..()
+
/obj/machinery/fishing_portal_generator/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
return TOOL_ACT_TOOLTYPE_SUCCESS
+/obj/machinery/fishing_portal_generator/examine(mob/user)
+ . = ..()
+ . += span_notice("You can unlock further portal settings by completing fish scanning experiments.")
+
+/obj/machinery/fishing_portal_generator/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ obj_flags |= EMAGGED
+ balloon_alert(user, "syndicate setting loaded")
+ playsound(src, SFX_SPARKS, 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
/obj/machinery/fishing_portal_generator/interact(mob/user, special_state)
. = ..()
if(active)
deactivate()
else
- activate()
+ select_fish_source(user)
-/obj/machinery/fishing_portal_generator/update_icon(updates)
+/obj/machinery/fishing_portal_generator/update_overlays()
. = ..()
- if(active)
- icon_state = "portal_on"
- else
- icon_state = "portal_off"
+ if(panel_open)
+ . += "portal_open"
+ if(!active)
+ return
+ . += "portal_on"
+ var/datum/fish_source/portal/portal = active.fish_source
+ . += portal.overlay_state
+ . += emissive_appearance(icon, "portal_emissive", src)
-/obj/machinery/fishing_portal_generator/proc/activate()
- active = AddComponent(/datum/component/fishing_spot, fishing_source)
+/obj/machinery/fishing_portal_generator/proc/activate(datum/fish_source/selected_source)
+ active = AddComponent(/datum/component/fishing_spot, selected_source)
use_power = ACTIVE_POWER_USE
update_icon()
@@ -46,3 +64,39 @@
/obj/machinery/fishing_portal_generator/on_set_is_operational(old_value)
if(old_value)
deactivate()
+
+///Create a radial menu from a list of available fish sources. If only the default is available, activate it right away.
+/obj/machinery/fishing_portal_generator/proc/select_fish_source(mob/user)
+ var/datum/fish_source/portal/default = GLOB.preset_fish_sources[/datum/fish_source/portal]
+ var/list/available_fish_sources = list(default.radial_name = default)
+ if(obj_flags & EMAGGED)
+ var/datum/fish_source/portal/syndicate = GLOB.preset_fish_sources[/datum/fish_source/portal/syndicate]
+ available_fish_sources[syndicate.radial_name] = syndicate
+ for (var/datum/techweb/techweb as anything in SSresearch.techwebs)
+ var/get_fish_sources = FALSE
+ for(var/obj/machinery/rnd/server/server as anything in techweb.techweb_servers)
+ if(!is_valid_z_level(get_turf(server), get_turf(src)))
+ continue
+ get_fish_sources = TRUE
+ break
+ if(!get_fish_sources)
+ continue
+ for(var/experiment_type in typesof(/datum/experiment/scanning/fish))
+ var/datum/experiment/scanning/fish/experiment = techweb.completed_experiments[experiment_type]
+ if(!experiment)
+ continue
+ var/datum/fish_source/portal/reward = GLOB.preset_fish_sources[experiment.fish_source_reward]
+ available_fish_sources[reward.radial_name] = reward
+
+ if(length(available_fish_sources) == 1)
+ activate(default)
+ return
+ var/list/choices = list()
+ for(var/radial_name in available_fish_sources)
+ var/datum/fish_source/portal/source = available_fish_sources[radial_name]
+ choices[radial_name] = image(icon = 'icons/hud/radial_fishing.dmi', icon_state = source.radial_state)
+
+ var/choice = show_radial_menu(user, src, choices, radius = 38, custom_check = CALLBACK(src, TYPE_PROC_REF(/atom, can_interact), user), tooltips = TRUE)
+ if(!choice || !can_interact(user))
+ return
+ activate(available_fish_sources[choice])
diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm
index 7e01f693dd5..1abba8e414c 100644
--- a/code/modules/fishing/fishing_rod.dm
+++ b/code/modules/fishing/fishing_rod.dm
@@ -30,14 +30,11 @@
var/obj/item/currently_hooked_item
/// Fishing line visual for the hooked item
- var/datum/beam/hooked_item_fishing_line
+ var/datum/beam/fishing_line/fishing_line
/// Are we currently casting
var/casting = FALSE
- /// List of fishing line beams
- var/list/fishing_lines = list()
-
/// The default color for the reel overlay if no line is equipped.
var/default_line_color = "gray"
@@ -66,13 +63,10 @@
return NONE
/obj/item/fishing_rod/Destroy(force)
- . = ..()
- //Remove any leftover fishing lines
- QDEL_LIST(fishing_lines)
+ return ..()
/obj/item/fishing_rod/examine(mob/user)
. = ..()
- . += "Right-Click in your active hand to access its slots UI"
var/list/equipped_stuff = list()
if(line)
equipped_stuff += "[icon2html(line, user)] [line.name]"
@@ -84,6 +78,7 @@
. += span_notice("\a [icon2html(bait, user)] [bait] is being used as bait.")
else
. += span_warning("It doesn't have any bait attached. Fishing will be more tedious!")
+ . += span_notice("Right-Click in your active hand to access its slots UI")
/**
* Catch weight modifier for the given fish_type (or FISHING_DUD)
@@ -140,7 +135,7 @@
// Should probably respect and used force move later
step_towards(currently_hooked_item, get_turf(src))
if(get_dist(currently_hooked_item,get_turf(src)) < 1)
- clear_hooked_item()
+ QDEL_NULL(fishing_line)
/obj/item/fishing_rod/attack_self_secondary(mob/user, modifiers)
. = ..()
@@ -159,30 +154,28 @@
var/mob/user = loc
if(!istype(user))
return
+ if(fishing_line)
+ QDEL_NULL(fishing_line)
var/beam_color = line?.line_color || default_line_color
- var/datum/beam/fishing_line/fishing_line_beam = new(user, target, icon_state = "fishing_line", beam_color = beam_color, emissive = FALSE, override_target_pixel_y = target_py)
- fishing_line_beam.lefthand = user.get_held_index_of_item(src) % 2 == 1
- RegisterSignal(fishing_line_beam, COMSIG_BEAM_BEFORE_DRAW, PROC_REF(check_los))
- RegisterSignal(fishing_line_beam, COMSIG_QDELETING, PROC_REF(clear_line))
- fishing_lines += fishing_line_beam
- INVOKE_ASYNC(fishing_line_beam, TYPE_PROC_REF(/datum/beam/, Start))
+ fishing_line = new(user, target, icon_state = "fishing_line", beam_color = beam_color, emissive = FALSE, override_target_pixel_y = target_py)
+ fishing_line.lefthand = user.get_held_index_of_item(src) % 2 == 1
+ RegisterSignal(fishing_line, COMSIG_BEAM_BEFORE_DRAW, PROC_REF(check_los))
+ RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(clear_line))
+ INVOKE_ASYNC(fishing_line, TYPE_PROC_REF(/datum/beam/, Start))
user.update_held_items()
- return fishing_line_beam
+ return fishing_line
/obj/item/fishing_rod/proc/clear_line(datum/source)
SIGNAL_HANDLER
- fishing_lines -= source
if(ismob(loc))
var/mob/user = loc
user.update_held_items()
+ fishing_line = null
+ currently_hooked_item = null
/obj/item/fishing_rod/dropped(mob/user, silent)
. = ..()
- if(currently_hooked_item)
- clear_hooked_item()
- for(var/datum/beam/fishing_line in fishing_lines)
- SEND_SIGNAL(fishing_line, COMSIG_FISHING_LINE_SNAPPED)
- QDEL_LIST(fishing_lines)
+ QDEL_NULL(fishing_line)
/// Hooks the item
/obj/item/fishing_rod/proc/hook_item(mob/user, atom/target_atom)
@@ -191,28 +184,21 @@
if(!can_be_hooked(target_atom))
return
currently_hooked_item = target_atom
- hooked_item_fishing_line = create_fishing_line(target_atom)
- RegisterSignal(hooked_item_fishing_line, COMSIG_FISHING_LINE_SNAPPED, PROC_REF(clear_hooked_item))
+ create_fishing_line(target_atom)
+ SEND_SIGNAL(src, COMSIG_FISHING_ROD_HOOKED_ITEM, target_atom, user)
/// Checks what can be hooked
/obj/item/fishing_rod/proc/can_be_hooked(atom/movable/target)
// Could be made dependent on actual hook, ie magnet to hook metallic items
return isitem(target)
-/obj/item/fishing_rod/proc/clear_hooked_item()
- SIGNAL_HANDLER
-
- if(!QDELETED(hooked_item_fishing_line))
- QDEL_NULL(hooked_item_fishing_line)
- currently_hooked_item = null
-
// Checks fishing line for interruptions and range
/obj/item/fishing_rod/proc/check_los(datum/beam/source)
SIGNAL_HANDLER
. = NONE
- if(!isturf(source.origin.loc) || !isturf(source.target.loc) || !CheckToolReach(src, source.target, cast_range))
- SEND_SIGNAL(source, COMSIG_FISHING_LINE_SNAPPED) //Stepped out of range or los interrupted
+ if(!CheckToolReach(src, source.target, cast_range))
+ qdel(source)
return BEAM_CANCEL_DRAW
/obj/item/fishing_rod/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
@@ -300,7 +286,7 @@
reel_overlay.color = line_color
. += reel_overlay
/// if we don't have anything hooked show the dangling hook & line
- if(isinhands && length(fishing_lines) == 0)
+ if(isinhands && !fishing_line)
var/mutable_appearance/line_overlay = mutable_appearance(icon_file, "line_overlay")
line_overlay.appearance_flags |= RESET_COLOR
line_overlay.color = line_color
@@ -507,8 +493,8 @@
balloon_alert(user, active ? "extended" : "collapsed")
playsound(src, 'sound/weapons/batonextend.ogg', 50, TRUE)
update_appearance(UPDATE_OVERLAYS)
- if(currently_hooked_item)
- clear_hooked_item()
+ if(fishing_line)
+ QDEL_NULL(fishing_line)
return COMPONENT_NO_DEFAULT_MESSAGE
/obj/item/fishing_rod/telescopic/master
@@ -523,16 +509,34 @@
/obj/item/fishing_rod/tech
name = "advanced fishing rod"
desc = "An embedded universal constructor along with micro-fusion generator makes this marvel of technology never run out of bait. Interstellar treaties prevent using it outside of recreational fishing. And you can fish with this. "
- ui_description = "This rod has an infinite supply of synthetic bait."
+ ui_description = "This rod has an infinite supply of synth-bait. Also doubles as an Experi-Scanner for fish."
icon_state = "fishing_rod_science"
reel_overlay = "reel_science"
/obj/item/fishing_rod/tech/Initialize(mapload)
. = ..()
+
+ var/static/list/fishing_signals = list(
+ COMSIG_FISHING_ROD_HOOKED_ITEM = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_FISHING_ROD_CAUGHT_FISH = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_PRE_ATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_AFTERATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, ignored_handheld_experiment_attempt),
+ )
+ AddComponent(/datum/component/experiment_handler, \
+ config_mode = EXPERIMENT_CONFIG_ALTCLICK, \
+ allowed_experiments = list(/datum/experiment/scanning/fish), \
+ config_flags = EXPERIMENT_CONFIG_SILENT_FAIL|EXPERIMENT_CONFIG_IMMEDIATE_ACTION, \
+ experiment_signals = fishing_signals, \
+ )
+
var/obj/item/food/bait/doughball/synthetic/infinite_supply_of_bait = new(src)
bait = infinite_supply_of_bait
update_icon()
+/obj/item/fishing_rod/tech/examine(mob/user)
+ . = ..()
+ . += span_notice("Alt-Click to access the Experiment Configuration UI")
+
/obj/item/fishing_rod/tech/consume_bait(atom/movable/reward)
return
diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm
index 5a34db9ce5f..657b2f30968 100644
--- a/code/modules/fishing/sources/_fish_source.dm
+++ b/code/modules/fishing/sources/_fish_source.dm
@@ -1,11 +1,47 @@
GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_source, list()))
+/**
+ * When adding new fishable rewards to a table/counts, you can specify an icon to show in place of the
+ * generic fish icon in the minigame UI should the user have the TRAIT_REVEAL_FISH trait, by adding it to
+ * this list.
+ *
+ * A lot of the icons here may be a tad inaccurate, but since we're limited to the free font awesome icons we
+ * have access to, we got to make do.
+ */
+GLOBAL_LIST_INIT(specific_fish_icons, zebra_typecacheof(list(
+ /mob/living/basic/carp = FISH_ICON_DEF,
+ /mob/living/basic/mining = FISH_ICON_HOSTILE,
+ /obj/effect/decal/remains = FISH_ICON_BONE,
+ /obj/effect/mob_spawn/corpse = FISH_ICON_BONE,
+ /obj/item/coin = FISH_ICON_COIN,
+ /obj/item/fish = FISH_ICON_DEF,
+ /obj/item/fish/armorfish = FISH_ICON_CRAB,
+ /obj/item/fish/boned = FISH_ICON_BONE,
+ /obj/item/fish/chasm_crab = FISH_ICON_CRAB,
+ /obj/item/fish/gunner_jellyfish = FISH_ICON_JELLYFISH,
+ /obj/item/fish/holo/crab = FISH_ICON_CRAB,
+ /obj/item/fish/holo/puffer = FISH_ICON_CHUNKY,
+ /obj/item/fish/mastodon = FISH_ICON_BONE,
+ /obj/item/fish/pufferfish = FISH_ICON_CHUNKY,
+ /obj/item/fish/slimefish = FISH_ICON_SLIME,
+ /obj/item/fish/sludgefish = FISH_ICON_SLIME,
+ /obj/item/fish/starfish = FISH_ICON_STAR,
+ /obj/item/storage/wallet = FISH_ICON_COIN,
+ /obj/item/stack/sheet/bone = FISH_ICON_BONE,
+ /obj/item/stack/sheet/mineral = FISH_ICON_GEM,
+ /obj/item/stack/ore = FISH_ICON_GEM,
+ /obj/structure/closet/crate = FISH_ICON_COIN,
+)))
+
/**
* Where the fish actually come from - every fishing spot has one assigned but multiple fishing holes
* can share single source, ie single shared one for ocean/lavaland river
*/
/datum/fish_source
- /// Fish catch weight table - these are relative weights
+ /**
+ * Fish catch weight table - these are relative weights
+ *
+ */
var/list/fish_table = list()
/// If a key from fish_table is present here, that fish is availible in limited quantity and is reduced by one on successful fishing
var/list/fish_counts = list()
@@ -25,6 +61,10 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
if(!(path in fish_table))
stack_trace("path [path] found in the 'fish_counts' list but not in the fish_table one of [type]")
+///Called when src is set as the fish source of a fishing spot component
+/datum/fish_source/proc/on_fishing_spot_init(/datum/component/fishing_spot/spot)
+ return
+
/// Can we fish in this spot at all. Returns DENIAL_REASON or null if we're good to go
/datum/fish_source/proc/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman)
return rod.reason_we_cant_fish(src)
@@ -35,7 +75,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
* This includes the source's fishing difficulty, that of the fish, the rod,
* favorite and disliked baits, fish traits and the fisherman skill.
*
- * For non-fish, it's just the source's fishing difficulty minus the fisherman skill, rod and settler modifiers.
+ * For non-fish, it's just the source's fishing difficulty minus the fisherman skill.
*/
/datum/fish_source/proc/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge)
. = fishing_difficulty
@@ -96,7 +136,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
* Used to register signals or add traits and the such right after conditions have been cleared
* and before the minigame starts.
*/
-/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user)
+/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
return
///Proc called when the challenge is interrupted within the fish source code.
@@ -116,7 +156,9 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = initial(caught.name))
var/turf/fishing_spot = get_turf(source.lure)
var/atom/movable/reward = dispense_reward(source.reward_path, user, fishing_spot)
- source.used_rod?.consume_bait(reward)
+ if(source.used_rod)
+ SEND_SIGNAL(source.used_rod, COMSIG_FISHING_ROD_CAUGHT_FISH, reward, user)
+ source.used_rod.consume_bait(reward)
/// Gives out the reward if possible
/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman, turf/fishing_spot)
diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm
index ffb37753881..e2e5491dd1d 100644
--- a/code/modules/fishing/sources/source_types.dm
+++ b/code/modules/fishing/sources/source_types.dm
@@ -19,11 +19,168 @@
/datum/fish_source/portal
fish_table = list(
- FISHING_DUD = 5,
+ FISHING_DUD = 7,
/obj/item/fish/goldfish = 10,
/obj/item/fish/guppy = 10,
+ /obj/item/fish/angelfish = 10,
+ )
+ catalog_description = "Aquarium dimension (Fishing portal generator)"
+ ///The name of this option shown in the radial menu on the fishing portal generator
+ var/radial_name = "Aquarium"
+ ///The icon state shown for this option in the radial menu
+ var/radial_state = "fish_tank"
+ ///The icon state of the overlay shown on the machine when active.
+ var/overlay_state = "portal_aquarium"
+
+/datum/fish_source/portal/beach
+ fish_table = list(
+ FISHING_DUD = 10,
+ /obj/item/fish/clownfish = 10,
+ /obj/item/fish/pufferfish = 10,
+ /obj/item/fish/cardinal = 10,
+ /obj/item/fish/greenchromis = 10,
+ )
+ catalog_description = "Beach dimension (Fishing portal generator)"
+ radial_name = "Beach"
+ radial_state = "palm_beach"
+
+/datum/fish_source/portal/chasm
+ background = "background_lavaland"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/chasm_crab = 10,
+ /obj/item/fish/boned = 5,
+ /obj/item/stack/sheet/bone = 5,
+ )
+ catalog_description = "Chasm dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Chasm"
+ overlay_state = "portal_chasm"
+ radial_state = "ground_hole"
+
+/datum/fish_source/portal/ocean
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/lanternfish = 5,
+ /obj/item/fish/firefish = 5,
+ /obj/item/fish/dwarf_moonfish = 5,
+ /obj/item/fish/gunner_jellyfish = 5,
+ /obj/item/fish/needlefish = 5,
+ /obj/item/fish/armorfish = 5,
+ )
+ catalog_description = "Ocean dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Ocean"
+ overlay_state = "portal_ocean"
+ radial_state = "seaboat"
+
+/datum/fish_source/portal/hyperspace
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/starfish = 6,
+ /obj/item/stack/ore/bluespace_crystal = 2,
+ /mob/living/basic/carp = 2,
+ )
+ fish_counts = list(
+ /obj/item/stack/ore/bluespace_crystal = 10,
)
- catalog_description = "Fish dimension (Fishing portal generator)"
+ catalog_description = "Hyperspace dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Hyperspace"
+ overlay_state = "portal_hyperspace"
+ radial_state = "space_rocket"
+
+///Unlocked by emagging the fishing portal generator with an emag.
+/datum/fish_source/portal/syndicate
+ background = "background_lavaland"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/donkfish = 5,
+ /obj/item/fish/emulsijack = 5,
+ )
+ catalog_description = "Syndicate dimension (Fishing portal generator)"
+ radial_name = "Syndicate"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15
+ overlay_state = "portal_syndicate"
+ radial_state = "syndi_snake"
+
+/**
+ * A special portal fish source which fish table is populated on init with the contents of all
+ * portal fish sources, except for FISHING_DUD, and a couple more caveats.
+ */
+/datum/fish_source/portal/random
+ fish_table = null //It's populated the first time the source is loaded on a fishing portal generator.
+ catalog_description = null // it'd make a bad entry in the catalog.
+ radial_name = "Randomizer"
+ overlay_state = "portal_randomizer"
+ var/static/list/all_portal_fish_sources_at_once
+ radial_state = "misaligned_question_mark"
+
+///Generate the fish table if we don't have one already.
+/datum/fish_source/portal/random/on_fishing_spot_init(datum/component/fishing_spot/spot)
+ if(fish_table)
+ return
+
+ ///rewards not found in other fishing portals
+ fish_table = list(
+ /obj/item/fish/holo/checkered = 1,
+ )
+
+ for(var/portal_type in GLOB.preset_fish_sources)
+ if(portal_type == type || !ispath(portal_type, /datum/fish_source/portal))
+ continue
+ var/datum/fish_source/portal/preset_portal = GLOB.preset_fish_sources[portal_type]
+ fish_table |= preset_portal.fish_table
+
+ ///We don't serve duds.
+ fish_table -= FISHING_DUD
+
+ for(var/reward_path in fish_table)
+ fish_table[reward_path] = rand(1, 4)
+
+///Difficulty has to be calculated before the rest, because of how it influences jump chances
+/datum/fish_source/portal/random/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge)
+ . = ..()
+ . += rand(-10, 15)
+
+///In the spirit of randomness, we skew a few values here and there
+/datum/fish_source/portal/random/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
+ challenge.bait_bounce_mult = clamp(challenge.bait_bounce_mult + (rand(-3, 3) * 0.1), 0.1, 1)
+ challenge.completion_loss = max(challenge.completion_loss + rand(-2, 2), 0)
+ challenge.completion_gain = max(challenge.completion_gain + rand(-1, 1), 2)
+ challenge.short_jump_velocity_limit += rand(-100, 100)
+ challenge.long_jump_velocity_limit += rand(-100, 100)
+ var/static/list/active_effects = bitfield_to_list(FISHING_MINIGAME_ACTIVE_EFFECTS)
+ for(var/effect in active_effects)
+ if(prob(30))
+ challenge.special_effects |= effect
+
+///Cherry on top, fish caught from the randomizer portal also have (almost completely) random traits
+/datum/fish_source/portal/random/spawn_reward(reward_path, mob/fisherman, turf/fishing_spot)
+ if(!ispath(reward_path, /obj/item/fish))
+ return ..()
+
+ var/static/list/weighted_traits
+ if(!weighted_traits)
+ weighted_traits = list()
+ for(var/trait_type as anything in GLOB.fish_traits)
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ weighted_traits[trait.type] = round(trait.inheritability**2/100)
+
+ var/obj/item/fish/caught_fish = new reward_path(get_turf(fisherman), FALSE)
+ var/list/fixed_traits = list()
+ for(var/trait_type in caught_fish.fish_traits)
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ if(caught_fish.type in trait.guaranteed_inheritance_types)
+ fixed_traits += trait_type
+ var/list/new_traits = list()
+ for(var/iteration in rand(1, 4))
+ new_traits |= pick_weight(weighted_traits)
+ caught_fish.inherit_traits(new_traits, fixed_traits = fixed_traits)
+ caught_fish.randomize_size_and_weight(deviation = 0.3)
+ caught_fish.progenitors = full_capitalize(caught_fish.name)
+ return caught_fish
+
/datum/fish_source/chasm
catalog_description = "Chasm depths"
@@ -99,7 +256,7 @@
fish_table = list(
FISHING_DUD = 18,
/obj/item/fish/sludgefish = 18,
- /obj/item/fish/slimefish = 2,
+ /obj/item/fish/slimefish = 4,
/obj/item/storage/wallet/money = 2,
)
fish_counts = list(
@@ -125,7 +282,7 @@
if(!istype(get_area(fisherman), /area/station/holodeck))
return "You need to be inside the Holodeck to catch holographic fish."
-/datum/fish_source/holographic/pre_challenge_started(obj/item/fishing_rod/rod, mob/user)
+/datum/fish_source/holographic/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(check_area))
/datum/fish_source/holographic/proc/check_area(mob/user)
diff --git a/code/modules/food_and_drinks/restaurant/_venue.dm b/code/modules/food_and_drinks/restaurant/_venue.dm
index a73cb6b4e35..f8e171948a6 100644
--- a/code/modules/food_and_drinks/restaurant/_venue.dm
+++ b/code/modules/food_and_drinks/restaurant/_venue.dm
@@ -60,10 +60,10 @@
if (initial(customer_type.is_unique))
customer_types -= customer_type
- var/mob/living/simple_animal/robot_customer/new_customer = new /mob/living/simple_animal/robot_customer(get_turf(restaurant_portal), customer_type, src)
+ var/mob/living/basic/robot_customer/new_customer = new /mob/living/basic/robot_customer(get_turf(restaurant_portal), customer_type, src)
current_visitors += new_customer
-/datum/venue/proc/order_food(mob/living/simple_animal/robot_customer/customer_pawn, datum/customer_data/customer_data)
+/datum/venue/proc/order_food(mob/living/basic/robot_customer/customer_pawn, datum/customer_data/customer_data)
var/order = pick_weight(customer_data.orderable_objects[venue_type])
var/list/order_args // Only for custom orders - arguments passed into New
var/image/food_image
@@ -113,7 +113,7 @@
return "broken venue pls call a coder"
///Effects for when a customer receives their order at this venue
-/datum/venue/proc/on_get_order(mob/living/simple_animal/robot_customer/customer_pawn, obj/item/order_item)
+/datum/venue/proc/on_get_order(mob/living/basic/robot_customer/customer_pawn, obj/item/order_item)
SHOULD_CALL_PARENT(TRUE)
// This is an item typepath, a reagent typepath, or a custom order datum instance.
@@ -152,7 +152,7 @@
open = FALSE
restaurant_portal.update_icon()
STOP_PROCESSING(SSobj, src)
- for(var/mob/living/simple_animal/robot_customer as anything in current_visitors)
+ for(var/mob/living/basic/robot_customer as anything in current_visitors)
robot_customer.ai_controller.set_blackboard_key(BB_CUSTOMER_LEAVING, TRUE) //LEAVEEEEEE
/obj/machinery/restaurant_portal
diff --git a/code/modules/food_and_drinks/restaurant/custom_order.dm b/code/modules/food_and_drinks/restaurant/custom_order.dm
index c74b4883c19..d87797b2578 100644
--- a/code/modules/food_and_drinks/restaurant/custom_order.dm
+++ b/code/modules/food_and_drinks/restaurant/custom_order.dm
@@ -34,7 +34,7 @@
* Return [TRANSACTION_SUCCESS] to denote the order went through successfully (Not generally necessary to include here)
* Return [TRANSACTION_HANDLED] to not do any further handling of the order by the
*/
-/datum/custom_order/proc/handle_get_order(mob/living/simple_animal/robot_customer/customer_pawn, obj/item/order_item)
+/datum/custom_order/proc/handle_get_order(mob/living/basic/robot_customer/customer_pawn, obj/item/order_item)
return NONE
/datum/custom_order/moth_clothing
@@ -153,7 +153,7 @@
food_image.add_overlay(drink_image)
return food_image
-/datum/custom_order/reagent/handle_get_order(mob/living/simple_animal/robot_customer/customer_pawn, obj/item/order_item)
+/datum/custom_order/reagent/handle_get_order(mob/living/basic/robot_customer/customer_pawn, obj/item/order_item)
. = TRANSACTION_HANDLED
for(var/datum/reagent/reagent as anything in order_item.reagents?.reagent_list)
@@ -184,7 +184,7 @@
/datum/custom_order/reagent/drink
container_needed = /obj/item/reagent_containers/cup/glass/drinkingglass
-/datum/custom_order/reagent/drink/handle_get_order(mob/living/simple_animal/robot_customer/customer_pawn, obj/item/order_item)
+/datum/custom_order/reagent/drink/handle_get_order(mob/living/basic/robot_customer/customer_pawn, obj/item/order_item)
customer_pawn.visible_message(
span_danger("[customer_pawn] slurps up [order_item] in one go!"),
span_danger("You slurp up [order_item] in one go."),
@@ -210,7 +210,7 @@
/datum/custom_order/reagent/soup/get_order_line(datum/venue/our_venue)
return "I'll take a [picked_serving] of [initial(reagent_type.name)]"
-/datum/custom_order/reagent/soup/handle_get_order(mob/living/simple_animal/robot_customer/customer_pawn, obj/item/order_item)
+/datum/custom_order/reagent/soup/handle_get_order(mob/living/basic/robot_customer/customer_pawn, obj/item/order_item)
customer_pawn.visible_message(
span_danger("[customer_pawn] pours [order_item] right down [customer_pawn.p_their()] hatch!"),
span_danger("You pour [order_item] down your hatch in one go."),
diff --git a/code/modules/food_and_drinks/restaurant/customers/_customer.dm b/code/modules/food_and_drinks/restaurant/customers/_customer.dm
index c5a7f809586..3f13b526056 100644
--- a/code/modules/food_and_drinks/restaurant/customers/_customer.dm
+++ b/code/modules/food_and_drinks/restaurant/customers/_customer.dm
@@ -52,10 +52,10 @@
/datum/customer_data/proc/can_use(datum/venue/venue)
return TRUE
-/datum/customer_data/proc/get_overlays(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/proc/get_overlays(mob/living/basic/robot_customer/customer)
return
-/datum/customer_data/proc/get_underlays(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/proc/get_underlays(mob/living/basic/robot_customer/customer)
return
/datum/customer_data/american
@@ -177,7 +177,7 @@
),
)
-/datum/customer_data/french/get_overlays(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/french/get_overlays(mob/living/basic/robot_customer/customer)
if(customer.ai_controller.blackboard[BB_CUSTOMER_LEAVING])
var/mutable_appearance/flag = mutable_appearance(customer.icon, "french_flag")
flag.appearance_flags = RESET_COLOR
@@ -224,7 +224,7 @@
),
)
-/datum/customer_data/japanese/get_overlays(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/japanese/get_overlays(mob/living/basic/robot_customer/customer)
//leaving and eaten
if(type == /datum/customer_data/japanese && customer.ai_controller.blackboard[BB_CUSTOMER_LEAVING] && customer.ai_controller.blackboard[BB_CUSTOMER_EATING])
var/mutable_appearance/you_won_my_heart = mutable_appearance('icons/effects/effects.dmi', "love_hearts")
@@ -303,13 +303,13 @@
return FALSE
return TRUE
-/datum/customer_data/moth/proc/get_wings(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/moth/proc/get_wings(mob/living/basic/robot_customer/customer)
var/customer_ref = WEAKREF(customer)
if (!LAZYACCESS(wings_chosen, customer_ref))
LAZYSET(wings_chosen, customer_ref, pick(GLOB.sprite_accessories["wings"]))
return wings_chosen[customer_ref]
-/datum/customer_data/moth/get_underlays(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/moth/get_underlays(mob/living/basic/robot_customer/customer)
var/list/underlays = list()
var/datum/sprite_accessory/moth_wings/wings = get_wings(customer)
@@ -320,7 +320,7 @@
return underlays
-/datum/customer_data/moth/get_overlays(mob/living/simple_animal/robot_customer/customer)
+/datum/customer_data/moth/get_overlays(mob/living/basic/robot_customer/customer)
var/list/overlays = list()
var/datum/sprite_accessory/moth_wings/wings = get_wings(customer)
diff --git a/code/modules/food_and_drinks/restaurant/generic_venues.dm b/code/modules/food_and_drinks/restaurant/generic_venues.dm
index 19f1ff61323..9e9b0a6d0e3 100644
--- a/code/modules/food_and_drinks/restaurant/generic_venues.dm
+++ b/code/modules/food_and_drinks/restaurant/generic_venues.dm
@@ -42,7 +42,7 @@
var/obj/item/object_to_order = order
return "I'll take \a [initial(object_to_order.name)]"
-/datum/venue/restaurant/on_get_order(mob/living/simple_animal/robot_customer/customer_pawn, obj/item/order_item)
+/datum/venue/restaurant/on_get_order(mob/living/basic/robot_customer/customer_pawn, obj/item/order_item)
var/transaction_result = ..()
if((transaction_result & TRANSACTION_HANDLED) || !(transaction_result & TRANSACTION_SUCCESS))
return
diff --git a/code/modules/holodeck/holo_effect.dm b/code/modules/holodeck/holo_effect.dm
index 02502c1e6fb..a8d51c87728 100644
--- a/code/modules/holodeck/holo_effect.dm
+++ b/code/modules/holodeck/holo_effect.dm
@@ -61,6 +61,7 @@
mobtype = pick(mobtype)
our_mob = new mobtype(loc)
our_mob.flags_1 |= HOLOGRAM_1
+ ADD_TRAIT(our_mob, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
// these vars are not really standardized but all would theoretically create stuff on death
for(var/v in list("butcher_results","corpse","weapon1","weapon2","blood_volume") & our_mob.vars)
diff --git a/code/modules/hydroponics/grown/replicapod.dm b/code/modules/hydroponics/grown/replicapod.dm
index 8e5dda5a7fa..c1fd02bf9f0 100644
--- a/code/modules/hydroponics/grown/replicapod.dm
+++ b/code/modules/hydroponics/grown/replicapod.dm
@@ -37,7 +37,7 @@
plant_icon_offset = 2
species = "replicapod"
plantname = "Replica Pod"
- product = /mob/living/carbon/human //verrry special -- Urist
+ product = null // the human mob is spawned in harvest()
lifespan = 50
endurance = 8
maturation = 10
diff --git a/code/modules/hydroponics/seed_extractor.dm b/code/modules/hydroponics/seed_extractor.dm
index a3804c9262f..04b0edf44bb 100644
--- a/code/modules/hydroponics/seed_extractor.dm
+++ b/code/modules/hydroponics/seed_extractor.dm
@@ -13,46 +13,33 @@
* * user - checks if we can remove the object from the inventory
* *
*/
-/proc/seedify(obj/item/O, t_max, obj/machinery/seed_extractor/extractor, mob/living/user)
- var/t_amount = 0
+/proc/seedify(obj/item/object, t_max, obj/machinery/seed_extractor/extractor, mob/living/user)
+ //try to get the seed from this item
+ var/obj/item/seeds/seed = object.get_plant_seed()
+ if(isnull(seed))
+ return null
+
+ //generate a random multiplier if value is not specified
var/list/seeds = list()
if(t_max == -1)
if(extractor)
t_max = rand(1,4) * extractor.seed_multiplier
else
t_max = rand(1,4)
-
- var/seedloc = O.loc
+ //drop location for the newly generated seeds
+ var/seedloc = object.loc
if(extractor)
seedloc = extractor.loc
- if(istype(O, /obj/item/food/grown/))
- var/obj/item/food/grown/F = O
- if(F.seed)
- if(user && !user.temporarilyRemoveItemFromInventory(O)) //couldn't drop the item
- return
- while(t_amount < t_max)
- var/obj/item/seeds/t_prod = F.seed.Copy()
- seeds.Add(t_prod)
- t_prod.forceMove(seedloc)
- t_amount++
- qdel(O)
- return seeds
-
- else if(istype(O, /obj/item/grown))
- var/obj/item/grown/F = O
- if(F.seed)
- if(user && !user.temporarilyRemoveItemFromInventory(O))
- return
- while(t_amount < t_max)
- var/obj/item/seeds/t_prod = F.seed.Copy()
- t_prod.forceMove(seedloc)
- t_amount++
- qdel(O)
- return 1
-
- return 0
-
+ //multiply the seeds and delete the item
+ if(user && !user.temporarilyRemoveItemFromInventory(object)) //couldn't drop the item
+ return null
+ for(var/_ in 0 to t_max)
+ var/obj/item/seeds/t_prod = seed.Copy()
+ seeds.Add(t_prod)
+ t_prod.forceMove(seedloc)
+ qdel(object)
+ return seeds
/obj/machinery/seed_extractor
name = "seed extractor"
@@ -80,6 +67,7 @@
if(held_item?.get_plant_seed())
context[SCREENTIP_CONTEXT_LMB] = "Make seeds"
+ context[SCREENTIP_CONTEXT_RMB] = "Make & Store seeds"
return CONTEXTUAL_SCREENTIP_SET
if(istype(held_item, /obj/item/storage/bag/plants) && (locate(/obj/item/seeds) in held_item.contents))
@@ -135,7 +123,17 @@
return TRUE
- if(seedify(attacking_item, -1, src, user))
+ var/list/generated_seeds = seedify(attacking_item, -1, src, user)
+ if(!isnull(generated_seeds))
+ if(LAZYACCESS(params2list(params), RIGHT_CLICK))
+ //find all seeds lying on the turf and add them to the machine
+ for(var/obj/item/seeds/seed as anything in generated_seeds)
+ //machine is full
+ if(contents.len >= max_seeds)
+ to_chat(user, span_warning("[src] is full."))
+ break
+ //add seed to machine. second argument is null which means just force move into the machine
+ add_seed(seed)
to_chat(user, span_notice("You extract some seeds."))
return TRUE
@@ -176,23 +174,16 @@
* needed to go to the ui handler
*
* to_add - what seed are we adding?
- * taking_from - where are we taking the seed from? A mob, a bag, etc?
- * user - who is inserting the seed?
+ * taking_from - where are we taking the seed from? A mob, a bag, etc? If null its means its just laying on the turf so force move it in
**/
/obj/machinery/seed_extractor/proc/add_seed(obj/item/seeds/to_add, atom/taking_from)
- if(ismob(taking_from))
- var/mob/mob_loc = taking_from
- if(!mob_loc.transferItemToLoc(to_add, src))
- return FALSE
-
- else if(!taking_from.atom_storage?.attempt_remove(to_add, src, silent = TRUE))
- return FALSE
-
var/seed_id = generate_seed_hash(to_add)
+ var/list/seed_data
+ var/has_seed_data // so we remember to add a seed obj weakref to piles[seed_id] at the end of the proc. That way if some reason we runtime in this proc it won't incorrectly add data to the list
if(piles[seed_id])
- piles[seed_id]["refs"] += WEAKREF(to_add)
+ has_seed_data = TRUE
else
- var/list/seed_data = list()
+ seed_data = list()
seed_data["icon"] = sanitize_css_class_name("[initial(to_add.icon)][initial(to_add.icon_state)]")
seed_data["name"] = capitalize(replacetext(to_add.name,"pack of ", ""));
seed_data["lifespan"] = to_add.lifespan
@@ -216,8 +207,8 @@
seed_data["mutatelist"] = list()
for(var/obj/item/seeds/mutant as anything in to_add.mutatelist)
seed_data["mutatelist"] += initial(mutant.plantname)
- var/obj/item/food/grown/product = new to_add.product
- if(product)
+ if(to_add.product)
+ var/obj/item/food/grown/product = new to_add.product
var/datum/reagent/product_distill_reagent = product.distill_reagent
seed_data["distill_reagent"] = initial(product_distill_reagent.name)
var/datum/reagent/product_juice_typepath = product.juice_typepath
@@ -225,8 +216,25 @@
seed_data["grind_results"] = list()
for(var/datum/reagent/reagent as anything in product.grind_results)
seed_data["grind_results"] += initial(reagent.name)
- qdel(product)
+ qdel(product)
+
+ if(!isnull(taking_from))
+ if(ismob(taking_from))
+ var/mob/mob_loc = taking_from
+ if(!mob_loc.transferItemToLoc(to_add, src))
+ return FALSE
+
+ else if(!taking_from.atom_storage?.attempt_remove(to_add, src, silent = TRUE))
+ return FALSE
+ else
+ to_add.forceMove(src)
+
+ // do this at the end, in case any of the previous steps failed
+ if(has_seed_data)
+ piles[seed_id]["refs"] += WEAKREF(to_add)
+ else
piles[seed_id] = seed_data
+
return TRUE
/obj/machinery/seed_extractor/ui_state(mob/user)
diff --git a/code/modules/industrial_lift/tram/tram_windows.dm b/code/modules/industrial_lift/tram/tram_windows.dm
index 1a98a56a0ba..55ec5aa283f 100644
--- a/code/modules/industrial_lift/tram/tram_windows.dm
+++ b/code/modules/industrial_lift/tram/tram_windows.dm
@@ -36,8 +36,9 @@
if(fulltile)
return ..()
smoothing_junction = new_junction
- var/smooth_left = (smoothing_junction & turn(dir, 90))
- var/smooth_right = (smoothing_junction & turn(dir, -90))
+ var/go_off = reverse_ndir(smoothing_junction)
+ var/smooth_left = (go_off & turn(dir, 90))
+ var/smooth_right = (go_off & turn(dir, -90))
if(smooth_left && smooth_right)
icon_state = "tram_mid"
else if (smooth_left)
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index ce39b5a7405..0d8e57f13eb 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -178,6 +178,8 @@
for(var/i in roundstart_experience)
spawned_human.mind.adjust_experience(i, roundstart_experience[i], TRUE)
+/// Announce that this job as joined the round to all crew members.
+/// Note the joining mob has no client at this point.
/datum/job/proc/announce_job(mob/living/joining_mob, job_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: /datum/job/proc/announce_job(mob/living/joining_mob)
if(head_announce)
announce_head(joining_mob, head_announce, job_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - Original: announce_head(joining_mob, head_announce)
@@ -191,13 +193,27 @@
/mob/living/proc/on_job_equipping(datum/job/equipping)
return
-/mob/living/carbon/human/on_job_equipping(datum/job/equipping, datum/preferences/used_pref) //SKYRAT EDIT CHANGE
+#define VERY_LATE_ARRIVAL_TOAST_PROB 20
+
+/mob/living/carbon/human/on_job_equipping(datum/job/equipping, datum/preferences/used_pref, client/player_client) //SKYRAT EDIT CHANGE - ORIGINAL: /mob/living/carbon/human/on_job_equipping(datum/job/equipping)
var/datum/bank_account/bank_account = new(real_name, equipping, dna.species.payday_modifier)
bank_account.payday(STARTING_PAYCHECKS, TRUE)
account_id = bank_account.account_id
bank_account.replaceable = FALSE
- dress_up_as_job(equipping, FALSE, used_pref) //SKYRAT EDIT CHANGE
+ add_mob_memory(/datum/memory/key/account, remembered_id = account_id)
+
+ dress_up_as_job(equipping, FALSE, used_pref) //SKYRAT EDIT CHANGE - ORIGINAL: dress_up_as_job(equipping)
+
+ if(EMERGENCY_PAST_POINT_OF_NO_RETURN && prob(VERY_LATE_ARRIVAL_TOAST_PROB))
+ //equipping.equip_to_slot_or_del(new /obj/item/food/griddle_toast(equipping), ITEM_SLOT_MASK) // SKYRAT EDIT REMOVAL - See below
+ // SKYRAT EDIT ADDITION - Lizards
+ if(islizard(equipping))
+ equip_to_slot_or_del(new /obj/item/food/breadslice/root(equipping), ITEM_SLOT_MASK)
+ else
+ equip_to_slot_or_del(new /obj/item/food/griddle_toast(equipping), ITEM_SLOT_MASK)
+ // SKYRAT EDIT ADDITION END - Lizards
+#undef VERY_LATE_ARRIVAL_TOAST_PROB
/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE)
return
@@ -267,8 +283,43 @@
return TRUE
-/datum/job/proc/radio_help_message(mob/M)
- to_chat(M, "Prefix your message with :h to speak on your department's radio. To see other prefixes, look closely at your headset.")
+/// Gets the message that shows up when spawning as this job
+/datum/job/proc/get_spawn_message(alt_title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: /datum/job/proc/get_spawn_message()
+ SHOULD_NOT_OVERRIDE(TRUE)
+ return examine_block(span_infoplain(jointext(get_spawn_message_information(alt_title), "\n• "))) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLED - ORIGINAL: return examine_block(span_infoplain(jointext(get_spawn_message_information(), "\n• ")))
+
+/// Returns a list of strings that correspond to chat messages sent to this mob when they join the round.
+/datum/job/proc/get_spawn_message_information(alt_title = title) // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: /datum/job/proc/get_spawn_message_information()
+ SHOULD_CALL_PARENT(TRUE)
+ var/list/info = list()
+ info += "You are the [alt_title].\n" // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: info += "You are the [title].\n"
+ var/related_policy = get_policy(title)
+ var/radio_info = get_radio_information()
+ if(related_policy)
+ info += related_policy
+ if(supervisors)
+ info += "As the [alt_title == title ? alt_title : "[alt_title] ([title])"] you answer directly to [supervisors]. Special circumstances may change this." // SKYRAT EDIT CHANGE - ALTERNATIVE_JOB_TITLES - ORIGINAL: info += "As the [title] you answer directly to [supervisors]. Special circumstances may change this."
+ if(radio_info)
+ info += radio_info
+ if(req_admin_notify)
+ info += "You are playing a job that is important for Game Progression. \
+ If you have to disconnect, please notify the admins via adminhelp."
+ if(CONFIG_GET(number/minimal_access_threshold))
+ info += span_boldnotice("As this station was initially staffed with a \
+ [CONFIG_GET(flag/jobs_have_minimal_access) ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] \
+ have been added to your ID card.")
+ //SKYRAT EDIT ADDITION START - ALTERNATIVE_JOB_TITLES
+ if(alt_title != title)
+ info += span_warning("Remember that alternate titles are purely for flavor and roleplay.")
+ info += span_doyourjobidiot("Do not use your \"[alt_title]\" alt title as an excuse to forego your duties as a [title].")
+ //SKYRAT EDIT END
+
+ return info
+
+/// Returns information pertaining to this job's radio.
+/datum/job/proc/get_radio_information()
+ if(job_flags & JOB_CREW_MEMBER)
+ return "Prefix your message with :h to speak on your department's radio. To see other prefixes, look closely at your headset."
/datum/outfit/job
name = "Standard Gear"
diff --git a/code/modules/jobs/job_types/ai.dm b/code/modules/jobs/job_types/ai.dm
index f3f8b23837b..0b38d6e081d 100644
--- a/code/modules/jobs/job_types/ai.dm
+++ b/code/modules/jobs/job_types/ai.dm
@@ -117,5 +117,5 @@
/datum/job/ai/config_check()
return CONFIG_GET(flag/allow_ai)
-/datum/job/ai/radio_help_message(mob/M)
- to_chat(M, "Prefix your message with :b to speak with cyborgs and other AIs.")
+/datum/job/ai/get_radio_information()
+ return "Prefix your message with :b to speak with cyborgs and other AIs."
diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm
index e788202b445..d8ae6d335b3 100755
--- a/code/modules/jobs/job_types/captain.dm
+++ b/code/modules/jobs/job_types/captain.dm
@@ -51,6 +51,9 @@
/datum/job/captain/get_captaincy_announcement(mob/living/captain)
return "Captain [captain.real_name] on deck!"
+/datum/job/captain/get_radio_information()
+ . = ..()
+ . += "\nYou have access to all radio channels, but they are not automatically tuned. Check your radio for more information."
/datum/outfit/job/captain
name = "Captain"
diff --git a/code/modules/jobs/job_types/chaplain/chaplain.dm b/code/modules/jobs/job_types/chaplain/chaplain.dm
index be4516a0db7..58821ec5358 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain.dm
@@ -25,7 +25,6 @@
mail_goodies = list(
/obj/item/reagent_containers/cup/glass/bottle/holywater = 30,
- /obj/item/toy/plush/awakenedplushie = 10,
/obj/item/grenade/chem_grenade/holy = 5,
/obj/item/toy/plush/narplush = 2,
/obj/item/toy/plush/ratplush = 1
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm b/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm
index f2ab21c9c35..637177adffb 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_costumes.dm
@@ -50,6 +50,14 @@
body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
flags_inv = HIDEJUMPSUIT
+/obj/item/clothing/suit/chaplainsuit/habit
+ name = "religious tunic"
+ desc = "No nunsene clothing."
+ icon_state = "habit"
+ alternate_worn_layer = GLOVES_LAYER // since the sleeves cover a part of the hands, this way it looks better while retaining glove overlay correctly.
+ body_parts_covered = CHEST|GROIN|LEGS|ARMS|HANDS
+ flags_inv = HIDEJUMPSUIT
+
/obj/item/clothing/suit/chaplainsuit/bishoprobe
name = "bishop's robes"
desc = "Glad to see the tithes you collected were well spent."
diff --git a/code/modules/jobs/job_types/cyborg.dm b/code/modules/jobs/job_types/cyborg.dm
index ae6b7c142cc..92094073033 100644
--- a/code/modules/jobs/job_types/cyborg.dm
+++ b/code/modules/jobs/job_types/cyborg.dm
@@ -4,7 +4,7 @@
auto_deadmin_role_flags = DEADMIN_POSITION_SILICON
faction = FACTION_STATION
total_positions = 3 // SKYRAT EDIT: Original value (0)
- spawn_positions = 3 // SKYRAT EDIT: Original value (1)
+ spawn_positions = 3
supervisors = "your laws and the AI" //Nodrak
spawn_type = /mob/living/silicon/robot
minimal_player_age = 21
@@ -58,5 +58,5 @@
if(!robot_spawn.connected_ai) // Only log if there's no Master AI
robot_spawn.log_current_laws()
-/datum/job/cyborg/radio_help_message(mob/M)
- to_chat(M, "Prefix your message with :b to speak with other cyborgs and AI.")
+/datum/job/cyborg/get_radio_information()
+ return "Prefix your message with :b to speak with other cyborgs and AI."
diff --git a/code/modules/jobs/job_types/paramedic.dm b/code/modules/jobs/job_types/paramedic.dm
index 3dac90b4baf..2fd4f3a93a6 100644
--- a/code/modules/jobs/job_types/paramedic.dm
+++ b/code/modules/jobs/job_types/paramedic.dm
@@ -32,7 +32,8 @@
/obj/item/reagent_containers/hypospray/medipen/salacid = 10,
/obj/item/reagent_containers/hypospray/medipen/salbutamol = 10,
/obj/item/reagent_containers/hypospray/medipen/penacid = 10,
- /obj/item/reagent_containers/hypospray/medipen/survival/luxury = 5
+ /obj/item/reagent_containers/hypospray/medipen/survival/luxury = 5,
+ /obj/item/storage/box/bandages = 5,
)
rpg_title = "Corpse Runner"
job_flags = STATION_JOB_FLAGS
@@ -56,6 +57,7 @@
gloves = /obj/item/clothing/gloves/latex/nitrile
shoes = /obj/item/clothing/shoes/sneakers/blue
l_pocket = /obj/item/modular_computer/pda/medical/paramedic
+ r_pocket = /obj/item/storage/box/bandages
backpack = /obj/item/storage/backpack/medic
satchel = /obj/item/storage/backpack/satchel/med
diff --git a/code/modules/library/skill_learning/skillchip.dm b/code/modules/library/skill_learning/skillchip.dm
index f9fd629c1ed..762e8e0162c 100644
--- a/code/modules/library/skill_learning/skillchip.dm
+++ b/code/modules/library/skill_learning/skillchip.dm
@@ -489,4 +489,13 @@
activate_message = span_notice("You recall learning from your grandmother how they baked their cookies with love.")
deactivate_message = span_notice("You forget all memories imparted upon you by your grandmother. Were they even your real grandma?")
+/obj/item/skillchip/master_angler
+ name = "Mast-Angl-Er skillchip"
+ auto_traits = list(TRAIT_REVEAL_FISH)
+ skill_name = "Fisherman's Discernment"
+ skill_description = "While fishing, it'll make a smidge easier to guess whatever you're trying to catch."
+ skill_icon = "fish"
+ activate_message = span_notice("You feel the knowledge and passion of several sunbaked, seasoned fishermen burn within you.")
+ deactivate_message = span_notice("You no longer feel like casting a fishing rod by the sunny riverside.")
+
#undef SKILLCHIP_CATEGORY_GENERAL
diff --git a/code/modules/mafia/controller.dm b/code/modules/mafia/controller.dm
index 1916a65f7b1..f8d9db13106 100644
--- a/code/modules/mafia/controller.dm
+++ b/code/modules/mafia/controller.dm
@@ -578,7 +578,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
for(var/datum/mafia_role/role as anything in all_roles)
var/mob/living/carbon/human/H = new(get_turf(role.assigned_landmark))
- H.add_traits(list(TRAIT_NOFIRE, TRAIT_NOBREATH, TRAIT_CANNOT_CRYSTALIZE), MAFIA_TRAIT)
+ H.add_traits(list(TRAIT_NOFIRE, TRAIT_NOBREATH, TRAIT_CANNOT_CRYSTALIZE, TRAIT_PERMANENTLY_MORTAL), MAFIA_TRAIT)
H.equipOutfit(outfit_to_distribute)
H.status_flags |= GODMODE
RegisterSignal(H, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(display_votes))
diff --git a/code/modules/mapping/access_helpers.dm b/code/modules/mapping/access_helpers.dm
index 16da29837fb..c208e2eea9e 100644
--- a/code/modules/mapping/access_helpers.dm
+++ b/code/modules/mapping/access_helpers.dm
@@ -386,6 +386,11 @@
access_list += list(ACCESS_CARGO, ACCESS_MAINT_TUNNELS)
return access_list
+/obj/effect/mapping_helpers/airlock/access/any/supply/bit_den/get_access()
+ var/list/access_list = ..()
+ access_list += ACCESS_BIT_DEN
+ return access_list
+
// -------------------- Syndicate access helpers
/obj/effect/mapping_helpers/airlock/access/any/syndicate
icon_state = "access_helper_syn"
diff --git a/code/modules/meteors/meteor_waves.dm b/code/modules/meteors/meteor_waves.dm
index 4235c186a94..7d832d53713 100644
--- a/code/modules/meteors/meteor_waves.dm
+++ b/code/modules/meteors/meteor_waves.dm
@@ -26,4 +26,4 @@ GLOBAL_LIST_INIT(meteors_stray, list(/obj/effect/meteor/medium=15, /obj/effect/m
GLOBAL_LIST_INIT(meteors_sandstorm, list(/obj/effect/meteor/sand=45, /obj/effect/meteor/dust=5)) //for sandstorm event
-GLOBAL_LIST_INIT(meteorsSPOOKY, list(/obj/effect/meteor/pumpkin))
+GLOBAL_LIST_INIT(meteorsSPOOKY, list(/obj/effect/meteor/pumpkin=1))
diff --git a/code/modules/mining/abandoned_crates.dm b/code/modules/mining/abandoned_crates.dm
index a656aa467d6..c9fdb9747e5 100644
--- a/code/modules/mining/abandoned_crates.dm
+++ b/code/modules/mining/abandoned_crates.dm
@@ -188,7 +188,9 @@
new /obj/item/clothing/suit/costume/wellworn_shirt/graphic/ian(src)
new /obj/item/clothing/suit/hooded/ian_costume(src)
if(67 to 68)
- new /obj/item/toy/plush/awakenedplushie(src)
+ var/obj/item/gibtonite/free_bomb = new /obj/item/gibtonite(src)
+ free_bomb.quality = rand(1, 3)
+ free_bomb.GibtoniteReaction(null, "A secure loot closet has spawned a live")
if(69 to 70)
new /obj/item/stack/ore/bluespace_crystal(src, 5)
if(71 to 72)
diff --git a/code/modules/mining/equipment/monster_organs/regenerative_core.dm b/code/modules/mining/equipment/monster_organs/regenerative_core.dm
index cb224438c00..98758d5a369 100644
--- a/code/modules/mining/equipment/monster_organs/regenerative_core.dm
+++ b/code/modules/mining/equipment/monster_organs/regenerative_core.dm
@@ -36,7 +36,7 @@
/// Log applications and apply moodlet.
/obj/item/organ/internal/monster_core/regenerative_core/apply_to(mob/living/target, mob/user)
- target.add_mood_event("regenerative core", /datum/mood_event/healsbadman)
+ target.add_mood_event(MOOD_CATEGORY_LEGION_CORE, /datum/mood_event/healsbadman)
if (target != user)
target.visible_message(span_notice("[user] forces [target] to apply [src]... Black tendrils entangle and reinforce [target.p_them()]!"))
SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "other"))
diff --git a/code/modules/mining/fulton.dm b/code/modules/mining/fulton.dm
index e7199e59938..20a436dc5c6 100644
--- a/code/modules/mining/fulton.dm
+++ b/code/modules/mining/fulton.dm
@@ -144,7 +144,7 @@ GLOBAL_LIST_EMPTY(total_extraction_beacons)
icon_state = "folded_extraction"
/obj/item/fulton_core/attack_self(mob/user)
- if(do_after(user,15,target = user) && !QDELETED(src))
+ if(do_after(user, 1.5 SECONDS, target = user) && !QDELETED(src))
new /obj/structure/extraction_point(get_turf(user))
playsound(src, 'sound/items/deconstruct.ogg', vol = 50, vary = TRUE, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE)
qdel(src)
@@ -156,6 +156,7 @@ GLOBAL_LIST_EMPTY(total_extraction_beacons)
icon_state = "extraction_point"
anchored = TRUE
density = FALSE
+ obj_flags = CAN_BE_HIT | UNIQUE_RENAME
var/beacon_network = "station"
/obj/structure/extraction_point/Initialize(mapload)
@@ -168,6 +169,15 @@ GLOBAL_LIST_EMPTY(total_extraction_beacons)
GLOB.total_extraction_beacons -= src
return ..()
+/obj/structure/extraction_point/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ balloon_alert_to_viewers("undeploying...")
+ if(!do_after(user, 1.5 SECONDS, src))
+ return
+ new /obj/item/fulton_core(drop_location())
+ playsound(src, 'sound/items/deconstruct.ogg', vol = 50, vary = TRUE, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE)
+ qdel(src)
+
/obj/structure/extraction_point/update_overlays()
. = ..()
. += emissive_appearance(icon, "[icon_state]_light", src, alpha = src.alpha)
diff --git a/code/modules/mining/ores_coins.dm b/code/modules/mining/ores_coins.dm
index e9c652fd7bd..b01b537ec80 100644
--- a/code/modules/mining/ores_coins.dm
+++ b/code/modules/mining/ores_coins.dm
@@ -267,7 +267,7 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
return
if(I.tool_behaviour == TOOL_MINING || istype(I, /obj/item/resonator) || I.force >= 10)
- GibtoniteReaction(user)
+ GibtoniteReaction(user, "A resonator has primed for detonation a")
return
if(istype(I, /obj/item/mining_scanner) || istype(I, /obj/item/t_scanner/adv_mining_scanner) || I.tool_behaviour == TOOL_MULTITOOL)
@@ -294,14 +294,14 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
return ..()
/obj/item/gibtonite/bullet_act(obj/projectile/P)
- GibtoniteReaction(P.firer)
+ GibtoniteReaction(P.firer, "A projectile has primed for detonation a")
return ..()
/obj/item/gibtonite/ex_act()
- GibtoniteReaction(null, 1)
+ GibtoniteReaction(null, "An explosion has primed for detonation a")
return TRUE
-/obj/item/gibtonite/proc/GibtoniteReaction(mob/user, triggered_by = 0)
+/obj/item/gibtonite/proc/GibtoniteReaction(mob/user, triggered_by)
if(primed)
return
primed = TRUE
@@ -311,18 +311,16 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
if(!is_mining_level(z))//Only annoy the admins ingame if we're triggered off the mining zlevel
notify_admins = TRUE
- if(triggered_by == 1)
- log_bomber(null, "An explosion has primed a", src, "for detonation", notify_admins)
- else if(triggered_by == 2)
- var/turf/bombturf = get_turf(src)
- if(notify_admins)
- message_admins("A signal has triggered a [name] to detonate at [ADMIN_VERBOSEJMP(bombturf)]. Igniter attacher: [ADMIN_LOOKUPFLW(attacher)]")
- var/bomb_message = "A signal has primed a [name] for detonation at [AREACOORD(bombturf)]. Igniter attacher: [key_name(attacher)]."
- log_game(bomb_message)
- GLOB.bombers += bomb_message
- else
+ if(user)
user.visible_message(span_warning("[user] strikes \the [src], causing a chain reaction!"), span_danger("You strike \the [src], causing a chain reaction."))
- log_bomber(user, "has primed a", src, "for detonation", notify_admins)
+
+ var/attacher_text = attacher ? "Igniter attacher: [ADMIN_LOOKUPFLW(attacher)]" : null
+
+ if(triggered_by)
+ log_bomber(user, triggered_by, src, attacher_text, notify_admins)
+ else
+ log_bomber(user, "Something has primed a", src, "for detonation.[attacher_text ? " " : ""][attacher_text]", notify_admins)
+
det_timer = addtimer(CALLBACK(src, PROC_REF(detonate), notify_admins), det_time, TIMER_STOPPABLE)
/obj/item/gibtonite/proc/detonate(notify_admins)
diff --git a/code/modules/mob/living/basic/basic_defense.dm b/code/modules/mob/living/basic/basic_defense.dm
index cc128a9f5e9..8afb40f2912 100644
--- a/code/modules/mob/living/basic/basic_defense.dm
+++ b/code/modules/mob/living/basic/basic_defense.dm
@@ -165,6 +165,9 @@
return TRUE
/mob/living/basic/blob_act(obj/structure/blob/attacking_blob)
+ . = ..()
+ if (!.)
+ return
apply_damage(20, damagetype = BRUTE)
/mob/living/basic/do_attack_animation(atom/attacked_atom, visual_effect_icon, used_item, no_effect)
diff --git a/code/modules/mob/living/basic/blob_minions/blob_ai.dm b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
new file mode 100644
index 00000000000..6168b7ca83b
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
@@ -0,0 +1,51 @@
+/**
+ * Extremely simple AI, this isn't a very smart boy
+ * Only notable quirk is that it uses JPS movement, simple avoidance would fail to realise it can path through blobs
+ */
+/datum/ai_controller/basic_controller/blobbernaut
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/**
+ * Move to a point designated by the overmind, otherwise just slap people nearby
+ */
+/datum/ai_controller/basic_controller/blob_zombie
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/**
+ * As blob zombie but will prioritise attacking corpses to zombify them
+ */
+/datum/ai_controller/basic_controller/blob_spore
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/blob_minions/blob_mob.dm b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
new file mode 100644
index 00000000000..35e41f09058
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
@@ -0,0 +1,37 @@
+/// Root of shared behaviour for mobs spawned by blobs, is abstract and should not be spawned
+/mob/living/basic/blob_minion
+ name = "Blob Error"
+ desc = "A nonfunctional fungal creature created by bad code or celestial mistake. Point and laugh."
+ icon = 'icons/mob/nonhuman-player/blob.dmi'
+ icon_state = "blob_head"
+ unique_name = TRUE
+ pass_flags = PASSBLOB
+ faction = list(ROLE_BLOB)
+ combat_mode = TRUE
+ bubble_icon = "blob"
+ speak_emote = null
+ habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ lighting_cutoff_red = 20
+ lighting_cutoff_green = 40
+ lighting_cutoff_blue = 30
+ initial_language_holder = /datum/language_holder/empty
+
+/mob/living/basic/blob_minion/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_BLOB_ALLY, TRAIT_MUTE), INNATE_TRAIT)
+ AddComponent(/datum/component/blob_minion, on_strain_changed = CALLBACK(src, PROC_REF(on_strain_updated)))
+
+/// Called when our blob overmind changes their variant, update some of our mob properties
+/mob/living/basic/blob_minion/proc/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ return
+
+/// Associates this mob with a specific blob factory node
+/mob/living/basic/blob_minion/proc/link_to_factory(obj/structure/blob/special/factory/factory)
+ RegisterSignal(factory, COMSIG_QDELETING, PROC_REF(on_factory_destroyed))
+
+/// Called when our factory is destroyed
+/mob/living/basic/blob_minion/proc/on_factory_destroyed()
+ SIGNAL_HANDLER
+ to_chat(src, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
diff --git a/code/modules/mob/living/basic/blob_minions/blob_spore.dm b/code/modules/mob/living/basic/blob_minions/blob_spore.dm
new file mode 100644
index 00000000000..e8c3acc8b97
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_spore.dm
@@ -0,0 +1,123 @@
+/**
+ * A floating fungus which turns people into zombies and explodes into reagent clouds upon death.
+ */
+/mob/living/basic/blob_minion/spore
+ name = "blob spore"
+ desc = "A floating, fragile spore."
+ icon = 'icons/mob/nonhuman-player/blob.dmi'
+ icon_state = "blobpod"
+ icon_living = "blobpod"
+ health_doll_icon = "blobpod"
+ health = BLOBMOB_SPORE_HEALTH
+ maxHealth = BLOBMOB_SPORE_HEALTH
+ verb_say = "psychically pulses"
+ verb_ask = "psychically probes"
+ verb_exclaim = "psychically yells"
+ verb_yell = "psychically screams"
+ melee_damage_lower = BLOBMOB_SPORE_DMG_LOWER
+ melee_damage_upper = BLOBMOB_SPORE_DMG_UPPER
+ obj_damage = 0
+ attack_verb_continuous = "batters"
+ attack_verb_simple = "batter"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ death_message = "explodes into a cloud of gas!"
+ gold_core_spawnable = HOSTILE_SPAWN
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/blob_spore
+ /// Size of cloud produced from a dying spore
+ var/death_cloud_size = 1
+ /// Type of mob to create
+ var/mob/living/zombie_type = /mob/living/basic/blob_minion/zombie
+
+/mob/living/basic/blob_minion/spore/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/spore/death(gibbed)
+ . = ..()
+ death_burst()
+
+/mob/living/basic/blob_minion/spore/on_factory_destroyed()
+ death()
+
+/// Create an explosion of spores on death
+/mob/living/basic/blob_minion/spore/proc/death_burst()
+ do_chem_smoke(range = death_cloud_size, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+
+/mob/living/basic/blob_minion/spore/melee_attack(mob/living/carbon/human/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!ishuman(target) || target.stat != DEAD)
+ return
+ zombify(target)
+
+/// Become a zombie
+/mob/living/basic/blob_minion/spore/proc/zombify(mob/living/carbon/human/target)
+ visible_message(span_warning("The corpse of [target.name] suddenly rises!"))
+ var/mob/living/basic/blob_minion/zombie/blombie = change_mob_type(zombie_type, loc, new_name = initial(zombie_type.name))
+ blombie.set_name()
+ if (istype(blombie)) // In case of badmin
+ blombie.consume_corpse(target)
+ SEND_SIGNAL(src, COMSIG_BLOB_ZOMBIFIED, blombie)
+ qdel(src)
+
+/// Variant of the blob spore which is actually spawned by blob factories
+/mob/living/basic/blob_minion/spore/minion
+ gold_core_spawnable = NO_SPAWN
+ zombie_type = /mob/living/basic/blob_minion/zombie/controlled
+ /// We die if we leave the same turf as this z level
+ var/turf/z_turf
+
+/mob/living/basic/blob_minion/spore/minion/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_changed))
+
+/// When we z-move check that we're on the same z level as our factory was
+/mob/living/basic/blob_minion/spore/minion/proc/on_z_changed()
+ SIGNAL_HANDLER
+ if (isnull(z_turf))
+ return
+ if (!is_valid_z_level(get_turf(src), z_turf))
+ death()
+
+/// Mark the turf we need to track from our factory
+/mob/living/basic/blob_minion/spore/minion/link_to_factory(obj/structure/blob/special/factory/factory)
+ . = ..()
+ z_turf = get_turf(factory)
+
+/// If the blob changes to distributed neurons then you can control the spores
+/mob/living/basic/blob_minion/spore/minion/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ if (isnull(overmind))
+ REMOVE_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
+ else
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
+
+ if (istype(new_strain, /datum/blobstrain/reagent/distributed_neurons))
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB_INFECTION,\
+ poll_candidates = TRUE,\
+ poll_ignore_key = POLL_IGNORE_BLOB,\
+ )
+ else
+ qdel(GetComponent(/datum/component/ghost_direct_control))
+
+/mob/living/basic/blob_minion/spore/minion/death_burst()
+ return // This behaviour is superceded by the overmind's intervention
+
+
+/// Weakened spore spawned by distributed neurons, can't zombify people and makes a teeny explosion
+/mob/living/basic/blob_minion/spore/minion/weak
+ name = "fragile blob spore"
+ health = 15
+ maxHealth = 15
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ death_cloud_size = 0
+
+/mob/living/basic/blob_minion/spore/minion/weak/zombify()
+ return
+
+/mob/living/basic/blob_minion/spore/minion/weak/on_strain_updated()
+ return
diff --git a/code/modules/mob/living/basic/blob_minions/blob_zombie.dm b/code/modules/mob/living/basic/blob_minions/blob_zombie.dm
new file mode 100644
index 00000000000..c9bf3b7346a
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_zombie.dm
@@ -0,0 +1,99 @@
+/// A shambling mob made out of a crew member
+/mob/living/basic/blob_minion/zombie
+ name = "blob zombie"
+ desc = "A shambling corpse animated by the blob."
+ icon_state = "zombie"
+ icon_living = "zombie"
+ health_doll_icon = "blobpod"
+ mob_biotypes = MOB_ORGANIC | MOB_HUMANOID
+ health = 70
+ maxHealth = 70
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ melee_attack_cooldown = CLICK_CD_MELEE
+ obj_damage = 20
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ death_message = "collapses to the ground!"
+ gold_core_spawnable = NO_SPAWN
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/blob_zombie
+ /// The dead body we have inside
+ var/mob/living/carbon/human/corpse
+
+/mob/living/basic/blob_minion/zombie/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT) // This mob doesn't function visually without a corpse and wouldn't respawn with one
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/zombie/death(gibbed)
+ corpse?.forceMove(loc)
+ death_burst()
+ return ..()
+
+/mob/living/basic/blob_minion/zombie/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != corpse)
+ return
+ corpse = null
+ death()
+
+/mob/living/basic/blob_minion/zombie/Destroy()
+ QDEL_NULL(corpse)
+ return ..()
+
+/mob/living/basic/blob_minion/zombie/on_factory_destroyed()
+ . = ..()
+ death()
+
+/mob/living/basic/blob_minion/zombie/update_overlays()
+ . = ..()
+ copy_overlays(corpse, TRUE)
+ var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "blob_head")
+ blob_head_overlay.color = LAZYACCESS(atom_colours, FIXED_COLOUR_PRIORITY) || COLOR_WHITE
+ color = initial(color) // reversing what our component did lol, but we needed the value for the overlay
+ . += blob_head_overlay
+
+/// Create an explosion of spores on death
+/mob/living/basic/blob_minion/zombie/proc/death_burst()
+ do_chem_smoke(range = 0, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+/// Store a body so that we can drop it on death
+/mob/living/basic/blob_minion/zombie/proc/consume_corpse(mob/living/carbon/human/new_corpse)
+ if(new_corpse.wear_suit)
+ maxHealth += new_corpse.get_armor_rating(MELEE)
+ health = maxHealth
+ new_corpse.set_facial_hairstyle("Shaved", update = FALSE)
+ new_corpse.set_hairstyle("Bald", update = TRUE)
+ new_corpse.forceMove(src)
+ corpse = new_corpse
+ update_appearance(UPDATE_ICON)
+ RegisterSignal(corpse, COMSIG_LIVING_REVIVE, PROC_REF(on_corpse_revived))
+
+/// Dynamic changeling reentry
+/mob/living/basic/blob_minion/zombie/proc/on_corpse_revived()
+ SIGNAL_HANDLER
+ visible_message(span_boldwarning("[src] bursts from the inside!"))
+ death()
+
+/// Blob-created zombies will ping for player control when they make a zombie
+/mob/living/basic/blob_minion/zombie/controlled
+
+/mob/living/basic/blob_minion/zombie/controlled/consume_corpse(mob/living/carbon/human/new_corpse)
+ . = ..()
+ if (!isnull(client))
+ return
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB_INFECTION,\
+ poll_candidates = TRUE,\
+ poll_ignore_key = POLL_IGNORE_BLOB,\
+ )
+
+/mob/living/basic/blob_minion/zombie/controlled/death_burst()
+ return
diff --git a/code/modules/mob/living/basic/blob_minions/blobbernaut.dm b/code/modules/mob/living/basic/blob_minions/blobbernaut.dm
new file mode 100644
index 00000000000..b483641993a
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blobbernaut.dm
@@ -0,0 +1,109 @@
+/**
+ * Player-piloted brute mob. Mostly just a "move and click" kind of guy.
+ * Has a variant which takes damage when away from blob tiles
+ */
+/mob/living/basic/blob_minion/blobbernaut
+ name = "blobbernaut"
+ desc = "A hulking, mobile chunk of blobmass."
+ icon_state = "blobbernaut"
+ icon_living = "blobbernaut"
+ icon_dead = "blobbernaut_dead"
+ health = BLOBMOB_BLOBBERNAUT_HEALTH
+ maxHealth = BLOBMOB_BLOBBERNAUT_HEALTH
+ damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER
+ melee_attack_cooldown = CLICK_CD_MELEE
+ obj_damage = BLOBMOB_BLOBBERNAUT_DMG_OBJ
+ attack_verb_continuous = "slams"
+ attack_verb_simple = "slam"
+ attack_sound = 'sound/effects/blobattack.ogg'
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ force_threshold = 10
+ pressure_resistance = 50
+ mob_size = MOB_SIZE_LARGE
+ hud_type = /datum/hud/living/blobbernaut
+ gold_core_spawnable = HOSTILE_SPAWN
+ ai_controller = /datum/ai_controller/basic_controller/blobbernaut
+
+/mob/living/basic/blob_minion/blobbernaut/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/blobbernaut/death(gibbed)
+ flick("blobbernaut_death", src)
+ return ..()
+
+/// This variant is the one actually spawned by blob factories, takes damage when away from blob tiles
+/mob/living/basic/blob_minion/blobbernaut/minion
+ gold_core_spawnable = NO_SPAWN
+ /// Is our factory dead?
+ var/orphaned = FALSE
+
+/mob/living/basic/blob_minion/blobbernaut/minion/Life(seconds_per_tick, times_fired)
+ . = ..()
+ if (!.)
+ return FALSE
+ var/damage_sources = 0
+ var/list/blobs_in_area = range(2, src)
+
+ if (!(locate(/obj/structure/blob) in blobs_in_area))
+ damage_sources++
+
+ if (orphaned)
+ damage_sources++
+ else
+ var/particle_colour = atom_colours[FIXED_COLOUR_PRIORITY] || COLOR_BLACK
+ if (locate(/obj/structure/blob/special/core) in blobs_in_area)
+ heal_overall_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALING_CORE * seconds_per_tick)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
+ heal_effect.color = particle_colour
+
+ if (locate(/obj/structure/blob/special/node) in blobs_in_area)
+ heal_overall_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALING_NODE * seconds_per_tick)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
+ heal_effect.color = particle_colour
+
+ if (damage_sources == 0)
+ return FALSE
+
+ // take 2.5% of max health as damage when not near the blob or if the naut has no factory, 5% if both
+ apply_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALTH_DECAY * damage_sources * seconds_per_tick, damagetype = TOX) // We reduce brute damage
+ var/mutable_appearance/harming = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "nautdamage", MOB_LAYER + 0.01)
+ harming.appearance_flags = RESET_COLOR
+ harming.color = atom_colours[FIXED_COLOUR_PRIORITY] || COLOR_WHITE
+ harming.dir = dir
+ flick_overlay_view(harming, 0.8 SECONDS)
+ return TRUE
+
+/// Called by the blob creation power to give us a mind and a basic task orientation
+/mob/living/basic/blob_minion/blobbernaut/minion/proc/assign_key(ckey, datum/blobstrain/blobstrain)
+ key = ckey
+ flick("blobbernaut_produce", src)
+ health = maxHealth / 2 // Start out injured to encourage not beelining away from the blob
+ SEND_SOUND(src, sound('sound/effects/blobattack.ogg'))
+ SEND_SOUND(src, sound('sound/effects/attackblob.ogg'))
+ to_chat(src, span_infoplain("You are powerful, hard to kill, and slowly regenerate near nodes and cores, [span_cultlarge("but will slowly die if not near the blob")] or if the factory that made you is killed."))
+ to_chat(src, span_infoplain("You can communicate with other blobbernauts and overminds telepathically by attempting to speak normally"))
+ to_chat(src, span_infoplain("Your overmind's blob reagent is: [blobstrain.name]!"))
+ to_chat(src, span_infoplain("The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]"))
+
+/// Set our attack damage based on blob's properties
+/mob/living/basic/blob_minion/blobbernaut/minion/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ if (isnull(overmind))
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ attack_verb_continuous = initial(attack_verb_continuous)
+ return
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
+ attack_verb_continuous = new_strain.blobbernaut_message
+
+/// Called by our factory to inform us that it's not going to support us financially any more
+/mob/living/basic/blob_minion/blobbernaut/minion/on_factory_destroyed()
+ . = ..()
+ orphaned = TRUE
+ throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
diff --git a/code/modules/mob/living/basic/heretic/heretic_summon.dm b/code/modules/mob/living/basic/heretic/heretic_summon.dm
index 0f7d63f903c..cdae7ea6786 100644
--- a/code/modules/mob/living/basic/heretic/heretic_summon.dm
+++ b/code/modules/mob/living/basic/heretic/heretic_summon.dm
@@ -13,6 +13,7 @@
unsuitable_heat_damage = 0
damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
speed = 0
+ melee_attack_cooldown = CLICK_CD_MELEE
attack_sound = 'sound/weapons/punch1.ogg'
response_help_continuous = "thinks better of touching"
diff --git a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
index 5951bd6b7fe..47280af4281 100644
--- a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
@@ -13,53 +13,33 @@
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/sculpt_statues,
- /datum/ai_planning_subtree/find_and_hunt_target/cannibalize,
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses/ice_whelp,
/datum/ai_planning_subtree/burn_trees,
)
-
-/datum/ai_planning_subtree/find_and_hunt_target/cannibalize
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/ice_whelp
target_key = BB_TARGET_CANNIBAL
- hunting_behavior = /datum/ai_behavior/cannibalize
- finding_behavior = /datum/ai_behavior/find_hunt_target/dragon_corpse
+ finding_behavior = /datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise
hunt_targets = list(/mob/living/basic/mining/ice_whelp)
hunt_range = 10
-/datum/ai_behavior/find_hunt_target/dragon_corpse
+/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
-/datum/ai_behavior/find_hunt_target/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
- if(dinner.stat != DEAD)
- return FALSE
+/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
if(dinner.pulledby) //someone already got him before us
return FALSE
+ return ..()
- return can_see(source, dinner, radius)
-
-/datum/ai_behavior/cannibalize
+/datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
-/datum/ai_behavior/cannibalize/setup(datum/ai_controller/controller, target_key)
- . = ..()
- var/atom/target = controller.blackboard[target_key]
- if(QDELETED(target))
- return FALSE
- set_movement_target(controller, target)
-
-/datum/ai_behavior/cannibalize/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
- . = ..()
- var/mob/living/basic/living_pawn = controller.pawn
+/datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
var/mob/living/target = controller.blackboard[target_key]
-
- if(QDELETED(target))
+ if(QDELETED(target) || target.stat != DEAD || target.pulledby) //we were too slow
finish_action(controller, FALSE)
return
-
- if(target.stat != DEAD || target.pulledby) //we were too slow
- finish_action(controller, FALSE)
- return
-
- living_pawn.melee_attack(target)
- finish_action(controller, TRUE)
+ return ..()
/datum/ai_behavior/cannibalize/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
diff --git a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
index e2c67af2467..c88178135dc 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
@@ -2,6 +2,7 @@
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
BB_BASIC_MOB_FLEEING = TRUE,
+ BB_BASIC_MOB_FLEE_DISTANCE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -47,7 +48,6 @@
/datum/ai_behavior/run_away_from_target/mega_arachnid
clear_failed_targets = FALSE
- run_distance = 5
///only engage in melee combat against cuffed targets, otherwise keep throwing restraints at them
/datum/ai_planning_subtree/basic_melee_attack_subtree/mega_arachnid
diff --git a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
index c662870393c..45bfd74d23b 100644
--- a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
+++ b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
@@ -75,6 +75,7 @@
/datum/ai_controller/basic_controller/basilisk
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_AGGRO_RANGE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
index 95df44a8326..b99e254853e 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
@@ -44,6 +44,8 @@
COOLDOWN_DECLARE(ability_animation_cooldown)
/// Our base tentacles ability
var/datum/action/cooldown/mob_cooldown/goliath_tentacles/tentacles
+ /// Our long-ranged tentacles ability
+ var/datum/action/cooldown/mob_cooldown/tentacle_grasp/tentacle_line
/// Things we want to eat off the floor (or a plate, we're not picky)
var/static/list/goliath_foods = list(/obj/item/food/grown/ash_flora, /obj/item/food/bait/worm)
@@ -70,9 +72,9 @@
var/datum/action/cooldown/mob_cooldown/tentacle_burst/melee_tentacles = new (src)
melee_tentacles.Grant(src)
AddComponent(/datum/component/revenge_ability, melee_tentacles, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], max_range = 1, target_self = TRUE)
- var/datum/action/cooldown/mob_cooldown/tentacle_grasp/ranged_tentacles = new (src)
- ranged_tentacles.Grant(src)
- AddComponent(/datum/component/revenge_ability, ranged_tentacles, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], min_range = 2, max_range = 9)
+ tentacle_line = new (src)
+ tentacle_line.Grant(src)
+ AddComponent(/datum/component/revenge_ability, tentacle_line, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], min_range = 2, max_range = 9)
tentacles_ready()
RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(used_ability))
@@ -82,6 +84,7 @@
/mob/living/basic/mining/goliath/Destroy()
QDEL_NULL(tentacles)
+ QDEL_NULL(tentacle_line)
return ..()
/mob/living/basic/mining/goliath/examine(mob/user)
@@ -167,6 +170,12 @@
. = ..()
faction = new_friend.faction.Copy()
+/mob/living/basic/mining/goliath/RangedAttack(atom/atom_target, modifiers)
+ tentacles?.Trigger(target = atom_target)
+
+/mob/living/basic/mining/goliath/ranged_secondary_attack(atom/atom_target, modifiers)
+ tentacle_line?.Trigger(target = atom_target)
+
/// Legacy Goliath mob with different sprites, largely the same behaviour
/mob/living/basic/mining/goliath/ancient
name = "ancient goliath"
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
new file mode 100644
index 00000000000..11043e58d11
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
@@ -0,0 +1,114 @@
+/// Mob which retreats and spawns annoying sub-mobs to attack you
+/mob/living/basic/mining/hivelord
+ name = "hivelord"
+ desc = "A levitating swarm of tiny creatures which act as a single individual. When threatened or hunting they rapidly replicate additional short-lived bodies."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "hivelord"
+ icon_living = "hivelord"
+ // icon_aggro = "hivelord_alert"
+ icon_dead = "hivelord_dead"
+ icon_gib = "syndicate_gib"
+ mob_biotypes = MOB_ORGANIC
+ speed = 2
+ maxHealth = 75
+ health = 75
+ melee_damage_lower = 0
+ melee_damage_upper = 0
+ attack_verb_continuous = "weakly tackles"
+ attack_verb_simple = "weakly tackles"
+ speak_emote = list("telepathically cries")
+ attack_sound = 'sound/weapons/pierce.ogg'
+ throw_blocked_message = "passes between the bodies of the"
+ obj_damage = 0
+ pass_flags = PASSTABLE
+ ai_controller = /datum/ai_controller/basic_controller/hivelord
+ /// Mobs to spawn when we die, varedit this to be recursive to give the players a fun surprise
+ var/death_spawn_type = /mob/living/basic/hivelord_brood
+ /// Action which spawns worms
+ var/datum/action/cooldown/mob_cooldown/hivelord_spawn/spawn_brood
+
+/mob/living/basic/mining/hivelord/Initialize(mapload)
+ . = ..()
+ var/static/list/death_loot = list(/obj/item/organ/internal/monster_core/regenerative_core)
+ AddElement(/datum/element/relay_attackers)
+ AddElement(/datum/element/death_drops, death_loot)
+ AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY, dead_state = "hivelord_dead") // They writhe so much.
+ AddComponent(/datum/component/appearance_on_aggro, aggro_state = "hivelord_alert")
+ spawn_brood = new(src)
+ spawn_brood.Grant(src)
+ ai_controller.set_blackboard_key(BB_TARGETTED_ACTION, spawn_brood)
+
+/mob/living/basic/mining/hivelord/Destroy()
+ QDEL_NULL(spawn_brood)
+ return ..()
+
+/mob/living/basic/mining/hivelord/death(gibbed)
+ . = ..()
+ var/list/safe_turfs = RANGE_TURFS(1, src) - get_turf(src)
+ for (var/turf/check_turf as anything in safe_turfs)
+ if (check_turf.is_blocked_turf(exclude_mobs = TRUE))
+ safe_turfs -= check_turf
+
+ var/turf/our_turf = get_turf(src)
+ for (var/i in 1 to 3)
+ if (!length(safe_turfs))
+ return
+ var/turf/land_turf = pick_n_take(safe_turfs)
+ var/obj/effect/temp_visual/hivebrood_spawn/forecast = new(land_turf)
+ forecast.create_from(death_spawn_type, our_turf, CALLBACK(src, PROC_REF(complete_spawn), land_turf))
+
+/// Spawns a worm on the specified turf
+/mob/living/basic/mining/hivelord/proc/complete_spawn(turf/spawn_turf)
+ var/mob/living/brood = new death_spawn_type(spawn_turf)
+ brood.faction = faction
+ brood.ai_controller?.set_blackboard_key(ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ brood.dir = get_dir(src, spawn_turf)
+
+/mob/living/basic/mining/hivelord/RangedAttack(atom/atom_target, modifiers)
+ spawn_brood?.Trigger(target = atom_target)
+
+/// Attack worms spawned by the hivelord
+/mob/living/basic/hivelord_brood
+ name = "hivelord brood"
+ desc = "Short-lived attack form of the hivelord. One isn't much of a threat, but..."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "hivelord_brood"
+ icon_living = "hivelord_brood"
+ icon_dead = "hivelord_brood"
+ icon_gib = "syndicate_gib"
+ friendly_verb_continuous = "chirrups near"
+ friendly_verb_simple = "chirrup near"
+ mob_size = MOB_SIZE_SMALL
+ basic_mob_flags = DEL_ON_DEATH
+ pass_flags = PASSTABLE | PASSMOB
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ faction = list(FACTION_MINING)
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ speed = 1.5
+ maxHealth = 1
+ health = 1
+ melee_damage_lower = 2
+ melee_damage_upper = 2
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ speak_emote = list("telepathically cries")
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ obj_damage = 0
+ density = FALSE
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile
+
+/mob/living/basic/hivelord_brood/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_PERMANENTLY_MORTAL), INNATE_TRAIT)
+ AddElement(/datum/element/simple_flying)
+ AddComponent(/datum/component/swarming)
+ AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY)
+ addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
+
+/mob/living/basic/hivelord_brood/death(gibbed)
+ if (!gibbed)
+ new /obj/effect/temp_visual/hive_spawn_wither(get_turf(src), /* copy_from = */ src)
+ return ..()
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/hivelord_ai.dm b/code/modules/mob/living/basic/lavaland/hivelord/hivelord_ai.dm
new file mode 100644
index 00000000000..fd7983de397
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/hivelord/hivelord_ai.dm
@@ -0,0 +1,14 @@
+/// Basically just keep away and shit out worms
+/datum/ai_controller/basic_controller/hivelord
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_AGGRO_RANGE = 5, // Only get mad at people nearby
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ )
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm b/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
new file mode 100644
index 00000000000..3fee2a003f3
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
@@ -0,0 +1,124 @@
+/// Spawns a little worm nearby
+/datum/action/cooldown/mob_cooldown/hivelord_spawn
+ name = "Spawn Brood"
+ desc = "Release an attack form to an adjacent square to attack your target or anyone nearby."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "hivelord_brood"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = TRUE
+ cooldown_time = 2 SECONDS
+ melee_cooldown_time = 0
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
+ shared_cooldown = NONE
+ /// If a mob is not clicked directly, inherit targetting data from this blackboard key and setting it upon this target key
+ var/ai_target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// What are we actually spawning?
+ var/spawn_type = /mob/living/basic/hivelord_brood
+ /// Do we automatically fire with no cooldown when damaged?
+ var/trigger_on_hit = TRUE
+ /// Minimum time between triggering on hit
+ var/on_hit_delay = 1 SECONDS
+ /// Delay between triggering on hit
+ COOLDOWN_DECLARE(on_hit_cooldown)
+
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/Grant(mob/granted_to)
+ . = ..()
+ if (isnull(owner))
+ return
+ if (trigger_on_hit)
+ RegisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/Remove(mob/removed_from)
+ UnregisterSignal(removed_from, COMSIG_ATOM_WAS_ATTACKED)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/Activate(atom/target)
+ . = ..()
+ if (!spawn_brood(target, target_turf = get_turf(target)))
+ StartCooldown(0.5 SECONDS)
+ return
+ StartCooldown()
+
+/// Called when someone whacks us
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/proc/on_attacked(atom/victim, atom/attacker, attack_flags)
+ SIGNAL_HANDLER
+ if (!trigger_on_hit || !(attack_flags & ATTACKER_DAMAGING_ATTACK) || !COOLDOWN_FINISHED(src, on_hit_cooldown))
+ return
+ COOLDOWN_START(src, on_hit_cooldown, on_hit_delay)
+ spawn_brood(attacker, target_turf = get_step_away(owner, attacker), feedback = FALSE)
+
+/// Spawn a funny little worm
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/proc/spawn_brood(target, turf/target_turf, feedback = TRUE)
+ var/ai_target = isliving(target) ? target : null
+ if (isnull(ai_target))
+ ai_target = owner.ai_controller?.blackboard[ai_target_key]
+
+ var/dir_to_target = get_dir(owner, target_turf)
+ var/list/target_turfs = list()
+ for(var/i in -1 to 1)
+ var/turn_amount = rand(-1, 1) * 45
+ var/test_dir = turn(dir_to_target, turn_amount)
+ var/turf/test_turf = get_step(owner, test_dir)
+ if (test_turf.is_blocked_turf(exclude_mobs = TRUE))
+ continue
+ target_turfs += test_turf
+
+ if (!length(target_turfs))
+ if (feedback)
+ owner.balloon_alert(owner, "no room!")
+ StartCooldown(0.5 SECONDS)
+ return FALSE
+
+ var/turf/land_turf = pick(target_turfs)
+ var/obj/effect/temp_visual/hivebrood_spawn/forecast = new(land_turf)
+ forecast.create_from(spawn_type, get_turf(owner), CALLBACK(src, PROC_REF(complete_spawn), land_turf, ai_target))
+ StartCooldown()
+
+ return TRUE
+
+/// Actually create a mob
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/proc/complete_spawn(turf/spawn_turf, target)
+ var/mob/living/brood = new spawn_type(spawn_turf)
+ brood.faction = owner.faction
+ brood.ai_controller?.set_blackboard_key(ai_target_key, target)
+ brood.dir = get_dir(owner, spawn_turf)
+
+#define BROOD_ARC_Y_OFFSET 8
+#define BROOD_ARC_ROTATION 45
+
+/// Fast animation to show a worm spawning
+/obj/effect/temp_visual/hivebrood_spawn
+ name = "brood spawn"
+ duration = 0.3 SECONDS
+ alpha = 0
+
+/// Set up our visuals and start a timer for a callback
+/obj/effect/temp_visual/hivebrood_spawn/proc/create_from(mob/living/spawn_type, turf/spawn_from, datum/callback/on_completed)
+ addtimer(on_completed, duration, TIMER_DELETE_ME)
+
+ var/turf/my_turf = get_turf(src)
+ dir = get_dir(spawn_from, my_turf)
+ var/move_x = (my_turf.x - spawn_from.x) * world.icon_size
+ var/move_y = (my_turf.y - spawn_from.y) * world.icon_size
+ pixel_x = -move_x
+ pixel_y = -move_y
+
+ icon = initial(spawn_type.icon)
+ icon_state = initial(spawn_type.icon_state)
+
+
+ animate(src, pixel_x = 0, time = duration)
+ animate(src, pixel_y = BROOD_ARC_Y_OFFSET - (move_y * 0.5), time = duration * 0.5, flags = ANIMATION_PARALLEL, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = duration * 0.5, easing = SINE_EASING | EASE_IN)
+ animate(src, alpha = 255, time = duration * 0.5, flags = ANIMATION_PARALLEL)
+
+ if (dir & (NORTH | EAST))
+ transform = matrix().Turn(-BROOD_ARC_ROTATION)
+ animate(src, transform = matrix(), time = duration, flags = ANIMATION_PARALLEL)
+ else
+ transform = matrix().Turn(BROOD_ARC_ROTATION)
+ animate(src, transform = matrix(), time = duration, flags = ANIMATION_PARALLEL)
+
+#undef BROOD_ARC_Y_OFFSET
+#undef BROOD_ARC_ROTATION
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion.dm b/code/modules/mob/living/basic/lavaland/legion/legion.dm
new file mode 100644
index 00000000000..7c6bd0fd170
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion.dm
@@ -0,0 +1,158 @@
+/**
+ * Avoids players while throwing skulls at them.
+ * Legion skulls heal allies, bite enemies, and infest dying humans to make more legions.
+ */
+/mob/living/basic/mining/legion
+ name = "legion"
+ desc = "You can still see what was once a human under the shifting mass of corruption."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion"
+ icon_living = "legion"
+ icon_dead = "legion"
+ icon_gib = "syndicate_gib"
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ basic_mob_flags = DEL_ON_DEATH
+ speed = 3
+ maxHealth = 75
+ health = 75
+ obj_damage = 60
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ attack_verb_continuous = "lashes out at"
+ attack_verb_simple = "lash out at"
+ speak_emote = list("gurgles")
+ attack_sound = 'sound/weapons/pierce.ogg'
+ throw_blocked_message = "bounces harmlessly off of"
+ crusher_loot = /obj/item/crusher_trophy/legion_skull
+ death_message = "wails in chorus and dissolves into quivering flesh."
+ ai_controller = /datum/ai_controller/basic_controller/legion
+ /// What kind of mob do we spawn?
+ var/brood_type = /mob/living/basic/legion_brood
+ /// What kind of corpse spawner do we leave behind on death?
+ var/corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested
+ /// Who is inside of us?
+ var/mob/living/stored_mob
+
+/mob/living/basic/mining/legion/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/death_drops, get_loot_list())
+ AddElement(/datum/element/content_barfer)
+
+ var/datum/action/cooldown/mob_cooldown/skull_launcher/skull_launcher = new(src)
+ skull_launcher.Grant(src)
+ skull_launcher.spawn_type = brood_type
+ ai_controller.blackboard[BB_TARGETTED_ACTION] = skull_launcher
+
+/// Create what we want to drop on death, in proc form so we can always return a static list
+/mob/living/basic/mining/legion/proc/get_loot_list()
+ var/static/list/death_loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion)
+ return death_loot
+
+/mob/living/basic/mining/legion/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != stored_mob)
+ return
+ ai_controller.clear_blackboard_key(BB_LEGION_CORPSE)
+ stored_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_LEGION_EATEN)
+ stored_mob.add_mood_event(MOOD_CATEGORY_LEGION_CORE, /datum/mood_event/healsbadman/long_term) // This will still probably mostly be gone before you are alive
+ stored_mob = null
+
+/mob/living/basic/mining/legion/death(gibbed)
+ if (isnull(stored_mob))
+ new corpse_type(loc)
+ return ..()
+
+/// Put a corpse in this guy
+/mob/living/basic/mining/legion/proc/consume(mob/living/consumed)
+ new /obj/effect/gibspawner/generic(consumed.loc)
+ gender = consumed.gender
+ name = consumed.real_name
+ consumed.investigate_log("has been killed by hivelord infestation.", INVESTIGATE_DEATHS)
+ consumed.death()
+ consumed.extinguish_mob()
+ consumed.fully_heal(HEAL_DAMAGE)
+ consumed.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_LEGION_EATEN)
+ consumed.forceMove(src)
+ ai_controller?.set_blackboard_key(BB_LEGION_CORPSE, consumed)
+ ai_controller?.set_blackboard_key(BB_LEGION_RECENT_LINES, consumed.copy_recent_speech(line_chance = 80))
+ stored_mob = consumed
+ visible_message(span_warning("[src] staggers to [p_their()] feet!"))
+ if (prob(75))
+ return
+ // Congratulations you have won a special prize: cancer
+ var/obj/item/organ/internal/legion_tumour/cancer = new()
+ cancer.Insert(consumed, special = TRUE, drop_if_replaced = FALSE)
+
+/// A Legion which only drops skeletons instead of corpses which might have fun loot, so it cannot be farmed
+/mob/living/basic/mining/legion/spawner_made
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/charred
+
+
+/// Like a Legion but it's an adorable snowman
+/mob/living/basic/mining/legion/snow
+ name = "snow legion"
+ desc = "You can vaguely see what was once a human under the densely packed snow. Cute, but macabre."
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "snowlegion"
+ icon_living = "snowlegion"
+ // icon_aggro = "snowlegion_alive"
+ icon_dead = "snowlegion"
+ brood_type = /mob/living/basic/legion_brood/snow
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/snow
+
+/mob/living/basic/mining/legion/snow/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/appearance_on_aggro, aggro_state = "snowlegion_alive") // Surprise! I was real!
+
+/// As Snow Legion but spawns corpses which don't have any exciting loot
+/mob/living/basic/mining/legion/snow/spawner_made
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/skeleton
+
+
+/// Like a Legion but shorter and faster
+/mob/living/basic/mining/legion/dwarf
+ name = "dwarf legion"
+ desc = "You can still see what was once a rather small human under the shifting mass of corruption."
+ icon_state = "dwarf_legion"
+ icon_living = "dwarf_legion"
+ icon_dead = "dwarf_legion"
+ maxHealth = 60
+ health = 60
+ speed = 2
+ crusher_drop_chance = 20
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/dwarf
+
+
+/// Like a Legion but larger and spawns regular Legions, not currently used anywhere and very soulful
+/mob/living/basic/mining/legion/large
+ name = "myriad"
+ desc = "A legion of legions, a dead end to whatever form the Necropolis was attempting to create."
+ icon = 'icons/mob/simple/lavaland/64x64megafauna.dmi'
+ icon_state = "legion"
+ icon_living = "legion"
+ icon_dead = "legion"
+ health_doll_icon = "legion"
+ speed = 5
+ health = 450
+ maxHealth = 450
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ obj_damage = 30
+ pixel_x = -16
+ sentience_type = SENTIENCE_BOSS
+
+/mob/living/basic/mining/legion/large/Initialize(mapload)
+ . = ..()
+ AddComponent(\
+ /datum/component/spawner,\
+ spawn_types = list(/mob/living/basic/mining/legion),\
+ spawn_time = 20 SECONDS,\
+ max_spawned = 3,\
+ spawn_text = "peels itself off from",\
+ faction = faction,\
+ )
+
+/// Create what we want to drop on death, in proc form so we can always return a static list
+/mob/living/basic/mining/legion/large/get_loot_list()
+ var/static/list/death_loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion = 3, /obj/effect/mob_spawn/corpse/human/legioninfested = 4)
+ return death_loot
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
new file mode 100644
index 00000000000..6b3525cb32a
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
@@ -0,0 +1,77 @@
+/// Keep away and launch skulls at every opportunity, prioritising injured allies
+/datum/ai_controller/basic_controller/legion
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
+ BB_BASIC_MOB_FLEEING = TRUE,
+ BB_AGGRO_RANGE = 5, // Unobservant
+ BB_BASIC_MOB_FLEE_DISTANCE = 6,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/random_speech/legion,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ /datum/ai_planning_subtree/flee_target/legion,
+ )
+
+/// Chase and attack whatever we are targetting, if it's friendly we will heal them
+/datum/ai_controller/basic_controller/legion_brood
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
+ )
+
+ 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/basic_melee_attack_subtree,
+ )
+
+/// 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)
+ if (!living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly))
+ return FALSE
+ if (istype(the_target, living_mob.type))
+ return TRUE
+ var/atom/created_by = living_mob.ai_controller.blackboard[BB_LEGION_BROOD_CREATOR]
+ if (!QDELETED(created_by) && istype(the_target, created_by.type))
+ return TRUE
+ return the_target.stat == DEAD || the_target.health >= the_target.maxHealth
+
+/// Don't run away from friendlies
+/datum/ai_planning_subtree/flee_target/legion
+
+/datum/ai_planning_subtree/flee_target/legion/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/target = controller.blackboard[target_key]
+ if (QDELETED(target) || target.faction_check_mob(controller.pawn))
+ return // Only flee if we have a hostile target
+ return ..()
+
+/// Make spooky sounds, if we have a corpse inside then impersonate them
+/datum/ai_planning_subtree/random_speech/legion
+ speech_chance = 1
+ speak = list("Come...", "Legion...", "Why...?")
+ emote_hear = list("groans.", "wails.", "whimpers.")
+ emote_see = list("twitches.", "shudders.")
+ /// Stuff to specifically say into a radio
+ var/list/radio_speech = list("Come...", "Why...?")
+
+/datum/ai_planning_subtree/random_speech/legion/speak(datum/ai_controller/controller)
+ var/mob/living/carbon/human/victim = controller.blackboard[BB_LEGION_CORPSE]
+ if (QDELETED(victim) || prob(30))
+ return ..()
+
+ var/list/remembered_speech = controller.blackboard[BB_LEGION_RECENT_LINES] || list()
+
+ if (length(remembered_speech) && prob(50)) // Don't spam the radio
+ controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(remembered_speech))
+ return
+
+ var/obj/item/radio/mob_radio = locate() in victim.contents
+ if (QDELETED(mob_radio))
+ return ..() // No radio, just talk funny
+ controller.queue_behavior(/datum/ai_behavior/perform_speech_radio, pick(radio_speech + remembered_speech), mob_radio, list(RADIO_CHANNEL_SUPPLY, RADIO_CHANNEL_COMMON))
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
new file mode 100644
index 00000000000..962d232c5ef
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
@@ -0,0 +1,99 @@
+/// A spooky skull which heals lavaland mobs, attacks miners, and infests their bodies
+/mob/living/basic/legion_brood
+ name = "legion"
+ desc = "One of many."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ icon_living = "legion_head"
+ icon_dead = "legion_head"
+ icon_gib = "syndicate_gib"
+ basic_mob_flags = DEL_ON_DEATH
+ mob_size = MOB_SIZE_SMALL
+ pass_flags = PASSTABLE | PASSMOB
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ faction = list(FACTION_MINING)
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ friendly_verb_continuous = "chatters near"
+ friendly_verb_simple = "chatter near"
+ maxHealth = 1
+ health = 1
+ melee_damage_lower = 12
+ melee_damage_upper = 12
+ obj_damage = 0
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ speak_emote = list("echoes") // who the fuck speaking as this mob it dies 10 seconds after it spawns
+ attack_sound = 'sound/weapons/pierce.ogg'
+ density = FALSE
+ ai_controller = /datum/ai_controller/basic_controller/legion_brood
+ /// Reference to a guy who made us
+ var/mob/living/created_by
+
+/mob/living/basic/legion_brood/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_PERMANENTLY_MORTAL), INNATE_TRAIT)
+ AddElement(/datum/element/simple_flying)
+ AddComponent(/datum/component/swarming)
+ AddComponent(/datum/component/clickbox, icon_state = "sphere", max_scale = 2)
+ addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
+
+/mob/living/basic/legion_brood/death(gibbed)
+ if (!gibbed)
+ new /obj/effect/temp_visual/hive_spawn_wither(get_turf(src), /* copy_from = */ src)
+ return ..()
+
+/mob/living/basic/legion_brood/melee_attack(mob/living/target, list/modifiers, ignore_cooldown)
+ if (ishuman(target) && target.stat > SOFT_CRIT)
+ infest(target)
+ return
+ if (isliving(target) && faction_check_mob(target) && !istype(target, created_by?.type))
+ visible_message(span_warning("[src] melds with [target]'s flesh!"))
+ target.apply_status_effect(/datum/status_effect/regenerative_core)
+ new /obj/effect/temp_visual/heal(get_turf(target), COLOR_HEALING_CYAN)
+ death()
+ return
+ return ..()
+
+/// Turn the targetted mob into one of us
+/mob/living/basic/legion_brood/proc/infest(mob/living/target)
+ visible_message(span_warning("[name] burrows into the flesh of [target]!"))
+ var/spawn_type = get_legion_type(target)
+ var/mob/living/basic/mining/legion/new_legion = new spawn_type(loc)
+ new_legion.consume(target)
+ new_legion.faction = faction.Copy()
+ qdel(src)
+
+/// Returns the kind of legion we make out of the target
+/mob/living/basic/legion_brood/proc/get_legion_type(mob/living/target)
+ if (HAS_TRAIT(target, TRAIT_DWARF))
+ return /mob/living/basic/mining/legion/dwarf
+ return /mob/living/basic/mining/legion
+
+/// Sets someone as our creator, mostly so you can't use skulls to heal yourself
+/mob/living/basic/legion_brood/proc/assign_creator(mob/living/creator, copy_full_faction = TRUE)
+ if (copy_full_faction)
+ faction = creator.faction.Copy()
+ else
+ faction |= REF(creator)
+ created_by = creator
+ ai_controller?.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator)
+ RegisterSignal(creator, COMSIG_QDELETING, PROC_REF(creator_destroyed))
+
+/// Reference handling
+/mob/living/basic/legion_brood/proc/creator_destroyed()
+ SIGNAL_HANDLER
+ created_by = null
+
+/// Like the Legion's summoned skull but funnier (it's snow now)
+/mob/living/basic/legion_brood/snow
+ name = "snow legion"
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "snowlegion_head"
+ icon_living = "snowlegion_head"
+ icon_dead = "snowlegion_head"
+
+/mob/living/basic/legion_brood/snow/get_legion_type(mob/living/target)
+ return /mob/living/basic/mining/legion/snow
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_tumour.dm b/code/modules/mob/living/basic/lavaland/legion/legion_tumour.dm
new file mode 100644
index 00000000000..078af57de2a
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_tumour.dm
@@ -0,0 +1,159 @@
+/// Left behind when a legion infects you, for medical enrichment
+/obj/item/organ/internal/legion_tumour
+ name = "legion tumour"
+ desc = "A mass of pulsing flesh and dark tendrils, containing the power to regenerate flesh at a terrible cost."
+ failing_desc = "pulses and writhes with horrible life, reaching towards you with its tendrils!"
+ icon = 'icons/obj/medical/organs/mining_organs.dmi'
+ icon_state = "legion_remains"
+ zone = BODY_ZONE_CHEST
+ slot = ORGAN_SLOT_PARASITE_EGG
+ decay_factor = STANDARD_ORGAN_DECAY * 3 // About 5 minutes outside of a host
+ /// What stage of growth the corruption has reached.
+ var/stage = 0
+ /// We apply this status effect periodically or when used on someone
+ var/applied_status = /datum/status_effect/regenerative_core
+ /// How long have we been in this stage?
+ var/elapsed_time = 0 SECONDS
+ /// How long does it take to advance one stage?
+ var/growth_time = 80 SECONDS // Long enough that if you go back to lavaland without realising it you're not totally fucked
+ /// What kind of mob will we transform into?
+ var/spawn_type = /mob/living/basic/mining/legion
+ /// Spooky sounds to play as you start to turn
+ var/static/list/spooky_sounds = list(
+ 'sound/voice/lowHiss1.ogg',
+ 'sound/voice/lowHiss2.ogg',
+ 'sound/voice/lowHiss3.ogg',
+ 'sound/voice/lowHiss4.ogg',
+ )
+
+/obj/item/organ/internal/legion_tumour/Initialize(mapload)
+ . = ..()
+ animate_pulse()
+
+/obj/item/organ/internal/legion_tumour/apply_organ_damage(damage_amount, maximum, required_organ_flag)
+ var/was_failing = organ_flags & ORGAN_FAILING
+ . = ..()
+ if (was_failing != (organ_flags & ORGAN_FAILING))
+ animate_pulse()
+
+/obj/item/organ/internal/legion_tumour/set_organ_damage(damage_amount, required_organ_flag)
+ . = ..()
+ animate_pulse()
+
+/// Do a heartbeat animation depending on if we're failing or not
+/obj/item/organ/internal/legion_tumour/proc/animate_pulse()
+ animate(src, transform = matrix()) // Stop any current animation
+
+ var/speed_divider = organ_flags & ORGAN_FAILING ? 2 : 1
+
+ animate(src, transform = matrix().Scale(1.1), time = 0.5 SECONDS / speed_divider, easing = SINE_EASING | EASE_OUT, loop = -1, flags = ANIMATION_PARALLEL)
+ animate(transform = matrix(), time = 0.5 SECONDS / speed_divider, easing = SINE_EASING | EASE_IN)
+ animate(transform = matrix(), time = 2 SECONDS / speed_divider)
+
+/obj/item/organ/internal/legion_tumour/Remove(mob/living/carbon/egg_owner, special)
+ . = ..()
+ stage = 0
+ elapsed_time = 0
+
+/obj/item/organ/internal/legion_tumour/attack(mob/living/target, mob/living/user, params)
+ if (try_apply(target, user))
+ qdel(src)
+ return
+ return ..()
+
+/// Smear it on someone like a regen core, why not. Make sure they're alive though.
+/obj/item/organ/internal/legion_tumour/proc/try_apply(mob/living/target, mob/user)
+ if(!user.Adjacent(target) || !isliving(target))
+ return FALSE
+
+ if (target.stat <= SOFT_CRIT && !(organ_flags & ORGAN_FAILING))
+ target.add_mood_event(MOOD_CATEGORY_LEGION_CORE, /datum/mood_event/healsbadman)
+ target.apply_status_effect(applied_status)
+
+ if (target != user)
+ target.visible_message(span_notice("[user] splatters [target] with [src]... Disgusting tendrils pull [target.p_their()] wounds shut!"))
+ else
+ to_chat(user, span_notice("You smear [src] on yourself. Disgusting tendrils pull your wounds closed."))
+ return TRUE
+
+ if (!ishuman(target))
+ return FALSE
+
+ target.visible_message(span_boldwarning("[user] splatters [target] with [src]... and it springs into horrible life!"))
+ var/mob/living/basic/legion_brood/skull = new(target.loc)
+ skull.melee_attack(target)
+ return TRUE
+
+/obj/item/organ/internal/legion_tumour/on_life(seconds_per_tick, times_fired)
+ . = ..()
+ if (QDELETED(src) || QDELETED(owner))
+ return
+
+ if (stage >= 2)
+ if(SPT_PROB(stage / 5, seconds_per_tick))
+ to_chat(owner, span_notice("You feel a bit better."))
+ owner.apply_status_effect(applied_status) // It's not all bad!
+ if(SPT_PROB(1, seconds_per_tick))
+ owner.emote("twitch")
+
+ switch(stage)
+ if(2, 3)
+ if(SPT_PROB(1, seconds_per_tick))
+ to_chat(owner, span_danger("Your chest spasms!"))
+ if(SPT_PROB(1, seconds_per_tick))
+ to_chat(owner, span_danger("You feel weak."))
+ if(SPT_PROB(1, seconds_per_tick))
+ SEND_SOUND(owner, sound(pick(spooky_sounds)))
+ if(SPT_PROB(2, seconds_per_tick))
+ owner.vomit()
+ if(4, 5)
+ if(SPT_PROB(2, seconds_per_tick))
+ to_chat(owner, span_danger("Something flexes under your skin."))
+ if(SPT_PROB(2, seconds_per_tick))
+ if (prob(40))
+ SEND_SOUND(owner, sound('sound/voice/ghost_whisper.ogg'))
+ else
+ SEND_SOUND(owner, sound(pick(spooky_sounds)))
+ if(SPT_PROB(3, seconds_per_tick))
+ owner.vomit(vomit_type = /obj/effect/decal/cleanable/vomit/old/black_bile)
+ if (prob(50))
+ var/turf/check_turf = get_step(owner.loc, owner.dir)
+ var/atom/land_turf = (check_turf.is_blocked_turf()) ? owner.loc : check_turf
+ var/mob/living/basic/legion_brood/child = new(land_turf)
+ child.assign_creator(owner, copy_full_faction = FALSE)
+
+ if(SPT_PROB(3, seconds_per_tick))
+ to_chat(owner, span_danger("Your muscles ache."))
+ owner.take_bodypart_damage(3)
+
+ if (stage == 5)
+ if (SPT_PROB(10, seconds_per_tick))
+ infest()
+ return
+
+ elapsed_time += seconds_per_tick SECONDS * ((organ_flags & ORGAN_FAILING) ? 3 : 1) // Let's call it "matured" rather than failed
+ if (elapsed_time < growth_time)
+ return
+ stage++
+ elapsed_time = 0
+ if (stage == 5)
+ to_chat(owner, span_bolddanger("Something is moving under your skin!"))
+
+/// Consume our host
+/obj/item/organ/internal/legion_tumour/proc/infest()
+ if (QDELETED(src) || QDELETED(owner))
+ return
+ owner.visible_message(span_boldwarning("Black tendrils burst from [owner]'s flesh, covering them in amorphous flesh!"))
+ var/mob/living/basic/mining/legion/new_legion = new spawn_type(owner.loc)
+ new_legion.consume(owner)
+ qdel(src)
+
+/obj/item/organ/internal/legion_tumour/on_find(mob/living/finder)
+ . = ..()
+ to_chat(finder, span_warning("There's an enormous tumour in [owner]'s [zone]!"))
+ if(stage < 4)
+ to_chat(finder, span_notice("Its tendrils seem to twitch towards the light."))
+ return
+ to_chat(finder, span_notice("Its pulsing tendrils reach all throughout the body."))
+ if(prob(stage * 2))
+ infest()
diff --git a/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm b/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
new file mode 100644
index 00000000000..1ffcafecd56
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
@@ -0,0 +1,109 @@
+/// Spawns a little worm nearby
+/datum/action/cooldown/mob_cooldown/skull_launcher
+ name = "Launch Legion"
+ desc = "Propel a living piece of your body to a distant location."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "legion_head"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = TRUE
+ cooldown_time = 2 SECONDS
+ melee_cooldown_time = 0
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
+ shared_cooldown = NONE
+ /// If a mob is not clicked directly, inherit targetting data from this blackboard key and setting it upon this target key
+ var/ai_target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// What are we actually spawning?
+ var/spawn_type = /mob/living/basic/legion_brood
+ /// How far can we fire?
+ var/max_range = 7
+
+/datum/action/cooldown/mob_cooldown/skull_launcher/Activate(atom/target)
+ var/turf/target_turf = get_turf(target)
+
+ if (get_dist(owner, target_turf) > max_range)
+ target_turf = get_ranged_target_turf_direct(owner, target_turf, max_range)
+
+ if (target_turf.is_blocked_turf())
+ var/list/near_turfs = RANGE_TURFS(1, target_turf) - target_turf
+ for (var/turf/check_turf as anything in near_turfs)
+ if (check_turf.is_blocked_turf())
+ near_turfs -= check_turf
+ if (length(near_turfs))
+ target_turf = pick(near_turfs)
+ else if(target_turf.is_blocked_turf(exclude_mobs = TRUE))
+ owner.balloon_alert(owner, "no room!")
+ StartCooldown(0.5 SECONDS)
+ return
+
+ var/ai_target = isliving(target) ? target : null
+ if (isnull(ai_target))
+ ai_target = owner.ai_controller?.blackboard[ai_target_key]
+
+ var/target_dir = get_dir(owner, target)
+
+ var/obj/effect/temp_visual/legion_skull_depart/launch = new(get_turf(owner))
+ launch.set_appearance(spawn_type)
+ launch.dir = target_dir
+ new /obj/effect/temp_visual/legion_brood_indicator(target_turf)
+ var/obj/effect/temp_visual/legion_skull_land/land = new(target_turf)
+ land.dir = target_dir
+ land.set_appearance(spawn_type, CALLBACK(src, PROC_REF(spawn_skull), target_turf, ai_target))
+ StartCooldown()
+
+/// Actually create a mob
+/datum/action/cooldown/mob_cooldown/skull_launcher/proc/spawn_skull(turf/spawn_location, target)
+ var/mob/living/basic/legion_brood/brood = new spawn_type(spawn_location)
+ if (istype(brood))
+ brood.assign_creator(owner)
+ brood.ai_controller?.set_blackboard_key(ai_target_key, target)
+ brood.dir = get_dir(owner, spawn_location)
+ if (!isnull(target))
+ brood.face_atom(target)
+ else
+ brood.dir = get_dir(owner, spawn_location)
+
+
+/// Animation for launching a skull
+/obj/effect/temp_visual/legion_skull_depart
+ name = "legion brood launch"
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ duration = 0.25 SECONDS
+
+/// Copy appearance from the passed atom type
+/obj/effect/temp_visual/legion_skull_depart/proc/set_appearance(atom/spawned_type)
+ icon = initial(spawned_type.icon)
+ icon_state = initial(spawned_type.icon_state)
+ animate(src, alpha = 0, pixel_y = 72, time = duration)
+
+/// Animation for landing a skull
+/obj/effect/temp_visual/legion_skull_land
+ name = "legion brood land"
+ duration = 0.5 SECONDS
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ alpha = 0
+ pixel_y = 72
+
+/// Copy appearance from the passed atom type and store what to do on animation complete
+/obj/effect/temp_visual/legion_skull_land/proc/set_appearance(atom/spawned_type, datum/callback/on_completed)
+ icon = initial(spawned_type.icon)
+ icon_state = initial(spawned_type.icon_state)
+ animate(src, alpha = 0, pixel_y = 72, time = duration / 2)
+ animate(alpha = 255, pixel_y = 0, time = duration / 2)
+ addtimer(on_completed, duration, TIMER_DELETE_ME)
+
+/// A skull is going to be here! Oh no!
+/obj/effect/temp_visual/legion_brood_indicator
+ name = "legion brood land"
+ duration = 0.75 SECONDS
+ layer = BELOW_MOB_LAYER
+ plane = GAME_PLANE
+ icon = 'icons/mob/telegraphing/telegraph.dmi'
+ icon_state = "skull"
+
+/obj/effect/temp_visual/legion_brood_indicator/Initialize(mapload)
+ . = ..()
+ animate(src, alpha = 255, time = 0.5 SECONDS)
+ animate(alpha = 0, time = 0.25 SECONDS)
diff --git a/code/modules/mob/living/basic/lavaland/mining.dm b/code/modules/mob/living/basic/lavaland/mining.dm
index 6b1d0de5739..0b6c4f321b6 100644
--- a/code/modules/mob/living/basic/lavaland/mining.dm
+++ b/code/modules/mob/living/basic/lavaland/mining.dm
@@ -2,12 +2,17 @@
/mob/living/basic/mining
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
combat_mode = TRUE
+ status_flags = NONE //don't inherit standard basicmob flags
mob_size = MOB_SIZE_LARGE
mob_biotypes = MOB_ORGANIC|MOB_BEAST
faction = list(FACTION_MINING)
unsuitable_atmos_damage = 0
minimum_survivable_temperature = 0
maximum_survivable_temperature = INFINITY
+ // Pale purple, should be red enough to see stuff on lavaland
+ lighting_cutoff_red = 25
+ lighting_cutoff_green = 15
+ lighting_cutoff_blue = 35
/// Message to output if throwing damage is absorbed
var/throw_blocked_message = "bounces off"
/// What crusher trophy this mob drops, if any
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 1f310ac229f..a25234817f3 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
@@ -9,9 +9,9 @@
planning_subtrees = list(
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
/datum/ai_planning_subtree/use_mob_ability/gaze,
/datum/ai_planning_subtree/ranged_skirmish/watcher,
- /datum/ai_planning_subtree/maintain_distance,
)
/datum/ai_planning_subtree/use_mob_ability/gaze
diff --git a/code/modules/mob/living/basic/minebots/minebot_ai.dm b/code/modules/mob/living/basic/minebots/minebot_ai.dm
index 897cddb1401..a4b082f5dd1 100644
--- a/code/modules/mob/living/basic/minebots/minebot_ai.dm
+++ b/code/modules/mob/living/basic/minebots/minebot_ai.dm
@@ -60,6 +60,7 @@
/datum/ai_behavior/basic_ranged_attack/minebot
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+ avoid_friendly_fire = TRUE
/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/living_pawn = controller.pawn
diff --git a/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_behavior.dm b/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_behavior.dm
index 4cdaba09759..28cffa4ed8e 100644
--- a/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_behavior.dm
+++ b/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_behavior.dm
@@ -64,6 +64,8 @@
/datum/ai_behavior/basic_ranged_attack/hivebot
action_cooldown = 3 SECONDS
+ avoid_friendly_fire = TRUE
/datum/ai_behavior/basic_ranged_attack/hivebot_rapid
action_cooldown = 1.5 SECONDS
+ avoid_friendly_fire = TRUE
diff --git a/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_subtree.dm b/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_subtree.dm
index 347219d0ef0..5bd957a7609 100644
--- a/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_subtree.dm
+++ b/code/modules/mob/living/basic/space_fauna/hivebot/hivebot_subtree.dm
@@ -32,7 +32,7 @@
/datum/ai_controller/basic_controller/hivebot/ranged/rapid
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/basic_ranged_attack_subtree,
+ /datum/ai_planning_subtree/basic_ranged_attack_subtree/hivebot_rapid,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/hive_communicate,
)
diff --git a/code/modules/mob/living/basic/space_fauna/morph.dm b/code/modules/mob/living/basic/space_fauna/morph.dm
index 3f31f7f3735..32115d05602 100644
--- a/code/modules/mob/living/basic/space_fauna/morph.dm
+++ b/code/modules/mob/living/basic/space_fauna/morph.dm
@@ -21,6 +21,7 @@
obj_damage = 50
melee_damage_lower = 20
melee_damage_upper = 20
+ melee_attack_cooldown = CLICK_CD_MELEE
// Oh you KNOW it's gonna be real green
lighting_cutoff_red = 10
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm
index 5a7bb075f19..d49932fb704 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm
@@ -26,22 +26,9 @@
lighting_cutoff_green = 15
lighting_cutoff_blue = 40
- ai_controller = /datum/ai_controller/basic_controller/blankbody
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles
/mob/living/basic/blankbody/Initialize(mapload)
. = ..()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_NETHER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 0)
AddComponent(/datum/component/health_scaling_effects, min_health_attack_modifier_lower = 8, min_health_attack_modifier_upper = 14)
-
-/datum/ai_controller/basic_controller/blankbody
- 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/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
index b38ada0f6e1..cdde6ad05e4 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
@@ -27,7 +27,7 @@
lighting_cutoff_green = 25
lighting_cutoff_blue = 15
- ai_controller = /datum/ai_controller/basic_controller/creature
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles
/mob/living/basic/creature/Initialize(mapload)
. = ..()
@@ -101,16 +101,3 @@
exit_jaunt(cast_on)
return
enter_jaunt(cast_on)
-
-/datum/ai_controller/basic_controller/creature
- 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/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm
index 18dca95013e..3f445ea1261 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm
@@ -28,7 +28,7 @@
lighting_cutoff_green = 15
lighting_cutoff_blue = 50
- ai_controller = /datum/ai_controller/basic_controller/migo
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles
var/static/list/migo_sounds
/// Odds migo will dodge
var/dodge_prob = 10
@@ -71,16 +71,3 @@
. = Move(get_step(loc,pick(cdir, ccdir)))
if(!.)//Can't dodge there so we just carry on
. = Move(moving_to, move_direction)
-
-/datum/ai_controller/basic_controller/migo
- 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/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
index 3b32fbb4ce7..519e8ba1a73 100644
--- a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
+++ b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
@@ -110,8 +110,7 @@
faction = list(FACTION_STICKMAN)
melee_damage_lower = 1
melee_damage_upper = 5
-
- ai_controller = /datum/ai_controller/basic_controller/wizard_copy
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile
/mob/living/basic/paper_wizard/copy/Initialize(mapload)
. = ..()
@@ -141,18 +140,6 @@
new /obj/effect/temp_visual/small_smoke/halfsecond(get_turf(src))
qdel(src) //I see through your ruse!
-/datum/ai_controller/basic_controller/wizard_copy
- 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/basic_melee_attack_subtree,
- )
-
//fancy effects
/obj/effect/temp_visual/paper_scatter
name = "scattering paper"
diff --git a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
index 85369b72eb8..164c25fb896 100644
--- a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
+++ b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
@@ -24,6 +24,7 @@
obj_damage = 10
melee_damage_lower = 13
melee_damage_upper = 15
+ melee_attack_cooldown = CLICK_CD_MELEE
attack_verb_continuous = "slashes"
attack_verb_simple = "slash"
attack_sound = 'sound/weapons/bladeslice.ogg'
diff --git a/code/modules/mob/living/simple_animal/friendly/robot_customer.dm b/code/modules/mob/living/basic/space_fauna/robot_customer.dm
similarity index 63%
rename from code/modules/mob/living/simple_animal/friendly/robot_customer.dm
rename to code/modules/mob/living/basic/space_fauna/robot_customer.dm
index 13eac2939f3..e084e11f403 100644
--- a/code/modules/mob/living/simple_animal/friendly/robot_customer.dm
+++ b/code/modules/mob/living/basic/space_fauna/robot_customer.dm
@@ -1,45 +1,56 @@
///Robot customers
-/mob/living/simple_animal/robot_customer
- name = "space-tourist bot"
+/mob/living/basic/robot_customer
+ name = "tourist bot"
maxHealth = 150
health = 150
desc = "I wonder what they'll order..."
gender = NEUTER
+
icon = 'icons/mob/simple/tourists.dmi'
icon_state = "amerifat"
icon_living = "amerifat"
- ///Override so it uses datum ai
- can_have_ai = FALSE
- AIStatus = AI_OFF
- del_on_death = TRUE
+
+ basic_mob_flags = DEL_ON_DEATH
mob_biotypes = MOB_ROBOTIC|MOB_HUMANOID
sentience_type = SENTIENCE_ARTIFICIAL
- ai_controller = /datum/ai_controller/robot_customer
+
unsuitable_atmos_damage = 0
- minbodytemp = 0
- maxbodytemp = 1000
+ minimum_survivable_temperature = TCMB
+ maximum_survivable_temperature = T0C + 1000
+
+ ai_controller = /datum/ai_controller/robot_customer
+
+ /// The clothes that we draw on this tourist.
var/clothes_set = "amerifat_clothes"
+ /// Reference to the hud that we show when the player hovers over us.
var/datum/atom_hud/hud_to_show_on_hover
-
-/mob/living/simple_animal/robot_customer/Initialize(mapload, datum/customer_data/customer_data = /datum/customer_data/american, datum/venue/attending_venue = SSrestaurant.all_venues[/datum/venue/restaurant])
- ADD_TRAIT(src, list(TRAIT_NOMOBSWAP, TRAIT_NO_TELEPORT, TRAIT_STRONG_GRABBER), INNATE_TRAIT) // never suffer a bitch to fuck with you
- AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)
+/mob/living/basic/robot_customer/Initialize(
+ mapload,
+ datum/customer_data/customer_data = /datum/customer_data/american,
+ datum/venue/attending_venue = SSrestaurant.all_venues[/datum/venue/restaurant],
+)
var/datum/customer_data/customer_info = SSrestaurant.all_customers[customer_data]
- clothes_set = pick(customer_info.clothing_sets)
ai_controller = customer_info.ai_controller_used
+
. = ..()
+
+ ADD_TRAIT(src, list(TRAIT_NOMOBSWAP, TRAIT_NO_TELEPORT, TRAIT_STRONG_GRABBER), INNATE_TRAIT) // never suffer a bitch to fuck with you
+ AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)
+
ai_controller.set_blackboard_key(BB_CUSTOMER_CUSTOMERINFO, customer_info)
ai_controller.set_blackboard_key(BB_CUSTOMER_ATTENDING_VENUE, attending_venue)
ai_controller.set_blackboard_key(BB_CUSTOMER_PATIENCE, customer_info.total_patience)
+
icon = customer_info.base_icon
icon_state = customer_info.base_icon_state
name = "[pick(customer_info.name_prefixes)]-bot"
color = rgb(rand(80,255), rand(80,255), rand(80,255))
- update_icon()
+ clothes_set = pick(customer_info.clothing_sets)
+ update_appearance(UPDATE_ICON)
///Clean up on the mobs seat etc when its deleted (Either by murder or because it left)
-/mob/living/simple_animal/robot_customer/Destroy()
+/mob/living/basic/robot_customer/Destroy()
var/datum/venue/attending_venue = ai_controller.blackboard[BB_CUSTOMER_ATTENDING_VENUE]
var/obj/structure/holosign/robot_seat/our_seat = ai_controller.blackboard[BB_CUSTOMER_MY_SEAT]
attending_venue.current_visitors -= src
@@ -49,18 +60,18 @@
return ..()
///Robots need robot gibs...!
-/mob/living/simple_animal/robot_customer/spawn_gibs()
+/mob/living/basic/robot_customer/spawn_gibs()
new /obj/effect/gibspawner/robot(drop_location(), src)
-/mob/living/simple_animal/robot_customer/MouseEntered(location, control, params)
+/mob/living/basic/robot_customer/MouseEntered(location, control, params)
. = ..()
hud_to_show_on_hover?.show_to(usr)
-/mob/living/simple_animal/robot_customer/MouseExited(location, control, params)
+/mob/living/basic/robot_customer/MouseExited(location, control, params)
. = ..()
hud_to_show_on_hover?.hide_from(usr)
-/mob/living/simple_animal/robot_customer/update_overlays()
+/mob/living/basic/robot_customer/update_overlays()
. = ..()
var/datum/customer_data/customer_info = ai_controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
@@ -82,21 +93,24 @@
if(bonus_overlays)
. += bonus_overlays
-/mob/living/simple_animal/robot_customer/send_speech(message, message_range, obj/source, bubble_type, list/spans, datum/language/message_language, list/message_mods, forced, tts_message, list/tts_filter)
+/mob/living/basic/robot_customer/send_speech(message, message_range, obj/source, bubble_type, list/spans, datum/language/message_language, list/message_mods, forced, tts_message, list/tts_filter)
. = ..()
var/datum/customer_data/customer_info = ai_controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
playsound(src, customer_info.speech_sound, 30, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE, falloff_distance = 5)
-/mob/living/simple_animal/robot_customer/examine(mob/user)
+/mob/living/basic/robot_customer/examine(mob/user)
. = ..()
- // this should be handled by the ai controller
- if(ai_controller.blackboard[BB_CUSTOMER_CURRENT_ORDER])
- var/datum/venue/attending_venue = ai_controller.blackboard[BB_CUSTOMER_ATTENDING_VENUE]
- var/wanted_item = ai_controller.blackboard[BB_CUSTOMER_CURRENT_ORDER]
- var/order
- if(istype(wanted_item, /datum/custom_order))
- var/datum/custom_order/custom_order = wanted_item
- order = custom_order.get_order_line(attending_venue)
- else
- order = attending_venue.order_food_line(wanted_item)
- . += span_notice("Their order was: \"[order].\"")
+ if(isnull(ai_controller.blackboard[BB_CUSTOMER_CURRENT_ORDER]))
+ return
+
+ var/datum/venue/attending_venue = ai_controller.blackboard[BB_CUSTOMER_ATTENDING_VENUE]
+ var/wanted_item = ai_controller.blackboard[BB_CUSTOMER_CURRENT_ORDER]
+ var/order = "nothing"
+
+ if(istype(wanted_item, /datum/custom_order))
+ var/datum/custom_order/custom_order = wanted_item
+ order = custom_order.get_order_line(attending_venue)
+ else
+ order = attending_venue.order_food_line(wanted_item)
+
+ . += span_notice("Their order was: \"[order].\"")
diff --git a/code/modules/mob/living/basic/space_fauna/snake/snake.dm b/code/modules/mob/living/basic/space_fauna/snake/snake.dm
new file mode 100644
index 00000000000..13b9a327cc5
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/snake/snake.dm
@@ -0,0 +1,86 @@
+
+/mob/living/basic/snake
+ name = "snake"
+ desc = "A slithery snake. These legless reptiles are the bane of mice and adventurers alike."
+ icon_state = "snake"
+ icon_living = "snake"
+ icon_dead = "snake_dead"
+ speak_emote = list("hisses")
+
+ health = 20
+ maxHealth = 20
+ melee_damage_lower = 5
+ melee_damage_upper = 6
+ obj_damage = 0
+ environment_smash = ENVIRONMENT_SMASH_NONE
+
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "shoos"
+ response_disarm_simple = "shoo"
+ response_harm_continuous = "steps on"
+ response_harm_simple = "step on"
+
+ density = FALSE
+ pass_flags = PASSTABLE | PASSMOB
+ mob_size = MOB_SIZE_SMALL
+
+ faction = list(FACTION_HOSTILE)
+ mob_biotypes = MOB_ORGANIC | MOB_BEAST | MOB_REPTILE
+ gold_core_spawnable = FRIENDLY_SPAWN
+
+ ai_controller = /datum/ai_controller/basic_controller/snake
+
+ /// List of stuff (mice) that we want to eat
+ var/static/list/edibles = list(
+ /mob/living/basic/mouse,
+ /obj/item/food/deadmouse,
+ )
+
+/mob/living/basic/snake/Initialize(mapload, special_reagent)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
+
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_SNAKE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+ AddElement(/datum/element/basic_eating, 2, 0, null, edibles)
+ ai_controller.set_blackboard_key(BB_BASIC_FOODS, edibles)
+
+ AddComponent(\
+ /datum/component/tameable,\
+ food_types = list(/obj/item/food/deadmouse),\
+ tame_chance = 75,\
+ bonus_tame_chance = 10,\
+ ) // snakes are really fond of food, especially in the cold darkness of space :)
+
+ if(isnull(special_reagent))
+ special_reagent = /datum/reagent/toxin
+
+ AddElement(/datum/element/venomous, special_reagent, 4)
+
+/mob/living/basic/snake/befriend(mob/living/new_friend)
+ . = ..()
+ visible_message("[src] hisses happily as it seems to bond with [new_friend].")
+
+/// Snakes are primarily concerned with getting those tasty, tasty mice, but aren't afraid to strike back at those who attack them
+/datum/ai_controller/basic_controller/snake
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends/allow_items,
+ )
+
+ ai_traits = STOP_MOVING_WHEN_PULLED
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/find_food,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/random_speech/snake,
+ )
diff --git a/code/modules/mob/living/basic/space_fauna/snake/snake_ai.dm b/code/modules/mob/living/basic/space_fauna/snake/snake_ai.dm
new file mode 100644
index 00000000000..3eb404761c5
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/snake/snake_ai.dm
@@ -0,0 +1,6 @@
+/datum/ai_planning_subtree/random_speech/snake
+ speech_chance = 5
+ speak = list("hsssss","sssSSsssss...","hiisssss")
+ sound = list('sound/creatures/snake_hissing1.ogg', 'sound/creatures/snake_hissing2.ogg')
+ emote_hear = list("hisses.")
+ emote_see = list("slithers around.", "glances.", "stares.")
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 e96482439f9..4bd773f6d0a 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/spider.dm
@@ -13,6 +13,7 @@
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently push aside"
initial_language_holder = /datum/language_holder/spider
+ melee_attack_cooldown = CLICK_CD_MELEE
damage_coeff = list(BRUTE = 1, BURN = 1.25, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
basic_mob_flags = FLAMMABLE_MOB
status_flags = NONE
diff --git a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
index 57b9da542b7..50ec85e342c 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
@@ -30,6 +30,7 @@
/datum/ai_controller/basic_controller/young_spider
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
+ BB_BASIC_MOB_FLEE_DISTANCE = 6,
)
ai_traits = STOP_MOVING_WHEN_PULLED
@@ -46,6 +47,3 @@
/datum/ai_planning_subtree/find_unwebbed_turf,
/datum/ai_planning_subtree/spin_web,
)
-
-/datum/ai_behavior/run_away_from_target/young_spider
- run_distance = 6
diff --git a/code/modules/mob/living/basic/space_fauna/supermatter_spider.dm b/code/modules/mob/living/basic/space_fauna/supermatter_spider.dm
new file mode 100644
index 00000000000..2a3ba326eac
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/supermatter_spider.dm
@@ -0,0 +1,102 @@
+/// A nasty little robotic bug that dusts people on attack. Jeepers. This should be a very, very, very rare spawn.
+/mob/living/basic/supermatter_spider
+ name = "supermatter spider"
+ desc= "A sliver of supermatter placed upon a robotically enhanced pedestal."
+
+ icon = 'icons/mob/simple/smspider.dmi'
+ icon_state = "smspider"
+ icon_living = "smspider"
+ icon_dead = "smspider_dead"
+
+ gender = NEUTER
+ mob_biotypes = MOB_BUG|MOB_ROBOTIC
+ speak_emote = list("vibrates")
+
+
+ attack_verb_continuous = "slices"
+ attack_verb_simple = "slice"
+ attack_sound = 'sound/effects/supermatter.ogg'
+ attack_vis_effect = ATTACK_EFFECT_CLAW
+
+ maxHealth = 10
+ health = 10
+ minimum_survivable_temperature = TCMB
+ maximum_survivable_temperature = T0C + 1250
+ habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ death_message = "falls to the ground, its shard dulling to a miserable grey!"
+
+ faction = list(FACTION_HOSTILE)
+
+ // Gold, supermatter tinted
+ lighting_cutoff_red = 30
+ lighting_cutoff_green = 30
+ lighting_cutoff_blue = 10
+
+ ai_controller = /datum/ai_controller/basic_controller/supermatter_spider
+
+ /// If we successfully dust something, should we die?
+ var/single_use = TRUE
+
+/mob/living/basic/supermatter_spider/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/swarming)
+
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
+
+ RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_attack))
+
+/// Proc that we call on attacking something to dust 'em.
+/mob/living/basic/supermatter_spider/proc/on_attack(mob/living/basic/source, atom/target)
+ SIGNAL_HANDLER
+
+ if(isliving(target))
+ var/mob/living/victim = target
+ victim.investigate_log("has been dusted by [src].", INVESTIGATE_DEATHS)
+ dust_feedback(target)
+ victim.dust()
+ if(single_use)
+ death()
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ if(!isturf(target))
+ dust_feedback(target)
+ qdel(target)
+ if(single_use)
+ death()
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/// Simple proc that plays the supermatter dusting sound and sends a visible message.
+/mob/living/basic/supermatter_spider/proc/dust_feedback(atom/target)
+ playsound(get_turf(src), 'sound/effects/supermatter.ogg', 10, TRUE)
+ visible_message(span_danger("[src] knocks into [target], turning [target.p_them()] to dust in a brilliant flash of light!"))
+
+/mob/living/basic/supermatter_spider/overcharged
+ name = "overcharged supermatter spider"
+ desc = "A sliver of overcharged supermatter placed upon a robotically enhanced pedestal. This one seems especially dangerous."
+ icon_state = "smspideroc"
+ icon_living = "smspideroc"
+ maxHealth = 25
+ health = 25
+ single_use = FALSE
+
+/datum/ai_controller/basic_controller/supermatter_spider
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/random_speech/supermatter_spider,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/datum/ai_planning_subtree/random_speech/supermatter_spider
+ speech_chance = 7
+ emote_hear = list("clinks", "clanks")
+ emote_see = list("vibrates")
diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm
index 0b3645b3e0f..46e175c5323 100644
--- a/code/modules/mob/living/basic/vermin/mouse.dm
+++ b/code/modules/mob/living/basic/vermin/mouse.dm
@@ -51,7 +51,7 @@
held_state = "mouse_[body_color]" // not handled by variety element
AddElement(/datum/element/animal_variety, "mouse", body_color, FALSE)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOUSE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 10)
- AddComponent(/datum/component/squeak, list('sound/effects/mousesqueek.ogg' = 1), 100, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) //as quiet as a mouse or whatever
+ AddComponent(/datum/component/squeak, list('sound/creatures/mousesqueek.ogg' = 1), 100, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) //as quiet as a mouse or whatever
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
@@ -381,6 +381,7 @@
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
+ BB_BASIC_MOB_FLEE_DISTANCE = 3,
)
ai_traits = STOP_MOVING_WHEN_PULLED
@@ -400,9 +401,6 @@
)
/// Don't look for anything to run away from if you are distracted by being adjacent to cheese
-/datum/ai_planning_subtree/flee_target/mouse
- flee_behaviour = /datum/ai_behavior/run_away_from_target/mouse
-
/datum/ai_planning_subtree/flee_target/mouse
/datum/ai_planning_subtree/flee_target/mouse/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
@@ -411,11 +409,6 @@
return // We see some cheese, which is more important than our life
return ..()
-/datum/ai_planning_subtree/flee_target/mouse/select
-
-/datum/ai_behavior/run_away_from_target/mouse
- run_distance = 3 // Mostly exists in small tunnels, don't get ahead of yourself
-
/// AI controller for rats, slightly more complex than mice becuase they attack people
/datum/ai_controller/basic_controller/mouse/rat
blackboard = list(
diff --git a/code/modules/mob/living/brain/MMI.dm b/code/modules/mob/living/brain/MMI.dm
index e182c51f54f..b63ca68d7d7 100644
--- a/code/modules/mob/living/brain/MMI.dm
+++ b/code/modules/mob/living/brain/MMI.dm
@@ -268,7 +268,7 @@
if(user)
to_chat(user, span_warning("\The [src] indicates that there is no mind present!"))
return FALSE
- if(brain.decoy_override)
+ if(brain?.decoy_override)
if(user)
to_chat(user, span_warning("This [name] does not seem to fit!"))
return FALSE
diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm
index 4bd275b5df1..10cff8c19ac 100644
--- a/code/modules/mob/living/brain/brain_item.dm
+++ b/code/modules/mob/living/brain/brain_item.dm
@@ -39,6 +39,21 @@
/// Maximum skillchip slots available. Do not reference this var directly and instead call get_max_skillchip_slots()
var/max_skillchip_slots = 5
+ /// Size modifier for the sprite
+ var/brain_size = 1
+
+/obj/item/organ/internal/brain/Initialize(mapload)
+ . = ..()
+ // Brain size logic
+ transform = transform.Scale(brain_size)
+
+/obj/item/organ/internal/brain/examine()
+ . = ..()
+ if(brain_size < 1)
+ . += span_notice("It is a bit on the smaller side...")
+ if(brain_size > 1)
+ . += span_notice("It is bigger than average...")
+
/obj/item/organ/internal/brain/Insert(mob/living/carbon/brain_owner, special = FALSE, drop_if_replaced = TRUE, no_id_transfer = FALSE)
. = ..()
if(!.)
@@ -406,6 +421,9 @@
. = ..()
organ_owner.gain_trauma(/datum/brain_trauma/special/bluespace_prophet, TRAUMA_RESILIENCE_ABSOLUTE)
+/obj/item/organ/internal/brain/felinid //A bit smaller than average
+ brain_size = 0.8
+
////////////////////////////////////TRAUMAS////////////////////////////////////////
/obj/item/organ/internal/brain/proc/has_trauma_type(brain_trauma_type = /datum/brain_trauma, resilience = TRAUMA_RESILIENCE_ABSOLUTE)
@@ -553,4 +571,4 @@
var/obj/item/organ/internal/brain/old_brain = new_owner.get_organ_slot(ORGAN_SLOT_BRAIN)
old_brain.Remove(new_owner, special = TRUE, no_id_transfer = TRUE)
qdel(old_brain)
- Insert(new_owner, special = TRUE, drop_if_replaced = FALSE, no_id_transfer = TRUE)
+ return Insert(new_owner, special = TRUE, drop_if_replaced = FALSE, no_id_transfer = TRUE)
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 8b97685d0a8..789a459b492 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -57,6 +57,13 @@
return null
+/mob/living/carbon/is_ears_covered()
+ for(var/obj/item/worn_thing as anything in get_equipped_items())
+ if(worn_thing.flags_cover & EARS_COVERED)
+ return worn_thing
+
+ return null
+
/mob/living/carbon/check_projectile_dismemberment(obj/projectile/P, def_zone)
var/obj/item/bodypart/affecting = get_bodypart(def_zone)
if(affecting && !(affecting.bodypart_flags & BODYPART_UNREMOVABLE) && affecting.get_damage() >= (affecting.max_damage - P.dismemberment))
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index 17718190ba5..6a8fc967cb5 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -449,6 +449,13 @@ GLOBAL_LIST_EMPTY(features_by_species)
wearer.hud_used?.update_locked_slots()
worn_items_fit_body_check(wearer)
+/**
+ * Normalizes blood in a human if it is excessive. If it is above BLOOD_VOLUME_NORMAL, this will clamp it to that value. It will not give the human more blodo than they have less than this value.
+ */
+/datum/species/proc/normalize_blood(mob/living/carbon/human/blood_possessing_human)
+ var/normalized_blood_values = max(blood_possessing_human.blood_volume, 0, BLOOD_VOLUME_NORMAL)
+ blood_possessing_human.blood_volume = normalized_blood_values
+
/**
* Proc called when a carbon becomes this species.
*
@@ -459,34 +466,37 @@ GLOBAL_LIST_EMPTY(features_by_species)
* * old_species - The species that the carbon used to be before becoming this race, used for regenerating organs.
* * pref_load - Preferences to be loaded from character setup, loads in preferred mutant things like bodyparts, digilegs, skin color, etc.
*/
-/datum/species/proc/on_species_gain(mob/living/carbon/human/C, datum/species/old_species, pref_load)
+/datum/species/proc/on_species_gain(mob/living/carbon/human/human_who_gained_species, datum/species/old_species, pref_load)
SHOULD_CALL_PARENT(TRUE)
// Drop the items the new species can't wear
- if(C.hud_used)
- C.hud_used.update_locked_slots()
+ if(human_who_gained_species.hud_used)
+ human_who_gained_species.hud_used.update_locked_slots()
- C.mob_biotypes = inherent_biotypes
- C.mob_respiration_type = inherent_respiration_type
- C.butcher_results = knife_butcher_results?.Copy()
+ human_who_gained_species.mob_biotypes = inherent_biotypes
+ human_who_gained_species.mob_respiration_type = inherent_respiration_type
+ human_who_gained_species.butcher_results = knife_butcher_results?.Copy()
if(old_species.type != type)
- replace_body(C, src)
+ replace_body(human_who_gained_species, src)
- regenerate_organs(C, old_species, visual_only = C.visual_only_organs)
+ regenerate_organs(human_who_gained_species, old_species, visual_only = human_who_gained_species.visual_only_organs)
// Drop the items the new species can't wear
- INVOKE_ASYNC(src, PROC_REF(worn_items_fit_body_check), C, TRUE)
+ INVOKE_ASYNC(src, PROC_REF(worn_items_fit_body_check), human_who_gained_species, TRUE)
//Assigns exotic blood type if the species has one
- if(exotic_bloodtype && C.dna.blood_type != exotic_bloodtype)
- C.dna.blood_type = exotic_bloodtype
+ if(exotic_bloodtype && human_who_gained_species.dna.blood_type != exotic_bloodtype)
+ human_who_gained_species.dna.blood_type = exotic_bloodtype
//Otherwise, check if the previous species had an exotic bloodtype and we do not have one and assign a random blood type
//(why the fuck is blood type not tied to a fucking DNA block?)
else if(old_species.exotic_bloodtype && !exotic_bloodtype)
- C.dna.blood_type = random_blood_type()
+ human_who_gained_species.dna.blood_type = random_blood_type()
- if(ishuman(C))
- var/mob/living/carbon/human/human = C
+ //Resets blood if it is excessively high for some reason
+ normalize_blood(human_who_gained_species)
+
+ if(ishuman(human_who_gained_species))
+ var/mob/living/carbon/human/human = human_who_gained_species
for(var/obj/item/organ/external/organ_path as anything in external_organs)
if(!should_external_organ_apply_to(organ_path, human))
continue
@@ -495,25 +505,27 @@ GLOBAL_LIST_EMPTY(features_by_species)
var/obj/item/organ/external/new_organ = SSwardrobe.provide_type(organ_path)
new_organ.Insert(human, special=TRUE, drop_if_replaced=FALSE)
+
+
if(length(inherent_traits))
- C.add_traits(inherent_traits, SPECIES_TRAIT)
+ human_who_gained_species.add_traits(inherent_traits, SPECIES_TRAIT)
if(inherent_factions)
for(var/i in inherent_factions)
- C.faction += i //Using +=/-= for this in case you also gain the faction from a different source.
+ human_who_gained_species.faction += i //Using +=/-= for this in case you also gain the faction from a different source.
// All languages associated with this language holder are added with source [LANGUAGE_SPECIES]
// rather than source [LANGUAGE_ATOM], so we can track what to remove if our species changes again
var/datum/language_holder/gaining_holder = GLOB.prototype_language_holders[species_language_holder]
for(var/language in gaining_holder.understood_languages)
- C.grant_language(language, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES)
+ human_who_gained_species.grant_language(language, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES)
for(var/language in gaining_holder.spoken_languages)
- C.grant_language(language, SPOKEN_LANGUAGE, LANGUAGE_SPECIES)
+ human_who_gained_species.grant_language(language, SPOKEN_LANGUAGE, LANGUAGE_SPECIES)
for(var/language in gaining_holder.blocked_languages)
- C.add_blocked_language(language, LANGUAGE_SPECIES)
- C.regenerate_icons()
+ human_who_gained_species.add_blocked_language(language, LANGUAGE_SPECIES)
+ human_who_gained_species.regenerate_icons()
- SEND_SIGNAL(C, COMSIG_SPECIES_GAIN, src, old_species)
+ SEND_SIGNAL(human_who_gained_species, COMSIG_SPECIES_GAIN, src, old_species)
properly_gained = TRUE
@@ -1484,7 +1496,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
*/
/datum/species/proc/handle_body_temperature(mob/living/carbon/human/humi, seconds_per_tick, times_fired)
//when in a cryo unit we suspend all natural body regulation
- if(istype(humi.loc, /obj/machinery/atmospherics/components/unary/cryo_cell))
+ if(istype(humi.loc, /obj/machinery/cryo_cell))
return
//Only stabilise core temp when alive and not in statis
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 40e4bf095d5..680e5c14f7e 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -372,7 +372,7 @@
var/obj/item/bodypart/the_part = isbodypart(target_zone) ? target_zone : get_bodypart(check_zone(target_zone)) //keep these synced
// Loop through the clothing covering this bodypart and see if there's any thiccmaterials
if(!(injection_flags & INJECT_CHECK_PENETRATE_THICK))
- for(var/obj/item/clothing/iter_clothing in clothingonpart(the_part))
+ for(var/obj/item/clothing/iter_clothing in get_clothing_on_part(the_part))
if(iter_clothing.clothing_flags & THICKMATERIAL)
. = FALSE
break
@@ -786,6 +786,7 @@
VV_DROPDOWN_OPTION(VV_HK_MOD_QUIRKS, "Add/Remove Quirks")
VV_DROPDOWN_OPTION(VV_HK_SET_SPECIES, "Set Species")
VV_DROPDOWN_OPTION(VV_HK_PURRBATION, "Toggle Purrbation")
+ VV_DROPDOWN_OPTION(VV_HK_APPLY_DNA_INFUSION, "Apply DNA Infusion")
/mob/living/carbon/human/vv_do_topic(list/href_list)
. = ..()
@@ -871,6 +872,19 @@
var/msg = span_notice("[key_name_admin(usr)] has removed [key_name(src)] from purrbation.")
message_admins(msg)
admin_ticket_log(src, msg)
+ if(href_list[VV_HK_APPLY_DNA_INFUSION])
+ if(!check_rights(R_SPAWN))
+ return
+ if(!ishuman(src))
+ to_chat(usr, "This can only be done to human species.")
+ return
+ var/result = usr.client.grant_dna_infusion(src)
+ if(result)
+ to_chat(usr, "Successfully applied DNA Infusion [result] to [src].")
+ log_admin("[key_name(usr)] has applied DNA Infusion [result] to [key_name(src)].")
+ else
+ to_chat(usr, "Failed to apply DNA Infusion to [src].")
+ log_admin("[key_name(usr)] failed to apply a DNA Infusion to [key_name(src)].")
/mob/living/carbon/human/limb_attack_self()
var/obj/item/bodypart/arm = hand_bodyparts[active_hand_index]
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index bc951d49bb7..b02aaf03b17 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -6,21 +6,21 @@
if(isbodypart(def_zone))
var/obj/item/bodypart/bp = def_zone
if(bp)
- return checkarmor(def_zone, type)
+ return check_armor(def_zone, type)
var/obj/item/bodypart/affecting = get_bodypart(check_zone(def_zone))
if(affecting)
- return checkarmor(affecting, type)
+ return check_armor(affecting, type)
//If a specific bodypart is targetted, check how that bodypart is protected and return the value.
//If you don't specify a bodypart, it checks ALL your bodyparts for protection, and averages out the values
for(var/X in bodyparts)
var/obj/item/bodypart/BP = X
- armorval += checkarmor(BP, type)
+ armorval += check_armor(BP, type)
organnum++
return (armorval/max(organnum, 1))
-/mob/living/carbon/human/proc/checkarmor(obj/item/bodypart/def_zone, damage_type)
+/mob/living/carbon/human/proc/check_armor(obj/item/bodypart/def_zone, damage_type)
if(!damage_type)
return 0
var/protection = 100
@@ -32,7 +32,7 @@
return 100 - protection
///Get all the clothing on a specific body part
-/mob/living/carbon/human/proc/clothingonpart(obj/item/bodypart/def_zone)
+/mob/living/carbon/human/proc/get_clothing_on_part(obj/item/bodypart/def_zone)
var/list/covering_part = list()
var/list/body_parts = list(head, wear_mask, wear_suit, w_uniform, back, gloves, shoes, belt, s_store, glasses, ears, wear_id, wear_neck) //Everything but pockets. Pockets are l_store and r_store. (if pockets were allowed, putting something armored, gloves or hats for example, would double up on the armor)
for(var/bp in body_parts)
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index c91fc7d6638..2e061d32a6b 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -100,7 +100,7 @@
/// Environment handlers for species
/mob/living/carbon/human/handle_environment(datum/gas_mixture/environment, seconds_per_tick, times_fired)
// If we are in a cryo bed do not process life functions
- if(istype(loc, /obj/machinery/atmospherics/components/unary/cryo_cell))
+ if(istype(loc, /obj/machinery/cryo_cell))
return
dna.species.handle_environment(src, environment, seconds_per_tick, times_fired)
diff --git a/code/modules/mob/living/carbon/human/species_types/abominations.dm b/code/modules/mob/living/carbon/human/species_types/abominations.dm
index b7be74565ac..43ca71311c2 100644
--- a/code/modules/mob/living/carbon/human/species_types/abominations.dm
+++ b/code/modules/mob/living/carbon/human/species_types/abominations.dm
@@ -3,6 +3,7 @@
name = "\improper Tall Boy"
id = SPECIES_TALLBOY
examine_limb_id = SPECIES_HUMAN
+ changesource_flags = MIRROR_BADMIN | WABBAJACK
bodypart_overrides = list(
BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left,
BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right,
@@ -13,8 +14,10 @@
)
/datum/species/monkey/human_legged
+ name = "human-legged monkey"
id = SPECIES_MONKEY_HUMAN_LEGGED
examine_limb_id = SPECIES_MONKEY
+ changesource_flags = MIRROR_BADMIN | WABBAJACK
bodypart_overrides = list(
BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/monkey,
BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right/monkey,
@@ -25,8 +28,10 @@
)
/datum/species/monkey/monkey_freak
+ name = "human-armed monkey"
id = SPECIES_MONKEY_FREAK
examine_limb_id = SPECIES_MONKEY
+ changesource_flags = MIRROR_BADMIN | WABBAJACK
bodypart_overrides = list(
BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left,
BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right,
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 793da47c05b..c8e14a6afd1 100644
--- a/code/modules/mob/living/carbon/human/species_types/felinid.dm
+++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm
@@ -4,6 +4,7 @@
id = SPECIES_FELINE
examine_limb_id = SPECIES_HUMAN
mutant_bodyparts = list("ears" = "Cat", "wings" = "None")
+ mutantbrain = /obj/item/organ/internal/brain/felinid
mutanttongue = /obj/item/organ/internal/tongue/cat
/* SKYRAT EDIT REMOVAL - CUSTOMIZATION
mutantears = /obj/item/organ/internal/ears/cat
@@ -23,8 +24,6 @@
family_heirlooms = list(/obj/item/toy/cattoy)
/// When false, this is a felinid created by mass-purrbation
var/original_felinid = TRUE
- /// Brain size for scaling
- var/brain_size = 0.8
// Prevents felinids from taking toxin damage from carpotoxin
/datum/species/human/felinid/handle_chemical(datum/reagent/chem, mob/living/carbon/human/affected, seconds_per_tick, times_fired)
@@ -47,16 +46,6 @@
ears.Insert(target_human, drop_if_replaced = FALSE)
else
mutantears = /obj/item/organ/internal/ears
- var/obj/item/organ/internal/brain/current_brain = target_human.get_organ_by_type(/obj/item/organ/internal/brain)
- if(current_brain)
- current_brain.transform = current_brain.transform.Scale(brain_size) //smaller brain
- return ..()
-
-/datum/species/human/felinid/on_species_loss(mob/living/carbon/former_feline, datum/species/old_species, pref_load)
- if(iscarbon(former_feline))
- var/obj/item/organ/internal/brain/current_brain = former_feline.get_organ_by_type(/obj/item/organ/internal/brain)
- if(current_brain)
- current_brain.transform = current_brain.transform.Scale(1 / brain_size) //bigger brain
return ..()
/datum/species/human/felinid/randomize_features(mob/living/carbon/human/human_mob)
diff --git a/code/modules/mob/living/carbon/human/species_types/humans.dm b/code/modules/mob/living/carbon/human/species_types/humans.dm
index 2c997274ffb..9db5a3253eb 100644
--- a/code/modules/mob/living/carbon/human/species_types/humans.dm
+++ b/code/modules/mob/living/carbon/human/species_types/humans.dm
@@ -18,7 +18,7 @@
human_mob.skin_tone = random_skin_tone()
/datum/species/human/get_scream_sound(mob/living/carbon/human/human)
- if(human.gender == MALE)
+ if(human.physique == MALE)
if(prob(1))
return 'sound/voice/human/wilhelm_scream.ogg'
return pick(
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index e66d72a2c44..990f27f0707 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -579,6 +579,9 @@
if(stat != DEAD) // If you are dead your body does not stabilize naturally
natural_bodytemperature_stabilization(environment, seconds_per_tick, times_fired)
+ else if(!on_fire && areatemp < bodytemperature) // lowers your dead body temperature to room temperature over time
+ adjust_bodytemperature((areatemp - bodytemperature), use_insulation=FALSE, use_steps=TRUE)
+
if(!on_fire || areatemp > bodytemperature) // If we are not on fire or the area is hotter
adjust_bodytemperature((areatemp - bodytemperature), use_insulation=TRUE, use_steps=TRUE)
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index 53a8d259393..217bc5bacba 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -58,6 +58,7 @@
dust_animation()
spawn_dust(just_ash)
+ ghostize()
QDEL_IN(src,5) // since this is sometimes called in the middle of movement, allow half a second for movement to finish, ghosting to happen and animation to play. Looks much nicer and doesn't cause multiple runtimes.
/mob/living/proc/dust_animation()
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index cb4d4a8241a..532d37ed1b2 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -193,7 +193,7 @@
return
var/mob/living/carbon/human/human_user = user
if(human_user.dna.species.id == SPECIES_HUMAN && !HAS_MIND_TRAIT(human_user, TRAIT_MIMING))
- if(human_user.gender == FEMALE)
+ if(human_user.physique == FEMALE)
return pick('sound/voice/human/gasp_female1.ogg', 'sound/voice/human/gasp_female2.ogg', 'sound/voice/human/gasp_female3.ogg')
else
return pick('sound/voice/human/gasp_male1.ogg', 'sound/voice/human/gasp_male2.ogg')
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index e4fe206cf63..fa96e7c2181 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1479,6 +1479,7 @@
var/picked_animal = pick(
/mob/living/basic/bat,
/mob/living/basic/bear,
+ /mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
@@ -1504,7 +1505,6 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
/mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
/mob/living/simple_animal/hostile/retaliate/goat,
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 17c6d7faa9c..687359a6ff6 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -88,6 +88,11 @@
/mob/living/proc/is_pepper_proof(check_flags = ALL)
return null
+/// Checks if the mob's ears (BOTH EARS, BOWMANS NEED NOT APPLY) are covered by something.
+/// Returns the atom covering the mob's ears, or null if their ears are uncovered.
+/mob/living/proc/is_ears_covered()
+ return null
+
/mob/living/proc/on_hit(obj/projectile/P)
return BULLET_ACT_HIT
@@ -176,6 +181,7 @@
return ..()
/mob/living/fire_act()
+ . = ..()
adjust_fire_stacks(3)
ignite_mob()
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index 766a7ef89a8..e9d306875c9 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -225,3 +225,6 @@
/// What our current gravity state is. Used to avoid duplicate animates and such
var/gravity_state = null
+
+ /// Whether this mob can be mutated into a cybercop via quantum server get_valid_domain_targets(). Specifically dodges megafauna
+ var/can_be_cybercop = TRUE
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index b3f842e6a25..3a09256ee69 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -394,13 +394,29 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
tts_message_to_use = message_raw
var/list/filter = list()
+ var/list/special_filter = list()
+ var/voice_to_use = voice
+ var/use_radio = FALSE
if(length(voice_filter) > 0)
filter += voice_filter
if(length(tts_filter) > 0)
filter += tts_filter.Join(",")
-
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = message_range, pitch = pitch, silicon = tts_silicon_voice_effect)
+ if(ishuman(src))
+ var/mob/living/carbon/human/human_speaker = src
+ if(human_speaker.wear_mask)
+ var/obj/item/clothing/mask/worn_mask = human_speaker.wear_mask
+ if(worn_mask.voice_override)
+ voice_to_use = worn_mask.voice_override
+ if(worn_mask.voice_filter)
+ filter += worn_mask.voice_filter
+ use_radio = worn_mask.use_radio_beeps_tts
+ if(use_radio)
+ special_filter += TTS_FILTER_RADIO
+ if(issilicon(src))
+ special_filter += TTS_FILTER_SILICON
+
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice_to_use, filter.Join(","), listened, message_range = message_range, pitch = pitch, special_filters = special_filter.Join("|"))
var/image/say_popup = image('icons/mob/effects/talk.dmi', src, "[bubble_type][talk_icon_state]", FLY_LAYER)
SET_PLANE_EXPLICIT(say_popup, ABOVE_GAME_PLANE, src)
diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm
index 277b36c274b..f820d3ca1cf 100644
--- a/code/modules/mob/living/silicon/robot/robot_defense.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defense.dm
@@ -439,11 +439,8 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
if(prob(75) && Proj.damage > 0)
spark_system.start()
-/mob/living/silicon/on_hit(obj/projectile/P)
- if(!has_movespeed_modifier(/datum/movespeed_modifier/borg_throw))
- add_movespeed_modifier(/datum/movespeed_modifier/borg_throw)
- addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living/silicon, clear_throw_slowdown)), (P.throwforce / 10) SECONDS)
- return ..()
-
-/mob/living/silicon/proc/clear_throw_slowdown()
- src.remove_movespeed_modifier(/datum/movespeed_modifier/borg_throw)
+/mob/living/silicon/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ . = ..()
+ if (. || AM.throwforce < CYBORG_THROW_SLOWDOWN_THRESHOLD)
+ return
+ apply_status_effect(/datum/status_effect/borg_throw_slow)
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index bf785954fe7..7bd48ab62fb 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -104,8 +104,6 @@
var/reset_access_timer_id
var/ignorelistcleanuptimer = 1 // This ticks up every automated action, at 300 we clean the ignore list
- /// Component which allows ghosts to take over this bot
- var/datum/component/ghost_direct_control/personality_download
/// If true we will allow ghosts to control this mob
var/can_be_possessed = FALSE
/// If true we will offer this
@@ -212,7 +210,6 @@
GLOB.bots_list -= src
QDEL_NULL(paicard)
QDEL_NULL(pa_system)
- QDEL_NULL(personality_download)
QDEL_NULL(internal_radio)
QDEL_NULL(access_card)
QDEL_NULL(path_hud)
@@ -225,14 +222,14 @@
return
can_be_possessed = TRUE
var/can_announce = !mapload && COOLDOWN_FINISHED(src, offer_ghosts_cooldown)
- personality_download = AddComponent(\
- /datum/component/ghost_direct_control,\
- ban_type = ROLE_BOT,\
- poll_candidates = can_announce,\
- poll_ignore_key = POLL_IGNORE_BOTS,\
- assumed_control_message = (bot_cover_flags & BOT_COVER_EMAGGED) ? get_emagged_message() : possessed_message,\
- extra_control_checks = CALLBACK(src, PROC_REF(check_possession)),\
- after_assumed_control = CALLBACK(src, PROC_REF(post_possession)),\
+ AddComponent(
+ /datum/component/ghost_direct_control, \
+ ban_type = ROLE_BOT, \
+ poll_candidates = can_announce, \
+ poll_ignore_key = POLL_IGNORE_BOTS, \
+ assumed_control_message = (bot_cover_flags & BOT_COVER_EMAGGED) ? get_emagged_message() : possessed_message, \
+ extra_control_checks = CALLBACK(src, PROC_REF(check_possession)), \
+ after_assumed_control = CALLBACK(src, PROC_REF(post_possession)), \
)
if (can_announce)
COOLDOWN_START(src, offer_ghosts_cooldown, 30 SECONDS)
@@ -240,7 +237,7 @@
/// Disables this bot from being possessed by ghosts
/mob/living/simple_animal/bot/proc/disable_possession(mob/user)
can_be_possessed = FALSE
- QDEL_NULL(personality_download)
+ qdel(GetComponent(/datum/component/ghost_direct_control))
if (isnull(key))
return
if (user)
diff --git a/code/modules/mob/living/simple_animal/hostile/alien.dm b/code/modules/mob/living/simple_animal/hostile/alien.dm
index b392417aab2..2d17820af0b 100644
--- a/code/modules/mob/living/simple_animal/hostile/alien.dm
+++ b/code/modules/mob/living/simple_animal/hostile/alien.dm
@@ -32,7 +32,7 @@
lighting_cutoff_red = 30
lighting_cutoff_green = 15
lighting_cutoff_blue = 50
- unique_name = 1
+ unique_name = TRUE
gold_core_spawnable = NO_SPAWN
death_sound = 'sound/voice/hiss6.ogg'
death_message = "lets out a waning guttural screech, green blood bubbling from its maw..."
@@ -91,7 +91,7 @@
projectiletype = /obj/projectile/neurotoxin/damaging
projectilesound = 'sound/weapons/pierce.ogg'
status_flags = 0
- unique_name = 0
+ unique_name = FALSE
var/sterile = 1
var/plants_off = 0
var/egg_cooldown = 30
diff --git a/code/modules/mob/living/simple_animal/hostile/blob.dm b/code/modules/mob/living/simple_animal/hostile/blob.dm
deleted file mode 100644
index 7a40a01239c..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/blob.dm
+++ /dev/null
@@ -1,101 +0,0 @@
-//Do not spawn
-/mob/living/simple_animal/hostile/blob
- icon = 'icons/mob/nonhuman-player/blob.dmi'
- pass_flags = PASSBLOB
- faction = list(ROLE_BLOB)
- bubble_icon = "blob"
- speak_emote = null //so we use verb_yell/verb_say/etc
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- maxbodytemp = INFINITY
- unique_name = 1
- combat_mode = TRUE
- // ... Blob colored lighting
- lighting_cutoff_red = 20
- lighting_cutoff_green = 40
- lighting_cutoff_blue = 30
- initial_language_holder = /datum/language_holder/empty
- retreat_distance = null //! retreat doesn't obey pass_flags, so won't work on blob mobs.
- /// Blob camera that controls the blob
- var/mob/camera/blob/overmind = null
- /// If this is related to anything else
- var/independent = FALSE
- /// The factory blob tile that generated this blob minion
- var/obj/structure/blob/special/factory/factory
-
-/mob/living/simple_animal/hostile/blob/update_icons()
- if(overmind)
- add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
- else
- remove_atom_colour(FIXED_COLOUR_PRIORITY)
-
-/mob/living/simple_animal/hostile/blob/Initialize(mapload)
- . = ..()
- if(!independent) //no pulling people deep into the blob
- remove_verb(src, /mob/living/verb/pulled)
- else
- pass_flags &= ~PASSBLOB
-
-/mob/living/simple_animal/hostile/blob/death()
- factory = null
- if(overmind)
- overmind.blob_mobs -= src
- overmind = null
- return ..()
-
-/mob/living/simple_animal/hostile/blob/get_status_tab_items()
- . = ..()
- if(overmind)
- . += "Blobs to Win: [overmind.blobs_legit.len]/[overmind.blobwincount]"
-
-/mob/living/simple_animal/hostile/blob/blob_act(obj/structure/blob/B)
- if(stat != DEAD && health < maxHealth)
- for(var/unused in 1 to 2)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
- adjustHealth(-maxHealth*BLOBMOB_HEALING_MULTIPLIER)
-
-/mob/living/simple_animal/hostile/blob/fire_act(exposed_temperature, exposed_volume)
- ..()
- if(exposed_temperature)
- adjustFireLoss(clamp(0.01 * exposed_temperature, 1, 5))
- else
- adjustFireLoss(5)
-
-/mob/living/simple_animal/hostile/blob/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(istype(mover, /obj/structure/blob))
- return TRUE
-
-///override to use astar/JPS instead of walk_to so we can take our blob pass_flags into account.
-/mob/living/simple_animal/hostile/blob/Goto(target, delay, minimum_distance)
- if(prevent_goto_movement)
- return FALSE
- if(target == src.target)
- approaching_target = TRUE
- else
- approaching_target = FALSE
-
- SSmove_manager.jps_move(moving = src, chasing = target, delay = delay, repath_delay = 2 SECONDS, minimum_distance = minimum_distance, simulated_only = FALSE, skip_first = TRUE, timeout = 5 SECONDS, flags = MOVEMENT_LOOP_IGNORE_GLIDE)
- return TRUE
-
-/mob/living/simple_animal/hostile/blob/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
- for(var/obj/structure/blob/blob in range(1, src))
- return 1
- return ..()
-
-/mob/living/simple_animal/hostile/blob/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null, message_range = 7, datum/saymode/saymode = null)
- if(sanitize)
- message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
- var/spanned_message = say_quote(message)
- var/rendered = "\[Blob Telepathy\] [real_name] [spanned_message]"
- for(var/creature in GLOB.mob_list)
- if(isovermind(creature) || isblobmonster(creature))
- to_chat(creature, rendered)
- if(isobserver(creature))
- var/link = FOLLOW_LINK(creature, src)
- to_chat(creature, "[link] [rendered]")
-
diff --git a/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm b/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm
deleted file mode 100644
index dc1d038795f..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm
+++ /dev/null
@@ -1,109 +0,0 @@
-/mob/living/simple_animal/hostile/blob/blobbernaut
- name = "blobbernaut"
- desc = "A hulking, mobile chunk of blobmass."
- icon_state = "blobbernaut"
- icon_living = "blobbernaut"
- icon_dead = "blobbernaut_dead"
- health = BLOBMOB_BLOBBERNAUT_HEALTH
- maxHealth = BLOBMOB_BLOBBERNAUT_HEALTH
- damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
- melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER
- melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER
- obj_damage = BLOBMOB_BLOBBERNAUT_DMG_OBJ
- attack_verb_continuous = "slams"
- attack_verb_simple = "slam"
- attack_sound = 'sound/effects/blobattack.ogg'
- verb_say = "gurgles"
- verb_ask = "demands"
- verb_exclaim = "roars"
- verb_yell = "bellows"
- force_threshold = 10
- pressure_resistance = 50
- mob_size = MOB_SIZE_LARGE
- hud_type = /datum/hud/living/blobbernaut
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Initialize(mapload)
- . = ..()
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/mind_initialize()
- . = ..()
- if(independent | !overmind)
- return
- var/datum/antagonist/blob_minion/blobbernaut/naut = new(overmind)
- mind.add_antag_datum(naut)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!..())
- return FALSE
- var/list/blobs_in_area = range(2, src)
-
- if(independent)
- return FALSE // strong independent blobbernaut that don't need no blob
-
- var/damagesources = 0
-
- if(!(locate(/obj/structure/blob) in blobs_in_area))
- damagesources++
-
- if(!factory)
- damagesources++
- else
- if(locate(/obj/structure/blob/special/core) in blobs_in_area)
- adjustHealth(-maxHealth*BLOBMOB_BLOBBERNAUT_HEALING_CORE * seconds_per_tick)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
- if(locate(/obj/structure/blob/special/node) in blobs_in_area)
- adjustHealth(-maxHealth*BLOBMOB_BLOBBERNAUT_HEALING_NODE * seconds_per_tick)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
-
- if(!damagesources)
- return FALSE
-
- adjustHealth(maxHealth * BLOBMOB_BLOBBERNAUT_HEALTH_DECAY * damagesources * seconds_per_tick) //take 2.5% of max health as damage when not near the blob or if the naut has no factory, 5% if both
- var/mutable_appearance/healing = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "nautdamage", MOB_LAYER+0.01)
- healing.appearance_flags = RESET_COLOR
-
- if(overmind)
- healing.color = overmind.blobstrain.complementary_color
-
- flick_overlay_view(healing, 0.8 SECONDS)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/AttackingTarget()
- . = ..()
- if(. && isliving(target) && overmind)
- overmind.blobstrain.blobbernaut_attack(target, src)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/update_icons()
- ..()
- if(overmind) //if we have an overmind, we're doing chemical reactions instead of pure damage
- melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
- melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
- attack_verb_continuous = overmind.blobstrain.blobbernaut_message
- else
- melee_damage_lower = initial(melee_damage_lower)
- melee_damage_upper = initial(melee_damage_upper)
- attack_verb_continuous = initial(attack_verb_continuous)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/death(gibbed)
- if(factory)
- factory.blobbernaut = null //remove this blobbernaut from its factory
- factory.max_integrity = initial(factory.max_integrity)
- flick("blobbernaut_death", src)
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/independent
- independent = TRUE
- gold_core_spawnable = HOSTILE_SPAWN
-
-
diff --git a/code/modules/mob/living/simple_animal/hostile/blobspore.dm b/code/modules/mob/living/simple_animal/hostile/blobspore.dm
deleted file mode 100644
index 501afde41b0..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/blobspore.dm
+++ /dev/null
@@ -1,168 +0,0 @@
-/mob/living/simple_animal/hostile/blob/blobspore
- name = "blob spore"
- desc = "A floating, fragile spore."
- icon_state = "blobpod"
- icon_living = "blobpod"
- health_doll_icon = "blobpod"
- health = BLOBMOB_SPORE_HEALTH
- maxHealth = BLOBMOB_SPORE_HEALTH
- verb_say = "psychically pulses"
- verb_ask = "psychically probes"
- verb_exclaim = "psychically yells"
- verb_yell = "psychically screams"
- melee_damage_lower = BLOBMOB_SPORE_DMG_LOWER
- melee_damage_upper = BLOBMOB_SPORE_DMG_UPPER
- environment_smash = ENVIRONMENT_SMASH_NONE
- obj_damage = 0
- attack_verb_continuous = "hits"
- attack_verb_simple = "hit"
- attack_sound = 'sound/weapons/genhit1.ogg'
- del_on_death = TRUE
- death_message = "explodes into a cloud of gas!"
- gold_core_spawnable = NO_SPAWN //gold slime cores should only spawn the independent subtype
- /// Size of cloud produced from a dying spore
- var/death_cloud_size = 1
- /// The attached person
- var/mob/living/carbon/human/corpse
- /// If this is attached to a person
- var/is_zombie = FALSE
- /// Whether or not this is a fragile spore from Distributed Neurons
- var/is_weak = FALSE
-
-/mob/living/simple_animal/hostile/blob/blobspore/Initialize(mapload, obj/structure/blob/special/linked_node)
- . = ..()
- AddElement(/datum/element/simple_flying)
-
- if(!istype(linked_node))
- return
-
- factory = linked_node
- factory.spores += src
- if(linked_node.overmind && istype(linked_node.overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons) && !istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak))
- notify_ghosts("A controllable spore has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Sentient Spore Created")
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/blob/blobspore/mind_initialize()
- . = ..()
- if(independent || !overmind)
- return FALSE
- var/datum/antagonist/blob_minion/blob_zombie/zombie = new(overmind)
- mind.add_antag_datum(zombie)
-
-/mob/living/simple_animal/hostile/blob/blobspore/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!is_zombie && isturf(loc))
- for(var/mob/living/carbon/human/target in view(src,1)) //Only for corpse right next to/on same tile
- if(!is_weak && target.stat == DEAD)
- zombify(target)
- break
- if(factory && !is_valid_z_level(get_turf(src), get_turf(factory)))
- death()
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/attack_ghost(mob/user)
- . = ..()
- if(.)
- return
- humanize_pod(user)
-
-/mob/living/simple_animal/hostile/blob/blobspore/death(gibbed)
- // On death, create a small smoke of harmful gas (s-Acid)
- var/datum/effect_system/fluid_spread/smoke/chem/spores = new
- var/turf/location = get_turf(src)
-
- // Create the reagents to put into the air
- create_reagents(10)
-
- if(overmind?.blobstrain)
- overmind.blobstrain.on_sporedeath(src)
- else
- reagents.add_reagent(/datum/reagent/toxin/spore, 10)
-
- // Attach the smoke spreader and setup/start it.
- spores.attach(location)
- spores.set_up(death_cloud_size, holder = src, location = location, carry = reagents, silent = TRUE)
- spores.start()
- if(factory)
- factory.spore_delay = world.time + factory.spore_cooldown //put the factory on cooldown
-
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/death()
- if(factory)
- factory.spores -= src
- corpse?.forceMove(loc)
- corpse = null
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/update_icons()
- if(overmind)
- add_atom_colour(overmind.blobstrain.complementary_color, FIXED_COLOUR_PRIORITY)
- else
- remove_atom_colour(FIXED_COLOUR_PRIORITY)
- if(!is_zombie)
- return FALSE
-
- copy_overlays(corpse, TRUE)
- var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "blob_head")
- if(overmind)
- blob_head_overlay.color = overmind.blobstrain.complementary_color
- color = initial(color) // looks better.
- add_overlay(blob_head_overlay)
-
-/mob/living/simple_animal/hostile/blob/blobspore/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/blob/blobspore/independent
- gold_core_spawnable = HOSTILE_SPAWN
- independent = TRUE
-
-/mob/living/simple_animal/hostile/blob/blobspore/weak
- name = "fragile blob spore"
- health = 15
- maxHealth = 15
- melee_damage_lower = 1
- melee_damage_upper = 2
- death_cloud_size = 0
- is_weak = TRUE
-
-/** Ghost control a blob zombie */
-/mob/living/simple_animal/hostile/blob/blobspore/proc/humanize_pod(mob/user)
- if((!overmind || istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak) || !istype(overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons)) && !is_zombie)
- return FALSE
- if(key || stat)
- return FALSE
- var/pod_ask = tgui_alert(usr, "Are you bulbous enough?", "Blob Spore", list("Yes", "No"))
- if(pod_ask != "Yes" || QDELETED(src))
- return FALSE
- if(key)
- to_chat(user, span_warning("Someone else already took this spore!"))
- return FALSE
- key = user.key
- log_message("took control of [name].", LOG_GAME)
-
-/** Zombifies a dead mob, turning it into a blob zombie */
-/mob/living/simple_animal/hostile/blob/blobspore/proc/zombify(mob/living/carbon/human/target)
- is_zombie = 1
- if(target.wear_suit)
- maxHealth += target.get_armor_rating(MELEE) // That zombie's got armor, I want armor!
- maxHealth += 40
- health = maxHealth
- name = "blob zombie"
- desc = "A shambling corpse animated by the blob."
- mob_biotypes |= MOB_HUMANOID
- melee_damage_lower += 8
- melee_damage_upper += 11
- obj_damage = 20 // now that it has a corpse to puppet, it can properly attack structures
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- movement_type = GROUND
- death_cloud_size = 0
- icon = target.icon
- icon_state = "zombie"
- target.set_facial_hairstyle("Shaved", update = FALSE)
- target.set_hairstyle("Bald", update = TRUE)
- target.forceMove(src)
- corpse = target
- update_icons()
- visible_message(span_warning("The corpse of [target.name] suddenly rises!"))
- if(!key)
- notify_ghosts("\A [src] has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Blob Zombie Created")
diff --git a/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm b/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
index 124b797d0da..23f7590dc8e 100644
--- a/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
+++ b/code/modules/mob/living/simple_animal/hostile/constructs/constructs.dm
@@ -28,7 +28,7 @@
maxbodytemp = INFINITY
faction = list(FACTION_CULT)
pressure_resistance = 100
- unique_name = 1
+ unique_name = TRUE
AIStatus = AI_OFF //normal constructs don't have AI
loot = list(/obj/item/ectoplasm)
del_on_death = TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm b/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm
index 2c0b9ba983a..1b8cfe9de68 100644
--- a/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm
+++ b/code/modules/mob/living/simple_animal/hostile/heretic_monsters.dm
@@ -198,6 +198,7 @@
for(var/i in 1 to worm_length)
current = new type(drop_location(), FALSE)
+ ADD_TRAIT(current, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
current.icon_state = "armsy_mid"
current.icon_living = "armsy_mid"
current.AIStatus = AI_OFF
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
index 3484c27375d..5bb9f314ed1 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm
@@ -70,7 +70,10 @@
return ..()
/mob/living/simple_animal/hostile/megafauna/death(gibbed, list/force_grant)
- if(health > 0)
+ if(gibbed) // in case they've been force dusted
+ return ..()
+
+ if(health > 0) // prevents instakills
return
var/datum/status_effect/crusher_damage/crusher_dmg = has_status_effect(/datum/status_effect/crusher_damage)
///Whether we killed the megafauna with primarily crusher damage or not
@@ -100,8 +103,8 @@
/mob/living/simple_animal/hostile/megafauna/gib()
if(health > 0)
return
- else
- ..()
+
+ return ..()
/mob/living/simple_animal/hostile/megafauna/singularity_act()
set_health(0)
@@ -110,8 +113,11 @@
/mob/living/simple_animal/hostile/megafauna/dust(just_ash, drop_items, force)
if(!force && health > 0)
return
- else
- ..()
+
+ crusher_loot.Cut()
+ loot.Cut()
+
+ return ..()
/mob/living/simple_animal/hostile/megafauna/AttackingTarget()
if(recovery_time >= world.time)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
index 66d487c8697..84e2d9c049b 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
@@ -272,7 +272,7 @@
if(!isclosedturf(T) && !islava(T))
var/lava_turf = /turf/open/lava/smooth
var/reset_turf = T.type
- T.ChangeTurf(lava_turf, flags = CHANGETURF_INHERIT_AIR)
+ T.TerraformTurf(lava_turf, flags = CHANGETURF_INHERIT_AIR)
addtimer(CALLBACK(T, TYPE_PROC_REF(/turf, ChangeTurf), reset_turf, null, CHANGETURF_INHERIT_AIR), reset_time, TIMER_OVERRIDE|TIMER_UNIQUE)
/obj/effect/temp_visual/drakewall
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
index 745a48c948c..828a78ccfb1 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
@@ -436,7 +436,7 @@ Difficulty: Hard
/mob/living/simple_animal/hostile/megafauna/hierophant/CanAttack(atom/the_target)
. = ..()
- if(istype(the_target, /mob/living/simple_animal/hostile/asteroid/hivelordbrood)) //ignore temporary targets in favor of more permanent targets
+ if(istype(the_target, /mob/living/basic/legion_brood)) //ignore temporary targets in favor of more permanent targets
return FALSE
/mob/living/simple_animal/hostile/megafauna/hierophant/GiveTarget(new_target)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
index 05695daf59a..777cb3b878f 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
@@ -141,10 +141,9 @@
///Attack proc. Spawns a singular legion skull.
/mob/living/simple_animal/hostile/megafauna/legion/proc/create_legion_skull()
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/A = new(loc)
- A.GiveTarget(target)
- A.friends = friends
- A.faction = faction
+ var/mob/living/basic/legion_brood/minion = new(loc)
+ minion.assign_creator(src)
+ minion.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] = target
//CHARGE
@@ -210,7 +209,7 @@
var/mob/living/living_target = target
switch(living_target.stat)
if(UNCONSCIOUS, HARD_CRIT)
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/legion = new(loc)
+ var/mob/living/basic/legion_brood/legion = new(loc)
legion.infest(living_target)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
index e9cf04b9c21..26f3690fef1 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
@@ -267,6 +267,10 @@ Difficulty: Hard
/mob/living/simple_animal/hostile/megafauna/wendigo/death(gibbed, list/force_grant)
if(health > 0)
return
+
+ if(!true_spawn)
+ return ..()
+
var/obj/effect/portal/permanent/one_way/exit = new /obj/effect/portal/permanent/one_way(starting)
exit.id = "wendigo arena exit"
exit.add_atom_colour(COLOR_RED_LIGHT, ADMIN_COLOUR_PRIORITY)
diff --git a/code/modules/mob/living/simple_animal/hostile/mimic.dm b/code/modules/mob/living/simple_animal/hostile/mimic.dm
index 2fec84385b2..d07775b42bd 100644
--- a/code/modules/mob/living/simple_animal/hostile/mimic.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mimic.dm
@@ -113,6 +113,7 @@ GLOBAL_LIST_INIT(animatable_blacklist, list(/obj/structure/table, /obj/structure
/mob/living/simple_animal/hostile/mimic/copy/Initialize(mapload, obj/copy, mob/living/creator, destroy_original = 0, no_googlies = FALSE)
. = ..()
+ ADD_TRAIT(src, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT) // They won't remember their original contents upon ressurection and would just be floating eyes
if (no_googlies)
overlay_googly_eyes = FALSE
CopyObject(copy, creator, destroy_original)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm
index 4cb4d97f38f..bf4d33a10ed 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/herald.dm
@@ -249,7 +249,8 @@
/obj/projectile/herald/teleshot/on_hit(atom/target, blocked = FALSE)
. = ..()
- firer.forceMove(get_turf(src))
+ if(!QDELETED(firer))
+ firer.forceMove(get_turf(src))
//Herald's loot: Cloak of the Prophet
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
index a705e11465b..ec6c843080c 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
@@ -328,10 +328,9 @@
/obj/item/crusher_trophy/legionnaire_spine/on_mark_detonation(mob/living/target, mob/living/user)
if(!prob(bonus_value) || target.stat == DEAD)
return
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/A = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(user.loc)
- A.GiveTarget(target)
- A.friends += user
- A.faction = user.faction.Copy()
+ var/mob/living/basic/legion_brood/minion = new (user.loc)
+ minion.assign_creator(user)
+ minion.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] = target
/obj/item/crusher_trophy/legionnaire_spine/attack_self(mob/user)
if(!isliving(user))
@@ -342,9 +341,9 @@
to_chat(LivingUser, "You need to wait longer to use this again.")
return
LivingUser.visible_message(span_boldwarning("[LivingUser] shakes the [src] and summons a legion skull!"))
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/LegionSkull = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(LivingUser.loc)
- LegionSkull.friends += LivingUser
- LegionSkull.faction = LivingUser.faction.Copy()
+
+ var/mob/living/basic/legion_brood/minion = new (LivingUser.loc)
+ minion.assign_creator(LivingUser)
next_use_time = world.time + 4 SECONDS
#undef LEGIONNAIRE_CHARGE
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
deleted file mode 100644
index 2d92ef88a65..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
+++ /dev/null
@@ -1,333 +0,0 @@
-/mob/living/simple_animal/hostile/asteroid/hivelord
- name = "hivelord"
- desc = "A levitating swarm of tiny creatures which act as a single individual. When threatened or hunting they rapidly replicate additional short-lived bodies."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "hivelord"
- icon_living = "hivelord"
- icon_aggro = "hivelord_alert"
- icon_dead = "hivelord_dead"
- icon_gib = "syndicate_gib"
- mob_biotypes = MOB_ORGANIC
- move_to_delay = 14
- ranged = 1
- vision_range = 5
- aggro_vision_range = 9
- speed = 3
- maxHealth = 75
- health = 75
- harm_intent_damage = 5
- melee_damage_lower = 0
- melee_damage_upper = 0
- attack_verb_continuous = "weakly tackles"
- attack_verb_simple = "weakly tackles"
- speak_emote = list("telepathically cries")
- attack_sound = 'sound/weapons/pierce.ogg'
- throw_message = "passes between the bodies of the"
- ranged_cooldown = 0
- ranged_cooldown_time = 20
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
- retreat_distance = 3
- minimum_distance = 3
- pass_flags = PASSTABLE
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core)
- var/brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood
- var/has_clickbox = TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/Initialize(mapload)
- . = ..()
- if(has_clickbox)
- AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY, dead_state = "hivelord_dead") //they writhe so much.
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/OpenFire(the_target)
- if(world.time < ranged_cooldown)
- return
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/brood = new brood_type(src.loc)
- brood.flags_1 |= (flags_1 & ADMIN_SPAWNED_1)
- brood.GiveTarget(target)
- brood.friends = friends
- brood.faction = faction.Copy()
- ranged_cooldown = world.time + ranged_cooldown_time
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/AttackingTarget()
- OpenFire()
- return TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/death(gibbed)
- mouse_opacity = MOUSE_OPACITY_ICON
- return ..()
-
-//A fragile but rapidly produced creature
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood
- name = "hivelord brood"
- desc = "Short-lived attack form of the hivelord. One isn't much of a threat, but..."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "hivelord_brood"
- icon_living = "hivelord_brood"
- icon_aggro = "hivelord_brood"
- icon_dead = "hivelord_brood"
- icon_gib = "syndicate_gib"
- move_to_delay = 1
- friendly_verb_continuous = "buzzes near"
- friendly_verb_simple = "buzz near"
- vision_range = 10
- speed = 3
- maxHealth = 1
- health = 1
- harm_intent_damage = 5
- melee_damage_lower = 2
- melee_damage_upper = 2
- attack_verb_continuous = "slashes"
- attack_verb_simple = "slash"
- speak_emote = list("telepathically cries")
- attack_sound = 'sound/weapons/pierce.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- throw_message = "falls right through the strange body of the"
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
- pass_flags = PASSTABLE | PASSMOB
- density = FALSE
- del_on_death = 1
- var/clickbox_state = "hivelord"
- var/clickbox_max_scale = INFINITY
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/Initialize(mapload)
- . = ..()
- addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
- AddElement(/datum/element/simple_flying)
- AddComponent(/datum/component/swarming)
- AddComponent(/datum/component/clickbox, icon_state = clickbox_state, max_scale = clickbox_max_scale)
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/death(gibbed)
- if (!gibbed)
- new /obj/effect/temp_visual/hive_spawn_wither(get_turf(src), /* copy_from = */ src)
- return ..()
-
-//Legion
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion
- name = "legion"
- desc = "You can still see what was once a human under the shifting mass of corruption."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "legion"
- icon_living = "legion"
- icon_aggro = "legion"
- icon_dead = "legion"
- icon_gib = "syndicate_gib"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- mouse_opacity = MOUSE_OPACITY_ICON
- obj_damage = 60
- melee_damage_lower = 15
- melee_damage_upper = 15
- attack_verb_continuous = "lashes out at"
- attack_verb_simple = "lash out at"
- speak_emote = list("echoes")
- attack_sound = 'sound/weapons/pierce.ogg'
- throw_message = "bounces harmlessly off of"
- crusher_loot = /obj/item/crusher_trophy/legion_skull
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion)
- brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion
- del_on_death = 1
- stat_attack = HARD_CRIT
- robust_searching = 1
- has_clickbox = FALSE
- var/dwarf_mob = FALSE
- var/snow_legion = FALSE
- var/mob/living/carbon/human/stored_mob
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/content_barfer)
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf
- name = "dwarf legion"
- desc = "You can still see what was once a rather small human under the shifting mass of corruption."
- icon_state = "dwarf_legion"
- icon_living = "dwarf_legion"
- icon_aggro = "dwarf_legion"
- icon_dead = "dwarf_legion"
- maxHealth = 60
- health = 60
- speed = 2 //faster!
- crusher_drop_mod = 20
- dwarf_mob = TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/death(gibbed)
- visible_message(span_warning("The skulls on [src] wail in anger as they flee from their dying host!"))
- if (!isnull(stored_mob))
- stored_mob = null
- return ..()
-
- // We didn't contain a real body so spawn a random one
- var/turf/our_turf = get_turf(src)
- if(our_turf)
- if(from_spawner)
- new /obj/effect/mob_spawn/corpse/human/charredskeleton(our_turf)
- else if(dwarf_mob)
- new /obj/effect/mob_spawn/corpse/human/legioninfested/dwarf(our_turf)
- else if(snow_legion)
- new /obj/effect/mob_spawn/corpse/human/snowlegioninfested(our_turf)
- else
- new /obj/effect/mob_spawn/corpse/human/legioninfested(our_turf)
- return ..()
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril
- from_spawner = TRUE
-
-//Legion skull
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion
- name = "legion"
- desc = "One of many."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "legion_head"
- icon_living = "legion_head"
- icon_aggro = "legion_head"
- icon_dead = "legion_head"
- icon_gib = "syndicate_gib"
- friendly_verb_continuous = "buzzes near"
- friendly_verb_simple = "buzz near"
- vision_range = 10
- maxHealth = 1
- health = 5
- harm_intent_damage = 5
- melee_damage_lower = 12
- melee_damage_upper = 12
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- attack_vis_effect = ATTACK_EFFECT_BITE
- speak_emote = list("echoes")
- attack_sound = 'sound/weapons/pierce.ogg'
- throw_message = "is shrugged off by"
- del_on_death = TRUE
- stat_attack = HARD_CRIT
- robust_searching = 1
- clickbox_state = "sphere"
- clickbox_max_scale = 2
- var/can_infest_dead = FALSE
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- . = ..()
- if(stat == DEAD || !isturf(loc))
- return
- for(var/mob/living/carbon/human/victim in range(src, 1)) //Only for corpse right next to/on same tile
- switch(victim.stat)
- if(UNCONSCIOUS, HARD_CRIT)
- infest(victim)
- return //This will qdelete the legion.
- if(DEAD)
- if(can_infest_dead)
- infest(victim)
- return //This will qdelete the legion.
-
-///Create a legion at the location of a corpse. Exists so that legion subtypes can override it with their own type of legion.
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/proc/make_legion(mob/living/carbon/human/H)
- if(HAS_TRAIT(H, TRAIT_DWARF)) //dwarf legions aren't just fluff!
- return new /mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf(H.loc)
- else
- return new /mob/living/simple_animal/hostile/asteroid/hivelord/legion(H.loc)
-
-///Create a new legion using the supplied human H
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/proc/infest(mob/living/carbon/human/H)
- visible_message(span_warning("[name] burrows into the flesh of [H]!"))
- var/mob/living/simple_animal/hostile/asteroid/hivelord/legion/L = make_legion(H)
- visible_message(span_warning("[L] staggers to [L.p_their()] feet!"))
- H.investigate_log("has been killed by hivelord infestation.", INVESTIGATE_DEATHS)
- H.death()
- H.adjustBruteLoss(1000)
- L.stored_mob = H
- H.forceMove(L)
- qdel(src)
-
-//Advanced Legion is slightly tougher to kill and can raise corpses (revive other legions)
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/advanced
- stat_attack = DEAD
- maxHealth = 120
- health = 120
- brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/advanced
- icon_state = "dwarf_legion"
- icon_living = "dwarf_legion"
- icon_aggro = "dwarf_legion"
- icon_dead = "dwarf_legion"
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/advanced
- stat_attack = DEAD
- can_infest_dead = TRUE
-
-//Legion that spawns Legions
-/mob/living/simple_animal/hostile/big_legion
- name = "legion"
- desc = "One of many."
- icon = 'icons/mob/simple/lavaland/64x64megafauna.dmi'
- icon_state = "legion"
- icon_living = "legion"
- icon_dead = "legion"
- health_doll_icon = "legion"
- health = 450
- maxHealth = 450
- melee_damage_lower = 20
- melee_damage_upper = 20
- anchored = FALSE
- AIStatus = AI_ON
- stop_automated_movement = FALSE
- wander = TRUE
- maxbodytemp = INFINITY
- layer = MOB_LAYER
- del_on_death = TRUE
- sentience_type = SENTIENCE_BOSS
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion = 3, /obj/effect/mob_spawn/corpse/human/legioninfested = 5)
- move_to_delay = 14
- vision_range = 5
- aggro_vision_range = 9
- speed = 3
- faction = list(FACTION_MINING)
- weather_immunities = list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE)
- obj_damage = 30
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- // Purple, but bright cause we're gonna need to spot mobs on lavaland
- lighting_cutoff_red = 35
- lighting_cutoff_green = 20
- lighting_cutoff_blue = 45
-
-
-/mob/living/simple_animal/hostile/big_legion/Initialize(mapload)
- .=..()
- AddComponent(\
- /datum/component/spawner,\
- spawn_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion),\
- spawn_time = 20 SECONDS,\
- max_spawned = 3,\
- spawn_text = "peels itself off from",\
- faction = faction,\
- )
-
-
-// Snow Legion
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow
- name = "snow legion"
- desc = "You can still see what was once a human under the shifting snowy mass, clearly decorated by a clown."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "snowlegion"
- icon_living = "snowlegion"
- icon_aggro = "snowlegion_alive"
- icon_dead = "snowlegion"
- crusher_loot = /obj/item/crusher_trophy/legion_skull
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion)
- brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
- snow_legion = TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow/make_legion(mob/living/carbon/human/H)
- return new /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow(H.loc)
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow/portal
- from_spawner = TRUE
-
-// Snow Legion skull
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow
- name = "snow legion"
- desc = "One of many."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "snowlegion_head"
- icon_living = "snowlegion_head"
- icon_aggro = "snowlegion_head"
- icon_dead = "snowlegion_head"
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/snake.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/snake.dm
deleted file mode 100644
index e128349bc2e..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/snake.dm
+++ /dev/null
@@ -1,76 +0,0 @@
-
-/mob/living/simple_animal/hostile/retaliate/snake
- name = "snake"
- desc = "A slithery snake. These legless reptiles are the bane of mice and adventurers alike."
- icon_state = "snake"
- icon_living = "snake"
- icon_dead = "snake_dead"
- speak_emote = list("hisses")
- health = 20
- maxHealth = 20
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- attack_sound = 'sound/weapons/bite.ogg'
- attack_vis_effect = ATTACK_EFFECT_BITE
- melee_damage_lower = 5
- melee_damage_upper = 6
- response_help_continuous = "pets"
- response_help_simple = "pet"
- response_disarm_continuous = "shoos"
- response_disarm_simple = "shoo"
- response_harm_continuous = "steps on"
- response_harm_simple = "step on"
- faction = list(FACTION_HOSTILE)
- density = FALSE
- pass_flags = PASSTABLE | PASSMOB
- mob_size = MOB_SIZE_SMALL
- mob_biotypes = MOB_ORGANIC|MOB_BEAST|MOB_REPTILE
- gold_core_spawnable = FRIENDLY_SPAWN
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
-
-/mob/living/simple_animal/hostile/retaliate/snake/Initialize(mapload, special_reagent)
- . = ..()
- add_cell_sample()
- ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
- if(!special_reagent)
- special_reagent = /datum/reagent/toxin
- AddElement(/datum/element/venomous, special_reagent, 4)
-
-/mob/living/simple_animal/hostile/retaliate/snake/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_SNAKE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/retaliate/snake/ListTargets(atom/the_target)
- var/atom/target_from = GET_TARGETS_FROM(src)
- . = oview(vision_range, target_from) //get list of things in vision range
- var/list/living_mobs = list()
- var/list/mice = list()
- for (var/HM in .)
- //Yum a tasty mouse
- if(ismouse(HM))
- mice += HM
- if(isliving(HM))
- living_mobs += HM
-
- // if no tasty mice to chase, lets chase any living mob enemies in our vision range
- if(length(mice))
- return mice
-
- var/list/actual_enemies = list()
- for(var/datum/weakref/enemy as anything in enemies)
- var/mob/flesh_and_blood = enemy.resolve()
- if(!flesh_and_blood)
- enemies -= enemy
- continue
- actual_enemies += flesh_and_blood
-
- //Filter living mobs (in range mobs) by those we consider enemies (retaliate behaviour)
- return living_mobs & actual_enemies
-
-/mob/living/simple_animal/hostile/retaliate/snake/AttackingTarget()
- if(ismouse(target))
- visible_message(span_notice("[name] consumes [target] in a single gulp!"), span_notice("You consume [target] in a single gulp!"))
- QDEL_NULL(target)
- adjustBruteLoss(-2)
- else
- return ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/smspider.dm b/code/modules/mob/living/simple_animal/hostile/smspider.dm
deleted file mode 100644
index 150180a11cf..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/smspider.dm
+++ /dev/null
@@ -1,64 +0,0 @@
-/mob/living/simple_animal/hostile/smspider
- name = "supermatter spider"
- desc= "A sliver of supermatter placed upon a robotically enhanced pedestal."
- icon = 'icons/mob/simple/smspider.dmi'
- icon_state = "smspider"
- icon_living = "smspider"
- icon_dead = "smspider_dead"
- gender = NEUTER
- mob_biotypes = MOB_BUG|MOB_ROBOTIC
- turns_per_move = 2
- speak_emote = list("vibrates")
- emote_see = list("vibrates")
- emote_taunt = list("vibrates")
- taunt_chance = 40
- combat_mode = TRUE
- maxHealth = 10
- health = 10
- minbodytemp = 0
- maxbodytemp = 1500
- attack_verb_continuous = "slices"
- attack_verb_simple = "slice"
- attack_sound = 'sound/effects/supermatter.ogg'
- attack_vis_effect = ATTACK_EFFECT_CLAW
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- robust_searching = 1
- faction = list(FACTION_HOSTILE)
- // Gold, supermatter tinted
- lighting_cutoff_red = 30
- lighting_cutoff_green = 30
- lighting_cutoff_blue = 10
- death_message = "falls to the ground, its shard dulling to a miserable grey!"
- footstep_type = FOOTSTEP_MOB_CLAW
- var/overcharged = FALSE // if true, spider will not die if it dusts a limb
-
-/mob/living/simple_animal/hostile/smspider/AttackingTarget()
- . = ..()
- if(isliving(target))
- playsound(get_turf(src), 'sound/effects/supermatter.ogg', 10, TRUE)
- visible_message(span_danger("[src] knocks into [target], turning them to dust in a brilliant flash of light!"))
- var/mob/living/victim = target
- victim.investigate_log("has been dusted by [src].", INVESTIGATE_DEATHS)
- victim.dust()
- if(!overcharged)
- death()
- else if(!isturf(target))
- playsound(get_turf(src), 'sound/effects/supermatter.ogg', 10, TRUE)
- visible_message(span_danger("[src] knocks into [target], turning it to dust in a brilliant flash of light!"))
- qdel(target)
- if(!overcharged)
- death()
- return FALSE
-
-/mob/living/simple_animal/hostile/smspider/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/swarming)
-
-/mob/living/simple_animal/hostile/smspider/overcharged
- name = "overcharged supermatter spider"
- desc = "A sliver of overcharged supermatter placed upon a robotically enhanced pedestal. This one seems especially dangerous."
- icon_state = "smspideroc"
- icon_living = "smspideroc"
- maxHealth = 25
- health = 25
- overcharged = TRUE
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index 74a27e2a014..4966ac76a8e 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -315,7 +315,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
if(parrot_state == PARROT_PERCH)
parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched
- parrot_interest = user
+ set_parrot_interest(user)
parrot_state = PARROT_SWOOP //The parrot just got hit, it WILL move, now to pick a direction..
if(health > 30) //Let's get in there and squawk it up!
@@ -344,7 +344,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched
if(user.melee_damage_upper > 0 && !stat)
- parrot_interest = user
+ set_parrot_interest(user)
parrot_state = PARROT_SWOOP | PARROT_ATTACK //Attack other animals regardless
icon_state = icon_living
@@ -355,7 +355,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
if(parrot_state == PARROT_PERCH)
parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched
- parrot_interest = user
+ set_parrot_interest(user)
parrot_state = PARROT_SWOOP
if(health > 30) //Let's get in there and squawk it up!
parrot_state |= PARROT_ATTACK
@@ -380,7 +380,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
if(parrot_state == PARROT_PERCH)
parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched
- parrot_interest = null
+ set_parrot_interest(null)
parrot_state = PARROT_WANDER | PARROT_FLEE //Been shot and survived! RUN LIKE HELL!
//parrot_been_shot += 5
icon_state = icon_living
@@ -475,7 +475,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
speak = newspeak
//Search for item to steal
- parrot_interest = search_for_item()
+ set_parrot_interest(search_for_item())
if(parrot_interest)
manual_emote("looks in [parrot_interest]'s direction and takes flight.")
parrot_state = PARROT_SWOOP | PARROT_STEAL
@@ -486,7 +486,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
else if(parrot_state == PARROT_WANDER)
//Stop movement, we'll set it later
SSmove_manager.stop_looping(src)
- parrot_interest = null
+ set_parrot_interest(null)
//Wander around aimlessly. This will help keep the loops from searches down
//and possibly move the mob into a new are in view of something they can use
@@ -498,7 +498,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
var/atom/movable/AM = search_for_perch_and_item() //This handles checking through lists so we know it's either a perch or stealable item
if(AM)
if(isitem(AM) || isliving(AM)) //If stealable item
- parrot_interest = AM
+ set_parrot_interest(AM)
manual_emote("turns and flies towards [parrot_interest].")
parrot_state = PARROT_SWOOP | PARROT_STEAL
return
@@ -543,7 +543,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
parrot_interest.forceMove(src)
visible_message(span_notice("[src] grabs [held_item]!"), span_notice("You grab [held_item]!"), span_hear("You hear the sounds of wings flapping furiously."))
- parrot_interest = null
+ set_parrot_interest(null)
parrot_state = PARROT_SWOOP | PARROT_RETURN
return
@@ -591,7 +591,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
//If we're attacking a nothing, an object, a turf or a ghost for some stupid reason, switch to wander
if(!parrot_interest || !isliving(parrot_interest))
- parrot_interest = null
+ set_parrot_interest(null)
parrot_state = PARROT_WANDER
return
@@ -605,7 +605,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
//If the mob we've been chasing/attacking dies or falls into crit, check for loot!
if(L.stat)
- parrot_interest = null
+ set_parrot_interest(null)
if(!held_item)
held_item = steal_from_ground()
if(!held_item)
@@ -629,7 +629,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
//-----STATE MISHAP
else //This should not happen. If it does lets reset everything and try again
SSmove_manager.stop_looping(src)
- parrot_interest = null
+ set_parrot_interest(null)
parrot_perch = null
drop_held_item()
parrot_state = PARROT_WANDER
@@ -639,6 +639,17 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
* Procs
*/
+/mob/living/simple_animal/parrot/proc/set_parrot_interest(atom/movable/shiny)
+ if(parrot_interest)
+ UnregisterSignal(parrot_interest, COMSIG_QDELETING)
+ parrot_interest = shiny
+ if(parrot_interest)
+ RegisterSignal(parrot_interest, COMSIG_QDELETING, PROC_REF(shiny_deleted))
+
+/mob/living/simple_animal/parrot/proc/shiny_deleted(datum/source)
+ SIGNAL_HANDLER
+ set_parrot_interest(null)
+
/mob/living/simple_animal/parrot/proc/isStuck()
//Check to see if the parrot is stuck due to things like windows or doors or windowdoors
if(parrot_lastmove)
@@ -1038,7 +1049,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
/mob/living/simple_animal/parrot/poly/ghost/handle_automated_movement()
if(isliving(parrot_interest))
if(!ishuman(parrot_interest))
- parrot_interest = null
+ set_parrot_interest(null)
else if(parrot_state == (PARROT_SWOOP | PARROT_ATTACK) && Adjacent(parrot_interest))
SSmove_manager.move_to(src, parrot_interest, 0, parrot_speed)
Possess(parrot_interest)
@@ -1051,7 +1062,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
P.parrot = src
forceMove(H)
H.ForceContractDisease(P, FALSE)
- parrot_interest = null
+ set_parrot_interest(null)
H.visible_message(span_danger("[src] dive bombs into [H]'s chest and vanishes!"), span_userdanger("[src] dive bombs into your chest, vanishing! This can't be good!"))
#undef PARROT_PERCH
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 041c76fac42..3b16ab685eb 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -449,19 +449,21 @@
drop_loot()
if(dextrous)
drop_all_held_items()
+
if(del_on_death)
..()
//Prevent infinite loops if the mob Destroy() is overridden in such
//a manner as to cause a call to death() again //Pain
del_on_death = FALSE
qdel(src)
- else
- health = 0
- icon_state = icon_dead
- if(flip_on_death)
- transform = transform.Turn(180)
- ADD_TRAIT(src, TRAIT_UNDENSE, BASIC_MOB_DEATH_TRAIT)
- ..()
+ return
+
+ health = 0
+ icon_state = icon_dead
+ if(flip_on_death)
+ transform = transform.Turn(180)
+ ADD_TRAIT(src, TRAIT_UNDENSE, BASIC_MOB_DEATH_TRAIT)
+ return ..()
/mob/living/simple_animal/proc/CanAttack(atom/the_target)
if(!isatom(the_target)) // no
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 2fdec746899..cebb747f6ba 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1405,8 +1405,11 @@
. = ..()
VV_DROPDOWN_OPTION("", "---------")
VV_DROPDOWN_OPTION(VV_HK_GIB, "Gib")
+ VV_DROPDOWN_OPTION(VV_HK_REMOVE_SPELL, "Remove Spell")
VV_DROPDOWN_OPTION(VV_HK_GIVE_SPELL, "Give Spell")
VV_DROPDOWN_OPTION(VV_HK_REMOVE_SPELL, "Remove Spell")
+ VV_DROPDOWN_OPTION(VV_HK_GIVE_MOB_ACTION, "Give Mob Ability")
+ VV_DROPDOWN_OPTION(VV_HK_REMOVE_MOB_ACTION, "Remove Mob Ability")
VV_DROPDOWN_OPTION(VV_HK_GIVE_DISEASE, "Give Disease")
VV_DROPDOWN_OPTION(VV_HK_GODMODE, "Toggle Godmode")
VV_DROPDOWN_OPTION(VV_HK_DROP_ALL, "Drop Everything")
@@ -1433,6 +1436,14 @@
if(!check_rights(R_ADMIN))
return
usr.client.cmd_admin_godmode(src)
+ if(href_list[VV_HK_GIVE_MOB_ACTION])
+ if(!check_rights(NONE))
+ return
+ usr.client.give_mob_action(src)
+ if(href_list[VV_HK_REMOVE_MOB_ACTION])
+ if(!check_rights(NONE))
+ return
+ usr.client.remove_mob_action(src)
if(href_list[VV_HK_GIVE_SPELL])
if(!check_rights(NONE))
return
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 3e561c75341..7df256d1873 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -207,20 +207,38 @@
return ""
// moved out of admins.dm because things other than admin procs were calling this.
-/// Returns TRUE if the game has started and we're either an AI with a 0th law, or we're someone with a special role/antag datum
-/proc/is_special_character(mob/M)
+/**
+ * Returns TRUE if the game has started and we're either an AI with a 0th law, or we're someone with a special role/antag datum
+ * If allow_fake_antags is set to FALSE, Valentines, ERTs, and any such roles with FLAG_FAKE_ANTAG won't pass.
+*/
+/proc/is_special_character(mob/M, allow_fake_antags = FALSE)
if(!SSticker.HasRoundStarted())
return FALSE
if(!istype(M))
return FALSE
if(iscyborg(M)) //as a borg you're now beholden to your laws rather than greentext
return FALSE
+
+
+ // Returns TRUE if AI has a zeroth law *and* either has a special role *or* an antag datum.
if(isAI(M))
var/mob/living/silicon/ai/A = M
return (A.laws?.zeroth && (A.mind?.special_role || !isnull(M.mind?.antag_datums)))
- if(M.mind?.special_role || !isnull(M.mind?.antag_datums)) //they have an antag datum!
+
+ if(M.mind?.special_role)
return TRUE
- return FALSE
+
+ // Turns 'faker' to TRUE if the antag datum is fake. If it's not fake, returns TRUE directly.
+ var/faker = FALSE
+ for(var/datum/antagonist/antag_datum as anything in M.mind?.antag_datums)
+ if((antag_datum.antag_flags & FLAG_FAKE_ANTAG))
+ faker = TRUE
+ else
+ return TRUE
+
+ // If 'faker' was assigned TRUE in the above loop and the argument 'allow_fake_antags' is set to TRUE, this passes.
+ // Else, return FALSE.
+ return (faker && allow_fake_antags)
/**
* Fancy notifications for ghosts
@@ -521,3 +539,33 @@
"[key_name(src)] manually changed selected zone",
data,
)
+
+/**
+ * Returns an associative list of the logs of a certain amount of lines spoken recently by this mob
+ * copy_amount - number of lines to return
+ * line_chance - chance to return a line, if you don't want just the most recent x lines
+ */
+/mob/proc/copy_recent_speech(copy_amount = LING_ABSORB_RECENT_SPEECH, line_chance = 100)
+ var/list/recent_speech = list()
+ var/list/say_log = list()
+ var/log_source = logging
+ for(var/log_type in log_source)
+ var/nlog_type = text2num(log_type)
+ if(nlog_type & LOG_SAY)
+ var/list/reversed = log_source[log_type]
+ if(islist(reversed))
+ say_log = reverse_range(reversed.Copy())
+ break
+
+ for(var/spoken_memory in say_log)
+ if(recent_speech.len >= copy_amount)
+ break
+ if(!prob(line_chance))
+ continue
+ recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
+
+ var/list/raw_lines = list()
+ for (var/key as anything in recent_speech)
+ raw_lines += recent_speech[key]
+
+ return raw_lines
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index e9b0f532da5..03712663249 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -132,7 +132,7 @@
//Basically an optional override for our glide size
//Sometimes you want to look like you're moving with a delay you don't actually have yet
visual_delay = 0
- var/old_dir = dir
+ var/old_dir = mob.dir
. = ..()
diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm
index 9bc6a5b22db..fe901b3ad9e 100644
--- a/code/modules/mob/mob_transformation_simple.dm
+++ b/code/modules/mob/mob_transformation_simple.dm
@@ -66,7 +66,7 @@
else
desired_mob.key = key
- SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE)
+ SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE, desired_mob)
if(delete_old_mob)
QDEL_IN(src, 1)
return desired_mob
diff --git a/code/modules/mob_spawn/corpses/job_corpses.dm b/code/modules/mob_spawn/corpses/job_corpses.dm
index c8dd458f42d..3893f3e1ba6 100644
--- a/code/modules/mob_spawn/corpses/job_corpses.dm
+++ b/code/modules/mob_spawn/corpses/job_corpses.dm
@@ -83,4 +83,4 @@
name = JOB_ROBOTICIST
outfit = /datum/outfit/job/roboticist
icon_state = "corpseroboticist"
-
+
diff --git a/code/modules/mob_spawn/corpses/mining_corpses.dm b/code/modules/mob_spawn/corpses/mining_corpses.dm
index c0ea4b6af42..8b7ad474b16 100644
--- a/code/modules/mob_spawn/corpses/mining_corpses.dm
+++ b/code/modules/mob_spawn/corpses/mining_corpses.dm
@@ -16,18 +16,21 @@
//Legion infested mobs
-//dwarf type which spawns dwarfy versions
-/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf
-
-/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf/special(mob/living/carbon/human/spawned_human)
- . = ..()
- spawned_human.dna.add_mutation(/datum/mutation/human/dwarfism)
-
-//main type, rolls a pool of legion victims
+/// Mob spawner used by Legion to spawn costumed bodies
/obj/effect/mob_spawn/corpse/human/legioninfested
brute_damage = 1000
/obj/effect/mob_spawn/corpse/human/legioninfested/Initialize(mapload)
+ outfit = select_outfit()
+ return ..()
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/special(mob/living/carbon/human/spawned_human)
+ . = ..()
+ var/obj/item/organ/internal/legion_tumour/cancer = new()
+ cancer.Insert(spawned_human, special = TRUE, drop_if_replaced = FALSE)
+
+/// Returns the outfit worn by our corpse
+/obj/effect/mob_spawn/corpse/human/legioninfested/proc/select_outfit()
var/corpse_theme = pick_weight(list(
"Miner" = 64,
"Clown" = 5,
@@ -40,26 +43,36 @@
"Shadow",
)) = 4,
))
+
switch(corpse_theme)
if("Miner")
- outfit = /datum/outfit/consumed_miner
+ return /datum/outfit/consumed_miner
if("Ashwalker")
- outfit = /datum/outfit/consumed_ashwalker
+ return /datum/outfit/consumed_ashwalker
if("Golem")
- outfit = /datum/outfit/consumed_golem
+ return /datum/outfit/consumed_golem
if("Clown")
- outfit = /datum/outfit/consumed_clown
+ return /datum/outfit/consumed_clown
if("Cultist")
- outfit = /datum/outfit/consumed_cultist
+ return /datum/outfit/consumed_cultist
if("Dame")
- outfit = /datum/outfit/consumed_dame
+ return /datum/outfit/consumed_dame
if("Operative")
- outfit = /datum/outfit/syndicatecommandocorpse/lessenedgear
+ return /datum/outfit/syndicatecommandocorpse/lessenedgear
if("Shadow")
- outfit = /datum/outfit/consumed_shadowperson
+ return /datum/outfit/consumed_shadowperson
+
+/// Corpse spawner used by dwarf legions to make small corpses
+/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf/special(mob/living/carbon/human/spawned_human)
. = ..()
+ spawned_human.dna.add_mutation(/datum/mutation/human/dwarfism)
+
+/// Corpse spawner used by snow legions with alternate costumes
+/obj/effect/mob_spawn/corpse/human/legioninfested/snow
-/obj/effect/mob_spawn/corpse/human/snowlegioninfested/Initialize(mapload)
+/obj/effect/mob_spawn/corpse/human/legioninfested/snow/select_outfit()
var/corpse_theme = pick_weight(list(
"Miner" = 64,
"Clown" = 5,
@@ -72,24 +85,49 @@
"Shadow",
)) = 4,
))
+
switch(corpse_theme)
if("Miner")
- outfit = /datum/outfit/consumed_miner
+ return /datum/outfit/consumed_miner
if("Settler")
- outfit = /datum/outfit/consumed_ice_settler
+ return /datum/outfit/consumed_ice_settler
if("Heremoth")
- outfit = /datum/outfit/consumed_heremoth
+ return /datum/outfit/consumed_heremoth
if("Clown")
- outfit = /datum/outfit/consumed_clown
+ return /datum/outfit/consumed_clown
if("Cultist")
- outfit = /datum/outfit/consumed_cultist
+ return /datum/outfit/consumed_cultist
if("Golem")
- outfit = /datum/outfit/consumed_golem
+ return /datum/outfit/consumed_golem
if("Operative")
- outfit = /datum/outfit/syndicatecommandocorpse/lessenedgear
+ return /datum/outfit/syndicatecommandocorpse/lessenedgear
if("Shadow")
- outfit = /datum/outfit/consumed_shadowperson
+ return /datum/outfit/consumed_shadowperson
+
+/// Creates a dead legion-infested skeleton
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton
+ name = "legion-infested skeleton"
+ mob_name = "skeleton"
+ mob_species = /datum/species/skeleton
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/select_outfit()
+ return null
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/special(mob/living/carbon/human/spawned_human)
. = ..()
+ spawned_human.gender = NEUTER
+
+/// Creates a dead and burned legion-infested skeleton
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/charred
+ name = "charred legion-infested skeleton"
+ mob_name = "charred skeleton"
+ brute_damage = 0
+ burn_damage = 1000
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/charred/special(mob/living/carbon/human/spawned_human)
+ . = ..()
+ spawned_human.color = "#454545"
+
/datum/outfit/consumed_miner
name = "Legion-Consumed Miner"
diff --git a/code/modules/mob_spawn/ghost_roles/drone_roles.dm b/code/modules/mob_spawn/ghost_roles/drone_roles.dm
new file mode 100644
index 00000000000..b8a31a16b13
--- /dev/null
+++ b/code/modules/mob_spawn/ghost_roles/drone_roles.dm
@@ -0,0 +1,6 @@
+/obj/effect/mob_spawn/ghost_role/drone/name_mob(mob/living/spawned_mob, forced_name)
+ if(!forced_name)
+ var/designation = pick(GLOB.posibrain_names)
+ forced_name = "Drone ([designation]-[rand(100, 999)])"
+
+ return ..()
diff --git a/code/modules/mob_spawn/ghost_roles/mining_roles.dm b/code/modules/mob_spawn/ghost_roles/mining_roles.dm
index de461380b54..62861c6f303 100644
--- a/code/modules/mob_spawn/ghost_roles/mining_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/mining_roles.dm
@@ -236,7 +236,7 @@
return ..()
/obj/effect/mob_spawn/ghost_role/human/ash_walker/allow_spawn(mob/user, silent = FALSE)
- if(!(user.key in team.players_spawned))//one per person unless you get a bonus spawn
+ if(!(user.ckey in team.players_spawned))//one per person unless you get a bonus spawn SKYRAT EDIT: Original: if(!(user.key in team.players_spawned))
return TRUE
if(!silent)
to_chat(user, span_warning("You have exhausted your usefulness to the Necropolis."))
@@ -254,7 +254,7 @@
spawned_human.mind.add_antag_datum(/datum/antagonist/ashwalker, team)
spawned_human.remove_language(/datum/language/common)
- team.players_spawned += (spawned_human.key)
+ team.players_spawned += (spawned_human.ckey) //SKYRAT EDIT: Original: team.players_spawned += (spawned_human.key)
eggshell.egg = null
QDEL_NULL(eggshell)
diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm
index a853d48c0ec..d24850b8732 100644
--- a/code/modules/mob_spawn/mob_spawn.dm
+++ b/code/modules/mob_spawn/mob_spawn.dm
@@ -32,6 +32,8 @@
var/facial_haircolor
///sets a human's skin tone
var/skin_tone
+ /// Weakref to the mob this spawner created - just if you needed to do something with it.
+ var/datum/weakref/spawned_mob_ref
/obj/effect/mob_spawn/Initialize(mapload)
. = ..()
@@ -44,6 +46,7 @@
name_mob(spawned_mob, newname)
special(spawned_mob, mob_possessor)
equip(spawned_mob)
+ spawned_mob_ref = WEAKREF(spawned_mob)
return spawned_mob
/obj/effect/mob_spawn/proc/special(mob/living/spawned_mob)
@@ -250,6 +253,7 @@
if(isnull(created)) // If we explicitly return FALSE instead of just not returning a mob, we don't want to spam the admins
CRASH("An instance of [type] didn't return anything when creating a mob, this might be broken!")
+ SEND_SIGNAL(src, COMSIG_GHOSTROLE_SPAWNED, created)
check_uses() // Now we check if the spawner should delete itself or not
return created
diff --git a/code/modules/mod/mod_link.dm b/code/modules/mod/mod_link.dm
index 8a3340fad3e..12ce7fa4827 100644
--- a/code/modules/mod/mod_link.dm
+++ b/code/modules/mod/mod_link.dm
@@ -173,6 +173,10 @@
/obj/item/clothing/neck/link_scryer/examine(mob/user)
. = ..()
+ // SKYRAT EDIT NIFSOFT SCRYERS - START
+ if(custom_examine_controls)
+ return
+ // SKYRAT EDIT NIFSOFT SCRYERS - END
if(cell)
. += span_notice("The battery charge reads [cell.percent()]%. Right-click with an empty hand to remove it.")
else
diff --git a/code/modules/mod/mod_types.dm b/code/modules/mod/mod_types.dm
index f11c6b9b4b9..f8daa9bab54 100644
--- a/code/modules/mod/mod_types.dm
+++ b/code/modules/mod/mod_types.dm
@@ -260,6 +260,12 @@
/obj/item/mod/module/jump_jet,
)
+/obj/item/mod/control/pre_equipped/nuclear/no_jetpack
+
+/obj/item/mod/control/pre_equipped/nuclear/no_jetpack/Initialize(mapload, new_theme, new_skin, new_core)
+ applied_modules -= list(/obj/item/mod/module/jetpack/advanced, /obj/item/mod/module/jump_jet)
+ return ..()
+
/obj/item/mod/control/pre_equipped/nuclear/plasmaman
/obj/item/mod/control/pre_equipped/nuclear/plasmaman/Initialize(mapload, new_theme, new_skin, new_core)
diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm
index 21d2b8352fb..6fe91c9575b 100644
--- a/code/modules/modular_computers/computers/item/computer.dm
+++ b/code/modules/modular_computers/computers/item/computer.dm
@@ -150,6 +150,7 @@
close_all_programs()
//Some components will actually try and interact with this, so let's do it later
QDEL_NULL(soundloop)
+ looping_sound = FALSE // Necessary to stop a possible runtime trying to call soundloop.stop() when soundloop has been qdel'd
QDEL_LIST(stored_files)
if(istype(inserted_disk))
@@ -802,14 +803,8 @@
/obj/item/modular_computer/wrench_act_secondary(mob/living/user, obj/item/tool)
. = ..()
tool.play_tool_sound(src, user, 20, volume=20)
- internal_cell?.forceMove(drop_location())
- computer_id_slot?.forceMove(drop_location())
- inserted_disk?.forceMove(drop_location())
- remove_pai()
- new /obj/item/stack/sheet/iron(get_turf(loc), steel_sheet_cost)
+ deconstruct(TRUE)
user.balloon_alert(user, "disassembled")
- relay_qdel()
- qdel(src)
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/item/modular_computer/welder_act(mob/living/user, obj/item/tool)
@@ -830,15 +825,26 @@
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/item/modular_computer/deconstruct(disassembled = TRUE)
- break_apart()
- return ..()
-
-/obj/item/modular_computer/proc/break_apart()
+ remove_pai()
+ eject_aicard()
if(!(flags_1 & NODECONSTRUCT_1))
- physical.visible_message(span_notice("\The [src] breaks apart!"))
- var/turf/newloc = get_turf(src)
- new /obj/item/stack/sheet/iron(newloc, round(steel_sheet_cost / 2))
+ if (disassembled)
+ internal_cell?.forceMove(drop_location())
+ computer_id_slot?.forceMove(drop_location())
+ inserted_disk?.forceMove(drop_location())
+ new /obj/item/stack/sheet/iron(drop_location(), steel_sheet_cost)
+ else
+ physical.visible_message(span_notice("\The [src] breaks apart!"))
+ new /obj/item/stack/sheet/iron(drop_location(), round(steel_sheet_cost * 0.5))
relay_qdel()
+ return ..()
+
+// Ejects the inserted intellicard, if one exists. Used when the computer is deconstructed.
+/obj/item/modular_computer/proc/eject_aicard()
+ var/datum/computer_file/program/ai_restorer/program = locate() in stored_files
+ if (program)
+ return program.try_eject(forced = TRUE)
+ return FALSE
// Used by processor to relay qdel() to machinery type.
/obj/item/modular_computer/proc/relay_qdel()
diff --git a/code/modules/modular_computers/computers/item/role_tablet_presets.dm b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
index b96f61d44b4..e2f1b354eda 100644
--- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm
+++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
@@ -270,6 +270,14 @@
/datum/computer_file/program/skill_tracker,
)
+/obj/item/modular_computer/pda/bitrunner
+ name = "bit runner PDA"
+ greyscale_colors = "#D6B328#6BC906"
+ starting_programs = list(
+ /datum/computer_file/program/arcade,
+ /datum/computer_file/program/skill_tracker,
+ )
+
/**
* Service
*/
diff --git a/code/modules/modular_computers/computers/machinery/modular_computer.dm b/code/modules/modular_computers/computers/machinery/modular_computer.dm
index 33f4bc29a16..293cdd9c5f7 100644
--- a/code/modules/modular_computers/computers/machinery/modular_computer.dm
+++ b/code/modules/modular_computers/computers/machinery/modular_computer.dm
@@ -125,9 +125,9 @@
return cpu.screwdriver_act(user, tool)
return ..()
-/obj/machinery/modular_computer/wrench_act(mob/user, obj/item/tool)
+/obj/machinery/modular_computer/wrench_act_secondary(mob/user, obj/item/tool)
if(cpu)
- return cpu.wrench_act(user, tool)
+ return cpu.wrench_act_secondary(user, tool)
return ..()
/obj/machinery/modular_computer/welder_act(mob/user, obj/item/tool)
diff --git a/code/modules/modular_computers/file_system/programs/budgetordering.dm b/code/modules/modular_computers/file_system/programs/budgetordering.dm
index c261f3a3d43..d2133697194 100644
--- a/code/modules/modular_computers/file_system/programs/budgetordering.dm
+++ b/code/modules/modular_computers/file_system/programs/budgetordering.dm
@@ -118,9 +118,11 @@
if(SSshuttle.supply_blocked)
message = blockade_warning
data["message"] = message
+ var/list/amount_by_name = list()
var/cart_list = list()
for(var/datum/supply_order/order in SSshuttle.shopping_list)
if(cart_list[order.pack.name])
+ amount_by_name[order.pack.name] += 1
cart_list[order.pack.name][1]["amount"]++
cart_list[order.pack.name][1]["cost"] += order.get_final_cost()
if(order.department_destination)
@@ -145,15 +147,23 @@
data["cart"] += cart_list[item_id]
data["requests"] = list()
- for(var/datum/supply_order/SO in SSshuttle.request_list)
+ for(var/datum/supply_order/order in SSshuttle.request_list)
+ var/datum/supply_pack/pack = order.pack
+ amount_by_name[pack.name] += 1
data["requests"] += list(list(
- "object" = SO.pack.name,
- "cost" = SO.pack.get_cost(),
- "orderer" = SO.orderer,
- "reason" = SO.reason,
- "id" = SO.id
+ "object" = pack.name,
+ "cost" = pack.get_cost(),
+ "orderer" = order.orderer,
+ "reason" = order.reason,
+ "id" = order.id
))
+ data["amount_by_name"] = amount_by_name
+
+ return data
+/datum/computer_file/program/budgetorders/ui_static_data(mob/user)
+ var/list/data = list()
+ data["max_order"] = CARGO_MAX_ORDER
return data
/datum/computer_file/program/budgetorders/ui_act(action, params, datum/tgui/ui)
@@ -233,15 +243,20 @@
return
if(pack.goody && !self_paid)
- playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ playsound(computer, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
computer.say("ERROR: Small crates may only be purchased by private accounts.")
return
+ if(SSshuttle.supply.get_order_count(pack) == OVER_ORDER_LIMIT)
+ playsound(computer, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ computer.say("ERROR: No more then [CARGO_MAX_ORDER] of any pack may be ordered at once")
+ return
+
if(!requestonly && !self_paid && ishuman(usr) && !account)
var/obj/item/card/id/id_card = computer.computer_id_slot?.GetID()
account = SSeconomy.get_dep_account(id_card?.registered_account?.account_job.paycheck_department)
- var/turf/T = get_turf(src)
+ var/turf/T = get_turf(computer)
var/datum/supply_order/SO = new(pack, name, rank, ckey, reason, account)
SO.generateRequisition(T)
if((requestonly && !self_paid) || !(computer.computer_id_slot?.GetID()))
diff --git a/code/modules/modular_computers/file_system/programs/crewmanifest.dm b/code/modules/modular_computers/file_system/programs/crewmanifest.dm
index 3215f62eef8..cdd05d6b4c6 100644
--- a/code/modules/modular_computers/file_system/programs/crewmanifest.dm
+++ b/code/modules/modular_computers/file_system/programs/crewmanifest.dm
@@ -4,7 +4,7 @@
category = PROGRAM_CATEGORY_CREW
program_icon_state = "id"
extended_desc = "Program for viewing and printing the current crew manifest"
- transfer_access = list(ACCESS_COMMAND)
+ transfer_access = list(ACCESS_SECURITY, ACCESS_COMMAND)
requires_ntnet = TRUE
size = 4
tgui_id = "NtosCrewManifest"
diff --git a/code/modules/modular_computers/file_system/programs/records.dm b/code/modules/modular_computers/file_system/programs/records.dm
index 960702d608c..9b5617364c0 100644
--- a/code/modules/modular_computers/file_system/programs/records.dm
+++ b/code/modules/modular_computers/file_system/programs/records.dm
@@ -45,6 +45,7 @@
current_record["rank"] = person.rank
current_record["species"] = person.species
current_record["wanted"] = person.wanted_status
+ current_record["voice"] = person.voice
all_records += list(current_record)
if("medical")
diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm
index 59b514a3d57..e5f29323223 100644
--- a/code/modules/movespeed/modifiers/mobs.dm
+++ b/code/modules/movespeed/modifiers/mobs.dm
@@ -84,9 +84,6 @@
/datum/movespeed_modifier/shove
multiplicative_slowdown = SHOVE_SLOWDOWN_STRENGTH
-/datum/movespeed_modifier/borg_throw
- multiplicative_slowdown = 0.9
-
/datum/movespeed_modifier/human_carry
multiplicative_slowdown = HUMAN_CARRY_SLOWDOWN
blacklisted_movetypes = FLOATING
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index 8c3039d8c62..e1a4dfb0ae0 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -111,6 +111,7 @@
"crow" = TRUE,
"duffel" = TRUE,
"fox" = FALSE,
+ "frog" = TRUE,
"hawk" = FALSE,
"lizard" = FALSE,
"monkey" = TRUE,
diff --git a/code/modules/paperwork/filingcabinet.dm b/code/modules/paperwork/filingcabinet.dm
index cb6aae768fa..ed99e7ea179 100644
--- a/code/modules/paperwork/filingcabinet.dm
+++ b/code/modules/paperwork/filingcabinet.dm
@@ -193,9 +193,9 @@ GLOBAL_LIST_EMPTY(employmentCabinets)
/obj/structure/filingcabinet/employment/proc/fillCurrent()
//This proc fills the cabinet with the current crew.
for(var/datum/record/locked/target in GLOB.manifest.locked)
- var/datum/mind/mind_ref = target.mind_ref
- if(mind_ref && ishuman(mind_ref.current))
- addFile(mind_ref.current)
+ var/datum/mind/filed_mind = target.mind_ref.resolve()
+ if(filed_mind && ishuman(filed_mind.current))
+ addFile(filed_mind.current)
/obj/structure/filingcabinet/employment/proc/addFile(mob/living/carbon/human/employee)
new /obj/item/paper/employment_contract(src, employee.mind.name)
diff --git a/code/modules/photography/_pictures.dm b/code/modules/photography/_pictures.dm
index 8c949892dbf..45fa5654ad9 100644
--- a/code/modules/photography/_pictures.dm
+++ b/code/modules/photography/_pictures.dm
@@ -5,6 +5,8 @@
var/list/mobs_seen = list()
/// List of weakrefs pointing at dead mobs that appear in this photo
var/list/dead_seen = list()
+ /// List of strings of face-visible humans in this photo
+ var/list/names_seen = list()
var/caption
var/icon/picture_image
var/icon/picture_icon
@@ -16,7 +18,7 @@
///Was this image capable of seeing ghosts?
var/see_ghosts = CAMERA_NO_GHOSTS
-/datum/picture/New(name, desc, mobs_spotted, dead_spotted, image, icon, size_x, size_y, bp, caption_, autogenerate_icon, can_see_ghosts)
+/datum/picture/New(name, desc, mobs_spotted, dead_spotted, names, image, icon, size_x, size_y, bp, caption_, autogenerate_icon, can_see_ghosts)
if(!isnull(name))
picture_name = name
if(!isnull(desc))
@@ -27,6 +29,9 @@
if(!isnull(dead_spotted))
for(var/mob/seen as anything in dead_spotted)
dead_seen += WEAKREF(seen)
+ if(!isnull(names))
+ for(var/seen in names)
+ names_seen += seen
if(!isnull(image))
picture_image = image
if(!isnull(icon))
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index 1c0e360ed75..b168aaf54da 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -188,6 +188,8 @@
var/list/mobs = list()
var/blueprints = FALSE
var/clone_area = SSmapping.request_turf_block_reservation(size_x * 2 + 1, size_y * 2 + 1, 1)
+ ///list of human names taken on picture
+ var/list/names = list()
var/width = size_x * 2 + 1
var/height = size_y * 2 + 1
@@ -218,8 +220,11 @@
var/icon/get_icon = camera_get_icon(turfs, target_turf, psize_x, psize_y, clone_area, size_x, size_y, (size_x * 2 + 1), (size_y * 2 + 1))
qdel(clone_area)
get_icon.Blend("#000", ICON_UNDERLAY)
+ for(var/mob/living/carbon/human/person in mobs)
+ if(person.is_face_visible())
+ names += "[person.name]"
- var/datum/picture/picture = new("picture", desc.Join(" "), mobs_spotted, dead_spotted, get_icon, null, psize_x, psize_y, blueprints, can_see_ghosts = see_ghosts)
+ var/datum/picture/picture = new("picture", desc.Join(" "), mobs_spotted, dead_spotted, names, get_icon, null, psize_x, psize_y, blueprints, can_see_ghosts = see_ghosts)
after_picture(user, picture)
SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user)
blending = FALSE
diff --git a/code/modules/photography/photos/photo.dm b/code/modules/photography/photos/photo.dm
index 9be79a58266..b34ff459c00 100644
--- a/code/modules/photography/photos/photo.dm
+++ b/code/modules/photography/photos/photo.dm
@@ -54,11 +54,11 @@
icon = I
return ..()
-/obj/item/photo/suicide_act(mob/living/carbon/user)
+/obj/item/photo/suicide_act(mob/living/carbon/human/user)
user.visible_message(span_suicide("[user] is taking one last look at \the [src]! It looks like [user.p_theyre()] giving in to death!"))//when you wanna look at photo of waifu one last time before you die...
- if (user.gender == MALE)
+ if (!ishuman(user) || user.physique == MALE)
playsound(user, 'sound/voice/human/manlaugh1.ogg', 50, TRUE)//EVERY TIME I DO IT MAKES ME LAUGH
- else if (user.gender == FEMALE)
+ else
playsound(user, 'sound/voice/human/womanlaugh.ogg', 50, TRUE)
return OXYLOSS
diff --git a/code/modules/power/rtg.dm b/code/modules/power/rtg.dm
index c49bc455165..974c2e66737 100644
--- a/code/modules/power/rtg.dm
+++ b/code/modules/power/rtg.dm
@@ -71,7 +71,7 @@
visible_message(span_danger("\The [src] lets out a shower of sparks as it starts to lose stability!"),\
span_hear("You hear a loud electrical crack!"))
playsound(src.loc, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
- tesla_zap(src, 5, power_gen * 0.05)
+ tesla_zap(src, 5, power_gen * 20)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(explosion), src, 2, 3, 4, null, 8), 10 SECONDS) // Not a normal explosion.
/obj/machinery/power/rtg/abductor/bullet_act(obj/projectile/Proj)
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index 654ea805440..8598b3cccf2 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -53,8 +53,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
var/damage_archived = 0
var/list/damage_factors
- /// How much extra power does the main zap generate.
- var/zap_multiplier = 1
+ /// The zap power transmission over internal energy. W/MeV.
+ var/zap_transmission_rate = BASE_POWER_TRANSMISSION_RATE
var/list/zap_factors
/// The temperature at which we start taking damage
@@ -95,7 +95,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
/// How much power decay is negated. Complete power decay negation at 1.
var/gas_powerloss_inhibition = 0
/// Affects the amount of power the main SM zap makes.
- var/gas_power_transmission = 0
+ var/gas_power_transmission_rate = 0
/// Affects the power gain the SM experiances from heat.
var/gas_heat_power_generation = 0
@@ -109,7 +109,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
var/external_damage_immediate = 0
///The cutoff for a bolt jumping, grows with heat, lowers with higher mol count,
- var/zap_cutoff = 1500
+ var/zap_cutoff = 1.2e6
///How much the bullets damage should be multiplied by when it is added to the internal variables
var/bullet_energy = SUPERMATTER_DEFAULT_BULLET_ENERGY
///How much hallucination should we produce per unit of power?
@@ -153,6 +153,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
///Stores the time of when the last zap occurred
var/last_power_zap = 0
+ var/last_high_energy_zap = 0
///Do we show this crystal in the CIMS modular program
var/include_in_cims = TRUE
@@ -275,7 +276,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
// PART 3: POWER PROCESSING
internal_energy_factors = calculate_internal_energy()
- zap_factors = calculate_zap_multiplier()
+ zap_factors = calculate_zap_transmission_rate()
if(internal_energy && (last_power_zap + (4 - internal_energy * 0.001) SECONDS) < world.time)
playsound(src, 'sound/weapons/emitter2.ogg', 70, TRUE)
hue_angle_shift = clamp(903 * log(10, (internal_energy + 8000)) - 3590, -50, 240)
@@ -286,9 +287,9 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
supermatter_zap(
zapstart = src,
range = 3,
- zap_str = 1.25 * internal_energy * zap_multiplier * delta_time,
+ zap_str = internal_energy * zap_transmission_rate * delta_time,
zap_flags = ZAP_SUPERMATTER_FLAGS,
- zap_cutoff = 300 * delta_time,
+ zap_cutoff = 2.4e5 * delta_time,
power_level = internal_energy,
color = zap_color,
)
@@ -374,15 +375,20 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
"name" = factor,
"amount" = amount * -1
))
+ var/list/internal_energy_si_derived_data = siunit_isolated(internal_energy * 1e6, "eV", 3)
data["internal_energy"] = internal_energy
+ data["internal_energy_coefficient"] = internal_energy_si_derived_data[SI_COEFFICIENT]
+ data["internal_energy_unit"] = internal_energy_si_derived_data[SI_UNIT]
data["internal_energy_factors"] = list()
for (var/factor in internal_energy_factors)
+ var/list/internal_energy_factor_si_derived_data = siunit_isolated(internal_energy_factors[factor] * 1e6, "eV", 3)
var/amount = round(internal_energy_factors[factor], 0.01)
if(!amount)
continue
data["internal_energy_factors"] += list(list(
"name" = factor,
- "amount" = amount
+ "amount" = internal_energy_factor_si_derived_data[SI_COEFFICIENT],
+ "unit" = internal_energy_factor_si_derived_data[SI_UNIT],
))
data["temp_limit"] = temp_limit
data["temp_limit_factors"] = list()
@@ -392,7 +398,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
continue
data["temp_limit_factors"] += list(list(
"name" = factor,
- "amount" = amount
+ "amount" = amount,
))
data["waste_multiplier"] = waste_multiplier
data["waste_multiplier_factors"] = list()
@@ -402,18 +408,42 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
continue
data["waste_multiplier_factors"] += list(list(
"name" = factor,
- "amount" = amount
+ "amount" = amount,
))
- data["zap_multiplier"] = zap_multiplier
- data["zap_multiplier_factors"] = list()
+
+ data["zap_transmission_factors"] = list()
for (var/factor in zap_factors)
- var/amount = round(zap_factors[factor], 0.01)
- if(!amount)
+ var/list/zap_factor_si_derived_data = siunit_isolated(zap_factors[factor] * internal_energy, "W", 2)
+ if(!zap_factor_si_derived_data[SI_COEFFICIENT])
continue
- data["zap_multiplier_factors"] += list(list(
+ data["zap_transmission_factors"] += list(list(
"name" = factor,
- "amount" = amount
+ "amount" = zap_factor_si_derived_data[SI_COEFFICIENT],
+ "unit" = zap_factor_si_derived_data[SI_UNIT],
))
+
+ ///Add high energy bonus to the zap transmission data so we can accurately measure our power generation from zaps.
+ var/high_energy_bonus = 0
+ var/zap_transmission = zap_transmission_rate * internal_energy
+ var/zap_power_multiplier = 1
+ if(internal_energy > POWER_PENALTY_THRESHOLD) //Supermatter zaps multiply power internally under some conditions for some reason, so we'll snowflake this for now.
+ ///Power multiplier bonus applied to all zaps. Zap power generation doubles when it reaches 7GeV and 9GeV.
+ zap_power_multiplier *= 2 ** clamp(round((internal_energy - POWER_PENALTY_THRESHOLD) / 2000), 0, 2)
+ ///The supermatter releases additional zaps after 5GeV, with more at 7GeV and 9GeV.
+ var/additional_zap_bonus = clamp(internal_energy * 3200, 6.4e6, 3.2e7) * clamp(round(INVERSE_LERP(1000, 3000, internal_energy)), 1, 4)
+ high_energy_bonus = (zap_transmission + additional_zap_bonus) * zap_power_multiplier - zap_transmission
+ var/list/zap_factor_si_derived_data = siunit_isolated(high_energy_bonus, "W", 2)
+ data["zap_transmission_factors"] += list(list(
+ "name" = "High Energy Bonus",
+ "amount" = zap_factor_si_derived_data[SI_COEFFICIENT],
+ "unit" = zap_factor_si_derived_data[SI_UNIT],
+ ))
+
+ var/list/zap_transmission_si_derived_data = siunit_isolated(zap_transmission + high_energy_bonus, "W", 2)
+ data["zap_transmission"] = zap_transmission + high_energy_bonus
+ data["zap_transmission_coefficient"] = zap_transmission_si_derived_data[SI_COEFFICIENT]
+ data["zap_transmission_unit"] = zap_transmission_si_derived_data[SI_UNIT]
+
data["absorbed_ratio"] = absorption_ratio
var/list/formatted_gas_percentage = list()
for (var/datum/gas/gas_path as anything in subtypesof(/datum/gas))
@@ -577,7 +607,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
*
* Updates:
* [/obj/machinery/power/supermatter_crystal/var/list/gas_percentage]
- * [/obj/machinery/power/supermatter_crystal/var/gas_power_transmission]
+ * [/obj/machinery/power/supermatter_crystal/var/gas_power_transmission_rate]
* [/obj/machinery/power/supermatter_crystal/var/gas_heat_modifier]
* [/obj/machinery/power/supermatter_crystal/var/gas_heat_resistance]
* [/obj/machinery/power/supermatter_crystal/var/gas_heat_power_generation]
@@ -590,7 +620,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
return
gas_percentage = list()
- gas_power_transmission = 0
+ gas_power_transmission_rate = 0
gas_heat_modifier = 0
gas_heat_resistance = 0
gas_heat_power_generation = 0
@@ -607,7 +637,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
var/datum/sm_gas/sm_gas = current_gas_behavior[gas_path]
if(!sm_gas)
continue
- gas_power_transmission += sm_gas.power_transmission * gas_percentage[gas_path]
+ gas_power_transmission_rate += sm_gas.power_transmission * gas_percentage[gas_path]
gas_heat_modifier += sm_gas.heat_modifier * gas_percentage[gas_path]
gas_heat_resistance += sm_gas.heat_resistance * gas_percentage[gas_path]
gas_heat_power_generation += sm_gas.heat_power_generation * gas_percentage[gas_path]
@@ -637,7 +667,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
external_power_trickle -= min(additive_power[SM_POWER_EXTERNAL_TRICKLE], external_power_trickle)
additive_power[SM_POWER_EXTERNAL_IMMEDIATE] = external_power_immediate
external_power_immediate = 0
- additive_power[SM_POWER_HEAT] = gas_heat_power_generation * absorbed_gasmix.temperature / 6
+ additive_power[SM_POWER_HEAT] = gas_heat_power_generation * absorbed_gasmix.temperature * GAS_HEAT_POWER_SCALING_COEFFICIENT
additive_power[SM_POWER_HEAT] && log_activation(who = "environmental factors")
// I'm sorry for this, but we need to calculate power lost immediately after power gain.
@@ -660,6 +690,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
if(internal_energy && !activation_logged)
stack_trace("Supermatter powered for the first time without being logged. Internal energy factors: [json_encode(internal_energy_factors)]")
activation_logged = TRUE // so we dont spam the log.
+ else if(!internal_energy)
+ last_power_zap = world.time
return additive_power
/** Log when the supermatter is activated for the first time.
@@ -685,24 +717,24 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
activation_logged = TRUE
/**
- * Perform calculation for the main zap power multiplier.
+ * Perform calculation for the main zap power transmission rate in W/MeV.
* Description of each factors can be found in the defines.
*
* Updates:
- * [/obj/machinery/power/supermatter_crystal/var/zap_multiplier]
+ * [/obj/machinery/power/supermatter_crystal/var/zap_transmission_rate]
*
* Returns: The factors that have influenced the calculation. list[FACTOR_DEFINE] = number
*/
-/obj/machinery/power/supermatter_crystal/proc/calculate_zap_multiplier()
- var/list/additive_transmission = list()
- additive_transmission[SM_ZAP_BASE] = 1
- additive_transmission[SM_ZAP_GAS] = gas_power_transmission
+/obj/machinery/power/supermatter_crystal/proc/calculate_zap_transmission_rate()
+ var/list/additive_transmission_rate = list()
+ additive_transmission_rate[SM_ZAP_BASE] = BASE_POWER_TRANSMISSION_RATE
+ additive_transmission_rate[SM_ZAP_GAS] = BASE_POWER_TRANSMISSION_RATE * gas_power_transmission_rate
- zap_multiplier = 0
- for (var/transmission_types in additive_transmission)
- zap_multiplier += additive_transmission[transmission_types]
- zap_multiplier = max(zap_multiplier, 0)
- return additive_transmission
+ zap_transmission_rate = 0
+ for (var/transmission_types in additive_transmission_rate)
+ zap_transmission_rate += additive_transmission_rate[transmission_types]
+ zap_transmission_rate = max(zap_transmission_rate, 0)
+ return additive_transmission_rate
/**
* Perform calculation for the waste multiplier.
@@ -836,7 +868,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
delamination_strategy.on_select(src)
return TRUE
-/obj/machinery/proc/supermatter_zap(atom/zapstart = src, range = 5, zap_str = 4000, zap_flags = ZAP_SUPERMATTER_FLAGS, list/targets_hit = list(), zap_cutoff = 1500, power_level = 0, zap_icon = DEFAULT_ZAP_ICON_STATE, color = null)
+/obj/machinery/proc/supermatter_zap(atom/zapstart = src, range = 5, zap_str = 3.2e6, zap_flags = ZAP_SUPERMATTER_FLAGS, list/targets_hit = list(), zap_cutoff = 1.2e6, power_level = 0, zap_icon = DEFAULT_ZAP_ICON_STATE, color = null)
if(QDELETED(zapstart))
return
. = zapstart.dir
@@ -931,13 +963,13 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
//Going boom should be rareish
if(prob(80))
zap_flags &= ~ZAP_MACHINE_EXPLOSIVE
- if(target_type == COIL)
- var/multi = 2
- switch(power_level)//Between 7k and 9k it's 4, above that it's 8
+ if(target_type == COIL || target_type == ROD)
+ var/multi = 1
+ switch(power_level)//Between 7k and 9k it's 2, above that it's 4
if(SEVERE_POWER_PENALTY_THRESHOLD to CRITICAL_POWER_PENALTY_THRESHOLD)
- multi = 4
+ multi = 2
if(CRITICAL_POWER_PENALTY_THRESHOLD to INFINITY)
- multi = 8
+ multi = 4
if(zap_flags & ZAP_SUPERMATTER_FLAGS)
var/remaining_power = target.zap_act(zap_str * multi, zap_flags)
zap_str = remaining_power / multi //Coils should take a lot out of the power of the zap
diff --git a/code/modules/power/supermatter/supermatter_extra_effects.dm b/code/modules/power/supermatter/supermatter_extra_effects.dm
index 7fe56d9c2d4..efd84c677fa 100644
--- a/code/modules/power/supermatter/supermatter_extra_effects.dm
+++ b/code/modules/power/supermatter/supermatter_extra_effects.dm
@@ -91,6 +91,7 @@
/obj/machinery/power/supermatter_crystal/proc/handle_high_power()
if(internal_energy <= POWER_PENALTY_THRESHOLD && damage <= danger_point) //If the power is above 5000 or if the damage is above 550
+ last_high_energy_zap = world.time //Prevent oddly high initial zap due to high energy zaps not getting triggered via too low energy.
return
var/range = 4
zap_cutoff = 1500
@@ -99,7 +100,7 @@
var/temp = absorbed_gasmix.temperature
if(pressure > 0 && temp > 0)
//You may be able to freeze the zapstate of the engine with good planning, we'll see
- zap_cutoff = clamp(3000 - (internal_energy * total_moles / 10) / temp, 350, 3000)//If the core is cold, it's easier to jump, ditto if there are a lot of mols
+ zap_cutoff = clamp(1.2e6 - (internal_energy * total_moles * 40) / temp, 1.4e5, 1.2e6)//If the core is cold, it's easier to jump, ditto if there are a lot of mols
//We should always be able to zap our way out of the default enclosure
//See supermatter_zap() for more details
range = clamp(internal_energy / pressure * 10, 2, 7)
@@ -128,9 +129,10 @@
if(zap_count >= 1)
playsound(loc, 'sound/weapons/emitter2.ogg', 100, TRUE, extrarange = 10)
+ var/delta_time = min((world.time - last_high_energy_zap) * 0.1, 16)
for(var/i in 1 to zap_count)
- supermatter_zap(src, range, clamp(internal_energy*2, 4000, 20000), flags, zap_cutoff = src.zap_cutoff, power_level = internal_energy, zap_icon = src.zap_icon)
-
+ supermatter_zap(src, range, clamp(internal_energy * 3200, 6.4e6, 3.2e7) * delta_time, flags, zap_cutoff = src.zap_cutoff * delta_time, power_level = internal_energy, zap_icon = src.zap_icon)
+ last_high_energy_zap = world.time
if(prob(5))
supermatter_anomaly_gen(src, FLUX_ANOMALY, rand(5, 10))
if(prob(5))
diff --git a/code/modules/power/supermatter/supermatter_gas.dm b/code/modules/power/supermatter/supermatter_gas.dm
index 141f78a38b8..df8ef8e5b4f 100644
--- a/code/modules/power/supermatter/supermatter_gas.dm
+++ b/code/modules/power/supermatter/supermatter_gas.dm
@@ -17,33 +17,40 @@
// Positive is true if more of the amount is a good thing.
var/list/numeric_data = list()
if(sm_gas.power_transmission)
+ var/list/si_derived_data = siunit_isolated(sm_gas.power_transmission * BASE_POWER_TRANSMISSION_RATE, "W/MeV", 2)
numeric_data += list(list(
- "name" = "Power Transmission",
- "amount" = sm_gas.power_transmission,
+ "name" = "Power Transmission Bonus",
+ "amount" = si_derived_data["coefficient"],
+ "unit" = si_derived_data["unit"],
"positive" = TRUE,
))
if(sm_gas.heat_modifier)
numeric_data += list(list(
"name" = "Waste Multiplier",
- "amount" = sm_gas.heat_modifier,
+ "amount" = 100 * sm_gas.heat_modifier,
+ "unit" = "%",
"positive" = FALSE,
))
if(sm_gas.heat_resistance)
numeric_data += list(list(
"name" = "Heat Resistance",
- "amount" = sm_gas.heat_resistance,
+ "amount" = 100 * sm_gas.heat_resistance,
+ "unit" = "%",
"positive" = TRUE,
))
if(sm_gas.heat_power_generation)
+ var/list/si_derived_data = siunit_isolated(sm_gas.heat_power_generation * GAS_HEAT_POWER_SCALING_COEFFICIENT * 1e7 / SSair.wait, "eV/K/s", 2)
numeric_data += list(list(
"name" = "Heat Power Gain",
- "amount" = sm_gas.heat_power_generation,
+ "amount" = si_derived_data["coefficient"],
+ "unit" = si_derived_data["unit"],
"positive" = TRUE,
))
if(sm_gas.powerloss_inhibition)
numeric_data += list(list(
"name" = "Power Decay Negation",
- "amount" = sm_gas.powerloss_inhibition,
+ "amount" = 100 * sm_gas.powerloss_inhibition,
+ "unit" = "%",
"positive" = TRUE,
))
singular_gas_data["numeric_data"] = numeric_data
@@ -59,8 +66,7 @@ GLOBAL_LIST_INIT(sm_gas_behavior, init_sm_gas())
/datum/sm_gas
/// Path of the [/datum/gas] involved with this interaction.
var/gas_path
-
- /// Influences zap power without interfering with the crystal's own energy.
+ /// Influences zap power without interfering with the crystal's own energy. Gets scaled by [BASE_POWER_TRANSMISSION_RATE].
var/power_transmission = 0
/// How much more waste heat and gas the SM generates.
var/heat_modifier = 0
@@ -216,7 +222,7 @@ GLOBAL_LIST_INIT(sm_gas_behavior, init_sm_gas())
sm.supermatter_zap(
sm,
range = 6,
- zap_str = clamp(sm.internal_energy * 2, 4000, 20000),
+ zap_str = clamp(sm.internal_energy * 1600, 3.2e6, 1.6e7),
zap_flags = ZAP_MOB_STUN,
zap_cutoff = sm.zap_cutoff,
power_level = sm.internal_energy,
diff --git a/code/modules/power/tesla/coil.dm b/code/modules/power/tesla/coil.dm
index 098ff7ceaee..3cf040b76ef 100644
--- a/code/modules/power/tesla/coil.dm
+++ b/code/modules/power/tesla/coil.dm
@@ -1,7 +1,5 @@
// zap needs to be over this amount to get power
-#define TESLA_COIL_THRESHOLD 80
-// each zap power unit produces 400 joules
-#define ZAP_TO_ENERGY(p) (joules_to_energy((p) * 400))
+#define TESLA_COIL_THRESHOLD 32000
/obj/machinery/power/energy_accumulator/tesla_coil
name = "tesla coil"
@@ -107,7 +105,7 @@
power /= 10
zap_buckle_check(power)
var/power_removed = powernet ? power * input_power_multiplier : power
- stored_energy += max(ZAP_TO_ENERGY(power_removed - TESLA_COIL_THRESHOLD), 0)
+ stored_energy += max(joules_to_energy(power_removed - TESLA_COIL_THRESHOLD), 0)
return max(power - power_removed, 0) //You get back the amount we didn't use
/obj/machinery/power/energy_accumulator/tesla_coil/proc/zap()
@@ -170,10 +168,9 @@
if(anchored && !panel_open)
flick("grounding_rodhit", src)
zap_buckle_check(power)
- stored_energy += ZAP_TO_ENERGY(power)
+ stored_energy += joules_to_energy(power)
return 0
else
. = ..()
#undef TESLA_COIL_THRESHOLD
-#undef ZAP_TO_ENERGY
diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm
index b21938e2608..7ee8c0085c9 100644
--- a/code/modules/power/tesla/energy_ball.dm
+++ b/code/modules/power/tesla/energy_ball.dm
@@ -1,5 +1,5 @@
-#define TESLA_DEFAULT_POWER 1738260
-#define TESLA_MINI_POWER 869130
+#define TESLA_DEFAULT_POWER 6.95304e8
+#define TESLA_MINI_POWER 3.47652e8
//Zap constants, speeds up targeting
#define BIKE (COIL + 1)
#define COIL (ROD + 1)
@@ -205,7 +205,7 @@
if(!(zap_flags & ZAP_ALLOW_DUPLICATES))
LAZYSET(shocked_targets, source, TRUE) //I don't want no null refs in my list yeah?
. = source.dir
- if(power < 1000)
+ if(power < 4e5)
return
/*
@@ -334,7 +334,7 @@
var/mob/living/closest_mob = closest_atom
ADD_TRAIT(closest_mob, TRAIT_BEING_SHOCKED, WAS_SHOCKED)
addtimer(TRAIT_CALLBACK_REMOVE(closest_mob, TRAIT_BEING_SHOCKED, WAS_SHOCKED), 1 SECONDS)
- var/shock_damage = (zap_flags & ZAP_MOB_DAMAGE) ? (min(round(power/600), 90) + rand(-5, 5)) : 0
+ var/shock_damage = (zap_flags & ZAP_MOB_DAMAGE) ? (min(round(power/2.4e5), 90) + rand(-5, 5)) : 0
closest_mob.electrocute_act(shock_damage, source, 1, SHOCK_TESLA | ((zap_flags & ZAP_MOB_STUN) ? NONE : SHOCK_NOSTUN))
if(issilicon(closest_mob))
var/mob/living/silicon/S = closest_mob
diff --git a/code/modules/procedural_mapping/mapGenerators/asteroid.dm b/code/modules/procedural_mapping/mapGenerators/asteroid.dm
index ab2bc6f2ca4..bf6c84ebf88 100644
--- a/code/modules/procedural_mapping/mapGenerators/asteroid.dm
+++ b/code/modules/procedural_mapping/mapGenerators/asteroid.dm
@@ -22,7 +22,7 @@
spawnableAtoms = list(
/mob/living/basic/mining/basilisk = 10,
/mob/living/basic/mining/goliath/ancient = 10,
- /mob/living/simple_animal/hostile/asteroid/hivelord = 10,
+ /mob/living/basic/mining/hivelord = 10,
)
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 6439b78a9bd..2b62416fe7f 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -275,10 +275,15 @@
user.visible_message(span_danger("[user.name]'s soul is captured by \the [src]!"), span_userdanger("You've lost the gamble! Your soul is forfeit!"))
/obj/item/gun/ballistic/revolver/reverse //Fires directly at its user... unless the user is a clown, of course.
- name = "\improper Syndicate Revolver"
clumsy_check = FALSE
icon_state = "revolversyndie"
+/obj/item/gun/ballistic/revolver/reverse/Initialize(mapload)
+ . = ..()
+ var/obj/item/gun/ballistic/revolver/syndicate/syndie_revolver = /obj/item/gun/ballistic/revolver/syndicate
+ name = initial(syndie_revolver.name)
+ desc = initial(syndie_revolver.desc)
+
/obj/item/gun/ballistic/revolver/reverse/can_trigger_gun(mob/living/user, akimbo_usage)
if(akimbo_usage)
return FALSE
diff --git a/code/modules/projectiles/guns/energy/energy_gun.dm b/code/modules/projectiles/guns/energy/energy_gun.dm
index 3fd1e7c1949..69acb79ac2a 100644
--- a/code/modules/projectiles/guns/energy/energy_gun.dm
+++ b/code/modules/projectiles/guns/energy/energy_gun.dm
@@ -9,6 +9,18 @@
ammo_x_offset = 3
dual_wield_spread = 60
+/obj/item/gun/energy/e_gun/Initialize(mapload)
+ . = ..()
+ // Only actual eguns can be converted
+ if(type != /obj/item/gun/energy/e_gun)
+ return
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/advancedegun, /datum/crafting_recipe/tempgun, /datum/crafting_recipe/beam_rifle)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/gun/energy/e_gun/add_seclight_point()
AddComponent(/datum/component/seclite_attachable, \
light_overlay_icon = 'icons/obj/weapons/guns/flashlights.dmi', \
@@ -65,7 +77,7 @@
name = "\improper X-01 MultiPhase Energy Gun"
desc = "This is an expensive, modern recreation of an antique laser gun. This gun has several unique firemodes, but lacks the ability to recharge over time."
icon_state = "hoslaser"
- cell_type = /obj/item/stock_parts/cell //SKYRAT EDIT ADDITION - GUNSGALORE
+ cell_type = /obj/item/stock_parts/cell/hos_gun
w_class = WEIGHT_CLASS_NORMAL
force = 10
ammo_type = list(/obj/item/ammo_casing/energy/disabler/hos, /obj/item/ammo_casing/energy/laser/hos, /obj/item/ammo_casing/energy/ion/hos)
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index 8d9a29d3b80..06e82a37aae 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -16,6 +16,19 @@
var/list/modkits = list()
gun_flags = NOT_A_REAL_GUN
+
+/obj/item/gun/energy/recharge/kinetic_accelerator/Initialize(mapload)
+ . = ..()
+ // Only actual KAs can be converted
+ if(type != /obj/item/gun/energy/recharge/kinetic_accelerator)
+ return
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/ebow)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/gun/energy/recharge/kinetic_accelerator/apply_fantasy_bonuses(bonus)
. = ..()
max_mod_capacity = modify_fantasy_variable("max_mod_capacity", max_mod_capacity, bonus * 10)
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index dafaaa6a7de..0409f2fe037 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -9,6 +9,18 @@
ammo_x_offset = 1
shaded_charge = 1
+/obj/item/gun/energy/laser/Initialize(mapload)
+ . = ..()
+ // Only actual lasguns can be converted
+ if(type != /obj/item/gun/energy/laser)
+ return
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/xraylaser, /datum/crafting_recipe/hellgun, /datum/crafting_recipe/ioncarbine, /datum/crafting_recipe/decloner)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
+
/obj/item/gun/energy/laser/practice
name = "practice laser gun"
desc = "A modified version of the basic laser gun, this one fires less concentrated energy bolts designed for target practice."
diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm
index 90a3d723bf5..5e938c49953 100644
--- a/code/modules/projectiles/projectile/bullets.dm
+++ b/code/modules/projectiles/projectile/bullets.dm
@@ -12,7 +12,6 @@
wound_bonus = 0
wound_falloff_tile = -5
embed_falloff_tile = -3
- wound_bonus = 20 //SKYRAT EDIT ADDITION
/obj/projectile/bullet/smite
name = "divine retribution"
diff --git a/code/modules/projectiles/projectile/energy/tesla.dm b/code/modules/projectiles/projectile/energy/tesla.dm
index 9afb816088f..687bd1b8e73 100644
--- a/code/modules/projectiles/projectile/energy/tesla.dm
+++ b/code/modules/projectiles/projectile/energy/tesla.dm
@@ -5,7 +5,7 @@
damage = 10 //A worse lasergun
var/zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN
var/zap_range = 3
- var/power = 10000
+ var/power = 4e6
/obj/projectile/energy/tesla/on_hit(atom/target)
. = ..()
@@ -22,7 +22,7 @@
/obj/projectile/energy/tesla/cannon
name = "tesla orb"
- power = 20000
+ power = 8e6
damage = 15 //Mech man big
/obj/projectile/energy/tesla_cannon
diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm
index afe59235478..c8da91b9dde 100644
--- a/code/modules/projectiles/projectile/magic.dm
+++ b/code/modules/projectiles/projectile/magic.dm
@@ -481,7 +481,7 @@
speed = 0.3
/// The power of the zap itself when it electrocutes someone
- var/zap_power = 20000
+ var/zap_power = 8e6
/// The range of the zap itself when it electrocutes someone
var/zap_range = 15
/// The flags of the zap itself when it electrocutes someone
@@ -503,7 +503,7 @@
return ..()
/obj/projectile/magic/aoe/lightning/no_zap
- zap_power = 10000
+ zap_power = 4e6
zap_range = 4
zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 902ccf35e6a..503560aa224 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -1136,7 +1136,8 @@
//If the reaction pollutes, pollute it here if we have an atom
if(equilibrium.reaction.pollutant_type && my_atom)
var/turf/my_turf = get_turf(my_atom)
- my_turf.pollute_turf(equilibrium.reaction.pollutant_type, equilibrium.reaction.pollutant_amount * equilibrium.reacted_vol)
+ if(my_turf) // reactions can happen in nullspace (like inside of a mob's stomach for instance).
+ my_turf.pollute_turf(equilibrium.reaction.pollutant_type, equilibrium.reaction.pollutant_amount * equilibrium.reacted_vol)
//SKYRAT EDIT END
qdel(equilibrium)
update_total()
@@ -1293,7 +1294,8 @@
//If the reaction pollutes, pollute it here if we have an atom
if(selected_reaction.pollutant_type && my_atom)
var/turf/my_turf = get_turf(my_atom)
- my_turf.pollute_turf(selected_reaction.pollutant_type, selected_reaction.pollutant_amount * multiplier)
+ if(my_turf) // just to be safe here
+ my_turf.pollute_turf(selected_reaction.pollutant_type, selected_reaction.pollutant_amount * multiplier)
//SKYRAT EDIT END
selected_reaction.on_reaction(src, null, multiplier)
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index 8de1d98cc2e..110107d103f 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -7,6 +7,10 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
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
diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
index f67b813be0c..b81e1600bbf 100644
--- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
@@ -57,6 +57,20 @@
booze_power *= 0.7
if(HAS_TRAIT(drinker, TRAIT_LIGHT_DRINKER))
booze_power *= 2
+
+ // water will dilute alcohol effects
+ var/total_water_volume = 0
+ var/total_alcohol_volume = 0
+ for(var/datum/reagent/water/sobriety in drinker.reagents.reagent_list)
+ total_water_volume += sobriety.volume
+
+ for(var/datum/reagent/consumable/ethanol/alcohol in drinker.reagents.reagent_list)
+ total_alcohol_volume += alcohol.volume
+
+ var/combined_dilute_volume = total_alcohol_volume + total_water_volume
+ if(combined_dilute_volume) // safety check to prevent division by zero
+ booze_power *= (total_alcohol_volume / combined_dilute_volume)
+
// Volume, power, and server alcohol rate effect how quickly one gets drunk
drinker.adjust_drunk_effect(sqrt(volume) * booze_power * ALCOHOL_RATE * REM * seconds_per_tick * 0.25) // SKYRAT EDIT CHANGE - Alcohol Tolerance - Original: (sqrt(volume) * booze_power * ALCOHOL_RATE * REM * seconds_per_tick)
if(boozepwr > 0)
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 1e0559d6034..17867389c3d 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -272,9 +272,9 @@
color = "#FFFFFF" // rgb: 255, 255, 255
taste_mult = 1.5 // stop sugar drowning out other flavours
nutriment_factor = 2
- metabolization_rate = 2 * REAGENTS_METABOLISM
+ metabolization_rate = 5 * REAGENTS_METABOLISM
creation_purity = 1 // impure base reagents are a big no-no
- overdose_threshold = 100 // Hyperglycaemic shock
+ overdose_threshold = 120 // Hyperglycaemic shock
taste_description = "sweetness"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
default_container = /obj/item/reagent_containers/condiment/sugar
@@ -286,11 +286,11 @@
/datum/reagent/consumable/sugar/overdose_start(mob/living/M)
to_chat(M, span_userdanger("You go into hyperglycaemic shock! Lay off the twinkies!"))
- M.AdjustSleeping(600)
+ M.AdjustSleeping(20 SECONDS)
. = TRUE
/datum/reagent/consumable/sugar/overdose_process(mob/living/M, seconds_per_tick, times_fired)
- M.AdjustSleeping(40 * REM * seconds_per_tick)
+ M.adjust_drowsiness_up_to((5 SECONDS * REM * seconds_per_tick), 60 SECONDS)
..()
. = TRUE
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 2a46d537233..dddf966be72 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -292,6 +292,7 @@
. = ..()
if(affected_mob.blood_volume)
affected_mob.blood_volume += 0.1 * REM * seconds_per_tick // water is good for you!
+ affected_mob.adjust_drunk_effect(-0.25 * REM * seconds_per_tick) // and even sobers you up slowly!!
// For weird backwards situations where water manages to get added to trays nutrients, as opposed to being snowflaked away like usual.
/datum/reagent/water/on_hydroponics_apply(obj/machinery/hydroponics/mytray, mob/user)
@@ -2686,13 +2687,16 @@
metal_morph(exposed_turf)
///turn an object into a special material
-/datum/reagent/metalgen/proc/metal_morph(atom/A)
+/datum/reagent/metalgen/proc/metal_morph(atom/target)
var/metal_ref = data["material"]
if(!metal_ref)
return
+ if(is_type_in_typecache(target, GLOB.blacklisted_metalgen_types)) //some stuff can lead to exploits if transmuted
+ return
+
var/metal_amount = 0
- var/list/materials_to_transmute = A.get_material_composition(BREAKDOWN_INCLUDE_ALCHEMY)
+ var/list/materials_to_transmute = target.get_material_composition(BREAKDOWN_INCLUDE_ALCHEMY)
for(var/metal_key in materials_to_transmute) //list with what they're made of
metal_amount += materials_to_transmute[metal_key]
@@ -2700,9 +2704,9 @@
metal_amount = default_material_amount //some stuff doesn't have materials at all. To still give them properties, we give them a material. Basically doesn't exist
var/list/metal_dat = list((metal_ref) = metal_amount)
- A.material_flags = applied_material_flags
- A.set_custom_materials(metal_dat)
- ADD_TRAIT(A, TRAIT_MAT_TRANSMUTED, type)
+ target.material_flags = applied_material_flags
+ target.set_custom_materials(metal_dat)
+ ADD_TRAIT(target, TRAIT_MAT_TRANSMUTED, type)
/datum/reagent/gravitum
name = "Gravitum"
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index 707a1dca350..9083de70902 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -542,9 +542,9 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_EXPLOSIVE | REACTION_TAG_DANGEROUS
/datum/chemical_reaction/reagent_explosion/teslium_lightning/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- var/T1 = created_volume * 20 //100 units : Zap 3 times, with powers 2000/5000/12000. Tesla revolvers have a power of 10000 for comparison.
- var/T2 = created_volume * 50
- var/T3 = created_volume * 120
+ var/T1 = created_volume * 8e3 //100 units : Zap 3 times, with powers 8e5/2e6/4.8e6. Tesla revolvers have a power of 10000 for comparison.
+ var/T2 = created_volume * 2e4
+ var/T3 = created_volume * 4.8e4
var/added_delay = 0.5 SECONDS
if(created_volume >= 75)
addtimer(CALLBACK(src, PROC_REF(zappy_zappy), holder, T1), added_delay)
diff --git a/code/modules/reagents/reagent_containers/cups/glassbottle.dm b/code/modules/reagents/reagent_containers/cups/glassbottle.dm
index e2f445c6a1a..e204c6803fb 100644
--- a/code/modules/reagents/reagent_containers/cups/glassbottle.dm
+++ b/code/modules/reagents/reagent_containers/cups/glassbottle.dm
@@ -40,6 +40,12 @@
tool_behaviour = TOOL_ROLLINGPIN // Used to knock out the Chef.
toolspeed = 1.3 //it's a little awkward to use, but it's a cylinder alright.
+/obj/item/reagent_containers/cup/glass/bottle/Initialize(mapload, vol)
+ . = ..()
+ AddComponent(/datum/component/slapcrafting,\
+ slapcraft_recipes = list(/datum/crafting_recipe/molotov)\
+ )
+
/obj/item/reagent_containers/cup/glass/bottle/small
name = "small glass bottle"
desc = "This blank bottle is unyieldingly anonymous, offering no clues to its contents."
diff --git a/code/modules/reagents/reagent_containers/cups/soda.dm b/code/modules/reagents/reagent_containers/cups/soda.dm
index 4908942b2c0..5bf0eb782c5 100644
--- a/code/modules/reagents/reagent_containers/cups/soda.dm
+++ b/code/modules/reagents/reagent_containers/cups/soda.dm
@@ -21,6 +21,12 @@
/// If the can hasn't been opened yet, this is the measure of how fizzed up it is from being shaken or thrown around. When opened, this is rolled as a percentage chance to burst
var/fizziness = 0
+/obj/item/reagent_containers/cup/soda_cans/Initialize(mapload, vol)
+ . = ..()
+ AddComponent(/datum/component/slapcrafting,\
+ slapcraft_recipes = list(/datum/crafting_recipe/improv_explosive)\
+ )
+
/obj/item/reagent_containers/cup/soda_cans/random/Initialize(mapload)
..()
var/T = pick(subtypesof(/obj/item/reagent_containers/cup/soda_cans) - /obj/item/reagent_containers/cup/soda_cans/random)
diff --git a/code/modules/religion/religion_sects.dm b/code/modules/religion/religion_sects.dm
index a322303b00d..9075a656ae9 100644
--- a/code/modules/religion/religion_sects.dm
+++ b/code/modules/religion/religion_sects.dm
@@ -55,8 +55,8 @@
/// Activates once selected and on newjoins, oriented around people who become holy.
/datum/religion_sect/proc/on_conversion(mob/living/chap)
SHOULD_CALL_PARENT(TRUE)
- to_chat(chap, "\"[quote]\"")
- to_chat(chap, "[desc]")
+ to_chat(chap, span_boldnotice("\"[quote]\""))
+ to_chat(chap, span_notice("[desc]"))
/// Activates if religious sect is reset by admins, should clean up anything you added on conversion.
/datum/religion_sect/proc/on_deconversion(mob/living/chap)
@@ -66,17 +66,17 @@
to_chat(chap, span_notice("Return to an altar to reform your sect."))
/// Returns TRUE if the item can be sacrificed. Can be modified to fit item being tested as well as person offering. Returning TRUE will stop the attackby sequence and proceed to on_sacrifice.
-/datum/religion_sect/proc/can_sacrifice(obj/item/I, mob/living/chap)
+/datum/religion_sect/proc/can_sacrifice(obj/item/sacrifice, mob/living/chap)
. = TRUE
if(chap.mind.holy_role == HOLY_ROLE_DEACON)
to_chat(chap, "You are merely a deacon of [GLOB.deity], and therefore cannot perform rites.")
return
- if(!is_type_in_typecache(I,desired_items_typecache))
+ if(!is_type_in_typecache(sacrifice, desired_items_typecache))
return FALSE
/// Activates when the sect sacrifices an item. This proc has NO bearing on the attackby sequence of other objects when used in conjunction with the religious_tool component.
-/datum/religion_sect/proc/on_sacrifice(obj/item/I, mob/living/chap)
- return adjust_favor(default_item_favor,chap)
+/datum/religion_sect/proc/on_sacrifice(obj/item/sacrifice, mob/living/chap)
+ return adjust_favor(default_item_favor, chap)
/// Returns a description for religious tools
/datum/religion_sect/proc/tool_examine(mob/living/holy_creature)
@@ -89,7 +89,7 @@
. = favor //if favor = 5 and we want to subtract 10, we'll only be able to subtract 5
if((favor + amount > max_favor))
. = (max_favor-favor) //if favor = 5 and we want to add 10 with a max of 10, we'll only be able to add 5
- favor = clamp(0,max_favor, favor+amount)
+ favor = clamp(0, max_favor, favor+amount)
/// Sets favor to a specific amount. Can provide optional features based on a user.
/datum/religion_sect/proc/set_favor(amount = 0, mob/living/chap)
@@ -190,16 +190,17 @@
blessed.add_mood_event("blessing", /datum/mood_event/blessing)
return TRUE
-/datum/religion_sect/mechanical/on_sacrifice(obj/item/I, mob/living/chap)
- var/obj/item/stock_parts/cell/the_cell = I
- if(!istype(the_cell)) //how...
+/datum/religion_sect/mechanical/on_sacrifice(obj/item/stock_parts/cell/power_cell, mob/living/chap)
+ if(!istype(power_cell))
return
- if(the_cell.charge < 300)
- to_chat(chap,span_notice("[GLOB.deity] does not accept pity amounts of power."))
+
+ if(power_cell.charge < 300)
+ to_chat(chap, span_notice("[GLOB.deity] does not accept pity amounts of power."))
return
- adjust_favor(round(the_cell.charge/300), chap)
- to_chat(chap, span_notice("You offer [the_cell]'s power to [GLOB.deity], pleasing them."))
- qdel(I)
+
+ adjust_favor(round(power_cell.charge/300), chap)
+ to_chat(chap, span_notice("You offer [power_cell]'s power to [GLOB.deity], pleasing them."))
+ qdel(power_cell)
return TRUE
/**** Pyre God ****/
diff --git a/code/modules/research/bepis.dm b/code/modules/research/bepis.dm
deleted file mode 100644
index d0640ed5d1c..00000000000
--- a/code/modules/research/bepis.dm
+++ /dev/null
@@ -1,296 +0,0 @@
-//This system is designed to act as an in-between for cargo and science, and the first major money sink in the game outside of just buying things from cargo (As of 10/9/19, anyway).
-
-//economics defined values, subject to change should anything be too high or low in practice.
-
-#define MACHINE_OPERATION 100000
-#define MACHINE_OVERLOAD 500000
-#define MAJOR_THRESHOLD (6*CARGO_CRATE_VALUE)
-#define MINOR_THRESHOLD (4*CARGO_CRATE_VALUE)
-#define STANDARD_DEVIATION (2*CARGO_CRATE_VALUE)
-#define PART_CASH_OFFSET_AMOUNT (0.5*CARGO_CRATE_VALUE)
-
-/obj/machinery/rnd/bepis
- name = "\improper B.E.P.I.S. Chamber"
- desc = "A high fidelity testing device which unlocks the secrets of the known universe using the two most powerful substances available to man: excessive amounts of electricity and capital."
- icon = 'icons/obj/machines/bepis.dmi'
- icon_state = "chamber"
- base_icon_state = "chamber"
- density = TRUE
- layer = ABOVE_MOB_LAYER
- plane = GAME_PLANE_UPPER
- circuit = /obj/item/circuitboard/machine/bepis
-
- ///How much cash the UI and machine are depositing at a time.
- var/banking_amount = 100
- ///How much stored player cash exists within the machine.
- var/banked_cash = 0
- ///Payer's bank account.
- var/datum/bank_account/account
- ///Name on the payer's bank account.
- var/account_name
- ///When the BEPIS fails to hand out any reward, the ERROR cause will be a randomly picked string displayed on the UI.
- var/error_cause = null
-
- //Vars related to probability and chance of success for testing, using gaussian normal distribution.
- ///How much cash you will need to obtain a Major Tech Disk reward.
- var/major_threshold = MAJOR_THRESHOLD
- ///How much cash you will need to obtain a minor invention reward.
- var/minor_threshold = MINOR_THRESHOLD
- ///The standard deviation of the BEPIS's gaussian normal distribution.
- var/std = STANDARD_DEVIATION
-
- //Stock part variables
- ///Multiplier that lowers how much the BEPIS' power costs are. Maximum of 1, upgraded to a minimum of 0.7. See RefreshParts.
- var/power_saver = 1
- ///Variability on the money you actively spend on the BEPIS, with higher inaccuracy making the most change, good and bad to spent cash.
- var/inaccuracy_percentage = 1.5
- ///How much "cash" is added to your inserted cash efforts for free. Based on manipulator stock part level.
- var/positive_cash_offset = 0
- ///How much "cost" is removed from both the minor and major threshold costs. Based on laser stock part level.
- var/negative_cash_offset = 0
- ///List of objects that constitute your minor rewards. All rewards are unique or rare outside of the BEPIS.
- var/minor_rewards = list(
- //To add a new minor reward, add it here.
- /obj/item/stack/circuit_stack/full,
- /obj/item/pen/survival,
- /obj/item/flashlight/flashdark,//SKYRAT EDIT
- /obj/item/circuitboard/machine/sleeper/party,
- /obj/item/toy/sprayoncan,
- )
-
-/obj/machinery/rnd/bepis/attackby(obj/item/O, mob/user, params)
- if(!is_operational)
- to_chat(user, span_notice("[src] can't accept money when it's not functioning."))
- return
- if(istype(O, /obj/item/holochip) || istype(O, /obj/item/stack/spacecash))
- var/deposit_value = O.get_item_credit_value()
- banked_cash += deposit_value
- qdel(O)
- say("Deposited [deposit_value] credits into storage.")
- update_appearance()
- return
- if(isidcard(O))
- var/obj/item/card/id/Card = O
- if(Card.registered_account)
- account = Card.registered_account
- account_name = Card.registered_name
- say("New account detected. Console Updated.")
- else
- say("No account detected on card. Aborting.")
- return
- return ..()
-
-/obj/machinery/rnd/bepis/screwdriver_act(mob/living/user, obj/item/tool)
- return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)
-
-/obj/machinery/rnd/bepis/screwdriver_act_secondary(mob/living/user, obj/item/tool)
- return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)
-
-/obj/machinery/rnd/bepis/RefreshParts()
- . = ..()
- var/C = 0
- var/M = 0
- var/L = 0
- var/S = 0
- for(var/datum/stock_part/capacitor/capacitor in component_parts)
- C += ((capacitor.tier - 1) * 0.1)
- power_saver = 1 - C
- for(var/datum/stock_part/servo/servo in component_parts)
- M += ((servo.tier - 1) * PART_CASH_OFFSET_AMOUNT)
- positive_cash_offset = M
- for(var/datum/stock_part/micro_laser/Laser in component_parts)
- L += ((Laser.tier - 1) * PART_CASH_OFFSET_AMOUNT)
- negative_cash_offset = L
- for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
- S += ((scanning_module.tier - 1) * 0.25)
- inaccuracy_percentage = (1.5 - S)
-
-/obj/machinery/rnd/bepis/update_icon_state()
- if(panel_open == TRUE)
- icon_state = "[base_icon_state]_open"
- return ..()
- if((use_power == ACTIVE_POWER_USE) && (banked_cash > 0) && (is_operational))
- icon_state = "[base_icon_state]_active_loaded"
- return ..()
- if (((use_power == IDLE_POWER_USE) && (banked_cash > 0)) || (banked_cash > 0) && (!is_operational))
- icon_state = "[base_icon_state]_loaded"
- return ..()
- if(use_power == ACTIVE_POWER_USE && is_operational)
- icon_state = "[base_icon_state]_active"
- return ..()
- if(((use_power == IDLE_POWER_USE) && (banked_cash == 0)) || (!is_operational))
- icon_state = base_icon_state
- return ..()
- return ..()
-
-/obj/machinery/rnd/bepis/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "Bepis", name)
- ui.open()
- RefreshParts()
- if(isliving(user))
- var/mob/living/customer = user
- account = customer.get_bank_account()
-
-/obj/machinery/rnd/bepis/ui_data(mob/user)
- var/list/data = list()
- var/powered = FALSE
- var/zvalue = ((banking_amount + banked_cash) - (major_threshold - positive_cash_offset - negative_cash_offset))/(std)
- var/std_success = 0
- var/prob_success = 0
- //Admittedly this is messy, but not nearly as messy as the alternative, which is jury-rigging an entire Z-table into the code, or making an adaptive z-table.
- var/z = abs(zvalue)
- if(z > 0 && z <= 0.5)
- std_success = 19.1
- else if(z > 0.5 && z <= 1.0)
- std_success = 34.1
- else if(z > 1.0 && z <= 1.5)
- std_success = 43.3
- else if(z > 1.5 && z <= 2.0)
- std_success = 47.7
- else if(z > 2.0 && z <= 2.5)
- std_success = 49.4
- else
- std_success = 50
- if(zvalue > 0)
- prob_success = 50 + std_success
- else if(zvalue == 0)
- prob_success = 50
- else
- prob_success = 50 - std_success
-
- if(use_power == ACTIVE_POWER_USE)
- powered = TRUE
- data["account_owner"] = account_name
- data["amount"] = banking_amount
- data["stored_cash"] = account?.account_balance
- data["mean_value"] = (major_threshold - positive_cash_offset - negative_cash_offset)
- data["error_name"] = error_cause
- data["power_saver"] = power_saver
- data["accuracy_percentage"] = inaccuracy_percentage * 100
- data["positive_cash_offset"] = positive_cash_offset
- data["negative_cash_offset"] = negative_cash_offset
- data["manual_power"] = powered ? FALSE : TRUE
- data["silicon_check"] = issilicon(user)
- data["success_estimate"] = prob_success
- return data
-
-/obj/machinery/rnd/bepis/ui_act(action,params)
- . = ..()
- if(.)
- return
- switch(action)
- if("begin_experiment")
- if(use_power == IDLE_POWER_USE)
- return
- depositcash()
- if(banked_cash == 0)
- say("Please select funds to deposit to begin testing.")
- return
- calcsuccess()
- use_power(MACHINE_OPERATION * power_saver) //This thing should eat your APC battery if you're not careful.
- update_use_power(IDLE_POWER_USE) //Machine shuts off after use to prevent spam and look better visually.
- update_appearance()
- if("amount")
- var/input = text2num(params["amount"])
- if(input)
- banking_amount = input
- if("toggle_power")
- if(use_power == ACTIVE_POWER_USE)
- update_use_power(IDLE_POWER_USE)
- else
- update_use_power(ACTIVE_POWER_USE)
- update_appearance()
- if("account_reset")
- if(use_power == IDLE_POWER_USE)
- return
- account_name = ""
- account = null
- say("Account settings reset.")
- . = TRUE
-
-/**
- * Proc that handles the user's account to deposit credits for the BEPIS.
- * Handles success and fail cases for transferring credits, then logs the transaction and uses small amounts of power.
- **/
-/obj/machinery/rnd/bepis/proc/depositcash()
- var/deposit_value = 0
- deposit_value = banking_amount
- if(deposit_value == 0)
- update_appearance()
- say("Attempting to deposit 0 credits. Aborting.")
- return
- deposit_value = clamp(round(deposit_value, 1), 1, 10000)
- if(!account)
- say("Cannot find user account. Please swipe a valid ID.")
- return
- if(!account.has_money(deposit_value))
- say("You do not possess enough credits.")
- return
- account.adjust_money(-deposit_value, "Vending: B.E.P.I.S. Chamber") //The money vanishes, not paid to any accounts.
- SSblackbox.record_feedback("amount", "BEPIS_credits_spent", deposit_value)
- log_econ("[deposit_value] credits were inserted into [src] by [account.account_holder]")
- banked_cash += deposit_value
- use_power(1000 * power_saver)
- return
-
-/**
- * Proc used to determine the experiment math and results all in one.
- * Uses banked_cash and stock part levels to determine minor, major, and real gauss values for the BEPIS to hold.
- * If by the end real is larger than major, You get a tech disk. If all the disks are earned or you at least beat minor, you get a minor reward.
- **/
-
-/obj/machinery/rnd/bepis/proc/calcsuccess()
- var/turf/dropturf = null
- var/gauss_major = 0
- var/gauss_minor = 0
- var/gauss_real = 0
-
- var/turf/my_turf = get_turf(src)
- var/list/turfs = TURF_NEIGHBORS(my_turf) //NO MORE DISCS IN WINDOWS
- while(length(turfs))
- var/turf/T = pick_n_take(turfs)
- if(T.is_blocked_turf(TRUE))
- continue
- else
- dropturf = T
- break
-
- if (!dropturf)
- dropturf = drop_location()
- gauss_major = (gaussian(major_threshold, std) - negative_cash_offset) //This is the randomized profit value that this experiment has to surpass to unlock a tech.
- gauss_minor = (gaussian(minor_threshold, std) - negative_cash_offset) //And this is the threshold to instead get a minor prize.
- gauss_real = (gaussian(banked_cash, std*inaccuracy_percentage) + positive_cash_offset) //this is the randomized profit value that your experiment expects to give.
- say("Real: [gauss_real]. Minor: [gauss_minor]. Major: [gauss_major].")
- flick("chamber_flash",src)
- update_appearance()
- banked_cash = 0
- if((gauss_real >= gauss_major)) //Major Success.
- if(SSresearch.techweb_nodes_experimental.len > 0)
- say("Experiment concluded with major success. New technology node discovered on technology disc.")
- new /obj/item/disk/design_disk/bepis/remove_tech(dropturf,1)
- return
- say("Expended all available experimental technology nodes. Resorting to minor rewards.")
- if(gauss_real >= gauss_minor) //Minor Success.
- var/reward = pick(minor_rewards)
- new reward(dropturf)
- say("Experiment concluded with partial success. Dispensing compiled research efforts.")
- return
- if(gauss_real <= -1) //Critical Failure
- say("ERROR: CRITICAL MACHIME MALFUNCTI- ON. CURRENCY IS NOT CRASH. CANNOT COMPUTE COMMAND: 'make bucks'") //not a typo, for once.
- new /mob/living/basic/deer(dropturf, 1)
- use_power(MACHINE_OVERLOAD * power_saver) //To prevent gambling at low cost and also prevent spamming for infinite deer.
- return
- //Minor Failure
- error_cause = pick("attempted to sell grey products to American dominated market.","attempted to sell gray products to British dominated market.","placed wild assumption that PDAs would go out of style.","simulated product #76 damaged brand reputation mortally.","simulated business model resembled 'pyramid scheme' by 98.7%.","product accidently granted override access to all station doors.")
- say("Experiment concluded with zero product viability. Cause of error: [error_cause]")
- return
-
-
-#undef MACHINE_OPERATION
-#undef MACHINE_OVERLOAD
-#undef MAJOR_THRESHOLD
-#undef MINOR_THRESHOLD
-#undef STANDARD_DEVIATION
-#undef PART_CASH_OFFSET_AMOUNT
diff --git a/code/modules/research/designs/biogenerator_designs.dm b/code/modules/research/designs/biogenerator_designs.dm
index f07ef21a8e7..14d5c12eb43 100644
--- a/code/modules/research/designs/biogenerator_designs.dm
+++ b/code/modules/research/designs/biogenerator_designs.dm
@@ -185,3 +185,11 @@
materials = list(/datum/material/biomass = 1)
build_path = /obj/item/rollingpaper
category = list(RND_CATEGORY_INITIAL, RND_CATEGORY_BIO_MATERIALS)
+
+/datum/design/candle
+ name = "Candle"
+ id = "candle"
+ build_type = BIOGENERATOR
+ materials = list(/datum/material/biomass = 3)
+ build_path = /obj/item/flashlight/flare/candle
+ category = list(RND_CATEGORY_INITIAL, RND_CATEGORY_BIO_MATERIALS)
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 1c44e8bc4fb..0a2c96dddd5 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -358,16 +358,6 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
-/datum/design/board/bepis
- name = "B.E.P.I.S. Board"
- desc = "The circuit board for a B.E.P.I.S."
- id = "bepis"
- build_path = /obj/item/circuitboard/machine/bepis
- category = list(
- RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_RESEARCH
- )
- departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_CARGO
-
/datum/design/board/protolathe
name = "Protolathe Board"
desc = "The circuit board for a protolathe."
@@ -1106,3 +1096,13 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ROBOTICS
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/board/fishing_portal_generator
+ name = "Fishing Portal Generator Board"
+ desc = "The circuit board for the fishing portal generator"
+ id = "fishing_portal_generator"
+ build_path = /obj/item/circuitboard/machine/fishing_portal_generator
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_SERVICE
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm
index 2bb779e318b..285878e7f26 100644
--- a/code/modules/research/designs/medical_designs.dm
+++ b/code/modules/research/designs/medical_designs.dm
@@ -1114,6 +1114,13 @@
surgery = /datum/surgery/advanced/wing_reconstruction
research_icon_state = "surgery_chest"
+/datum/design/surgery/advanced_plastic_surgery
+ name = "Advanced Plastic Surgery"
+ desc = "An advanced form of the plastic surgery, allowing oneself to remodel someone's face and voice based off a picture of someones face"
+ surgery = /datum/surgery/plastic_surgery/advanced
+ id = "surgery_advanced_plastic_surgery"
+ research_icon_state = "surgery_head"
+
/datum/design/surgery/experimental_dissection
name = "Experimental Dissection"
desc = "An experimental surgical procedure that dissects bodies in exchange for research points at ancient R&D consoles."
diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm
index 521504da351..8a06607ec59 100644
--- a/code/modules/research/techweb/_techweb.dm
+++ b/code/modules/research/techweb/_techweb.dm
@@ -283,7 +283,7 @@
var/datum/experiment/experiment = completed_experiment
if (experiment == experiment_type)
return FALSE
- available_experiments += new experiment_type()
+ available_experiments += new experiment_type(src)
/**
* Adds a list of experiments to this techweb by their types, ensures that no duplicates are added.
@@ -310,13 +310,21 @@
var/refund = skipped_experiment_types[completed_experiment.type] || 0
if(refund > 0)
add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = refund))
- result_text += ", refunding [refund] points."
+ result_text += ", refunding [refund] points"
// Nothing more to gain here, but we keep it in the list to prevent double dipping
skipped_experiment_types[completed_experiment.type] = -1
- else
- result_text += "!"
-
- log_research("[completed_experiment.name] ([completed_experiment.type]) has been completed on techweb [id]/[organization][refund ? ", refunding [refund] points" : ""].")
+ var/points_rewarded
+ if(completed_experiment.points_reward)
+ add_point_list(completed_experiment.points_reward)
+ points_rewarded = ",[refund > 0 ? " and" : ""] rewarding "
+ var/list/english_list_keys = list()
+ for(var/points_type in completed_experiment.points_reward)
+ english_list_keys += "[completed_experiment.points_reward[points_type]] [points_type]"
+ points_rewarded += "[english_list(english_list_keys)] points"
+ result_text += points_rewarded
+ result_text += "!"
+
+ log_research("[completed_experiment.name] ([completed_experiment.type]) has been completed on techweb [id]/[organization][refund ? ", refunding [refund] points" : ""][points_rewarded].")
return result_text
/datum/techweb/proc/printout_points()
diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm
index 2f01252548a..ae50ea7f65f 100644
--- a/code/modules/research/techweb/_techweb_node.dm
+++ b/code/modules/research/techweb/_techweb_node.dm
@@ -16,7 +16,7 @@
var/description = "Why are you seeing this?"
/// Whether it starts off hidden
var/hidden = FALSE
- /// If the tech can be randomly generated by the BEPIS as a reward. MEant to be fully given in tech disks, not researched
+ /// If the tech can be randomly generated by BEPIS tech as a reward. Meant to be fully given in tech disks, not researched
var/experimental = FALSE
/// Whether it's available without any research
var/starting_node = FALSE
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 2ec692f0575..507199164b8 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -14,7 +14,6 @@
"basic_matter_bin",
"basic_micro_laser",
"basic_scanning",
- "bepis",
"blast",
"bounced_radio",
"bowl",
@@ -50,6 +49,7 @@
"extinguisher",
"fax",
"fishing_rod",
+ "fishing_portal_generator",
"flashlight",
"fluid_ducts",
"foam_dart",
@@ -1491,6 +1491,19 @@
required_experiments = list(/datum/experiment/scanning/random/plants/wild)
discount_experiments = list(/datum/experiment/scanning/random/plants/traits = 3000)
+/datum/techweb_node/fishing
+ id = "fishing"
+ display_name = "Fishing Technology"
+ description = "Cutting edge fishing advancements."
+ prereq_ids = list("base")
+ design_ids = list(
+ "fishing_rod_tech",
+ "stabilized_hook",
+ "fish_analyzer",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2000)
+ required_experiments = list(/datum/experiment/scanning/fish)
+
/datum/techweb_node/exp_tools
id = "exp_tools"
display_name = "Experimental Tools"
@@ -2355,15 +2368,13 @@
hidden = TRUE
experimental = TRUE
-/datum/techweb_node/fishing
- id = "fishing"
- display_name = "Fishing Technology"
- description = "Cutting edge fishing advancements."
+/datum/techweb_node/advanced_plastic_surgery
+ id = "plastic_surgery"
+ display_name = "Advanced Plastic Surgery"
+ description = "A Procedure long lost due to licensing problems now once again available."
prereq_ids = list("base")
design_ids = list(
- "fishing_rod_tech",
- "stabilized_hook",
- "fish_analyzer",
+ "surgery_advanced_plastic_surgery"
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
hidden = TRUE
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
index abb764d0689..0152b343c45 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
@@ -223,7 +223,7 @@
/datum/reagent/consumable/corn_syrup = -6,
/datum/reagent/sulfur = -3) //sulfur repels snakes according to professor google.
- resulting_atoms = list(/mob/living/simple_animal/hostile/retaliate/snake = 1)
+ resulting_atoms = list(/mob/living/basic/snake = 1)
///////////////////////////////////////////
@@ -263,7 +263,7 @@
/datum/reagent/medicine/psicodine = -2) //Blob zombies likely wouldn't appreciate psicodine so why this is here
virus_suspectibility = 0
- resulting_atoms = list(/mob/living/simple_animal/hostile/blob/blobspore/independent = 2) //These are useless so we might as well spawn 2.
+ resulting_atoms = list(/mob/living/basic/blob_minion/spore = 2) //These are useless so we might as well spawn 2.
/datum/micro_organism/cell_line/blobbernaut
desc = "Blobular myocytes"
@@ -282,7 +282,7 @@
suppressive_reagents = list(/datum/reagent/consumable/tinlux = -6)
virus_suspectibility = 0
- resulting_atoms = list(/mob/living/simple_animal/hostile/blob/blobbernaut/independent = 1)
+ resulting_atoms = list(/mob/living/basic/blob_minion/blobbernaut = 1)
/datum/micro_organism/cell_line/gelatinous_cube
desc = "Cubic ooze particles"
diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm
index aba37b37a55..cdabfdc4926 100644
--- a/code/modules/shuttle/supply.dm
+++ b/code/modules/shuttle/supply.dm
@@ -61,17 +61,61 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
/obj/docking_port/mobile/supply/proc/check_blacklist(areaInstances)
for(var/place in areaInstances)
var/area/shuttle/shuttle_area = place
- for(var/turf/shuttle_turf in shuttle_area)
+ for(var/turf/shuttle_turf in shuttle_area.get_contained_turfs())
for(var/atom/passenger in shuttle_turf.get_all_contents())
if((is_type_in_typecache(passenger, GLOB.blacklisted_cargo_types) || HAS_TRAIT(passenger, TRAIT_BANNED_FROM_CARGO_SHUTTLE)) && !istype(passenger, /obj/docking_port))
return FALSE
return TRUE
+/// Returns anything on the cargo blacklist found within areas_to_check back to the turf of the home docking port via Centcom branded supply pod.
+/obj/docking_port/mobile/supply/proc/return_blacklisted_things_home(list/area/areas_to_check, obj/docking_port/stationary/home)
+ var/list/stuff_to_send_home = list()
+ for(var/area/shuttle_area as anything in areas_to_check)
+ for(var/turf/shuttle_turf in shuttle_area.get_contained_turfs())
+ for(var/atom/passenger in shuttle_turf.get_all_contents())
+ if((is_type_in_typecache(passenger, GLOB.blacklisted_cargo_types) || HAS_TRAIT(passenger, TRAIT_BANNED_FROM_CARGO_SHUTTLE)) && !istype(passenger, /obj/docking_port))
+ stuff_to_send_home += passenger
+
+ if(!length(stuff_to_send_home))
+ return FALSE
+
+ var/obj/structure/closet/supplypod/centcompod/et_go_home = new()
+
+ for(var/atom/movable/et as anything in stuff_to_send_home)
+ et.forceMove(et_go_home)
+
+ new /obj/effect/pod_landingzone(get_turf(home), et_go_home)
+
+ return stuff_to_send_home
+
/obj/docking_port/mobile/supply/request(obj/docking_port/stationary/S)
if(mode != SHUTTLE_IDLE)
return 2
return ..()
+/obj/docking_port/mobile/supply/check_dock(obj/docking_port/stationary/S, silent)
+ . = ..()
+
+ if(!.)
+ return
+
+ // If we're not trying to dock at Centcom, we don't care.
+ if(S.shuttle_id != "cargo_away")
+ return
+
+ // Else we are docking at Centcom, check the blacklist to make sure no contraband was put onto the shuttle mid-transit.
+ // If there's anything contrabandy, send these items back to the origin docking port.
+ // This is a sort of catch-all Centcom exploit check.
+ var/list/stuff_sent_home = return_blacklisted_things_home(shuttle_areas, previous)
+ if(!length(stuff_sent_home))
+ return
+
+ for(var/atom/thing_sent_home as anything in stuff_sent_home)
+ investigate_log("Blacklisted item found on in-transit Cargo Shuttle: [thing_sent_home] ([thing_sent_home.type])", INVESTIGATE_CARGO)
+
+ message_admins("Blacklisted item found on in-transit Cargo Shuttle. See cargo logs for more details.")
+ SSshuttle.centcom_message = "Contraband found on Cargo Shuttle. This has been returned via drop pod."
+
/obj/docking_port/mobile/supply/initiate_docking()
if(getDockedId() == "cargo_away") // Buy when we leave home.
buy()
@@ -278,5 +322,17 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
new /obj/structure/closet/crate/mail/economy(pick(empty_turfs))
+/// Takes a supply pack, returns the amount we currently have on order (or OVER_ORDER_LIMIT if we are over the hardcap on orders of this type)
+/obj/docking_port/mobile/supply/proc/get_order_count(datum/supply_pack/ordering)
+ var/similar_count = 0
+ for(var/datum/supply_order/order as anything in (SSshuttle.shopping_list | SSshuttle.request_list))
+ if(order.pack == ordering)
+ similar_count += 1
+
+ if(similar_count >= CARGO_MAX_ORDER)
+ return OVER_ORDER_LIMIT
+
+ return similar_count
+
#undef GOODY_FREE_SHIPPING_MAX
#undef CRATE_TAX
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 966f618376d..f03cd4927f8 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -180,11 +180,6 @@
to_chat(owner, span_warning("Some form of antimagic is preventing you from casting [src]!"))
return FALSE
- if(!(spell_requirements & SPELL_CASTABLE_WHILE_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED))
- if(feedback)
- to_chat(owner, span_warning("[src] cannot be cast unless you are completely manifested in the material plane!"))
- return FALSE
-
if(!try_invoke(owner, feedback = feedback))
return FALSE
diff --git a/code/modules/spells/spell_types/jaunt/_jaunt.dm b/code/modules/spells/spell_types/jaunt/_jaunt.dm
index 4a94f03c041..207a7ed8b5b 100644
--- a/code/modules/spells/spell_types/jaunt/_jaunt.dm
+++ b/code/modules/spells/spell_types/jaunt/_jaunt.dm
@@ -62,7 +62,7 @@
var/obj/effect/dummy/phased_mob/jaunt = new jaunt_type(loc_override || get_turf(jaunter), jaunter)
RegisterSignal(jaunt, COMSIG_MOB_EJECTED_FROM_JAUNT, PROC_REF(on_jaunt_exited))
- spell_requirements |= SPELL_CASTABLE_WHILE_PHASED
+ check_flags &= ~AB_CHECK_PHASED
jaunter.add_traits(list(TRAIT_MAGICALLY_PHASED, TRAIT_RUNECHAT_HIDDEN, TRAIT_WEATHER_IMMUNE), REF(src))
// Don't do the feedback until we have runechat hidden.
// Otherwise the text will follow the jaunt holder, which reveals where our caster is travelling.
@@ -106,7 +106,7 @@
*/
/datum/action/cooldown/spell/jaunt/proc/on_jaunt_exited(obj/effect/dummy/phased_mob/jaunt, mob/living/unjaunter)
SHOULD_CALL_PARENT(TRUE)
- spell_requirements &= ~SPELL_CASTABLE_WHILE_PHASED
+ check_flags |= AB_CHECK_PHASED
unjaunter.remove_traits(list(TRAIT_MAGICALLY_PHASED, TRAIT_RUNECHAT_HIDDEN, TRAIT_WEATHER_IMMUNE), REF(src))
// This needs to happen at the end, after all the traits and stuff is handled
SEND_SIGNAL(unjaunter, COMSIG_MOB_AFTER_EXIT_JAUNT, src)
diff --git a/code/modules/spells/spell_types/self/splattercasting_spell.dm b/code/modules/spells/spell_types/self/splattercasting_spell.dm
index 1af0cacd2aa..184a2afab7c 100644
--- a/code/modules/spells/spell_types/self/splattercasting_spell.dm
+++ b/code/modules/spells/spell_types/self/splattercasting_spell.dm
@@ -29,6 +29,7 @@
merely a vessel for the arcane flow. Soon, all that is left is not pain, but hunger."))
cast_on.set_species(/datum/species/vampire)
+ cast_on.blood_volume = BLOOD_VOLUME_NORMAL ///for predictable blood total amounts when the spell is first cast.
cast_on.AddComponent(/datum/component/splattercasting)
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 4b9c0114024..c6d0f25cb4c 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -152,7 +152,7 @@
/// How much generic bleedstacks we have on this bodypart
var/generic_bleedstacks
/// If we have a gauze wrapping currently applied (not including splints)
- var/obj/item/stack/current_gauze
+ var/obj/item/stack/medical/gauze/current_gauze
/// If something is currently grasping this bodypart and trying to staunch bleeding (see [/obj/item/hand_item/self_grasp])
var/obj/item/hand_item/self_grasp/grasped_by
@@ -408,10 +408,10 @@
var/atom/drop_loc = drop_location()
if(IS_ORGANIC_LIMB(src))
playsound(drop_loc, 'sound/misc/splort.ogg', 50, TRUE, -1)
- seep_gauze(9999) // destroy any existing gauze if any exists
- for(var/obj/item/organ/bodypart_organ in get_organs())
+ QDEL_NULL(current_gauze)
+ for(var/obj/item/organ/bodypart_organ as anything in get_organs())
bodypart_organ.transfer_to_limb(src, owner)
- for(var/obj/item/organ/external/external in external_organs)
+ for(var/obj/item/organ/external/external as anything in external_organs)
external.remove_from_limb()
external.forceMove(drop_loc)
for(var/obj/item/item_in_bodypart in src)
@@ -1308,17 +1308,18 @@
* Arguments:
* * gauze- Just the gauze stack we're taking a sheet from to apply here
*/
-/obj/item/bodypart/proc/apply_gauze(obj/item/stack/gauze)
- if(!istype(gauze) || !gauze.absorption_capacity)
+/obj/item/bodypart/proc/apply_gauze(obj/item/stack/medical/gauze/new_gauze)
+ if(!istype(new_gauze) || !new_gauze.absorption_capacity)
return
var/newly_gauzed = FALSE
if(!current_gauze)
newly_gauzed = TRUE
QDEL_NULL(current_gauze)
- current_gauze = new gauze.type(src, 1)
- gauze.use(1)
+ current_gauze = new new_gauze.type(src, 1)
+ new_gauze.use(1)
+ current_gauze.gauzed_bodypart = src
if(newly_gauzed)
- SEND_SIGNAL(src, COMSIG_BODYPART_GAUZED, gauze)
+ SEND_SIGNAL(src, COMSIG_BODYPART_GAUZED, current_gauze, new_gauze)
/**
* seep_gauze() is for when a gauze wrapping absorbs blood or pus from wounds, lowering its absorption capacity.
@@ -1335,7 +1336,6 @@
if(current_gauze.absorption_capacity <= 0)
owner.visible_message(span_danger("\The [current_gauze.name] on [owner]'s [name] falls away in rags."), span_warning("\The [current_gauze.name] on your [name] falls away in rags."), vision_distance=COMBAT_MESSAGE_RANGE)
QDEL_NULL(current_gauze)
- SEND_SIGNAL(src, COMSIG_BODYPART_GAUZE_DESTROYED)
///Loops through all of the bodypart's external organs and update's their color.
/obj/item/bodypart/proc/recolor_external_organs()
diff --git a/code/modules/surgery/bodyparts/wounds.dm b/code/modules/surgery/bodyparts/wounds.dm
index 1b50dbc8fd1..94c503614a2 100644
--- a/code/modules/surgery/bodyparts/wounds.dm
+++ b/code/modules/surgery/bodyparts/wounds.dm
@@ -85,7 +85,7 @@
// quick re-check to see if bare_wound_bonus applies, for the benefit of log_wound(), see about getting the check from check_woundings_mods() somehow
if(ishuman(owner))
var/mob/living/carbon/human/human_wearer = owner
- var/list/clothing = human_wearer.clothingonpart(src)
+ var/list/clothing = human_wearer.get_clothing_on_part(src)
for(var/obj/item/clothing/clothes_check as anything in clothing)
// unlike normal armor checks, we tabluate these piece-by-piece manually so we can also pass on appropriate damage the clothing's limbs if necessary
if(clothes_check.get_armor_rating(WOUND))
@@ -242,7 +242,7 @@
if(owner && ishuman(owner))
var/mob/living/carbon/human/human_owner = owner
- var/list/clothing = human_owner.clothingonpart(src)
+ var/list/clothing = human_owner.get_clothing_on_part(src)
for(var/obj/item/clothing/clothes as anything in clothing)
// unlike normal armor checks, we tabluate these piece-by-piece manually so we can also pass on appropriate damage the clothing's limbs if necessary
armor_ablation += clothes.get_armor_rating(WOUND)
diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm
index 04103648fda..632e4c8b511 100644
--- a/code/modules/surgery/organs/_organ.dm
+++ b/code/modules/surgery/organs/_organ.dm
@@ -57,6 +57,8 @@
var/list/organ_traits
/// Status Effects that are given to the holder of the organ.
var/list/organ_effects
+ /// String displayed when the organ has decayed.
+ var/failing_desc = "has decayed for too long, and has turned a sickly color. It probably won't work without repairs."
// Players can look at prefs before atoms SS init, and without this
// they would not be able to see external organs, such as moth wings.
@@ -242,10 +244,7 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
. += span_notice("It should be inserted in the [parse_zone(zone)].")
if(organ_flags & ORGAN_FAILING)
- if(IS_ROBOTIC_ORGAN(src))
- . += span_warning("[src] seems to be broken.")
- return
- . += span_warning("[src] has decayed for too long, and has turned a sickly color. It probably won't work without repairs.")
+ . += span_warning("[src] [failing_desc]")
return
if(damage > high_threshold)
@@ -424,4 +423,4 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
/// Tries to replace the existing organ on the passed mob with this one, with special handling for replacing a brain without ghosting target
/obj/item/organ/proc/replace_into(mob/living/carbon/new_owner)
- Insert(new_owner, special = TRUE, drop_if_replaced = FALSE)
+ return Insert(new_owner, special = TRUE, drop_if_replaced = FALSE)
diff --git a/code/modules/surgery/organs/external/_external_organ.dm b/code/modules/surgery/organs/external/_external_organ.dm
index 54eb937e0ae..fd1af3d5f93 100644
--- a/code/modules/surgery/organs/external/_external_organ.dm
+++ b/code/modules/surgery/organs/external/_external_organ.dm
@@ -82,14 +82,13 @@
return
if(bodypart_overlay.imprint_on_next_insertion) //We only want this set *once*
-
- // SKYRAT EDIT - Customization - ORIGINAL: bodypart_overlay.set_appearance_from_name(receiver.dna.features[bodypart_overlay.feature_key])
- if(receiver.dna.features[bodypart_overlay.feature_key])
- bodypart_overlay.set_appearance_from_name(receiver.dna.features[bodypart_overlay.feature_key])
-
+ var/feature_name = receiver.dna.features[bodypart_overlay.feature_key]
+ if (isnull(feature_name))
+ bodypart_overlay.set_appearance_from_dna(receiver.dna) // SKYRAT EDIT CHANGE - ORIGINAL: feature_name = receiver.dna.species.external_organs[type]
+ // SKYRAT EDIT CHANGE START - Puts the following line in an else block
else
- bodypart_overlay.set_appearance_from_dna(receiver.dna)
- // SKYRAT EDIT END
+ bodypart_overlay.set_appearance_from_name(feature_name)
+ // SKYRAT EDIT CHANGE END
bodypart_overlay.imprint_on_next_insertion = FALSE
ownerlimb = limb
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
index 0a7332c0dd8..f0578832969 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
@@ -4,6 +4,7 @@
desc = "A state-of-the-art implant that improves a baseline's functionality."
visual = FALSE
organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
var/implant_color = "#FFFFFF"
var/implant_overlay
diff --git a/code/modules/surgery/organs/internal/ears/_ears.dm b/code/modules/surgery/organs/internal/ears/_ears.dm
index 52f5d740520..b77efe90c2e 100644
--- a/code/modules/surgery/organs/internal/ears/_ears.dm
+++ b/code/modules/surgery/organs/internal/ears/_ears.dm
@@ -109,6 +109,7 @@
desc = "A basic cybernetic organ designed to mimic the operation of ears."
damage_multiplier = 0.9
organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
/obj/item/organ/internal/ears/cybernetic/upgraded
name = "cybernetic ears"
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index f2365c3988e..aca812a6186 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -318,6 +318,7 @@
icon_state = "cybernetic_eyeballs"
desc = "Your vision is augmented."
organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
/obj/item/organ/internal/eyes/robotic/emp_act(severity)
. = ..()
@@ -656,7 +657,7 @@
if(QDELETED(eye_owner) || !ishuman(eye_owner)) //Other carbon mobs don't have eye color.
return
-
+
if(!eye.on)
eye_icon_state = initial(eye_icon_state)
overlay_ignore_lighting = FALSE
diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm
index ce4b948c653..e553f8f1307 100644
--- a/code/modules/surgery/organs/internal/heart/_heart.dm
+++ b/code/modules/surgery/organs/internal/heart/_heart.dm
@@ -203,6 +203,7 @@
base_icon_state = "heart-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD*0.75 //This also hits defib timer, so a bit higher than its less important counterparts
+ failing_desc = "seems to be broken."
var/dose_available = FALSE
var/rid = /datum/reagent/medicine/epinephrine
diff --git a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
index f29ee2e731d..bb0b30ddd62 100644
--- a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
+++ b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm
@@ -108,7 +108,7 @@
if(!COOLDOWN_FINISHED(src, crystalize_cooldown) || ethereal.stat != DEAD)
return //Should probably not happen, but lets be safe.
- if(ismob(location) || isitem(location) || HAS_TRAIT_FROM(src, TRAIT_HUSK, CHANGELING_DRAIN)) //Stops crystallization if they are eaten by a dragon, turned into a legion, consumed by his grace, etc.
+ if(ismob(location) || isitem(location) || iseffect(location) || HAS_TRAIT_FROM(src, TRAIT_HUSK, CHANGELING_DRAIN)) //Stops crystallization if they are eaten by a dragon, turned into a legion, consumed by his grace, etc.
to_chat(ethereal, span_warning("You were unable to finish your crystallization, for obvious reasons."))
stop_crystalization_process(ethereal, FALSE)
return
diff --git a/code/modules/surgery/organs/internal/liver/_liver.dm b/code/modules/surgery/organs/internal/liver/_liver.dm
index fe5ca01df4f..7baeb04b8b6 100644
--- a/code/modules/surgery/organs/internal/liver/_liver.dm
+++ b/code/modules/surgery/organs/internal/liver/_liver.dm
@@ -245,6 +245,7 @@
/obj/item/organ/internal/liver/cybernetic
name = "basic cybernetic liver"
desc = "A very basic device designed to mimic the functions of a human liver. Handles toxins slightly worse than an organic liver."
+ failing_desc = "seems to be broken."
icon_state = "liver-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD*0.5
diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm
index 5e4e0648067..71dc305ac5f 100644
--- a/code/modules/surgery/organs/internal/lungs/_lungs.dm
+++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm
@@ -837,6 +837,7 @@
/obj/item/organ/internal/lungs/cybernetic
name = "basic cybernetic lungs"
desc = "A basic cybernetic version of the lungs found in traditional humanoid entities."
+ failing_desc = "seems to be broken."
icon_state = "lungs-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD * 0.5
diff --git a/code/modules/surgery/organs/internal/stomach/_stomach.dm b/code/modules/surgery/organs/internal/stomach/_stomach.dm
index bebeaacf110..e7d22a3ece6 100644
--- a/code/modules/surgery/organs/internal/stomach/_stomach.dm
+++ b/code/modules/surgery/organs/internal/stomach/_stomach.dm
@@ -289,6 +289,7 @@
/obj/item/organ/internal/stomach/cybernetic
name = "basic cybernetic stomach"
desc = "A basic device designed to mimic the functions of a human stomach"
+ failing_desc = "seems to be broken."
icon_state = "stomach-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD * 0.5
diff --git a/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm b/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm
index 4d43b6a3a0a..4cc8ee404c1 100644
--- a/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm
+++ b/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm
@@ -92,7 +92,7 @@
playsound(carbon, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
carbon.cut_overlay(overcharge)
- tesla_zap(carbon, 2, crystal_charge*2.5, ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN | ZAP_ALLOW_DUPLICATES)
+ tesla_zap(carbon, 2, crystal_charge * 1e3, ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN | ZAP_ALLOW_DUPLICATES)
adjust_charge(ETHEREAL_CHARGE_FULL - crystal_charge)
carbon.visible_message(span_danger("[carbon] violently discharges energy!"), span_warning("You violently discharge energy!"))
diff --git a/code/modules/surgery/organs/internal/tongue/_tongue.dm b/code/modules/surgery/organs/internal/tongue/_tongue.dm
index e9c47a0f237..08ee3d20faf 100644
--- a/code/modules/surgery/organs/internal/tongue/_tongue.dm
+++ b/code/modules/surgery/organs/internal/tongue/_tongue.dm
@@ -190,7 +190,7 @@
languages_native = list(/datum/language/draconic, /datum/language/ashtongue) //SKYRAT EDIT: Ashtongue for Ashwalkers
liked_foodtypes = GORE | MEAT | SEAFOOD | NUTS | BUGS
disliked_foodtypes = GRAIN | DAIRY | CLOTH | GROSS
-
+ voice_filter = @{"[0:a] asplit [out0][out2]; [out0] asetrate=%SAMPLE_RATE%*0.9,aresample=%SAMPLE_RATE%,atempo=1/0.9,aformat=channel_layouts=mono,volume=0.2 [p0]; [out2] asetrate=%SAMPLE_RATE%*1.1,aresample=%SAMPLE_RATE%,atempo=1/1.1,aformat=channel_layouts=mono,volume=0.2[p2]; [p0][0][p2] amix=inputs=3"}
/obj/item/organ/internal/tongue/lizard/modify_speech(datum/source, list/speech_args)
var/static/regex/lizard_hiss = new("s+", "g")
var/static/regex/lizard_hiSS = new("S+", "g")
@@ -499,7 +499,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
say_mod = "hisses"
taste_sensitivity = 10 // LIZARDS ARE ALIENS CONFIRMED
modifies_speech = TRUE // not really, they just hiss
-
+ voice_filter = @{"[0:a] asplit [out0][out2]; [out0] asetrate=%SAMPLE_RATE%*0.8,aresample=%SAMPLE_RATE%,atempo=1/0.8,aformat=channel_layouts=mono [p0]; [out2] asetrate=%SAMPLE_RATE%*1.2,aresample=%SAMPLE_RATE%,atempo=1/1.2,aformat=channel_layouts=mono[p2]; [p0][0][p2] amix=inputs=3"}
// Aliens can only speak alien and a few other languages.
/obj/item/organ/internal/tongue/alien/get_possible_languages()
return list(
@@ -560,6 +560,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
/obj/item/organ/internal/tongue/robot
name = "robotic voicebox"
desc = "A voice synthesizer that can interface with organic lifeforms."
+ failing_desc = "seems to be broken."
organ_flags = ORGAN_ROBOTIC
icon_state = "tonguerobot"
say_mod = "states"
@@ -607,6 +608,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
toxic_foodtypes = NONE //no food is particularly toxic to ethereals
attack_verb_continuous = list("shocks", "jolts", "zaps")
attack_verb_simple = list("shock", "jolt", "zap")
+ voice_filter = @{"[0:a] asplit [out0][out2]; [out0] asetrate=%SAMPLE_RATE%*0.99,aresample=%SAMPLE_RATE%,volume=0.3 [p0]; [p0][out2] amix=inputs=2"}
// Ethereal tongues can speak all default + voltaic
/obj/item/organ/internal/tongue/ethereal/get_possible_languages()
diff --git a/code/modules/surgery/plastic_surgery.dm b/code/modules/surgery/plastic_surgery.dm
index 440d48d1c5e..424251143c3 100644
--- a/code/modules/surgery/plastic_surgery.dm
+++ b/code/modules/surgery/plastic_surgery.dm
@@ -1,3 +1,9 @@
+/// Disk containing info for doing advanced plastic surgery. Spawns in maint and available as a role-restricted item in traitor uplinks.
+/obj/item/disk/surgery/advanced_plastic_surgery
+ name = "Advanced Plastic Surgery Disk"
+ desc = "The disk provides instructions on how to do an Advanced Plastic Surgery, this surgery allows one-self to completely remake someone's face with that of another. Provided they have a picture of them in their offhand when reshaping the face. With the surgery long becoming obsolete with the rise of genetics technology. This item became an antique to many collectors, With only the cheaper and easier basic form of plastic surgery remaining in use in most places."
+ surgeries = list(/datum/surgery/plastic_surgery/advanced)
+
/datum/surgery/plastic_surgery
name = "Plastic surgery"
surgery_flags = SURGERY_REQUIRE_RESTING | SURGERY_REQUIRE_LIMB | SURGERY_REQUIRES_REAL_LIMB | SURGERY_MORBID_CURIOSITY
@@ -9,6 +15,41 @@
/datum/surgery_step/close,
)
+/datum/surgery/plastic_surgery/advanced
+ name = "advanced plastic surgery"
+ steps = list(
+ /datum/surgery_step/incise,
+ /datum/surgery_step/retract_skin,
+ /datum/surgery_step/insert_plastic,
+ /datum/surgery_step/reshape_face,
+ /datum/surgery_step/close,
+ )
+
+//Insert plastic step, It ain't called plastic surgery for nothing! :)
+/datum/surgery_step/insert_plastic
+ name = "insert plastic (plastic)"
+ implements = list(
+ /obj/item/stack/sheet/plastic = 100,
+ /obj/item/stack/sheet/meat = 100)
+ time = 3.2 SECONDS
+ preop_sound = 'sound/effects/blobattack.ogg'
+ success_sound = 'sound/effects/attackblob.ogg'
+ failure_sound = 'sound/effects/blobattack.ogg'
+
+/datum/surgery_step/insert_plastic/preop(mob/user, mob/living/target, target_zone, obj/item/stack/tool, datum/surgery/surgery)
+ display_results(
+ user,
+ target,
+ span_notice("You begin to insert [tool] into the incision in [target]'s [parse_zone(target_zone)]..."),
+ span_notice("[user] begins to insert [tool] into the incision in [target]'s [parse_zone(target_zone)]."),
+ span_notice("[user] begins to insert [tool] into the incision in [target]'s [parse_zone(target_zone)]."),
+ )
+ display_pain(target, "You feel something inserting just below the skin in your [parse_zone(target_zone)].")
+
+/datum/surgery_step/insert_plastic/success(mob/user, mob/living/target, target_zone, obj/item/stack/tool, datum/surgery/surgery, default_display_results)
+ . = ..()
+ tool.use(1)
+
//reshape_face
/datum/surgery_step/reshape_face
name = "reshape face (scalpel)"
@@ -43,8 +84,15 @@
else
var/list/names = list()
if(!isabductor(user))
- for(var/i in 1 to 10)
- names += target.dna.species.random_name(target.gender, TRUE)
+ var/obj/item/offhand = user.get_inactive_held_item()
+ if(istype(offhand, /obj/item/photo) && istype(surgery, /datum/surgery/plastic_surgery/advanced))
+ var/obj/item/photo/disguises = offhand
+ for(var/namelist as anything in disguises.picture?.names_seen)
+ names += namelist
+ else
+ user.visible_message(span_warning("You have no picture to base the appearance on, reverting to random appearances."))
+ for(var/i in 1 to 10)
+ names += target.dna.species.random_name(target.gender, TRUE)
else
for(var/_i in 1 to 9)
names += "Subject [target.gender == MALE ? "i" : "o"]-[pick("a", "b", "c", "d", "e")]-[rand(10000, 99999)]"
diff --git a/code/modules/surgery/tools.dm b/code/modules/surgery/tools.dm
index 49fbecc2271..c5f62883b5f 100644
--- a/code/modules/surgery/tools.dm
+++ b/code/modules/surgery/tools.dm
@@ -259,6 +259,12 @@
butcher_sound = 'sound/weapons/circsawhit.ogg', \
)
//saws are very accurate and fast at butchering
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/chainsaw)
+
+ AddComponent(
+ /datum/component/slapcrafting,\
+ slapcraft_recipes = slapcraft_recipe_list,\
+ )
/obj/item/circular_saw/get_surgery_tool_overlay(tray_extended)
return surgical_tray_overlay
diff --git a/code/modules/unit_tests/fish_unit_tests.dm b/code/modules/unit_tests/fish_unit_tests.dm
index 1ef15f8d0f5..d0d39227f43 100644
--- a/code/modules/unit_tests/fish_unit_tests.dm
+++ b/code/modules/unit_tests/fish_unit_tests.dm
@@ -28,10 +28,10 @@
var/obj/structure/aquarium/traits/aquarium = allocate(/obj/structure/aquarium/traits)
TEST_ASSERT(!aquarium.sterile.try_to_reproduce(), "The test aquarium's sterile fish managed to reproduce when it shouldn't have")
var/obj/item/fish/crossbreeder_jr = aquarium.crossbreeder.try_to_reproduce()
- TEST_ASSERT(crossbreeder_jr, "The test aquarium's crossbreeder fish didn't manage to reproduce when it should have.")
+ TEST_ASSERT(crossbreeder_jr, "The test aquarium's crossbreeder fish didn't manage to reproduce when it should have")
TEST_ASSERT_EQUAL(crossbreeder_jr.type, aquarium.cloner.type, "The test aquarium's crossbreeder fish mated with the wrong type of fish")
var/obj/item/fish/cloner_jr = aquarium.cloner.try_to_reproduce()
- TEST_ASSERT(cloner_jr, "The test aquarium's cloner fish didn't manage to reproduce when it should have.")
+ TEST_ASSERT(cloner_jr, "The test aquarium's cloner fish didn't manage to reproduce when it should have")
TEST_ASSERT_NOTEQUAL(cloner_jr.type, aquarium.sterile.type, "The test aquarium's cloner fish mated with the sterile fish")
///Checks that fish evolutions work correctly.
@@ -41,11 +41,24 @@
var/obj/structure/aquarium/evolution/aquarium = allocate(/obj/structure/aquarium/evolution)
var/obj/item/fish/evolve_jr = aquarium.evolve.try_to_reproduce()
TEST_ASSERT(evolve_jr, "The test aquarium's evolution fish didn't manage to reproduce when it should have")
- TEST_ASSERT_NOTEQUAL(evolve_jr.type, /obj/item/fish/goldfish, "The test aquarium's evolution fish managed to pass the conditions of an impossible evolution.")
+ TEST_ASSERT_NOTEQUAL(evolve_jr.type, /obj/item/fish/goldfish, "The test aquarium's evolution fish managed to pass the conditions of an impossible evolution")
TEST_ASSERT_EQUAL(evolve_jr.type, /obj/item/fish/clownfish, "The test aquarium's evolution fish's offspring isn't of the expected type")
TEST_ASSERT(!(/datum/fish_trait/dummy in evolve_jr.fish_traits), "The test aquarium's evolution fish's offspring still has the old trait that ought to be removed by the evolution datum")
TEST_ASSERT(/datum/fish_trait/dummy/two in evolve_jr.fish_traits, "The test aquarium's evolution fish's offspring doesn't have the evolution trait")
+/datum/unit_test/fish_scanning
+
+/datum/unit_test/fish_scanning/Run()
+ var/scannable_fishes = 0
+ for(var/obj/item/fish/fish_prototype as anything in subtypesof(/obj/item/fish))
+ if(initial(fish_prototype.experisci_scannable))
+ scannable_fishes++
+ for(var/datum/experiment/scanning/fish/fish_scan as anything in typesof(/datum/experiment/scanning/fish))
+ fish_scan = new fish_scan
+ var/scan_key = fish_scan.required_atoms[1]
+ if(fish_scan.required_atoms[scan_key] > scannable_fishes)
+ TEST_FAIL("[fish_scan.type] has requirements higher than the number of scannable fish types in the game: [scannable_fishes]")
+
///dummy fish item used for the tests, as well with related subtypes and datums.
/obj/item/fish/testdummy
grind_results = list()
diff --git a/code/modules/unit_tests/greyscale_config.dm b/code/modules/unit_tests/greyscale_config.dm
index 9c5106be5b0..d3d9ce9d4fd 100644
--- a/code/modules/unit_tests/greyscale_config.dm
+++ b/code/modules/unit_tests/greyscale_config.dm
@@ -38,4 +38,4 @@
continue
var/number_of_colors = length(colors) - 1
if(config.expected_colors != number_of_colors)
- TEST_FAIL("[thing] has the wrong amount of colors configured for [config.DebugName()]. Expected [config.expected_colors] but only found [number_of_colors].")
+ TEST_FAIL("[thing] has the wrong amount of colors configured for [config.DebugName()]. Expected [config.expected_colors] colors but found [number_of_colors].")
diff --git a/code/modules/unit_tests/heretic_rituals.dm b/code/modules/unit_tests/heretic_rituals.dm
index 7298a163274..4ac5bce8d3d 100644
--- a/code/modules/unit_tests/heretic_rituals.dm
+++ b/code/modules/unit_tests/heretic_rituals.dm
@@ -63,6 +63,8 @@
var/list/created_atoms = list()
for(var/ritual_item_path in knowledge.required_atoms)
var/amount_to_create = knowledge.required_atoms[ritual_item_path]
+ if(islist(ritual_item_path))
+ ritual_item_path = pick(ritual_item_path)
for(var/i in 1 to amount_to_create)
created_atoms += new ritual_item_path(get_turf(our_heretic))
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_cyberpolice.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_cyberpolice.png
new file mode 100644
index 00000000000..180be6064f8
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_cyberpolice.png differ
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 8de11513eaa..181eeee72dc 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -64,28 +64,10 @@
/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/hivelord,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/advanced,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow/portal,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/advanced,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow,
/mob/living/simple_animal/hostile/asteroid/ice_demon,
/mob/living/simple_animal/hostile/asteroid/polarbear,
/mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/asteroid/wolf,
- /mob/living/simple_animal/hostile/big_legion,
- /mob/living/simple_animal/hostile/blob,
- /mob/living/simple_animal/hostile/blob/blobbernaut,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
- /mob/living/simple_animal/hostile/blob/blobspore,
- /mob/living/simple_animal/hostile/blob/blobspore/independent,
- /mob/living/simple_animal/hostile/blob/blobspore/weak,
/mob/living/simple_animal/hostile/construct,
/mob/living/simple_animal/hostile/construct/artificer,
/mob/living/simple_animal/hostile/construct/artificer/angelic,
@@ -140,21 +122,28 @@
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom,
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/guidance,
/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/hunter,
+ /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/bubblegum,
/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination,
+ /mob/living/simple_animal/hostile/megafauna/bubblegum/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/clockwork_defender,
/mob/living/simple_animal/hostile/megafauna/colossus,
+ /mob/living/simple_animal/hostile/megafauna/colossus/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner,
/mob/living/simple_animal/hostile/megafauna/dragon,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
+ /mob/living/simple_animal/hostile/megafauna/dragon/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/hierophant,
+ /mob/living/simple_animal/hostile/megafauna/hierophant/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/legion,
+ /mob/living/simple_animal/hostile/megafauna/legion/virtual_domain,
/mob/living/simple_animal/hostile/megafauna/legion/medium,
/mob/living/simple_animal/hostile/megafauna/legion/medium/eye,
/mob/living/simple_animal/hostile/megafauna/legion/medium/left,
/mob/living/simple_animal/hostile/megafauna/legion/medium/right,
/mob/living/simple_animal/hostile/megafauna/legion/small,
/mob/living/simple_animal/hostile/megafauna/wendigo,
+ /mob/living/simple_animal/hostile/megafauna/wendigo/virtual_domain,
/mob/living/simple_animal/hostile/mimic,
/mob/living/simple_animal/hostile/mimic/copy,
/mob/living/simple_animal/hostile/mimic/copy/machine,
@@ -181,7 +170,6 @@
/mob/living/simple_animal/hostile/retaliate/goose/vomit,
/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace,
/mob/living/simple_animal/hostile/retaliate/nanotrasenpeace/ranged,
- /mob/living/simple_animal/hostile/retaliate/snake,
/mob/living/simple_animal/hostile/retaliate/trader,
/mob/living/simple_animal/hostile/retaliate/trader/mrbones,
/mob/living/simple_animal/hostile/skeleton,
@@ -190,8 +178,6 @@
/mob/living/simple_animal/hostile/skeleton/plasmaminer,
/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
/mob/living/simple_animal/hostile/skeleton/templar,
- /mob/living/simple_animal/hostile/smspider,
- /mob/living/simple_animal/hostile/smspider/overcharged,
/mob/living/simple_animal/hostile/space_dragon,
/mob/living/simple_animal/hostile/space_dragon/spawn_with_antag,
/mob/living/simple_animal/hostile/vatbeast,
@@ -214,8 +200,8 @@
/mob/living/simple_animal/pet/cat/space,
/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/robot_customer,
/mob/living/simple_animal/shade,
/mob/living/simple_animal/slime,
/mob/living/simple_animal/slime/pet,
diff --git a/code/modules/uplink/uplink_items/implant.dm b/code/modules/uplink/uplink_items/implant.dm
index 34fc9eedb0f..87c9fd6c96c 100644
--- a/code/modules/uplink/uplink_items/implant.dm
+++ b/code/modules/uplink/uplink_items/implant.dm
@@ -9,11 +9,14 @@
/datum/uplink_item/implants/freedom
name = "Freedom Implant"
- desc = "An implant injected into the body and later activated at the user's will. It will attempt to free the \
- user from common restraints such as handcuffs."
+ desc = "Can be activated to release common restraints such as handcuffs, legcuffs, and even bolas tethered around the legs."
item = /obj/item/storage/box/syndie_kit/imp_freedom
cost = 5
+/datum/uplink_item/implants/freedom/New()
+ . = ..()
+ desc += " Implant has enough energy for [FREEDOM_IMPLANT_CHARGES] uses before it becomes inert and harmlessly self-destructs."
+
/datum/uplink_item/implants/radio
name = "Internal Syndicate Radio Implant"
desc = "An implant injected into the body, allowing the use of an internal Syndicate radio. \
diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm
index b971e07619c..e585b07bb5f 100644
--- a/code/modules/uplink/uplink_items/job.dm
+++ b/code/modules/uplink/uplink_items/job.dm
@@ -173,6 +173,16 @@
cost = 5
surplus = 50
+/datum/uplink_item/role_restricted/advanced_plastic_surgery
+ name = "Advanced Plastic Surgery Program"
+ desc = "A bootleg copy of an collector item, this disk contains the procedure to perform advanced plastic surgery, allowing you to model someone's face and voice based on a picture taken by a camera on your offhand. \
+ All changes are superficial and does not change ones genetic makeup. \
+ Insert into an Operating Console to enable the procedure."
+ item = /obj/item/disk/surgery/brainwashing
+ restricted_roles = list(JOB_MEDICAL_DOCTOR, JOB_CHIEF_MEDICAL_OFFICER, JOB_ROBOTICIST)
+ cost = 1
+ surplus = 50
+
/datum/uplink_item/role_restricted/springlock_module
name = "Heavily Modified Springlock MODsuit Module"
desc = "A module that spans the entire size of the MOD unit, sitting under the outer shell. \
diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm
index 54c9bbe9adc..491f8e8e99d 100644
--- a/code/modules/uplink/uplink_items/stealthy.dm
+++ b/code/modules/uplink/uplink_items/stealthy.dm
@@ -90,7 +90,16 @@
slur as if inebriated. It can produce an infinite number \
of bolts, but takes time to automatically recharge after each shot."
item = /obj/item/gun/energy/recharge/ebow
- progression_minimum = 30 MINUTES
cost = 10
surplus = 50
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
+
+/datum/uplink_item/stealthy_weapons/contrabaton
+ name = "Contractor Baton"
+ desc = "A compact, specialised baton assigned to Syndicate contractors. Applies light electrical shocks to targets. \
+ These shocks are capable of affecting the inner circuitry of most robots as well, applying a short stun. \
+ Has the added benefit of affecting the vocal cords of your victim, causing them to slur as if inebriated."
+ item = /obj/item/melee/baton/telescopic/contractor_baton
+ cost = 12
+ surplus = 50
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
diff --git a/code/modules/vending/autodrobe.dm b/code/modules/vending/autodrobe.dm
index 55e19152528..02daa0ce7e7 100644
--- a/code/modules/vending/autodrobe.dm
+++ b/code/modules/vending/autodrobe.dm
@@ -66,6 +66,8 @@
"products" = list(
/obj/item/clothing/suit/costume/imperium_monk = 1,
/obj/item/clothing/suit/chaplainsuit/holidaypriest = 1,
+ /obj/item/clothing/suit/chaplainsuit/habit = 1,
+ /obj/item/clothing/head/chaplain/habit_veil = 1,
/obj/item/clothing/suit/chaplainsuit/whiterobe = 1,
/obj/item/clothing/head/wizard/marisa/fake = 1,
/obj/item/clothing/suit/wizrobe/marisa/fake = 1,
diff --git a/code/modules/vending/games.dm b/code/modules/vending/games.dm
index 80fb1350841..e51205c00e4 100644
--- a/code/modules/vending/games.dm
+++ b/code/modules/vending/games.dm
@@ -56,6 +56,7 @@
/obj/item/skillchip/sabrage = 2,
/obj/item/skillchip/useless_adapter = 5,
/obj/item/skillchip/wine_taster = 2,
+ /obj/item/skillchip/master_angler = 2,
),
),
list(
diff --git a/code/modules/vending/medical.dm b/code/modules/vending/medical.dm
index 576cbb0b8b2..ad1c63e7e79 100644
--- a/code/modules/vending/medical.dm
+++ b/code/modules/vending/medical.dm
@@ -19,6 +19,7 @@
/obj/item/stack/medical/bone_gel = 4,
/obj/item/cane/white = 2,
/obj/item/clothing/glasses/eyepatch/medical = 2,
+ /obj/item/storage/box/bandages = 2,
)
contraband = list(
/obj/item/storage/box/gum/happiness = 3,
diff --git a/code/modules/vending/medical_wall.dm b/code/modules/vending/medical_wall.dm
index 4fd120bdc48..66badd4adf2 100644
--- a/code/modules/vending/medical_wall.dm
+++ b/code/modules/vending/medical_wall.dm
@@ -15,6 +15,7 @@
/obj/item/reagent_containers/medigel/sterilizine = 1,
/obj/item/healthanalyzer/simple = 2,
/obj/item/stack/medical/bone_gel = 2,
+ /obj/item/storage/box/bandages = 1,
)
contraband = list(
/obj/item/reagent_containers/pill/tox = 2,
diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm
index 6a7edd8e854..9b5af87ab44 100644
--- a/code/modules/vending/security.dm
+++ b/code/modules/vending/security.dm
@@ -29,6 +29,7 @@
/obj/item/clothing/gloves/tackler = 5,
/obj/item/grenade/stingbang = 1,
/obj/item/watertank/pepperspray = 2,
+ /obj/item/storage/belt/holster/energy = 4,
)
refill_canister = /obj/item/vending_refill/security
default_price = PAYCHECK_CREW
diff --git a/code/modules/vending/wardrobes.dm b/code/modules/vending/wardrobes.dm
index da2c08e04c9..e43314aa307 100644
--- a/code/modules/vending/wardrobes.dm
+++ b/code/modules/vending/wardrobes.dm
@@ -61,6 +61,7 @@
/obj/item/clothing/head/utility/surgerycap/green = 4,
/obj/item/clothing/head/beret/medical/paramedic = 4,
/obj/item/clothing/head/soft/paramedic = 4,
+ /obj/item/clothing/head/utility/head_mirror = 4,
/obj/item/clothing/mask/bandana/striped/medical = 4,
/obj/item/clothing/mask/surgical = 4,
/obj/item/clothing/under/rank/medical/doctor = 4,
@@ -523,6 +524,8 @@
/obj/item/storage/backpack/cultpack = 1,
/obj/item/storage/fancy/candle_box = 2,
/obj/item/radio/headset/headset_srv = 2,
+ /obj/item/clothing/suit/chaplainsuit/habit = 1,
+ /obj/item/clothing/head/chaplain/habit_veil = 1,
)
contraband = list(
/obj/item/toy/plush/ratplush = 1,
diff --git a/code/modules/zombie/items.dm b/code/modules/zombie/items.dm
index 9d3a298812e..4258dc5a304 100644
--- a/code/modules/zombie/items.dm
+++ b/code/modules/zombie/items.dm
@@ -11,20 +11,24 @@
bare_wound_bonus = 15
sharpness = SHARP_EDGED
-/obj/item/mutant_hand/zombie/afterattack(atom/target, mob/user, proximity_flag)
+/obj/item/mutant_hand/zombie/afterattack(atom/target, mob/living/user, proximity_flag)
. = ..()
if(!proximity_flag)
return
else if(isliving(target))
if(ishuman(target))
- try_to_zombie_infect(target)
+ try_to_zombie_infect(target, user, user.zone_selected)
else
. |= AFTERATTACK_PROCESSED_ITEM
check_feast(target, user)
-/proc/try_to_zombie_infect(mob/living/carbon/human/target)
+/proc/try_to_zombie_infect(mob/living/carbon/human/target, mob/living/user, def_zone = BODY_ZONE_CHEST)
CHECK_DNA_AND_SPECIES(target)
+ // Can't zombify with no head
+ if(!target.get_bodypart(BODY_ZONE_HEAD))
+ return
+
if(HAS_TRAIT(target, TRAIT_NO_ZOMBIFY))
// cannot infect any TRAIT_NO_ZOMBIFY human
return
@@ -33,11 +37,31 @@
if(HAS_TRAIT(target, TRAIT_VIRUS_RESISTANCE) && prob(75))
return
+ var/obj/item/bodypart/actual_limb = target.get_bodypart(def_zone)
+
+ // What you hitting bro?
+ if(!actual_limb)
+ return
+
+ var/limb_damage = actual_limb.get_damage()
+ var/limb_armor = max(0, target.getarmor(actual_limb, BIO) - 25)
+
+ // This is a pretty jank way to do this, but in short:
+ // if they have thick material on that bodypart it will always need at least 25 previous limb damage to trigger an infection.
+ // and if their bio armor isn't thick it's a bit weaker.
+ for(var/obj/item/clothing/iter_clothing in target.get_clothing_on_part(actual_limb))
+ if(iter_clothing.clothing_flags & THICKMATERIAL)
+ limb_armor += 25
+
+ if(limb_armor > limb_damage)
+ return
+
var/obj/item/organ/internal/zombie_infection/infection
infection = target.get_organ_slot(ORGAN_SLOT_ZOMBIE)
if(!infection)
infection = new()
infection.Insert(target)
+ to_chat(user, span_alien("You see [target] twitch for a moment as [target.p_their()] head is covered in \a [infection] - [target.p_Theyve()] been infected."))
/obj/item/mutant_hand/zombie/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is ripping [user.p_their()] brains out! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/html/changelogs/archive/2023-09.yml b/html/changelogs/archive/2023-09.yml
index aa41425b439..a45513671ce 100644
--- a/html/changelogs/archive/2023-09.yml
+++ b/html/changelogs/archive/2023-09.yml
@@ -972,3 +972,291 @@
lukevale:
- rscadd: Adds bras as a selectable preference in the same vein as underwear. Separates
all tops from undershirts.
+2023-09-25:
+ Iamgoofball:
+ - bugfix: Fixes a bug with the steampunk goggles that deletes items that aren't
+ welding goggles when hit by it.
+ Melbert:
+ - rscadd: Changelings can now speak through their decoy brain if it is placed in
+ an MMI, to maintain the illusion they are actually dead and have been debrained.
+ SkyratBot:
+ - bugfix: Fix secret documents steal objective failing while inside folder.
+ - rscadd: Added the Hippocrates bust to medbay heirlooms. Paramedics don't get one.
+ - rscadd: You can now swear the Hippocratic oath with these busts! It'll give you
+ pacifism but nothing else. The process is reversible.
+ - rscadd: There's a very small chance that the Hippocrates bust was once wielded
+ by a certain German doctor. This chance is increased for coroner heirlooms.
+ - bugfix: Fixed players being able to roll antagonist without ever being eligible
+ to play any role. Players who have their preferences set up so that they're
+ likely to return to lobby when the round starts have a lowered chance of becoming
+ antagonist.
+ - bugfix: Fixed beams rendering below mobs by default.
+ - bugfix: The fishing line beam is no longer emissive (it doesn't glow in the dark).
+ - bugfix: Fixed the overflow role having less slots than it actually should.
+ - code_imp: New flags/args to electrocute_act()
+ - balance: Makes it so Ephedrine spasms have a 10 * (1.5 - purity)% chance per second
+ to happen, Adding a downside to pure Ephedrine
+ - bugfix: Syndicate Modsuit AI's now downloads the current codespeak book upon being
+ downloaded.
+ Vekter:
+ - bugfix: Fixes Birdshot's recycler being turned the wrong direction.
+ distributivgesetz:
+ - bugfix: Clamping/closing a wound should now heal the bodypart that was damaged
+ instead of a random one.
+ honkpocket:
+ - rscdel: Removed wall-mushroom outbreak event
+2023-09-26:
+ LT3:
+ - spellcheck: Improved wording in greyscale JSON error message
+ - admin: Successful restart votes will now restart on the current map
+ - code_imp: End round and persistence data will be saved before executing successful
+ restart vote
+ Rhials:
+ - bugfix: '"Spooky" meteors will now properly spawn during halloween.'
+ SkyratBot:
+ - image: the security records suspected status is now teal instead of orange
+ - bugfix: Intellicards in computers are no longer deleted when the computer is destroyed.
+ - bugfix: Modular consoles can now be deconstructed by right clicking with a wrench.
+ - bugfix: cigarettes no longer smoke themselves from inside your pockets or on your
+ hands.
+ - admin: First time user connections are now logged
+ - qol: NT CIMs shows how much power the supermatter is releasing.
+ - qol: NT CIMs internal energy will adjust its prefix.
+ - qol: Energy displays (such as multitooling grid) will use the full range of SI
+ prefixes available, up to the peta prefix if you somehow managed to reach that.
+ - rscdel: Removes the per cubic centimeter part of internal energy.
+ - bugfix: Fix unnecessary delta time scaling on inactive supermatters.
+ - bugfix: Fix high energy zaps not scaling with delta time.
+ - bugfix: Fixes grounding rods lying about potential power you can generate.
+ - code_imp: Convert supermatter_zap() and tesla_zap() zap_str argument unit to be
+ in joules, and scales everything that uses that argument.
+ - rscadd: Adds an advanced plastic surgery procedure, allowing you to imitate people
+ in pictures. Simply hold a picture in your offhand of the person you wish to
+ imitate as while conducting the surgery! Remember, it's not foolproof, it only
+ changes your name and voice!
+ - rscadd: You can obtain the disk containing the afromentioned surgery. as a role-restricted
+ item to doctors and roboticists for 1TC, as a rare maint loot and BEPIS technode
+ reward
+ - bugfix: Centcom now rejects contraband that somehow makes it way onto the cargo
+ shuttle mid-transit and returns it.
+ - bugfix: you can no longer push watchers (and any other lavaland basic mob) around
+ by running into them
+ - bugfix: posibrains can be inserted again
+ - bugfix: Metalgen can no longer be used to transmute indestructible turfs.
+ - image: adds a frog holoform for pAIs
+ - bugfix: The custom error message for when there is only one map to vote for should
+ pop up in all cases rather than just a select few.
+ - bugfix: Stun immune people should no longer have issues with gripper gloves and
+ other tackle gloves.
+ sqnztb:
+ - rscadd: New medicine to clear up scars, made from synthflesh, ethanol, and miner's
+ salve. Be sure to apply it via patches!
+2023-09-27:
+ SkyratBot:
+ - rscadd: added ranged attack friendly fire checks for basic mobs. minebots and
+ hivebots will now try to avoid shooting their friends
+ - bugfix: fixed Strong Stone ruin generation
+ - rscadd: Added a candle box crate for all your candle needs!
+ - bugfix: '[Tramstation] Mass Driver in chapel now has tiny fan so you don''t space
+ yourself.'
+ jjpark-kb:
+ - bugfix: actually fixed the ash revival ritual for basic mobs
+ - bugfix: basic animals now will have their health affected positively and negatively
+ by medicine and toxins respectively.
+2023-09-28:
+ Melbert:
+ - rscadd: Doctors can now get head mirrors from their clothes vendor, to complete
+ the doctor outfit
+ SkyratBot:
+ - bugfix: Fixed job configs not being loaded properly.
+ - bugfix: You can no longer break the game by AI rolling in a card or APC
+ - qol: AI Roll now doesnt require you to click the exact turf to move you
+ - qol: AI roll cooldown and roll time is now a variable, making it possible for
+ AIs to become terrifying catamari damacy balls
+ - bugfix: Fixes full advanced surgery trays spawning with 'nothing'
+ - spellcheck: Tweaks the message that players get when not being able to qualify
+ for roundstart antag to be more accurate as to what's happening.
+ - qol: you can undeploy fulton beacons by clicking them with an empty hand
+ - qol: you can rename fulton beacons with a pen
+ - bugfix: Fix altars not allowing items to be sacrificed
+ - bugfix: Seeds will no longer be removed from existence after receiving the "You
+ can't seem to add [seed type] to the seed extractor" message
+ - bugfix: Some seeds that were previously not able to be added to the seed extractor
+ may now be added (starthistle for example)
+ - bugfix: fixes replica pod seeds spawning humans in nullspace
+ - qol: right clicking the seed extractor with a plant/food stores the extracted
+ seeds in the machine.
+ - bugfix: Valentines and ERTs will no longer get mood boosts from traitor moodener
+ items
+ - bugfix: Fixed zombies being able to infect headless corpses (Including former
+ zombies)
+ - bugfix: 'Fixed bio armor being totally useless against zombies. Now it checks
+ how hurt your limb is: If it''s more than the bio armor value, you get infected.
+ THICKMATERIAL clothing guarantees at least 25 damage required to infect you,
+ non-thick clothing reduces effective defence by 25. In practice this means people
+ with MODsuits, biosuits will resist infection unless they''re pummeled into
+ crit, and wearing a firesuit will save you from the first few slashes.'
+ - bugfix: Fixed the bomb hood armor not having the same bio armor value as bomb
+ armor.
+ - qol: Added a message to the zed when they succesfully infect someone.
+ - code_imp: Turned some proc names into snake_case rather than, uh, nospacecase.
+ - rscdel: removes surgical duffelbags
+ - bugfix: the surgery supply order now comes with a surgery tray
+ - bugfix: (skyrat) Hospital gowns will now spawn in surgery trays like they used
+ to in the surgery duffelbags
+ - bugfix: Left-clicking an empty surgery tray will now tell you exactly why it does
+ nothing.
+ - refactor: Turned slapcrafting into a component! You can examine compatible items
+ to see what recipes they can be used in, and what the ingredients for them are.
+ For example, spears and the head-on-spear crafting recipe.
+ - bugfix: The flight potion wings will no longer fail to work on lavaland/icemoon
+ on rare occasions.
+ - bugfix: Throwing things at cyborgs will now slow them down, as intended
+ - balance: Adjusted the calculation of throwforce -> slowdown for cyborgs such that
+ it is simply a flat duration for anything above a certain damage threshold (the
+ value of throwing iron rods)
+ - bugfix: Selecting "Monkey" on a magic mirror will now once again turn you into
+ a Monkey rather than a disgusting freak of nature.
+ - bugfix: Tall Boys have once again been barred from joining the Wizard Federation.
+ - rscadd: Contractor baton in traitor uplink for 12 TC
+ - balance: Ebow no longer has a reputation requirement.
+ - bugfix: Added complexity factors to foods that were missing them.
+ - balance: Gave the bluespace geode pirates 4 more teleporter bolt turrets.
+ - bugfix: The bluespace geode pirates no longer have a bluespace portal to the bottomless
+ pit dimension.
+ - rscadd: Station-safe dirt tiles for all your mapping needs, but surely no station
+ maps use the chasm baseturf ones, right? Right?
+ - bugfix: you can now deconstruct exodrone scanner arrays
+ - bugfix: the tree in space exodrone adventure no longer softlocks you
+ - qol: the exodrone launchers now tell you on examine how to remove their fuel canister
+ if you somehow needed to do that
+ - balance: exodrone wide scans are now capped at 10 minutes
+ - balance: exodrone travel times are 18% faster
+ - balance: you can now upgrade scanner arrays for faster wide scan
+ - balance: exodrone point scan and deep scan are faster
+ - spellcheck: fixes several typos related to exodrones and gives scanner control
+ console a description
+ - qol: Gas masks now muffle your voice with TTS.
+ - qol: Security Hailer masks now disguise your voice to protect your right to brutalize
+ greytiders.
+ - qol: Lizards, Ethereals, and Xenomorphs now have a vocal effect.
+ - qol: Security Records now show someone's voice name.
+ - bugfix: fixed geysers spawning on turfs with plants
+ Tattle:
+ - qol: drones now have individual names, instead of just "drone"
+ Vekter:
+ - rscadd: Added a holodeck to Birdshot Station. It can be reached via the Crew Facilities
+ hallway.
+ softcerv:
+ - rscadd: Adds in a wiki-book NIFSoft, allowing the user to access various wiki
+ books.
+ - qol: the speech impairment on the adult gas mask can now be toggled
+ tattle:
+ - qol: Basic animals now make sounds for audible emotes
+ - sound: Added new sound effects for chicks, chickens, crabs, and insects
+2023-09-29:
+ A.C.M.O.:
+ - bugfix: Fixes the death sandwich, making it safe to examine.
+ BurgerBB:
+ - bugfix: Scrubbers and Vents will no longer reset their settings on map load.
+ GoldenAlpharex:
+ - server: Added a way for calls to be made to interfere with player ranks on live
+ servers (updating the players if they're connected) from outside of the game.
+ Rhials:
+ - qol: The freedom implant has received minor feedback and other minor usage improvements.
+ - bugfix: The Polymorph Belt should now update its sprite when active.
+ SkyratBot:
+ - qol: allowed names to start with a number if AI/Borg
+ - rscadd: Add drinking water causes drunk mobs to become sober
+ - balance: Diabetics rejoice! Nerfed sugar OD/hyperglycaemic shock to be an immediate
+ KO followed by drowsiness afterwards until the OD is gone.
+ - code_imp: Robot Customers have recently been touched codewise, please report any
+ bugs or unexpected behavior as there really should not be any.
+ - admin: There is now a tool to apply a DNA Infuser entry to any human.
+ - bugfix: The Nuke Op MODsuit AI downloader only works once per purchase, as intended.
+ - code_imp: adds a gas connector component that allows connection to the atmos piping
+ system without the need of repathing
+ - refactor: changes the cryo machine to use this new system
+ - refactor: Hivelords and Legions now use the basic mob framework. Please report
+ any unusual behaviour.
+ - rscadd: Hivelords shed more spawn when they are attacked.
+ - rscadd: Legions have learned how to fling their skulls across long distances.
+ - rscadd: Legions can heal other lavaland mobs with their skulls.
+ - rscadd: Legions are better at preserving corpses they consume, and sometimes make
+ use of their radios.
+ - rscadd: Legions may leave behind an unpleasant surprise after you are rescued
+ from them.
+ - balance: The crew monitoring console will now display you as dead if you are dead,
+ an critically injured if in crit, rather than setting those icons purely based
+ on your current health.
+ - qol: You won't continue burning to a husk if consumed by a snow legion after being
+ set on fire by an ice drake.
+ - bugfix: removes incorrect stack traces when using some admin secrets
+ - balance: Head revolutionaries and heads of staff are no longer immediately considered
+ disqualified when going AFK or disconnecting and are given a 2 minute grace
+ period.
+ - admin: Admins now get a log when a head revolutionary or head of staff disconnects
+ or goes AFK during a revolution. They also get the same log 1 minute after to
+ give them a chance to act on the information.
+ - refactor: Snakes have been refactored into basic mobs. This means that they are
+ a bit more intelligent than previous snakes, making them more docile and averse
+ to harming people (unless otherwise provoked). They do chomp all sorts of mice
+ though. You can feed them a dead mouse to make them your friend if you'd want
+ that.
+ - sound: If you listen closely to snakes, you might be able to hear a small hissing
+ sound...
+ - bugfix: fixed lobstrosities becoming unmovable when killed during their charge
+ windup
+ - bugfix: Splattercasting resets your blood to normal values when you transsform
+ into a vampire.
+ - bugfix: Gaining a new species will set your blood volume down to the normal volume
+ levels if higher than normal.
+ - bugfix: Fix water puddle runtime when washing items
+ - bugfix: the parole status and discharged status are now green and blue respectively
+ in the security record interface
+ - bugfix: Dimensional Anomalies no longer destroy wall-mounted equipment.
+ - code_imp: Your bodytype now decides what gendered sounds you make.
+ - bugfix: Fixed crabs not correctly (kinda) walking sideway.
+ - bugfix: dead bodies now cool down to room temperature over time
+ - rscadd: Add candle design to biogenerator
+ sergeirocks100:
+ - bugfix: Undershirts will now look as they should if you have a body type that
+ differs from the gender default.
+ softcerv:
+ - rscadd: Adds in the ability for certain NIFSofts to be kept between rounds.
+2023-09-30:
+ DrDiasyl aka DrTuxedo:
+ - balance: Holsters can now be clipped to any suit, and house Captain antique gun
+ and HoS gun. You now can buy holsters from the SecTech premium section.
+ Paxilmaniac:
+ - qol: The half mask respirator can have its TTS voice muffling properties toggled
+ with control click
+ - qol: Icecats are now listed in the round end report, so you can see who was up
+ icing they cat
+ - bugfix: Icecats should hopefully spawn with their special language correctly now
+ SkyratBot:
+ - code_imp: removed some redundant code for airlocks
+ - admin: Mob abilities can be granted to arbitrary mobs via the VV menu in a similar
+ way to spells.
+ - bugfix: Lavaland syndicate operatives can no longer trivially use the jetpack
+ on their modsuit to fly over the lava.
+ - bugfix: If two cosmic heretics ascend in the same round, their star gazer survival
+ will be linked to each individual heretic and not shared by just one of them.
+ - bugfix: You can't click the Knock heretic portal to join as a mob while already
+ signed up to become a mob.
+ - balance: Cosmic heretics can't order the Star Gazer around while jaunting.
+ - balance: The Knock Heretic portal cannot summon Flesh Worms, but can summon Fire
+ Sharks.
+ - balance: The Knock Heretic portal will disperse if its creator is killed.
+ - rscadd: SM crystal can now dust someone or something if it falls on it.
+ - bugfix: The reverse revolver now looks like a normal Syndicate revolver on inspection.
+ - bugfix: fixed the stamp in the metastation CMO office always spawning on the floor
+ - bugfix: You can now spray paint the SM without getting dusted
+ Smol42:
+ - rscadd: Added some new hairstyles
+ Zergspower:
+ - bugfix: Crew Monitor works again properly
+ nikothedude:
+ - rscadd: A waterbreathing quirk
+ - qol: Waterbreathing is now documented on species pages of the species that have
+ it
diff --git a/html/changelogs/archive/2023-10.yml b/html/changelogs/archive/2023-10.yml
new file mode 100644
index 00000000000..ccff91affb0
--- /dev/null
+++ b/html/changelogs/archive/2023-10.yml
@@ -0,0 +1,122 @@
+2023-10-01:
+ Hatterhat:
+ - balance: Bullets have had their base type's wound bonus reduced back to 0, down
+ from 20, because wounds are actually quite punishing. Funnily enough, most bullets
+ already have modified wound bonuses - except c9mm, c10mm, and most incendiaries,
+ so this probably doesn't change much.
+ - balance: .50 (used in the snipers and renamed to .416 or whatever) is now back
+ to TG balance standards. Knockdown on hit, 60 instead of 110 damage, etc. etc.
+ - bugfix: .50 Soporific was removed because disruptor ammo was right there and nobody
+ realized it existed.
+ - bugfix: After review of a missing equipment complaint, Nanotrasen remembered to
+ pay Lopland's quartermasters to put the customary flashbang and teargas grenade
+ boxes into the Void Raptor's armory.
+ Melbert:
+ - qol: Examine blocked out roundstart / latejoin job information.
+ - qol: Captain gets a little bit more information about how their radio works roundstart.
+ - bugfix: Fixed roundstart players not getting radio information.
+ Paxilmaniac:
+ - image: The buttondown shirts (underwear) have been updated with a better look
+ and more contrasted palette
+ SkyratBot:
+ - rscadd: A new export has arrived in the imports section, the Galactic Materials
+ Market! You can use this to buy and sell minerals for profit or cost, as well
+ as stock your station when you don't have any miners.
+ - rscadd: Insert sheets of minerals into the Galactic Materials Market to convert
+ them into a stock block, allowing you to lock in your price for 5 minutes. Wait
+ too long and it'll be subject to market value again!
+ - rscadd: Minerals can be bought on the market either using the station's cargo
+ budget by cargo crew, or privately by everyone else.
+ - rscdel: Any material stacks that can be bought and sold on the market before have
+ been removed from the cargo catalog.
+ - rscadd: Adds Bitrunning to supply department- a semi-offstation role that rewards
+ teamwork.
+ - rscadd: Adds new machines to complement the job- net pod, quantum server, quantum
+ consoles, and the nexacache vendor.
+ - rscadd: Adds several new maps which can be loaded and unloaded at will.
+ - rscadd: Some flair for the new bitrunning vendor.
+ - rscadd: Adds a new antagonist for the virtual domain only. Short lived ghost role
+ that fights bitrunners.
+ - rscdel: Removes the BEPIS machine, moves its tech into the Bitrunning vendor.
+ - bugfix: Fixes missing baseturfs and clowns in mining planet VDOM..
+ - qol: Font settings in the chat panel applies to all text now.
+ - image: new chaplain outfit
+ - bugfix: Blob spores will respond to rallies more reliably (it won't runtime every
+ time they try and pathfind).
+ - bugfix: Blobbernaut pain animation overlays should align with the direction the
+ mob is facing instead of always facing South
+ - refactor: Blob spores, zombies, and blobbernauts now all use the basic mob framework.
+ They should work the same, but please report any issues.
+ - bugfix: Added warden to list of default required enemies for rulesets.
+ - bugfix: Blob Zombies and Blobbernauts have had their attack speed restored to
+ its original value
+ - refactor: Supermatter Spiders have been refactored into basic mobs, on the extremely
+ off chance you spot one and also notice any weird bugs regarding it, please
+ report it.
+ - balance: There are now 3 roundstart cyborg job slots open by default.
+ - rscadd: Quantum servers now talk over supply channel when they're done cooling
+ off. Go outside!
+ - bugfix: You can no longer use dragon swoop to bypass cordons.
+ - bugfix: Netpod brain damage is now properly reduced upon server upgrades.
+ - bugfix: Fixed an bug where swapping bodies in vdom prevented you from disconnecting.
+ - bugfix: Fixed a bug where a quantum server could get locked out of loading new
+ domains.
+ - bugfix: Changed quantum console UI to display "no bandwidth" rather than "none"
+ - bugfix: Actually fixed the hooked item exploit.
+ - rscadd: Heretic Rebalance
+ - balance: Researching the Main Knowledge paths that unlock Side Paths will grant
+ one Side Point that can be used only on those side paths. You can still spend
+ normal knowledge points on them if you wish.
+ - balance: Rune drawing time has been reduced from 30->20 seconds. Codex drawing
+ time has been reduced from 15->8.
+ - balance: 'Codex Cicatrix is now a roundstart knowledge, works as an amber focus
+ when held in-hand and opened, and has had its recipe changed to: 1 of any non-standard
+ pen (literally anything that isn''t the base pen), any book, and either animal
+ hide OR a corpse, any kind.'
+ - code_imp: Added support for using a list inside ritual requirements and a special
+ 'snowflake check' rituals can utilize.
+ - balance: The first non-path knowledge, the Mansus Hand Mark, has had its cost
+ reduced from 2->1 points.
+ - bugfix: Aloe and other baked foods that don't have reagents can be baked again
+ without turning to ash
+ Vekter:
+ - bugfix: Fixes the missing grinder in Birdshot's Virology department
+ jjpark-kb:
+ - bugfix: the ashwalker tendril will allow you to respawn again (the tendril blessing)
+ - bugfix: the round end report will accurately report ashwalker sacrifices
+ nikothedude:
+ - code_imp: Gauze removal is now handled by the gauze's destroy instead of seep_gauze
+ ninjanomnom:
+ - rscdel: An easter egg plushie that was spawning where it shouldn't has been brought
+ back home.
+ - rscadd: The secure closet can now spawn live gibtonite, enjoy your free bomb.
+2023-10-02:
+ SkyratBot:
+ - balance: Sci now has access to the materials & canisters section in their departmental
+ order console
+ - rscadd: Expanded the fishing portal generator. It now comes with several portal
+ options that can be unlocked by performing fish scanning experiments, which
+ also award a modest amount of techweb points.
+ - balance: The fishing portal generator is now buildable and no longer orderable.
+ The board can be printed from cargo, service and science lathes.
+ - balance: Advanced fishing tech is no longer a BEPIS design. It now requires the
+ base fish scanning experiment and 2000 points to be unlocked.
+ - rscadd: The advanced fishing rod now comes with an incorporated experiscanner
+ specific for fish scanning.
+ - rscadd: Added a new skillchip that may change the icon of the "fish" shown in
+ the minigame UI to less generic ones. Reaching master level in fishing also
+ does that.
+ - qol: The experiment handler UI no longer shows unselectable experiments.
+ - bugfix: Security officers can now download the crew manifest PDA app that they
+ start with.
+ - rscadd: Wizards who complete the grand ritual can now gift everyone with eternal
+ life
+ distributivgesetz:
+ - bugfix: Font scaling in TGUI chat has been reverted to its original implementation.
+ softcerv:
+ - rscadd: Adds the mini-soulcatcher, a more lightweight soulcatcher that can be
+ attached to objects
+ - rscadd: Adds in the RSD brain interface, an item that allows for soulcatcher souls,
+ that died within a round and were scanned, to be transferred to a new brain.
+ - rscadd: Adds in the NIFSoft Scryer, a NIFSoft that gives the user a Scryer they
+ can use to communicate with other Scryer users.
diff --git a/icons/area/areas_station.dmi b/icons/area/areas_station.dmi
index b07ea38a159..cbfe463efa5 100644
Binary files a/icons/area/areas_station.dmi and b/icons/area/areas_station.dmi differ
diff --git a/icons/effects/96x96.dmi b/icons/effects/96x96.dmi
index 38d1d44a000..31f26c3e6e1 100644
Binary files a/icons/effects/96x96.dmi and b/icons/effects/96x96.dmi differ
diff --git a/icons/effects/bitrunning.dmi b/icons/effects/bitrunning.dmi
new file mode 100644
index 00000000000..bfdc7c63436
Binary files /dev/null and b/icons/effects/bitrunning.dmi differ
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index 29e59dc6c71..d59f065da28 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/hud/fishing_hud.dmi b/icons/hud/fishing_hud.dmi
index b68acee09b7..58c478d0710 100644
Binary files a/icons/hud/fishing_hud.dmi and b/icons/hud/fishing_hud.dmi differ
diff --git a/icons/hud/radial_fishing.dmi b/icons/hud/radial_fishing.dmi
new file mode 100644
index 00000000000..65fd55176b7
Binary files /dev/null and b/icons/hud/radial_fishing.dmi differ
diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi
index a1fc01434e4..1edd4d29cb5 100755
Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ
diff --git a/icons/mob/clothing/belt.dmi b/icons/mob/clothing/belt.dmi
index 546e3da0f86..d1a1777e4e4 100644
Binary files a/icons/mob/clothing/belt.dmi and b/icons/mob/clothing/belt.dmi differ
diff --git a/icons/mob/clothing/head/chaplain.dmi b/icons/mob/clothing/head/chaplain.dmi
index efb6ec3c9e9..100b7ee922f 100644
Binary files a/icons/mob/clothing/head/chaplain.dmi and b/icons/mob/clothing/head/chaplain.dmi differ
diff --git a/icons/mob/clothing/head/pai_head.dmi b/icons/mob/clothing/head/pai_head.dmi
index 0a04e7e8ab2..e5dd4965d8b 100644
Binary files a/icons/mob/clothing/head/pai_head.dmi and b/icons/mob/clothing/head/pai_head.dmi differ
diff --git a/icons/mob/clothing/head/plasmaman_head.dmi b/icons/mob/clothing/head/plasmaman_head.dmi
index 9846cf02200..1917ae7bcf5 100644
Binary files a/icons/mob/clothing/head/plasmaman_head.dmi and b/icons/mob/clothing/head/plasmaman_head.dmi differ
diff --git a/icons/mob/clothing/head/utility.dmi b/icons/mob/clothing/head/utility.dmi
index 3f3a668181c..ada1b90c4b0 100644
Binary files a/icons/mob/clothing/head/utility.dmi and b/icons/mob/clothing/head/utility.dmi differ
diff --git a/icons/mob/clothing/suits/chaplain.dmi b/icons/mob/clothing/suits/chaplain.dmi
index 4b6368fb291..8806bf5f679 100644
Binary files a/icons/mob/clothing/suits/chaplain.dmi and b/icons/mob/clothing/suits/chaplain.dmi differ
diff --git a/icons/mob/clothing/suits/jacket.dmi b/icons/mob/clothing/suits/jacket.dmi
index cd924e847eb..a6f25d91c59 100644
Binary files a/icons/mob/clothing/suits/jacket.dmi and b/icons/mob/clothing/suits/jacket.dmi differ
diff --git a/icons/mob/clothing/under/cargo.dmi b/icons/mob/clothing/under/cargo.dmi
index 4bf30a67a2d..180f0e4ec87 100644
Binary files a/icons/mob/clothing/under/cargo.dmi and b/icons/mob/clothing/under/cargo.dmi differ
diff --git a/icons/mob/clothing/under/plasmaman.dmi b/icons/mob/clothing/under/plasmaman.dmi
index 41cbfb4482b..fcc8f008cd7 100644
Binary files a/icons/mob/clothing/under/plasmaman.dmi and b/icons/mob/clothing/under/plasmaman.dmi differ
diff --git a/icons/mob/huds/hud.dmi b/icons/mob/huds/hud.dmi
index ec9be118f57..d71ba4b0940 100644
Binary files a/icons/mob/huds/hud.dmi and b/icons/mob/huds/hud.dmi differ
diff --git a/icons/mob/inhands/clothing/hats_lefthand.dmi b/icons/mob/inhands/clothing/hats_lefthand.dmi
index 7111ee4d748..191c85cf482 100644
Binary files a/icons/mob/inhands/clothing/hats_lefthand.dmi and b/icons/mob/inhands/clothing/hats_lefthand.dmi differ
diff --git a/icons/mob/inhands/clothing/hats_righthand.dmi b/icons/mob/inhands/clothing/hats_righthand.dmi
index 96756fc44db..8038e7474ee 100644
Binary files a/icons/mob/inhands/clothing/hats_righthand.dmi and b/icons/mob/inhands/clothing/hats_righthand.dmi differ
diff --git a/icons/mob/inhands/clothing/suits_lefthand.dmi b/icons/mob/inhands/clothing/suits_lefthand.dmi
index 757fb8b8593..8b9fa5256a9 100644
Binary files a/icons/mob/inhands/clothing/suits_lefthand.dmi and b/icons/mob/inhands/clothing/suits_lefthand.dmi differ
diff --git a/icons/mob/inhands/clothing/suits_righthand.dmi b/icons/mob/inhands/clothing/suits_righthand.dmi
index c749a2ed98a..c88f4d22444 100644
Binary files a/icons/mob/inhands/clothing/suits_righthand.dmi and b/icons/mob/inhands/clothing/suits_righthand.dmi differ
diff --git a/icons/mob/inhands/equipment/medical_lefthand.dmi b/icons/mob/inhands/equipment/medical_lefthand.dmi
index 7ce3674c86a..feaed169078 100644
Binary files a/icons/mob/inhands/equipment/medical_lefthand.dmi and b/icons/mob/inhands/equipment/medical_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/medical_righthand.dmi b/icons/mob/inhands/equipment/medical_righthand.dmi
index 6ceb5efe4d8..15ccf5c090e 100644
Binary files a/icons/mob/inhands/equipment/medical_righthand.dmi and b/icons/mob/inhands/equipment/medical_righthand.dmi differ
diff --git a/icons/mob/silicon/pai.dmi b/icons/mob/silicon/pai.dmi
index 624ed669519..2be986d411d 100644
Binary files a/icons/mob/silicon/pai.dmi and b/icons/mob/silicon/pai.dmi differ
diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi
index 13c37dca594..38b78cf468f 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/telegraphing/telegraph.dmi b/icons/mob/telegraphing/telegraph.dmi
index d5e03419cd8..de525ead4ee 100644
Binary files a/icons/mob/telegraphing/telegraph.dmi and b/icons/mob/telegraphing/telegraph.dmi differ
diff --git a/icons/obj/aquarium.dmi b/icons/obj/aquarium.dmi
index 3a27c83c906..19e2e68c4f8 100644
Binary files a/icons/obj/aquarium.dmi and b/icons/obj/aquarium.dmi differ
diff --git a/icons/obj/card.dmi b/icons/obj/card.dmi
index a5c4e828301..2bbec93eed2 100644
Binary files a/icons/obj/card.dmi and b/icons/obj/card.dmi differ
diff --git a/icons/obj/clothing/glasses.dmi b/icons/obj/clothing/glasses.dmi
index 20f24dd2240..fd898d3105f 100644
Binary files a/icons/obj/clothing/glasses.dmi and b/icons/obj/clothing/glasses.dmi differ
diff --git a/icons/obj/clothing/head/chaplain.dmi b/icons/obj/clothing/head/chaplain.dmi
index d95436fdd2d..ed6f6248b31 100644
Binary files a/icons/obj/clothing/head/chaplain.dmi and b/icons/obj/clothing/head/chaplain.dmi differ
diff --git a/icons/obj/clothing/head/plasmaman_hats.dmi b/icons/obj/clothing/head/plasmaman_hats.dmi
index adcf9129c45..f593a08b88c 100644
Binary files a/icons/obj/clothing/head/plasmaman_hats.dmi and b/icons/obj/clothing/head/plasmaman_hats.dmi differ
diff --git a/icons/obj/clothing/head/utility.dmi b/icons/obj/clothing/head/utility.dmi
index 9571b2add78..17040f5bb8b 100644
Binary files a/icons/obj/clothing/head/utility.dmi and b/icons/obj/clothing/head/utility.dmi differ
diff --git a/icons/obj/clothing/suits/chaplain.dmi b/icons/obj/clothing/suits/chaplain.dmi
index 64474a04d31..730e47cd6fa 100644
Binary files a/icons/obj/clothing/suits/chaplain.dmi and b/icons/obj/clothing/suits/chaplain.dmi differ
diff --git a/icons/obj/clothing/suits/jacket.dmi b/icons/obj/clothing/suits/jacket.dmi
index c63f262f104..dc507017cd2 100644
Binary files a/icons/obj/clothing/suits/jacket.dmi and b/icons/obj/clothing/suits/jacket.dmi differ
diff --git a/icons/obj/clothing/under/cargo.dmi b/icons/obj/clothing/under/cargo.dmi
index fc04a897d5e..63e40538899 100644
Binary files a/icons/obj/clothing/under/cargo.dmi and b/icons/obj/clothing/under/cargo.dmi differ
diff --git a/icons/obj/clothing/under/plasmaman.dmi b/icons/obj/clothing/under/plasmaman.dmi
index 4277c43d54b..4d416d5b05f 100644
Binary files a/icons/obj/clothing/under/plasmaman.dmi and b/icons/obj/clothing/under/plasmaman.dmi differ
diff --git a/icons/obj/economy.dmi b/icons/obj/economy.dmi
index dc90265b6e9..04abc41cae1 100644
Binary files a/icons/obj/economy.dmi and b/icons/obj/economy.dmi differ
diff --git a/icons/obj/exploration.dmi b/icons/obj/exploration.dmi
index 2f9d004bee2..b7224d2df84 100644
Binary files a/icons/obj/exploration.dmi and b/icons/obj/exploration.dmi differ
diff --git a/icons/obj/fishing.dmi b/icons/obj/fishing.dmi
index 39bcc853442..f7ab9fc1ad9 100644
Binary files a/icons/obj/fishing.dmi and b/icons/obj/fishing.dmi differ
diff --git a/icons/obj/machines/bepis.dmi b/icons/obj/machines/bepis.dmi
deleted file mode 100644
index f348c2e1b05..00000000000
Binary files a/icons/obj/machines/bepis.dmi and /dev/null differ
diff --git a/icons/obj/machines/bitrunning.dmi b/icons/obj/machines/bitrunning.dmi
new file mode 100644
index 00000000000..a910a16b35c
Binary files /dev/null and b/icons/obj/machines/bitrunning.dmi differ
diff --git a/icons/obj/machines/computer.dmi b/icons/obj/machines/computer.dmi
index 10974f97bac..5ffa3445db6 100644
Binary files a/icons/obj/machines/computer.dmi and b/icons/obj/machines/computer.dmi differ
diff --git a/icons/obj/medical/organs/mining_organs.dmi b/icons/obj/medical/organs/mining_organs.dmi
index f3fc298284b..172f94001ff 100644
Binary files a/icons/obj/medical/organs/mining_organs.dmi and b/icons/obj/medical/organs/mining_organs.dmi differ
diff --git a/icons/obj/medical/stack_medical.dmi b/icons/obj/medical/stack_medical.dmi
index d12949da595..c4ec905786c 100644
Binary files a/icons/obj/medical/stack_medical.dmi and b/icons/obj/medical/stack_medical.dmi differ
diff --git a/icons/obj/railings.dmi b/icons/obj/railings.dmi
index 28332e21324..3dbbd7c8318 100644
Binary files a/icons/obj/railings.dmi and b/icons/obj/railings.dmi differ
diff --git a/icons/obj/storage/box.dmi b/icons/obj/storage/box.dmi
index 7ff4067c288..d660e1b7bfe 100644
Binary files a/icons/obj/storage/box.dmi and b/icons/obj/storage/box.dmi differ
diff --git a/icons/turf/floors.dmi b/icons/turf/floors.dmi
index 8a1575fcec6..6ddc178b98c 100644
Binary files a/icons/turf/floors.dmi and b/icons/turf/floors.dmi differ
diff --git a/modular_skyrat/master_files/code/datums/traits/good.dm b/modular_skyrat/master_files/code/datums/traits/good.dm
index 22987f7705d..08f265145a9 100644
--- a/modular_skyrat/master_files/code/datums/traits/good.dm
+++ b/modular_skyrat/master_files/code/datums/traits/good.dm
@@ -68,6 +68,16 @@
right_arm.unarmed_miss_sound = initial(right_arm.unarmed_miss_sound)
right_arm.unarmed_sharpness = initial(right_arm.unarmed_sharpness)
+/datum/quirk/water_breathing
+ name = "Water breathing"
+ desc = "You are able to breathe underwater!"
+ value = 2
+ mob_trait = TRAIT_WATER_BREATHING
+ gain_text = span_notice("You become acutely aware of the moisture in your lungs and in the air. It feels nice.")
+ lose_text = span_danger("You suddenly realize the moisture in your lungs feels really weird, and you almost choke on it!")
+ medical_record_text = "Patient possesses biology compatible with aquatic respiration."
+ icon = FA_ICON_FISH
+
// AdditionalEmotes *turf quirks
/datum/quirk/water_aspect
name = "Water aspect (Emotes)"
diff --git a/modular_skyrat/master_files/code/modules/bitrunning/orders/tech.dm b/modular_skyrat/master_files/code/modules/bitrunning/orders/tech.dm
new file mode 100644
index 00000000000..6c9a0626517
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/bitrunning/orders/tech.dm
@@ -0,0 +1,3 @@
+/datum/orderable_item/bepis/flashdark
+ item_path = /obj/item/flashlight/flashdark
+ cost_per_order = 750
diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
index b2b9eaeecba..30df82a7f11 100644
--- a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
+++ b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm
@@ -32,3 +32,14 @@
/datum/species/proc/apply_supplementary_body_changes(mob/living/carbon/human/target, datum/preferences/preferences, visuals_only = FALSE)
return
+
+/datum/species/create_pref_traits_perks()
+ . = ..()
+
+ if (TRAIT_WATER_BREATHING in inherent_traits)
+ . += list(list(
+ SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK,
+ SPECIES_PERK_ICON = FA_ICON_FISH,
+ SPECIES_PERK_NAME = "Waterbreathing",
+ SPECIES_PERK_DESC = "[plural_form] can breathe in water, making pools a lot safer to be in!",
+ ))
diff --git a/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm b/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm
index e263c4062c7..b09de049865 100644
--- a/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm
+++ b/modular_skyrat/master_files/code/modules/research/techweb/all_nodes.dm
@@ -206,12 +206,14 @@
. = ..()
design_ids += list(
"borg_upgrade_snacks",
+ "mini_soulcatcher",
)
/datum/techweb_node/neural_programming/New()
. = ..()
design_ids += list(
"soulcatcher_device",
+ "rsd_interface",
)
/datum/techweb_node/cyborg_upg_util/New()
diff --git a/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi b/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi
index 0e3d125038e..eac81c9cca9 100644
Binary files a/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi and b/modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi b/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi
index 9a8042cf0a0..8d18e50d623 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi and b/modular_skyrat/master_files/icons/mob/clothing/under/cargo.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi b/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi
index c4ecb9e4f81..faceea6def3 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi and b/modular_skyrat/master_files/icons/mob/clothing/under/cargo_digi.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi b/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi
index 3d9209905cb..ff6f4218c27 100644
Binary files a/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi and b/modular_skyrat/master_files/icons/mob/clothing/underwear.dmi differ
diff --git a/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi b/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi
index fdfc3fee1c8..681457fe596 100644
Binary files a/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi and b/modular_skyrat/master_files/icons/mob/sprite_accessory/hair.dmi differ
diff --git a/modular_skyrat/modules/aesthetics/guns/code/guns.dm b/modular_skyrat/modules/aesthetics/guns/code/guns.dm
index ccc43793c33..767ca1608c1 100644
--- a/modular_skyrat/modules/aesthetics/guns/code/guns.dm
+++ b/modular_skyrat/modules/aesthetics/guns/code/guns.dm
@@ -383,12 +383,11 @@
advanced_print_req = TRUE // you are NOT printing more ammo for this without effort.
// then again the offstations with ammo printers and sniper rifles come with an ammo disk anyway, so
-/obj/item/ammo_casing/p50/soporific
- name = ".416 Stabilis tranquilizer casing"
+/obj/item/ammo_casing/p50/disruptor
+ name = ".416 Stabilis disruptor casing"
desc = "A .416 bullet casing that specialises in sending the target to sleep rather than hell.\
\
- SOPORIFIC: Forces targets to sleep, deals no damage."
- projectile_type = /obj/projectile/bullet/p50/soporific
+ DISRUPTOR: Forces humanoid targets to sleep, does heavy damage against cyborgs, EMPs struck targets."
/obj/item/ammo_casing/p50/penetrator
name = ".416 Stabilis APFSDS ++P bullet casing"
@@ -528,8 +527,11 @@
/obj/projectile/bullet/incendiary/c46x30mm
name = "8mm incendiary bullet"
-/obj/projectile/bullet/p50/soporific // COMMON BULLET IS ALREADY OVERRIDEN IN MODULAR > BULLETREBALANCE > CODE > sniper.dm
- name = ".416 tranquilizer"
+/obj/projectile/bullet/p50
+ name = ".416 Stabilis bullet"
+
+/obj/projectile/bullet/p50/disruptor
+ name = ".416 disruptor bullet"
/obj/projectile/bullet/p50/penetrator
name = ".416 penetrator bullet"
diff --git a/modular_skyrat/modules/alerts/code/default_announcer.dm b/modular_skyrat/modules/alerts/code/default_announcer.dm
index 7ae320f01f8..86add70db58 100644
--- a/modular_skyrat/modules/alerts/code/default_announcer.dm
+++ b/modular_skyrat/modules/alerts/code/default_announcer.dm
@@ -43,7 +43,6 @@
ANNOUNCER_KLAXON = 'modular_skyrat/modules/black_mesa/sound/siren1_long.ogg',
ANNOUNCER_ICARUS = 'modular_skyrat/modules/assault_operatives/sound/icarus_alarm.ogg',
ANNOUNCER_NRI_RAIDERS = 'modular_skyrat/modules/encounters/sounds/morse.ogg',
- ANNOUNCER_FUNGI = 'modular_skyrat/modules/alerts/sound/alerts/fungi.ogg',
ANNOUNCER_DEPARTMENTAL = 'modular_skyrat/modules/alerts/sound/alerts/alert3.ogg',
ANNOUNCER_SHUTTLE = 'modular_skyrat/modules/alerts/sound/alerts/alert3.ogg',
)
diff --git a/modular_skyrat/modules/alerts/sound/alerts/fungi.ogg b/modular_skyrat/modules/alerts/sound/alerts/fungi.ogg
deleted file mode 100644
index 7eb45de94a2..00000000000
Binary files a/modular_skyrat/modules/alerts/sound/alerts/fungi.ogg and /dev/null differ
diff --git a/modular_skyrat/modules/alternative_job_titles/code/job.dm b/modular_skyrat/modules/alternative_job_titles/code/job.dm
index 2f47a4c1f05..13edfe45aa4 100644
--- a/modular_skyrat/modules/alternative_job_titles/code/job.dm
+++ b/modular_skyrat/modules/alternative_job_titles/code/job.dm
@@ -11,6 +11,9 @@
if(!player_client)
return
+ if(!ishuman(equipping))
+ return
+
var/chosen_title = player_client.prefs.alt_job_titles[job.title] || job.title
var/obj/item/card/id/card = equipping.wear_id
diff --git a/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm b/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm
index eaa80cc5941..18d3baa3b51 100644
--- a/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm
+++ b/modular_skyrat/modules/ashwalkers/code/buildings/ash_tendril.dm
@@ -99,7 +99,7 @@
viewable_living.gib()
continue
- if(viewable_living.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (viewable_living.key || viewable_living.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
+ if(viewable_living.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (viewable_living.ckey || viewable_living.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
revive_ashwalker(viewable_living)
continue
@@ -129,6 +129,8 @@
else
living_observers.add_mood_event("oogabooga", /datum/mood_event/sacrifice_bad)
+ ashies.sacrifices_made++
+
/**
* Proc that will spawn the egg that will revive the ashwalker
* This is also the Skyrat replacement for /proc/remake_walker
diff --git a/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm b/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm
index e34a414cf8e..ba1dd931c3a 100644
--- a/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm
+++ b/modular_skyrat/modules/ashwalkers/code/effects/ash_rituals.dm
@@ -173,7 +173,7 @@
. = ..()
var/mob_type = pick(
/mob/living/basic/mining/goliath,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion,
+ /mob/living/basic/mining/legion,
/mob/living/basic/mining/brimdemon,
/mob/living/basic/mining/watcher,
/mob/living/basic/mining/lobstrosity/lava,
@@ -373,5 +373,5 @@
find_animal.faction = list(FACTION_ASHWALKER)
- find_animal.revive()
+ find_animal.revive(HEAL_ALL)
return TRUE
diff --git a/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm b/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm
index 357c25e02f9..430d627d526 100644
--- a/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm
+++ b/modular_skyrat/modules/assault_operatives/code/equipment_items/guns.dm
@@ -238,7 +238,7 @@
possible_types = list(AMMO_TYPE_LETHAL, AMMO_TYPE_RUBBER, AMMO_TYPE_AP)
/obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper/sleepytime
- ammo_type = /obj/item/ammo_casing/p50/soporific
+ ammo_type = /obj/item/ammo_casing/p50/disruptor
round_type = AMMO_TYPE_RUBBER
/obj/item/ammo_box/magazine/multi_sprite/assault_ops_sniper/penetrator
diff --git a/modular_skyrat/modules/bulletrebalance/code/sniper.dm b/modular_skyrat/modules/bulletrebalance/code/sniper.dm
deleted file mode 100644
index 632f80251fd..00000000000
--- a/modular_skyrat/modules/bulletrebalance/code/sniper.dm
+++ /dev/null
@@ -1,22 +0,0 @@
-/obj/projectile/bullet/p50
- name =".416 Stabilis bullet"
- speed = 0.2 //This means it's insanely fast, not insanely slow
- damage = 110 //You have 135 health, which does not make this an instant crit
- paralyze = 0 //Knocks you on your ass hard enough as-is, we won't need the paralyze stat
- dismemberment = 30
- armour_penetration = 61 //Bulletproof armor alone will not stop this
- wound_bonus = 90 //Theoretically guaranteed wound
-
-/obj/projectile/bullet/p50/soporific
- name = ".416 Stabilis tranquilizer casing"
- damage_type = STAMINA
- dismemberment = 0
- catastropic_dismemberment = FALSE
- object_damage = 0
-
-/obj/projectile/bullet/p50/soporific/on_hit(atom/target, blocked = FALSE)
- . = ..()
- if((blocked != 100) && isliving(target))
- var/mob/living/living_guy = target
- living_guy.Sleeping(40 SECONDS) //Yes, its really 40 seconds of sleep, I hope you had your morning coffee.
-
diff --git a/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm b/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm
index 3f9b9b73b28..40f8ef775f7 100644
--- a/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm
+++ b/modular_skyrat/modules/company_imports/code/armament_datums/deforest_medical.dm
@@ -121,7 +121,7 @@
cost = PAYCHECK_COMMAND
/datum/armament_entry/company_import/deforest/equipment/surgical_tools
- item_type = /obj/item/storage/backpack/duffelbag/med/surgery
+ item_type = /obj/item/surgery_tray/full
cost = PAYCHECK_COMMAND
/datum/armament_entry/company_import/deforest/equipment/advanced_health_analyer
diff --git a/modular_skyrat/modules/customization/game/objects/items/plushes.dm b/modular_skyrat/modules/customization/game/objects/items/plushes.dm
index d718d409b49..1d2992cf8ef 100644
--- a/modular_skyrat/modules/customization/game/objects/items/plushes.dm
+++ b/modular_skyrat/modules/customization/game/objects/items/plushes.dm
@@ -467,7 +467,7 @@
gender = FEMALE
attack_verb_continuous = list("pats", "hugs", "scolds", "pets")
attack_verb_simple = list("pat", "hug", "scold", "pet")
- squeak_override = list('sound/effects/mousesqueek.ogg' = 1, 'modular_skyrat/modules/emotes/sound/voice/mothsqueak.ogg' = 1,)
+ squeak_override = list('sound/creatures/mousesqueek.ogg' = 1, 'modular_skyrat/modules/emotes/sound/voice/mothsqueak.ogg' = 1,)
responses = list("Rabbits are prey animals and are therefore constantly aware of their surroundings.", "Things to jump up on (they like to be in high places)", "become a rabbit today!", "Be cunning and full of tricks...", "Subscription confirmed! Thank you for choosing RABBITFACTS +TM+!", "Holland Lops are a breed of rabbit originating in the Netherlands.", "Rabbits may need medication to keep themselves healthy, and that's ok! Make sure to take yours too!", "rabbits really liked this product", "A healthy rabbit diet includes fresh vegetables.", "Rabbits do not hibernate. Their schedules are much too busy.", "the rate of bunnies is measured by RPB (rabbits per bunny)", )
/obj/item/toy/plush/skyrat/chunko/andrew
@@ -477,7 +477,7 @@
gender = MALE
attack_verb_continuous = list("pats", "hugs", "scolds", "pets")
attack_verb_simple = list("pat", "hug", "scold", "pet")
- squeak_override = list('sound/effects/mousesqueek.ogg' = 1, 'modular_skyrat/modules/emotes/sound/voice/mothsqueak.ogg' = 1,)
+ squeak_override = list('sound/creatures/mousesqueek.ogg' = 1, 'modular_skyrat/modules/emotes/sound/voice/mothsqueak.ogg' = 1,)
// All lowercase messages are intentional
responses = list("bunny who you best pray you never encounter, lest you suffer a fate worse than death.", "this is a bunny!", "I wonder what would happen if you took bunnies, and combined them with rabbits, and merged their properties and characteristics. It's something to think about.", "If you're cold, they're cold. Give them the deed to your house.", "bunny that goes yeah! woo! yeah! woo! yeah! woo! yeah! woo! yeah! woo! yeah!", "the bunnies are beyond my comprehension", "it's a bunny thing, you wouldn't get it", "this bunny has an unfathomable power level", "%pull the string and I'll bink at you...I'm your bunny.", "Bunny (1954)", "the bunny that pulls the strings....", )
diff --git a/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm b/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm
index 9174700e738..47ae6a48e89 100644
--- a/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm
+++ b/modular_skyrat/modules/customization/modules/clothing/masks/gasmask.dm
@@ -94,6 +94,19 @@
greyscale_config_worn_vox = /datum/greyscale_config/respirator/worn/vox
greyscale_config_worn_teshari = /datum/greyscale_config/respirator/worn/teshari
+/obj/item/clothing/mask/gas/respirator/examine(mob/user)
+ . = ..()
+ . += span_notice("You can toggle its ability to muffle your TTS voice with control click.")
+
+/obj/item/clothing/mask/gas/respirator/CtrlClick(mob/living/user)
+ if(!isliving(user))
+ return
+ if(user.get_active_held_item() != src)
+ to_chat(user, span_warning("You must hold the [src] in your hand to do this!"))
+ return
+ voice_filter = voice_filter ? null : initial(voice_filter)
+ to_chat(user, span_notice("Mask voice muffling [voice_filter ? "enabled" : "disabled"]."))
+
/obj/item/clothing/mask/gas/clown_hat/vox
desc = "A true prankster's facial attire. A clown is incomplete without his wig and mask. This one's got an easily accessible feeding port to be more suitable for the Vox crewmembers."
icon = 'modular_skyrat/master_files/icons/mob/clothing/species/vox/mask.dmi'
diff --git a/modular_skyrat/modules/customization/modules/clothing/~donator/donator_clothing.dm b/modular_skyrat/modules/customization/modules/clothing/~donator/donator_clothing.dm
index 64a6c5c1339..746380b59cb 100644
--- a/modular_skyrat/modules/customization/modules/clothing/~donator/donator_clothing.dm
+++ b/modular_skyrat/modules/customization/modules/clothing/~donator/donator_clothing.dm
@@ -936,7 +936,8 @@
/obj/item/clothing/glasses/welding/steampunk_goggles/attackby(obj/item/attacking_item, mob/living/user, params)
if(!istype(attacking_item, /obj/item/clothing/glasses/welding))
- ..()
+ return ..()
+
if(welding_upgraded)
to_chat(user, span_warning("\The [src] was already upgraded to have welding protection!"))
return
diff --git a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm
index 172ea352ac2..cbb1565392f 100644
--- a/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm
+++ b/modular_skyrat/modules/customization/modules/mob/dead/new_player/sprite_accessories/hair.dm
@@ -479,6 +479,146 @@
name = "Hairfre"
icon_state = "hair_hairfre"
+/datum/sprite_accessory/hair/skyrat/bobcut_over_eye_1
+ name = "Bobcut over eye 1"
+ icon_state = "hair_bobcut_over_eye_1"
+
+/datum/sprite_accessory/hair/skyrat/bobcut_over_eye_2
+ name = "Bobcut over eye 2"
+ icon_state = "hair_bobcut_over_eye_2"
+
+/datum/sprite_accessory/hair/skyrat/bobcut_over_eye_3
+ name = "Bobcut over eye 3"
+ icon_state = "hair_bobcut_over_eye_3"
+
+/datum/sprite_accessory/hair/skyrat/bonnie
+ name = "Bonnie"
+ icon_state = "hair_bonnie"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_short
+ name = "Bonnie short"
+ icon_state = "hair_bonnie_short"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_long
+ name = "Bonnie long"
+ icon_state = "hair_bonnie_long"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_2
+ name = "Bonnie 2"
+ icon_state = "hair_bonnie_2"
+
+/datum/sprite_accessory/hair/skyrat/bonnie_2_long
+ name = "Bonnie long 2"
+ icon_state = "hair_bonnie_2_long"
+
+/datum/sprite_accessory/hair/skyrat/bonie_2_short
+ name = "Bonnie short 2"
+ icon_state = "hair_bonnie_2_short"
+
+/datum/sprite_accessory/hair/skyrat/dawn
+ name = "Dawn"
+ icon_state = "hair_dawn"
+
+/datum/sprite_accessory/hair/skyrat/fluffy
+ name = "Fluffy"
+ icon_state = "hair_fluffy"
+
+/datum/sprite_accessory/hair/skyrat/fluffy_long
+ name = "Fluffy long"
+ icon_state = "hair_fluffy_long"
+
+/datum/sprite_accessory/hair/skyrat/khmuro
+ name = "Khmuro"
+ icon_state = "hair_khmuro"
+
+/datum/sprite_accessory/hair/skyrat/kobeni_1
+ name = "Kobeni 1"
+ icon_state = "hair_kobeni_1"
+
+/datum/sprite_accessory/hair/skyrat/kobeni_2
+ name = "Kobeni 2"
+ icon_state = "hair_kobeni_2"
+
+/datum/sprite_accessory/hair/skyrat/low_bun
+ name = "Low bun"
+ icon_state = "hair_low_bun"
+
+/datum/sprite_accessory/hair/skyrat/low_ponytail
+ name = "Low ponytail"
+ icon_state = "hair_low_ponytail"
+
+/datum/sprite_accessory/hair/skyrat/morning
+ name = "Morning"
+ icon_state = "hair_morning"
+
+/datum/sprite_accessory/hair/skyrat/over_ear_1
+ name = "Over ear 1"
+ icon_state = "hair_over_ear_1"
+
+/datum/sprite_accessory/hair/skyrat/over_ear_2
+ name = "Over ear 2"
+ icon_state = "hair_over_ear_2"
+
+/datum/sprite_accessory/hair/skyrat/over_eye
+ name = "Over eye"
+ icon_state = "hair_over_eye"
+
+/datum/sprite_accessory/hair/skyrat/ponytail
+ name = "Fluffy ponytail"
+ icon_state = "hair_ponytail"
+
+/datum/sprite_accessory/hair/skyrat/ponytail_short
+ name = "Short fluffy ponytail"
+ icon_state = "hair_ponytail_short"
+
+/datum/sprite_accessory/hair/skyrat/simple
+ name = "Simple"
+ icon_state = "hair_simple"
+
+/datum/sprite_accessory/hair/skyrat/simple_long
+ name = "Simple long"
+ icon_state = "hair_simple_long"
+
+/datum/sprite_accessory/hair/skyrat/simple_short
+ name = "Simple short"
+ icon_state = "hair_simple_short"
+
+/datum/sprite_accessory/hair/skyrat/strict
+ name = "Strict"
+ icon_state = "hair_strict"
+
+/datum/sprite_accessory/hair/skyrat/strict_long
+ name = "Strict long"
+ icon_state = "hair_strict_long"
+
+/datum/sprite_accessory/hair/skyrat/strict_short
+ name = "Strict short"
+ icon_state = "hair_strict_short"
+
+/datum/sprite_accessory/hair/skyrat/thin_ponytail
+ name = "Thin ponytail"
+ icon_state = "hair_thin_ponytail"
+
+/datum/sprite_accessory/hair/skyrat/thin_ponytail_long
+ name = "Long thin ponytail"
+ icon_state = "hair_thin_ponytail_long"
+
+/datum/sprite_accessory/hair/skyrat/twintails_2
+ name = "Twintails 2"
+ icon_state = "hair_twintails_2"
+
+/datum/sprite_accessory/hair/skyrat/twintails_2_long
+ name = "Long twintails"
+ icon_state = "hair_twintails_2_long"
+
+/datum/sprite_accessory/hair/skyrat/twintails_2_short
+ name = "Short twintails"
+ icon_state = "hair_twintails_2_short"
+
+/datum/sprite_accessory/hair/skyrat/upwards
+ name = "Upwards"
+ icon_state = "hair_upwards"
+
// Facial hair
/datum/sprite_accessory/facial_hair/skyrat
diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
index 3d0aaf94db5..76ebf34b682 100644
--- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
+++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species.dm
@@ -149,7 +149,7 @@ GLOBAL_LIST_EMPTY(customizable_races)
var/datum/sprite_accessory/undershirt/undershirt = GLOB.undershirt_list[species_human.undershirt]
if(undershirt)
var/mutable_appearance/undershirt_overlay
- if(species_human.dna.species.sexes && species_human.gender == FEMALE)
+ if(species_human.dna.species.sexes && species_human.physique == FEMALE)
undershirt_overlay = wear_female_version(undershirt.icon_state, undershirt.icon, BODY_LAYER)
else
undershirt_overlay = mutable_appearance(undershirt.icon, undershirt.icon_state, -BODY_LAYER)
diff --git a/modular_skyrat/modules/customization/modules/reagents/chemistry/reagents/other_reagents.dm b/modular_skyrat/modules/customization/modules/reagents/chemistry/reagents/other_reagents.dm
index fabba52db0f..1c03bbf2ca3 100644
--- a/modular_skyrat/modules/customization/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/modular_skyrat/modules/customization/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -70,3 +70,30 @@
else
to_chat(M, span_notice("[pick("I feel oddly calm.", "I feel relaxed.", "Mew?")]"))
..()
+
+#define DERMAGEN_SCAR_FIX_AMOUNT 10
+
+/datum/reagent/medicine/dermagen
+ name = "Dermagen"
+ description = "Heals scars formed by past physical trauma when applied. Minimum 10u needed, only works when applied topically."
+ reagent_state = LIQUID
+ color = "#FFEBEB"
+ ph = 6
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+
+/datum/reagent/medicine/dermagen/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE)
+ . = ..()
+ if(!iscarbon(exposed_mob))
+ return
+ if(!(methods & (PATCH|TOUCH|VAPOR)))
+ return
+ var/mob/living/carbon/scarred = exposed_mob
+ if(scarred.stat == DEAD)
+ show_message = FALSE
+ if(show_message)
+ to_chat(scarred, span_danger("The scars on your body start to fade and disappear."))
+ if(reac_volume >= DERMAGEN_SCAR_FIX_AMOUNT)
+ for(var/i in scarred.all_scars)
+ qdel(i)
+
+#undef DERMAGEN_SCAR_FIX_AMOUNT
diff --git a/modular_skyrat/modules/customization/modules/reagents/chemistry/recipes/medicine.dm b/modular_skyrat/modules/customization/modules/reagents/chemistry/recipes/medicine.dm
index afcfa2da8cd..34b97214bb8 100644
--- a/modular_skyrat/modules/customization/modules/reagents/chemistry/recipes/medicine.dm
+++ b/modular_skyrat/modules/customization/modules/reagents/chemistry/recipes/medicine.dm
@@ -24,3 +24,8 @@
results = list(/datum/reagent/medicine/taste_suppressor = 3, /datum/reagent/chlorine = 1) // The chlorine dissociated from the sodium to allow for the synthesis of the taste suppressor
required_reagents = list(/datum/reagent/consumable/salt = 2, /datum/reagent/sulfur = 1, /datum/reagent/water = 1)
required_temp = 300
+
+/datum/chemical_reaction/medicine/dermagen
+ results = list(/datum/reagent/medicine/dermagen = 5)
+ required_reagents = list(/datum/reagent/consumable/ethanol = 4, /datum/reagent/medicine/c2/synthflesh = 3, /datum/reagent/medicine/mine_salve = 3)
+ mix_message = "The slurry congeals into a thick cream."
diff --git a/modular_skyrat/modules/decay_subsystem/code/nests.dm b/modular_skyrat/modules/decay_subsystem/code/nests.dm
index 3a8dad43888..74424371e87 100644
--- a/modular_skyrat/modules/decay_subsystem/code/nests.dm
+++ b/modular_skyrat/modules/decay_subsystem/code/nests.dm
@@ -200,7 +200,7 @@
desc = "These pulsating eggs are oozing out a puss like substance..."
icon_state = "nest_eggs"
light_color = LIGHT_COLOR_BRIGHT_YELLOW
- monster_types = list(/mob/living/simple_animal/hostile/retaliate/snake)
+ monster_types = list(/mob/living/basic/snake)
max_mobs = 8
spawn_cooldown = 5 SECONDS
diff --git a/modular_skyrat/modules/emotes/code/emotes.dm b/modular_skyrat/modules/emotes/code/emotes.dm
index 5f41b22751b..16c8ecbcef6 100644
--- a/modular_skyrat/modules/emotes/code/emotes.dm
+++ b/modular_skyrat/modules/emotes/code/emotes.dm
@@ -143,7 +143,7 @@
message = "squeaks!"
emote_type = EMOTE_AUDIBLE
vary = TRUE
- sound = 'sound/effects/mousesqueek.ogg'
+ sound = 'sound/creatures/mousesqueek.ogg'
/datum/emote/living/merp
key = "merp"
diff --git a/modular_skyrat/modules/faction/code/outfit.dm b/modular_skyrat/modules/faction/code/outfit.dm
index 4728353e4ec..a808f58c05f 100644
--- a/modular_skyrat/modules/faction/code/outfit.dm
+++ b/modular_skyrat/modules/faction/code/outfit.dm
@@ -43,7 +43,7 @@
back = /obj/item/storage/backpack
id = /obj/item/card/id/faction_crew
l_pocket = /obj/item/melee/energy/sword
- l_hand = /obj/item/storage/backpack/duffelbag/med/surgery
+ l_hand = /obj/item/surgery_tray/full
backpack_contents = list(/obj/item/storage/box/survival/engineer=1, /obj/item/storage/medkit/tactical=1,/obj/item/storage/medkit/regular=1,/obj/item/storage/medkit/toxin=1, /obj/item/ammo_box/magazine/m45=2, /obj/item/gun/ballistic/automatic/pistol/m1911=1,/obj/item/healthanalyzer=1,/obj/item/stack/spacecash/c1000=1)
/datum/outfit/faction_tech
diff --git a/modular_skyrat/modules/fauna_reagent/fauna_reagent.dm b/modular_skyrat/modules/fauna_reagent/fauna_reagent.dm
index cee0e7efab5..796dbef0daf 100644
--- a/modular_skyrat/modules/fauna_reagent/fauna_reagent.dm
+++ b/modular_skyrat/modules/fauna_reagent/fauna_reagent.dm
@@ -15,14 +15,57 @@
/mob/living/simple_animal/Life(seconds_per_tick, times_fired)
. = ..()
- if(reagent_health && reagents)
- for(var/datum/reagent/reagents_within as anything in reagents.reagent_list)
- if(istype(reagents_within, /datum/reagent/toxin))
- var/datum/reagent/toxin/toxin_reagent = reagents_within
- var/toxin_damage = round(toxin_reagent.toxpwr)
- adjustHealth(toxin_damage + 1)
- reagents.remove_reagent(toxin_reagent.type, 0.5)
- continue
- if(istype(reagents_within, /datum/reagent/medicine))
- adjustHealth(-1)
- reagents.remove_reagent(reagents_within.type, 0.5)
+
+ if(!reagent_health)
+ return
+
+ if(!reagents)
+ return
+
+ if(stat == DEAD)
+ return
+
+ for(var/datum/reagent/reagents_within as anything in reagents.reagent_list)
+ if(istype(reagents_within, /datum/reagent/toxin))
+ var/datum/reagent/toxin/toxin_reagent = reagents_within
+ var/toxin_damage = round(toxin_reagent.toxpwr)
+ adjustHealth(toxin_damage + 1)
+ reagents.remove_reagent(toxin_reagent.type, 0.5)
+ continue
+
+ if(istype(reagents_within, /datum/reagent/medicine))
+ adjustHealth(-1)
+ reagents.remove_reagent(reagents_within.type, 0.5)
+
+/mob/living/basic
+ /// whether the simple animal can be healed/damaged through reagents
+ var/reagent_health = TRUE
+
+/mob/living/basic/Initialize(mapload)
+ . = ..()
+ if(reagent_health)
+ create_reagents(1000, REAGENT_HOLDER_ALIVE)
+
+/mob/living/basic/Life(seconds_per_tick, times_fired)
+ . = ..()
+
+ if(!reagent_health)
+ return
+
+ if(!reagents)
+ return
+
+ if(stat == DEAD)
+ return
+
+ for(var/datum/reagent/reagents_within as anything in reagents.reagent_list)
+ if(istype(reagents_within, /datum/reagent/toxin))
+ var/datum/reagent/toxin/toxin_reagent = reagents_within
+ var/toxin_damage = round(toxin_reagent.toxpwr)
+ adjust_health(toxin_damage + 1)
+ reagents.remove_reagent(toxin_reagent.type, 0.5)
+ continue
+
+ if(istype(reagents_within, /datum/reagent/medicine))
+ adjust_health(-1)
+ reagents.remove_reagent(reagents_within.type, 0.5)
diff --git a/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm b/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm
index ea06dc1163c..f1bece74ab4 100644
--- a/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm
+++ b/modular_skyrat/modules/goofsec/code/sec_clothing_overrides.dm
@@ -762,6 +762,8 @@
/obj/item/gun/energy/dueling,
/obj/item/gun/energy/laser/thermal,
/obj/item/gun/ballistic/rifle/boltaction, //fits if you make it an obrez
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
/obj/item/storage/belt/holster/detective
@@ -785,6 +787,8 @@
/obj/item/gun/energy/dueling,
/obj/item/gun/energy/laser/thermal,
/obj/item/gun/ballistic/rifle/boltaction, //fits if you make it an obrez
+ /obj/item/gun/energy/laser/captain,
+ /obj/item/gun/energy/e_gun/hos,
))
/*
diff --git a/modular_skyrat/modules/ices_events/code/ICES_event_config.dm b/modular_skyrat/modules/ices_events/code/ICES_event_config.dm
index bd49fc1de17..c65aafe5da5 100644
--- a/modular_skyrat/modules/ices_events/code/ICES_event_config.dm
+++ b/modular_skyrat/modules/ices_events/code/ICES_event_config.dm
@@ -588,12 +588,6 @@
max_occurrences = 2
weight = VERY_HIGH_EVENT_FREQ
-/**
- * Wall Fungus
- */
-/datum/round_event_control/wall_fungus
- weight = MED_EVENT_FREQ
-
/**
* Wisdom Cow
*
diff --git a/modular_skyrat/modules/mapping/code/lavaland_ruin_code.dm b/modular_skyrat/modules/mapping/code/lavaland_ruin_code.dm
index c66faed0810..c2e667004c2 100644
--- a/modular_skyrat/modules/mapping/code/lavaland_ruin_code.dm
+++ b/modular_skyrat/modules/mapping/code/lavaland_ruin_code.dm
@@ -108,3 +108,15 @@
/obj/item/radio/headset/interdyne/comms
keyslot = new /obj/item/encryptionkey/headset_syndicate/interdyne
keyslot2 = new /obj/item/encryptionkey/syndicate
+
+//MOBS
+
+// hivelords that stand guard where they spawn
+/mob/living/basic/mining/hivelord/no_wander
+ ai_controller = /datum/ai_controller/basic_controller/hivelord/no_wander
+
+//MOB AI
+
+// same as a regular hivelord minus the idle walking
+/datum/ai_controller/basic_controller/hivelord/no_wander
+ idle_behavior = null
diff --git a/modular_skyrat/modules/medical/code/wounds/medical.dm b/modular_skyrat/modules/medical/code/wounds/medical.dm
index 80662956e5f..5a3723fff4b 100644
--- a/modular_skyrat/modules/medical/code/wounds/medical.dm
+++ b/modular_skyrat/modules/medical/code/wounds/medical.dm
@@ -4,8 +4,6 @@
/obj/item/stack/medical/gauze
/// The amount of direct hits our limb can take before we fall off.
var/integrity = 2
- /// The bodypart we are attached to. Nullable if we aren't applied to anything.
- var/obj/item/bodypart/bodypart
/// If we are splinting a limb, this is the overlay prefix we will use.
var/splint_prefix = "splint"
/// If we are bandaging a limb, this is the overlay prefix we will use.
@@ -14,25 +12,16 @@
var/can_splint = TRUE
/obj/item/bodypart/apply_gauze(obj/item/stack/gauze)
- RegisterSignal(src, COMSIG_BODYPART_GAUZED, PROC_REF(got_gauzed))
-
. = ..()
- UnregisterSignal(src, COMSIG_BODYPART_GAUZED)
+ owner?.update_bandage_overlays()
-/// Signal handler that allows us to modularly detect if we were applied to a limb or not.
-/obj/item/bodypart/proc/got_gauzed(datum/signal_source, obj/item/stack/medical/gauze/new_gauze)
- SIGNAL_HANDLER
+/obj/item/stack/medical/gauze/Destroy()
+ var/mob/living/carbon/previously_gauzed = gauzed_bodypart?.owner
- if (istype(current_gauze, /obj/item/stack/medical/gauze))
- var/obj/item/stack/medical/gauze/applied_gauze = current_gauze
- applied_gauze.set_limb(src) // new_gauze isnt actually the gauze that was applied weirdly
+ . = ..()
-/obj/item/stack/medical/gauze/Destroy()
- bodypart?.current_gauze = null
- bodypart?.owner?.update_bandage_overlays()
- set_limb(null)
- return ..()
+ previously_gauzed?.update_bandage_overlays()
/**
* rip_off() called when someone rips it off
@@ -43,7 +32,7 @@
/obj/item/stack/medical/gauze/proc/rip_off()
if (is_pristine())
. = new src.type(null, 1)
- bodypart?.owner?.update_bandage_overlays()
+
qdel(src)
/// Returns either [splint_prefix] or [gauze_prefix] depending on if we are splinting or not. Suffixes it with a digitigrade flag if applicable for the limb.
@@ -56,8 +45,8 @@
else
prefix = gauze_prefix
- var/suffix = bodypart.body_zone
- if(bodypart.bodytype & BODYTYPE_DIGITIGRADE)
+ var/suffix = gauzed_bodypart.body_zone
+ if(gauzed_bodypart.bodytype & BODYTYPE_DIGITIGRADE)
suffix += "_digitigrade"
return "[prefix]_[suffix]"
@@ -69,7 +58,7 @@
if (!can_splint)
return FALSE
- for (var/datum/wound/iterated_wound as anything in bodypart.wounds)
+ for (var/datum/wound/iterated_wound as anything in gauzed_bodypart.wounds)
if (iterated_wound.wound_flags & SPLINT_OVERLAY)
return TRUE
@@ -95,35 +84,30 @@
/obj/item/stack/medical/gauze/proc/get_hit()
integrity--
if(integrity <= 0)
- if(bodypart.owner)
- to_chat(bodypart.owner, span_warning("The [name] on your [bodypart.name] tears and falls off!"))
+ if(gauzed_bodypart.owner)
+ to_chat(gauzed_bodypart.owner, span_warning("The [name] on your [gauzed_bodypart.name] tears and falls off!"))
qdel(src)
/obj/item/stack/medical/gauze/Topic(href, href_list)
. = ..()
if(href_list["remove"])
- if(!bodypart.owner)
+ if(!gauzed_bodypart.owner)
return
if(!iscarbon(usr))
return
- if(!in_range(usr, bodypart.owner))
+ if(!in_range(usr, gauzed_bodypart.owner))
return
- var/mob/living/carbon/C = usr
- var/self = (C == bodypart.owner)
- C.visible_message(span_notice("[C] begins removing [name] from [self ? "[bodypart.owner.p_Their()]" : "[bodypart.owner]'s" ] [bodypart.name]..."), span_notice("You begin to remove [name] from [self ? "your" : "[bodypart.owner]'s"] [bodypart.name]..."))
- if(!do_after(C, (self ? SELF_AID_REMOVE_DELAY : OTHER_AID_REMOVE_DELAY), target=bodypart.owner))
+ var/mob/living/carbon/carbon_user = usr
+ var/self = (carbon_user == gauzed_bodypart.owner)
+ carbon_user.visible_message(span_notice("[carbon_user] begins removing [name] from [self ? "[gauzed_bodypart.owner.p_Their()]" : "[gauzed_bodypart.owner]'s" ] [gauzed_bodypart.name]..."), span_notice("You begin to remove [name] from [self ? "your" : "[gauzed_bodypart.owner]'s"] [gauzed_bodypart.name]..."))
+ if(!do_after(carbon_user, (self ? SELF_AID_REMOVE_DELAY : OTHER_AID_REMOVE_DELAY), target = gauzed_bodypart.owner))
return
if(QDELETED(src))
return
- C.visible_message(span_notice("[C] removes [name] from [self ? "[bodypart.owner.p_Their()]" : "[bodypart.owner]'s" ] [bodypart.name]."), span_notice("You remove [name] from [self ? "your" : "[bodypart.owner]'s" ] [bodypart.name]."))
+ carbon_user.visible_message(span_notice("[carbon_user] removes [name] from [self ? "[gauzed_bodypart.owner.p_Their()]" : "[gauzed_bodypart.owner]'s" ] [gauzed_bodypart.name]."), span_notice("You remove [name] from [self ? "your" : "[gauzed_bodypart.owner]'s" ] [gauzed_bodypart.name]."))
var/obj/item/gotten = rip_off()
- if(gotten && !C.put_in_hands(gotten))
- gotten.forceMove(get_turf(C))
-
-/// Sets bodypart to limb, and then updates owner's bandage overlays. Limb is nullable.
-/obj/item/stack/medical/gauze/proc/set_limb(limb)
- bodypart = limb
- bodypart?.owner?.update_bandage_overlays()
+ if(gotten && !carbon_user.put_in_hands(gotten))
+ gotten.forceMove(get_turf(carbon_user))
/// Returns the name of ourself when used in a "owner is [usage_prefix] by [name]" examine_more situation/
/obj/item/stack/proc/get_gauze_description()
diff --git a/modular_skyrat/modules/modular_implants/code/nif_persistence.dm b/modular_skyrat/modules/modular_implants/code/nif_persistence.dm
index 1dc272c434e..fd24fd3b579 100644
--- a/modular_skyrat/modules/modular_implants/code/nif_persistence.dm
+++ b/modular_skyrat/modules/modular_implants/code/nif_persistence.dm
@@ -15,6 +15,8 @@
var/nif_is_calibrated
/// How many rewards points does the NIF have stored on it?
var/stored_rewards_points
+ /// A string containing programs that are transfered from one round to the next.
+ var/persistent_nifsofts
/// Saves the NIF data for a individual user.
/mob/living/carbon/human/proc/save_nif_data(datum/modular_persistence/persistence, remove_nif = FALSE)
@@ -51,14 +53,19 @@
persistence.stored_rewards_points = installed_nif.rewards_points
var/datum/component/nif_examine/examine_component = GetComponent(/datum/component/nif_examine)
-
persistence.nif_examine_text = examine_component?.nif_examine_text
+
+ var/persistent_nifsoft_paths = "" // We need to convert all of the paths in the list into a single string
for(var/datum/nifsoft/nifsoft as anything in installed_nif.loaded_nifsofts)
- if(!nifsoft.persistence)
+ if(nifsoft.persistence)
+ nifsoft.save_persistence_data(persistence)
+
+ if(!nifsoft.able_to_keep || !nifsoft.keep_installed)
continue
- nifsoft.save_persistence_data(persistence)
+ persistent_nifsoft_paths += "&[(nifsoft.type)]"
+ persistence.persistent_nifsofts = persistent_nifsoft_paths
/// Loads the NIF data for an individual user.
/mob/living/carbon/human/proc/load_nif_data(datum/modular_persistence/persistence)
@@ -74,6 +81,16 @@
new_nif.current_theme = persistence.nif_theme
new_nif.is_calibrated = persistence.nif_is_calibrated
new_nif.rewards_points = persistence.stored_rewards_points
+
+ var/list/persistent_nifsoft_paths = list()
+ for(var/text as anything in splittext(persistence.persistent_nifsofts, "&"))
+ var/datum/nifsoft/nifsoft_to_add = text2path(text)
+ if(!ispath(nifsoft_to_add, /datum/nifsoft) || !initial(nifsoft_to_add.able_to_keep))
+ continue
+
+ persistent_nifsoft_paths.Add(nifsoft_to_add)
+
+ new_nif.persistent_nifsofts = persistent_nifsoft_paths.Copy()
new_nif.Insert(src)
var/datum/component/nif_examine/examine_component = GetComponent(/datum/component/nif_examine)
diff --git a/modular_skyrat/modules/modular_implants/code/nif_research.dm b/modular_skyrat/modules/modular_implants/code/nif_research.dm
index 7e8e1ca94f8..4e88040cddf 100644
--- a/modular_skyrat/modules/modular_implants/code/nif_research.dm
+++ b/modular_skyrat/modules/modular_implants/code/nif_research.dm
@@ -28,6 +28,16 @@
category = list(RND_CATEGORY_TOOLS + RND_SUBCATEGORY_EQUIPMENT_MEDICAL) // look, the anesthetic machine's there too
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
+/datum/design/mini_soulcatcher
+ name = "Poltergeist-Type RSD"
+ desc = "A miniature version of a Soulcatcher that can be attached to various objects."
+ id = "mini_soulcatcher"
+ build_type = PROTOLATHE | AWAY_LATHE
+ build_path = /obj/item/attachable_soulcatcher
+ materials = list(/datum/material/glass = SMALL_MATERIAL_AMOUNT * 5, /datum/material/iron = SMALL_MATERIAL_AMOUNT * 5)
+ category = list(RND_CATEGORY_AI + RND_SUBCATEGORY_AI_MISC) // look, the anesthetic machine's there too
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_SERVICE | DEPARTMENT_BITFLAG_MEDICAL
+
/datum/design/nifsoft_hud
build_type = PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/silver = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/plastic = SHEET_MATERIAL_AMOUNT)
diff --git a/modular_skyrat/modules/modular_implants/code/nifs.dm b/modular_skyrat/modules/modular_implants/code/nifs.dm
index 69c0a581c35..9dd1af28751 100644
--- a/modular_skyrat/modules/modular_implants/code/nifs.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifs.dm
@@ -92,6 +92,8 @@
var/list/loaded_nifsofts = list()
///What programs come already installed on the NIF?
var/list/preinstalled_nifsofts = list(/datum/nifsoft/soul_poem)
+ ///What programs do we want to carry between rounds?
+ var/list/persistent_nifsofts = list()
///This shows up in the NIF settings screen as a way to ICly display lore.
var/manufacturer_notes = "There is no data currently avalible for this product."
@@ -147,8 +149,8 @@
linked_mob.AddComponent(/datum/component/nif_examine)
RegisterSignal(linked_mob, COMSIG_LIVING_DEATH, PROC_REF(damage_on_death))
- if(preinstalled_nifsofts)
- send_message("Loading preinstalled NIFSofts, please wait...")
+ if(preinstalled_nifsofts || persistent_nifsofts)
+ send_message("Loading preinstalled and stored NIFSofts, please wait...")
addtimer(CALLBACK(src, PROC_REF(install_preinstalled_nifsofts)), 3 SECONDS)
/obj/item/organ/internal/cyberimp/brain/nif/Remove(mob/living/carbon/organ_owner, special = FALSE)
@@ -174,6 +176,10 @@
for(var/datum/nifsoft/preinstalled_nifsoft as anything in preinstalled_nifsofts)
new preinstalled_nifsoft(src)
+ for(var/stored_nifsoft in persistent_nifsofts)
+ var/datum/nifsoft/new_stored_nifsoft = new stored_nifsoft(src)
+ new_stored_nifsoft.keep_installed = TRUE
+
return TRUE
/obj/item/organ/internal/cyberimp/brain/nif/process(seconds_per_tick)
diff --git a/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm b/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm
index 0423e28c830..6da74c6b89f 100644
--- a/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifs_tgui.dm
@@ -45,6 +45,8 @@
"active_cost" = nifsoft.active_cost,
"reference" = REF(nifsoft),
"ui_icon" = nifsoft.ui_icon,
+ "able_to_keep" = nifsoft.able_to_keep,
+ "keep_installed" = nifsoft.keep_installed,
)
data["loaded_nifsofts"] += list(nifsoft_data)
@@ -126,3 +128,11 @@
return FALSE
activated_nifsoft.activate()
+
+ if("toggle_keeping_nifsoft")
+ var/datum/nifsoft/nifsoft_to_keep = locate(params["nifsoft_to_keep"]) in loaded_nifsofts
+ if(!nifsoft_to_keep || !nifsoft_to_keep.able_to_keep)
+ return FALSE
+
+ nifsoft_to_keep.keep_installed = !nifsoft_to_keep.keep_installed
+ update_static_data_for_all_viewers()
diff --git a/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm b/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm
index aac352bab4c..4cd6beea379 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsoft_catalog.dm
@@ -1,10 +1,13 @@
GLOBAL_LIST_INIT(purchasable_nifsofts, list(
/datum/nifsoft/hivemind,
/datum/nifsoft/summoner,
- /datum/nifsoft/shapeshifter,
+ /datum/nifsoft/action_granter/shapeshifter,
/datum/nifsoft/summoner/dorms,
/datum/nifsoft/soul_poem,
/datum/nifsoft/soulcatcher,
+ /datum/nifsoft/scryer,
+ /datum/nifsoft/summoner/book,
+ /datum/nifsoft/action_granter/hypnosis,
))
/datum/computer_file/program/nifsoft_downloader
@@ -79,6 +82,7 @@ GLOBAL_LIST_INIT(purchasable_nifsofts, list(
"category" = initial(buyable_nifsoft.buying_category),
"ui_icon" = initial(buyable_nifsoft.ui_icon),
"reference" = buyable_nifsoft,
+ "keepable" = initial(buyable_nifsoft.able_to_keep),
)
var/category = nifsoft_details["category"]
if(!(category in product_list))
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts.dm b/modular_skyrat/modules/modular_implants/code/nifsofts.dm
index d7c382cfddb..1bf551866f5 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts.dm
@@ -47,6 +47,10 @@
var/rewards_points_eligible = TRUE
///Does the NIFSoft have anything that is saved cross-round?
var/persistence = FALSE
+ /// Is the NIFSoft something that we want to allow the user to keep?
+ var/able_to_keep = FALSE
+ /// Are we keeping the NIFSoft installed between rounds? This is decided by the user
+ var/keep_installed = FALSE
///Is it a lewd item?
var/lewd_nifsoft = FALSE
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/base_types/action_granter.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/base_types/action_granter.dm
new file mode 100644
index 00000000000..05d1012d629
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/base_types/action_granter.dm
@@ -0,0 +1,26 @@
+/// This type of NIFSoft grans the user an action when active.
+/datum/nifsoft/action_granter
+ active_mode = TRUE
+ activation_cost = 10
+ active_cost = 1
+ /// What is the path of the action that we want to grant?
+ var/action_to_grant = /datum/action/innate
+ /// What action are we giving the user of the NIFSoft?
+ var/datum/action/innate/granted_action
+
+/datum/nifsoft/action_granter/activate()
+ . = ..()
+ if(active)
+ granted_action = new action_to_grant
+ granted_action.Grant(linked_mob)
+ return
+
+ if(granted_action)
+ granted_action.Remove(linked_mob)
+
+/datum/nifsoft/action_granter/Destroy()
+ if(granted_action)
+ QDEL_NULL(granted_action)
+ return ..()
+
+
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/book_summoner.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/book_summoner.dm
new file mode 100644
index 00000000000..954a56e9ff9
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/book_summoner.dm
@@ -0,0 +1,36 @@
+/obj/item/disk/nifsoft_uploader/summoner/book
+ name = "Grimoire Akasha"
+ loaded_nifsoft = /datum/nifsoft/summoner/book
+
+/datum/nifsoft/summoner/book
+ name = "Grimoire Akasha"
+ program_desc = "Grimoire Akasha is a fork of the Grimoire Caeruleam NIFSoft that is designed around giving the user access to various educational hardlight books. \
+ Due to its educational nature and miniscule size, Grimoire Akasha is typically provided for free at most NIFSoft marketplaces."
+ summonable_items = list()
+ purchase_price = 0 // This is a tool intended to help out newer players.
+ max_summoned_items = 2
+ buying_category = NIFSOFT_CATEGORY_INFORMATION
+ ui_icon = "book"
+
+/datum/nifsoft/summoner/book/New()
+ . = ..()
+ summonable_items += subtypesof(/obj/item/book/manual/wiki) //That's right! all of the manual books!
+
+/datum/nifsoft/summoner/book/apply_custom_properties(obj/item/book/generated_book)
+ if(!istype(generated_book))
+ return FALSE
+
+ generated_book.cannot_carve = TRUE
+ return TRUE
+
+// Need this code here so that we don't have people carving out the summoned books
+/obj/item/book
+ /// Is the parent book unable to be carved? TRUE prevents carving. By default this is unset
+ var/cannot_carve
+
+/obj/item/book/try_carve(obj/item/carving_item, mob/living/user, params)
+ if(cannot_carve)
+ balloon_alert(user, "unable to be carved!")
+ return FALSE
+
+ return ..()
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/hypnosis.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/hypnosis.dm
new file mode 100644
index 00000000000..b4f59561872
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/hypnosis.dm
@@ -0,0 +1,62 @@
+/datum/nifsoft/action_granter/hypnosis
+ name = "Purpura Eye"
+ program_desc = "Based on the hypnotic equipment provided by the LustWish vendor, the purpura eyes NIFSoft allows the user to ensnare others in a hypnotic trance. ((This is intended as a tool for ERP, don't use this for gameplay reasons.))"
+ buying_category = NIFSOFT_CATEGORY_FUN
+ lewd_nifsoft = TRUE
+ purchase_price = 150
+ able_to_keep = TRUE
+ active_cost = 0.1
+ ui_icon = "eye"
+ action_to_grant = /datum/action/innate/nif_hypnotize
+
+/datum/action/innate/nif_hypnotize
+ name = "Hypnotize"
+ background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
+ background_icon_state = "android"
+ button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
+ button_icon_state = "hypnotize"
+
+/datum/action/innate/nif_hypnotize/Activate()
+ var/mob/living/carbon/human/user = owner
+ if(!istype(user))
+ return FALSE
+
+ var/mob/living/carbon/human/target_human = user.pulling
+ if(!istype(target_human) || user.grab_state < GRAB_AGGRESSIVE)
+ to_chat(user, span_warning("You need to aggressively grab someone to hypnotize them."))
+ return FALSE
+
+ if(!target_human.client?.prefs?.read_preference(/datum/preference/toggle/erp/sex_toy))
+ to_chat(user, span_warning("[target_human] doesn't want to be hypnotized."))
+ return FALSE
+
+ to_chat(user, span_notice("You begin to place [target_human] into a hypnotic trance."))
+
+ if(!do_after(user, 12 SECONDS, target_human))
+ return FALSE
+
+ var/choice = tgui_alert(target_human, "Do you believe in hypnosis? (This will allow [user] to issue hypnotic suggestions.)", "Hypnosis", list("Yes", "No"))
+ if(choice != "Yes")
+ to_chat(user, span_warning("[target_human]'s attention breaks despite your efforts. They clearly don't seem interested!"))
+ to_chat(target_human, span_warning("Your attention breaks as you realize that you don't want to listen to [user]'s suggestions."))
+ return FALSE
+
+ user.visible_message(span_purple("[target_human] falls into a deep, hypnotic slumber right at the snap of your fingers."), span_purple("You suddenly fall limp at the snap of [user]'s fingers."))
+ user.emote("snap")
+ target_human.SetSleeping(60 SECONDS)
+ target_human.log_message("[target_human] was placed into a hypnotic sleep by [user].", LOG_GAME)
+
+ var/secondary_choice = tgui_alert(user, "Would you like to give [target_human] a hypnotic suggestion or release them?", "Hypnosis", list("Suggestion", "Release"))
+ while(secondary_choice == "Suggestion" && target_human.IsSleeping())
+ if(!in_range(user, target_human))
+ to_chat(user, span_warning("You must be in whisper range to [target_human] in order to give hypnotic suggestions."))
+ target_human.SetSleeping(0)
+ return FALSE
+
+ var/input_text = tgui_input_text(user, "What would you like to suggest?", "Hypnotic Suggestion")
+ to_chat(user, span_purple("You whisper into [target_human]'s ears in a soothing voice."))
+ to_chat(target_human, span_hypnophrase("[input_text]"))
+ secondary_choice = tgui_alert(user, "Would you like to give [target_human] an additional hypnotic suggestion or release them?", "Hypnosis", list("Suggestion", "Release"))
+
+ user.visible_message(span_purple("You wake up from your deep, hypnotic slumber. The suggestions from [user] now settled into your mind."), span_purple("[target_human] wakes up from their slumber."))
+ target_human.SetSleeping(0)
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm
index 5cd8a0443cb..aab1cc52b7a 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/prop_summoner.dm
@@ -16,6 +16,7 @@
activation_cost = 100 // Around 1/10th the energy of a standard NIF
buying_category = NIFSOFT_CATEGORY_FUN
ui_icon = "book-open"
+ able_to_keep = TRUE // These NIFSofts are mostly for comsetic/fun reasons anyways.
/// Does the resulting object have a holographic like filter appiled to it?
var/holographic_filter = TRUE
@@ -88,9 +89,17 @@
refund_activation_cost()
return FALSE
+ apply_custom_properties(new_item)
summoned_items += new_item
new_item.AddComponent(/datum/component/summoned_item, holographic_filter)
+/// This proc is called while an item is being summoned, use this to modifiy aspects of the item that aren't modified by the component.
+/datum/nifsoft/summoner/proc/apply_custom_properties(obj/item/target_item)
+ if(!target_item)
+ return FALSE
+
+ return TRUE
+
/datum/nifsoft/summoner/Destroy()
QDEL_LIST(summoned_items)
return ..()
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/scryer.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/scryer.dm
new file mode 100644
index 00000000000..3cf689624e0
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/scryer.dm
@@ -0,0 +1,117 @@
+/obj/item/disk/nifsoft_uploader/scryer
+ name = "NIFSoft Scryer Uploader Disk"
+ loaded_nifsoft = /datum/nifsoft/scryer
+
+/datum/nifsoft/scryer
+ name = "NIFLink Holocaller"
+ program_desc = "This ubiquitous NIFSoft adds Scryer functionality similar to MODSuits to the user's NIF; allowing for real-time communication through AR hologlass screens from a hardlight projector sat around the wearer's neck"
+ active_mode = TRUE
+ active_cost = 1
+ activation_cost = 20
+ purchase_price = 200
+ buying_category = NIFSOFT_CATEGORY_UTILITY
+ ui_icon = "video"
+ /// What is the scryer currently associated with the NIFSoft?
+ var/obj/item/clothing/neck/link_scryer/loaded/nifsoft/linked_scryer
+
+/datum/nifsoft/scryer/New()
+ . = ..()
+ var/obj/item/organ/internal/cyberimp/brain/nif/parent_resolved = parent_nif.resolve()
+ if(!istype(parent_resolved))
+ stack_trace("[src] ([REF(src)]) tried to create a linked scryer but it had no parent_nif!")
+ if(!linked_scryer)
+ stack_trace("[src] ([REF(src)]) created with no linked scryer!")
+ linked_scryer = new (parent_resolved)
+ linked_scryer.parent_nifsoft = WEAKREF(src)
+
+/datum/nifsoft/scryer/Destroy()
+ if(!QDELETED(linked_scryer))
+ QDEL_NULL(linked_scryer)
+
+ return ..()
+
+/datum/nifsoft/scryer/activate()
+ . = ..()
+ if(. == FALSE)
+ return FALSE
+
+ if(!active)
+ if(linked_scryer)
+ var/parent_resolved = parent_nif.resolve()
+ if(parent_resolved)
+ return linked_mob.transferItemToLoc(linked_scryer, parent_resolved, TRUE)
+ return FALSE
+
+ if(linked_mob.handcuffed)
+ linked_mob.balloon_alert(linked_mob, "handcuffed")
+ activate()
+ return FALSE
+
+ if(!linked_mob.equip_to_slot_if_possible(linked_scryer, ITEM_SLOT_NECK)) //This sends out a message to the mob if it can't be put on.
+ activate()
+ return FALSE
+
+ return TRUE
+
+/obj/item/clothing/neck/link_scryer
+ /// Do we have custom controls? This is only affects the text shown when examining
+ var/custom_examine_controls = FALSE
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft
+ name = "\improper NIFLink Holocaller"
+ desc = "A nanomachine construct working as a modified version of the MODlink scryer, conjured using a NIF; functionally the same, but able to carry out holocalls in a more portable format."
+ custom_examine_controls = TRUE
+ /// A weakref of the parent NIFSoft that the scryer belongs to.
+ var/datum/weakref/parent_nifsoft
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/Initialize(mapload)
+ . = ..()
+ if(cell)
+ QDEL_NULL(cell)
+
+ cell = new /obj/item/stock_parts/cell/infinite/nif_cell(src)
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/Destroy()
+ if(parent_nifsoft)
+ var/datum/nifsoft/scryer/resolved_nifsoft = parent_nifsoft.resolve()
+ if(!QDELETED(resolved_nifsoft))
+ resolved_nifsoft.linked_scryer = null
+
+ return ..()
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/examine(mob/user)
+ . = ..()
+ . += span_notice("The MODlink ID is [mod_link.id], frequency is [mod_link.frequency || "unset"]. Right-click with a multitool to copy/imprint the frequency.")
+ . += span_notice("Right-click with an empty hand to change the name.")
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot & ITEM_SLOT_NECK)
+ return TRUE
+
+ var/datum/nifsoft/scryer/scryer_nifsoft = parent_nifsoft.resolve()
+ if(!istype(scryer_nifsoft))
+ return FALSE
+
+ scryer_nifsoft.activate() //If it's not on the neck, it shouldn't be active.
+ return TRUE
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/screwdriver_act(mob/living/user, obj/item/tool)
+ balloon_alert(user, "cell non-removable!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/item/clothing/neck/link_scryer/loaded/nifsoft/attack_hand_secondary(mob/user, list/modifiers)
+ var/new_label = reject_bad_text(tgui_input_text(user, "Change the visible name", "Set Name", label, MAX_NAME_LEN))
+ if(!new_label)
+ balloon_alert(user, "invalid name!")
+ return
+ label = new_label
+ balloon_alert(user, "name set!")
+ update_name()
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/// This cell is only meant for use in items temporarily created by a NIF. Do not let players extract this from devices.
+/obj/item/stock_parts/cell/infinite/nif_cell
+ name = "Nanite Cell"
+ desc = "If you see this, please make an issue on GitHub."
+
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm
index bbbf7dc2c07..dfa70dc78eb 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/shapeshifter.dm
@@ -1,35 +1,15 @@
/obj/item/disk/nifsoft_uploader/shapeshifter
name = "Polymorph"
- loaded_nifsoft = /datum/nifsoft/shapeshifter
+ loaded_nifsoft = /datum/nifsoft/action_granter
-/datum/nifsoft/shapeshifter
+/datum/nifsoft/action_granter/shapeshifter
name = "Polymorph"
program_desc = "This program is a large-scale refitting of the nanomachine channels running over the skin of a NIF user. This allows the nanites to reach under the skin and even into the very bone structure of the host; including incorporation of mimetic materials and femto-level manipulation devices all for the purpose of allowing the user to, essentially, shapeshift on a low level. However, despite the incredible complexity behind these processes, there are still limits on the range of 'forms' a user can take. Mass can neither be created nor destroyed, after all, and you can only distribute and rearrange it in so many ways across a functioning humanoid body; meaning, the user cannot adopt forms too far out of their 'true' one."
- activation_cost = 10
- active_mode = TRUE
- active_cost = 1
compatible_nifs = list(/obj/item/organ/internal/cyberimp/brain/nif/standard)
purchase_price = 350
buying_category = NIFSOFT_CATEGORY_COSMETIC
ui_icon = "paintbrush"
-
- ///The NIF version of the Shapeshifter Ability
- var/datum/action/innate/alter_form/nif/shapeshifter
-
-/datum/nifsoft/shapeshifter/activate()
- . = ..()
- if(active)
- shapeshifter = new
- shapeshifter.Grant(linked_mob)
- return
-
- if(shapeshifter)
- shapeshifter.Remove(linked_mob)
-
-/datum/nifsoft/shapeshifter/Destroy()
- . = ..()
- if(shapeshifter)
- QDEL_NULL(shapeshifter)
+ action_to_grant = /datum/action/innate/alter_form/nif
/// The NIF version of alter form. This lacks the ability to change body color.
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
index 9236a49f963..019ff546edc 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
@@ -7,6 +7,7 @@
program_desc = "The 'Soulcatcher' coreware is a near-complete upgrade of the nanomachine systems in a NIF, meant for one purpose; supposedly, channeling the dead. This upgrade, in truth, functions as a Resonance Simulation Device; an RSD for short, an instrument capable of hosting someone's consciousness, context or otherwise. 'Resonance', a term for the specific pattern of neural activity that gives way to someone's consciousness, was discovered in the early 2500s by researchers Yun-Seo Jin and Kamakshi Padmanabhan, coining what is now called 'Jin-Padmanabhan Resonance,' or 'JP/Soul Resonance.' This 'Resonance' gives off a sophont's consciousness, their sense of continuation, and their 'I am me.' This Resonance can vary in structure and 'strength' from person to person, and even change over someone's life. When the brain of a sophont undergoes death and stops neural activity, then Resonance dissipates entirely and lingering consciousness becomes essentially an echo, rapidly fading over time.\n\nThe earliest RSDs were massive machines, drawing incredible power and utilizing bleeding-edge, clunky software to 'play' someone's Resonance at 1:1 accuracy with their original brain. However, complications arose that are still being studied. Resonance is replicable and can be re-created artificially; however, like trying to duplicate genetic code, the capture needs to be extremely accurate, and rapidly put into place. Instruments such as RSDs are capable of picking up on lingering consciousness after the end of Resonance, and resuming it through artificial neural activity can give it strength to continue once more. RSDs such as Soulcatchers can only work at such a distance, otherwise running the risk of the Resonance essentially corrupting due to poor signal.\n\nIt is currently impossible to run Resonance in two places at once, because the same Resonance over two places experiences interference; like noise canceling headphones. Slimes and other gestalt consciousnesses can modulate their harmonics to a degree, bearing a partial disconnect and bringing themselves into constructive interference with similar harmonic signatures. A deepscan of the person's brain is necessary to give their consciousness 'context;' running their Resonance and capturing their consciousness alone results in a person with their same original intelligence, but zero memories or identity. These scans rapidly become outdated due to the growth of the brain, and it is prohibitively complex to store them in their entirety.\n\nThe first portable RSD, or Soulcatcher, was developed by the Spider Clan. These were initially designed for the captive interrogation of a person's consciousness without having to worry about the struggling of their body, and for dead or aging members of the mysterious group of orbital shinobi to be able to guide field operatives. These Soulcatchers are the main instrument to play Resonance, but recent advances in medical science have been leading to more. Occasionally, it is known for unusual sources of 'wild' Resonance, called Phantoms, to end up inside of the nearest Soulcatcher, a key finding its own lock; with a wide array of theories as to how these come into existence. Much as how some people intentionally become stable Engrams to achieve digital immortality, such as the witches of the Altspace Coven, it is possible for others to forcibly enter a Soulcatcher and act as a sort of Phantom by hacking their way in."
purchase_price = 150 //RP tool
persistence = TRUE
+ able_to_keep = TRUE
ui_icon = "ghost"
/// What is the linked soulcatcher datum used by this NIFSoft?
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
new file mode 100644
index 00000000000..cd84179e083
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
@@ -0,0 +1,111 @@
+/datum/component/soulcatcher/small_device
+ max_souls = 1
+
+/datum/component/soulcatcher/attachable_soulcatcher
+ max_souls = 1
+ communicate_as_parent = TRUE
+ removable = TRUE
+
+/datum/component/soulcatcher/attachable_soulcatcher/New()
+ . = ..()
+ var/obj/item/parent_item = parent
+ if(!istype(parent_item))
+ return COMPONENT_INCOMPATIBLE
+
+ name = parent_item.name
+ var/datum/soulcatcher_room/first_room = soulcatcher_rooms[1]
+ first_room.name = parent_item.name
+ first_room.room_description = parent_item.desc
+
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT, PROC_REF(bring_up_ui))
+ RegisterSignal(parent, COMSIG_PREQDELETED, PROC_REF(remove_self))
+
+/// Adds text to the examine text of the parent item, explaining that the item can be used to enable the use of NIFSoft HUDs
+/datum/component/soulcatcher/attachable_soulcatcher/proc/on_examine(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ examine_text += span_cyan("[source] has a soulcatcher attached to it, Ctrl+Shift+Click to use it.")
+
+/datum/component/soulcatcher/attachable_soulcatcher/proc/bring_up_ui(datum/source, mob/user)
+ SIGNAL_HANDLER
+ INVOKE_ASYNC(src, PROC_REF(ui_interact), user)
+
+/datum/component/soulcatcher/attachable_soulcatcher/Destroy(force)
+ UnregisterSignal(parent, COMSIG_ATOM_EXAMINE)
+ UnregisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT)
+ UnregisterSignal(parent, COMSIG_PREQDELETED)
+ return ..()
+
+/datum/component/soulcatcher/attachable_soulcatcher/remove_self()
+ var/obj/item/parent_item = parent
+ var/turf/drop_turf = get_turf(parent_item)
+ var/obj/item/attachable_soulcatcher/dropped_item = new (drop_turf)
+
+ var/datum/component/soulcatcher/dropped_soulcatcher = dropped_item.GetComponent(/datum/component/soulcatcher)
+ var/datum/soulcatcher_room/target_room = dropped_soulcatcher.soulcatcher_rooms[1]
+ var/list/current_souls = get_current_souls()
+
+ if(current_souls) // If we have souls inside of here, they should be transferred to the new object
+ for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
+ var/datum/soulcatcher_room/current_room = soul.current_room.resolve()
+ if(istype(current_room))
+ current_room.transfer_soul(soul, target_room)
+
+ return ..()
+
+/obj/item/attachable_soulcatcher
+ name = "Poltergeist-Type RSD"
+ desc = "This device, a polymorphic nanomachine net, wraps around objects of most sizes and allows them to function as a container for Resonance. The soul in question within the vessel is imbued much like it would be in a body or a normal Soulcatcher, perceiving the world and even speaking out of their new form. The nanomachine net of the device allows for the consciousness to somewhat manipulate their container, but any large-scale movement is out of the question."
+ icon = 'modular_skyrat/modules/modular_implants/icons/obj/devices.dmi'
+ icon_state = "attachable-soulcatcher"
+ w_class = WEIGHT_CLASS_SMALL
+ /// Do we want to destory the item once it is attached to an item?
+ var/destroy_on_use = TRUE
+ /// What items do we want to prevent the viewer from attaching this to?
+ var/list/blacklisted_items = list(
+ /obj/item/organ,
+ /obj/item/mmi,
+ /obj/item/pai_card,
+ /obj/item/aicard,
+ /obj/item/card,
+ /obj/item/radio,
+ /obj/item/disk/nuclear, // Woah there
+ )
+ /// What soulcathcer component is currnetly linked to this object?
+ var/datum/component/soulcatcher/small_device/linked_soulcatcher
+
+/obj/item/attachable_soulcatcher/Initialize(mapload)
+ . = ..()
+ linked_soulcatcher = AddComponent(/datum/component/soulcatcher/small_device)
+ linked_soulcatcher.name = name
+
+/obj/item/attachable_soulcatcher/attack_self(mob/user, modifiers)
+ linked_soulcatcher.ui_interact(user)
+
+/obj/item/attachable_soulcatcher/afterattack(obj/item/target_item, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!proximity_flag || !istype(target_item))
+ return FALSE
+
+ if(target_item.GetComponent(/datum/component/soulcatcher))
+ balloon_alert(user, "already attached!")
+ return FALSE
+
+ if(is_type_in_list(target_item, blacklisted_items))
+ balloon_alert(user, "incompatible!")
+ return FALSE
+
+ var/datum/component/soulcatcher/new_soulcatcher = target_item.AddComponent(/datum/component/soulcatcher/attachable_soulcatcher)
+ playsound(target_item.loc, 'sound/weapons/circsawhit.ogg', 50, vary = TRUE)
+
+ var/datum/soulcatcher_room/target_room = new_soulcatcher.soulcatcher_rooms[1]
+ var/list/current_souls = linked_soulcatcher.get_current_souls()
+ if(current_souls)
+ for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
+ var/datum/soulcatcher_room/current_room = soul.current_room.resolve()
+ if(istype(current_room))
+ current_room.transfer_soul(soul, target_room)
+ current_room.transfer_soul(soul, target_room)
+
+ if(destroy_on_use)
+ qdel(src)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
index 842aee82d59..767f396057e 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
@@ -111,4 +111,52 @@
return TRUE
+/obj/item/handheld_soulcatcher/attack_secondary(mob/living/carbon/human/target_mob, mob/living/user, params)
+ if(!istype(target_mob))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/obj/item/organ/internal/brain/target_brain = target_mob.get_organ_slot(ORGAN_SLOT_BRAIN)
+ if(!istype(target_brain))
+ to_chat(user, span_warning("[target_mob] lacks a brain!"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(!HAS_TRAIT(target_brain, TRAIT_RSD_COMPATIBLE))
+ to_chat(user, span_warning("[target_mob]'s brain isn't compatible."))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(target_mob.mind || target_mob.ckey || GetComponent(/datum/component/previous_body))
+ to_chat(user, span_warning("[target_mob] is not able to receive a soul"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/list/soul_list = list()
+ for(var/datum/soulcatcher_room/room as anything in linked_soulcatcher.soulcatcher_rooms)
+ for(var/mob/living/soulcatcher_soul/soul as anything in room.current_souls)
+ if(!soul.round_participant || soul.body_scan_needed)
+ continue
+
+ soul_list += soul
+
+ if(!length(soul_list))
+ to_chat(user, span_warning("There are no souls that can be transferred to [target_mob]."))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/mob/living/soulcatcher_soul/chosen_soul = tgui_input_list(user, "Choose a soul to transfer into the body", name, soul_list)
+ if(!chosen_soul)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(chosen_soul.previous_body)
+ var/mob/living/old_body = chosen_soul.previous_body.resolve()
+ if(!old_body)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ SEND_SIGNAL(old_body, COMSIG_SOULCATCHER_CHECK_SOUL, FALSE)
+
+ chosen_soul.mind.transfer_to(target_mob, TRUE)
+ playsound(src, 'modular_skyrat/modules/modular_implants/sounds/default_good.ogg', 50, FALSE, ignore_walls = FALSE)
+ visible_message(span_notice("[src] beeps: Body transfer complete."))
+ log_admin("[src] was used by [user] to transfer [chosen_soul]'s soulcatcher soul to [target_mob].")
+
+ qdel(chosen_soul)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
#undef RSD_ATTEMPT_COOLDOWN
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
index 97158ba5d91..03512b2c205 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
@@ -23,6 +23,12 @@ GLOBAL_LIST_EMPTY(soulcatchers)
var/ghost_joinable = TRUE
/// Do we want to ask the user permission before the ghost joins?
var/require_approval = TRUE
+ /// What is the max number of people we can keep in this soulcatcher? If this is set to `FALSE` we don't have a limit
+ var/max_souls = FALSE
+ /// Are are the souls inside able to emote/speak as the parent?
+ var/communicate_as_parent = FALSE
+ /// Is the soulcatcher removable from the parent object?
+ var/removable = FALSE
/datum/component/soulcatcher/New()
. = ..()
@@ -136,6 +142,32 @@ GLOBAL_LIST_EMPTY(soulcatchers)
return TRUE
+/// Returns a list containing all of the souls currently present within a soulcatcher.
+/datum/component/soulcatcher/proc/get_current_souls()
+ var/list/current_souls = list()
+ for(var/datum/soulcatcher_room/room as anything in soulcatcher_rooms)
+ for(var/mob/living/soulcatcher_soul as anything in room.current_souls)
+ current_souls += soulcatcher_soul
+
+ return current_souls
+
+/// Checks the total number of souls present and compares it with `max_souls` returns `TRUE` if there is room (or no limit), otherwise returns `FALSE`
+/datum/component/soulcatcher/proc/check_for_vacancy()
+ if(!max_souls)
+ return TRUE
+
+ if(length(get_current_souls()) >= max_souls)
+ return FALSE
+
+ return TRUE
+
+/// Attempts to remove the soulcatcher from the attached object
+/datum/component/soulcatcher/proc/remove_self()
+ if(!removable)
+ return FALSE
+
+ qdel(src)
+
/**
* Soulcatcher Room
*
@@ -266,10 +298,6 @@ GLOBAL_LIST_EMPTY(soulcatchers)
if(!message_to_send) //Why say nothing?
return FALSE
- var/sender_name = ""
- if(message_sender)
- sender_name = "[message_sender] "
-
var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
var/tag = sheet.icon_tag("nif-soulcatcher")
var/soulcatcher_icon = ""
@@ -277,6 +305,33 @@ GLOBAL_LIST_EMPTY(soulcatchers)
if(tag)
soulcatcher_icon = tag
+ var/mob/living/soulcatcher_soul/soul_sender = message_sender
+ if(istype(soul_sender) && soul_sender.communicating_externally)
+ var/master_resolved = master_soulcatcher.resolve()
+ if(!master_resolved)
+ return FALSE
+ var/datum/component/soulcatcher/parent_soulcatcher = master_resolved
+ var/obj/item/parent_object = parent_soulcatcher.parent
+ if(!istype(parent_object))
+ return FALSE
+
+ var/temp_name = parent_object.name
+ parent_object.name = "[parent_object.name] [soulcatcher_icon]"
+
+ if(emote)
+ parent_object.manual_emote(html_decode(message_to_send))
+ log_emote("[soul_sender] in [name] soulcatcher room emoted: [message_to_send], as an external object")
+ else
+ parent_object.say(html_decode(message_to_send))
+ log_say("[soul_sender] in [name] soulcatcher room said: [message_to_send], as an external object")
+
+ parent_object.name = temp_name
+ return TRUE
+
+ var/sender_name = ""
+ if(message_sender)
+ sender_name = "[message_sender] "
+
var/first_room_name_word = splittext(name, " ")
var/message = ""
var/owner_message = ""
@@ -334,7 +389,7 @@ GLOBAL_LIST_EMPTY(soulcatchers)
var/list/joinable_soulcatchers = list()
for(var/datum/component/soulcatcher/soulcatcher in GLOB.soulcatchers)
- if(!soulcatcher.ghost_joinable || !isobj(soulcatcher.parent))
+ if(!soulcatcher.ghost_joinable || !isobj(soulcatcher.parent) || !soulcatcher.check_for_vacancy())
continue
var/obj/item/soulcatcher_parent = soulcatcher.parent
@@ -360,6 +415,7 @@ GLOBAL_LIST_EMPTY(soulcatchers)
var/datum/soulcatcher_room/room_to_join
if(length(rooms_to_join) < 1)
+ to_chat(src, span_warning("There no rooms that you can join."))
return FALSE
if(length(rooms_to_join) == 1)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
index fefaf69c25e..8ae004b9667 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
@@ -23,6 +23,12 @@
var/able_to_speak = TRUE
/// Is the soul able to change their own name?
var/able_to_rename = TRUE
+ /// Is the soul able to speak as the object it is inside?
+ var/able_to_speak_as_container = TRUE
+ /// Is the soul able to emote as the object it is inside?
+ var/able_to_emote_as_container = TRUE
+ /// Are emote's and Say's done through the container the mob is in?
+ var/communicating_externally = FALSE
/// Is the soul able to leave the soulcatcher?
var/able_to_leave = TRUE
@@ -110,7 +116,7 @@
if(!message || message == "")
return
- if(!able_to_speak)
+ if((!able_to_speak && !communicating_externally) || (!able_to_speak_as_container && communicating_externally))
to_chat(src, span_warning("You are unable to speak!"))
return FALSE
@@ -126,7 +132,7 @@
if(!message)
return FALSE
- if(!able_to_emote)
+ if((!able_to_emote && !communicating_externally) || (!able_to_emote_as_container && communicating_externally))
to_chat(src, span_warning("You are unable to emote!"))
return FALSE
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
index b59cee1f7ad..0c1d11c272e 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
@@ -13,6 +13,10 @@
data["ghost_joinable"] = ghost_joinable
data["require_approval"] = require_approval
+ data["communicate_as_parent"] = communicate_as_parent
+ data["current_soul_count"] = length(get_current_souls())
+ data["max_souls"] = max_souls
+ data["removable"] = removable
data["current_rooms"] = list()
for(var/datum/soulcatcher_room/room in soulcatcher_rooms)
@@ -41,6 +45,8 @@
"able_to_rename" = soul.able_to_rename,
"ooc_notes" = soul.ooc_notes,
"scan_needed" = soul.body_scan_needed,
+ "able_to_speak_as_container" = soul.able_to_speak_as_container,
+ "able_to_emote_as_container" = soul.able_to_emote_as_container,
)
room_data["souls"] += list(soul_list)
@@ -147,7 +153,7 @@
continue
var/datum/component/soulcatcher/soulcatcher_component = held_item.GetComponent(/datum/component/soulcatcher)
- if(!soulcatcher_component)
+ if(!soulcatcher_component || !soulcatcher_component.check_for_vacancy())
continue
for(var/datum/soulcatcher_room/room in soulcatcher_component.soulcatcher_rooms)
@@ -191,6 +197,14 @@
return TRUE
+ if("toggle_soul_external_communication")
+ if(params["communication_type"] == "emote")
+ target_soul.able_to_emote_as_container = !target_soul.able_to_emote_as_container
+ else
+ target_soul.able_to_speak_as_container = !target_soul.able_to_speak_as_container
+
+ return TRUE
+
if("toggle_soul_renaming")
target_soul.able_to_rename = !target_soul.able_to_rename
return TRUE
@@ -224,6 +238,13 @@
target_room.send_message(message_to_send, message_sender, emote)
return TRUE
+ if("delete_self")
+ if(tgui_alert(usr, "Are you sure you want to detach the soulcatcher?", parent, list("Yes", "No")) != "Yes")
+ return FALSE
+
+ remove_self()
+ return TRUE
+
/datum/component/soulcatcher_user/New()
. = ..()
var/mob/living/soulcatcher_soul/parent_soul = parent
@@ -259,6 +280,9 @@
"able_to_emote" = user_soul.able_to_emote,
"able_to_speak" = user_soul.able_to_speak,
"able_to_rename" = user_soul.able_to_rename,
+ "able_to_speak_as_container" = user_soul.able_to_speak_as_container,
+ "able_to_emote_as_container" = user_soul.able_to_emote_as_container,
+ "communicating_externally" = user_soul.communicating_externally,
"ooc_notes" = user_soul.ooc_notes,
"scan_needed" = user_soul.body_scan_needed,
)
@@ -272,6 +296,9 @@
"owner" = current_room.outside_voice,
)
+ var/datum/component/soulcatcher/master_soulcatcher = current_room.master_soulcatcher.resolve()
+ data["communicate_as_parent"] = master_soulcatcher.communicate_as_parent
+
for(var/mob/living/soulcatcher_soul/soul in current_room.current_souls)
if(soul == user_soul)
continue
@@ -310,3 +337,6 @@
user_soul.reset_name()
+ if("toggle_external_communication")
+ user_soul.communicating_externally = !user_soul.communicating_externally
+ return TRUE
diff --git a/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi b/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi
index d0a3f736fa6..401441f95de 100644
Binary files a/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi and b/modular_skyrat/modules/modular_implants/icons/obj/devices.dmi differ
diff --git a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm
index 81cd35398b0..6a8f8a37c58 100644
--- a/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm
+++ b/modular_skyrat/modules/modular_items/lewd_items/code/lewd_clothing/bdsm_mask.dm
@@ -27,11 +27,14 @@
actions_types = list(
/datum/action/item_action/toggle_breathcontrol,
/datum/action/item_action/mask_inhale,
+ /datum/action/item_action/toggle_gag,
)
var/list/moans = list("Mmmph...", "Hmmphh", "Mmmfhg", "Gmmmh...") // Phrases to be said when the player attempts to talk when speech modification / voicebox is enabled.
var/list/moans_alt = list("Mhgm...", "Hmmmp!...", "Gmmmhp!") // Power probability phrases to be said when talking.
var/moans_alt_probability = 5 // Probability for alternative sounds to play.
var/temp_check = TRUE //Used to check if user unconsious to prevent choking him until he wakes up
+ /// Does the gasmask impede the user's ability to talk?
+ var/speech_disabled
w_class = WEIGHT_CLASS_SMALL
modifies_speech = TRUE
flags_cover = MASKCOVERSMOUTH
@@ -53,6 +56,9 @@
update_icon()
/obj/item/clothing/mask/gas/bdsm_mask/handle_speech(datum/source, list/speech_args)
+ if(speech_disabled)
+ return
+
speech_args[SPEECH_MESSAGE] = pick((prob(moans_alt_probability) && LAZYLEN(moans_alt)) ? moans_alt : moans)
play_lewd_sound(loc, pick('modular_skyrat/modules/modular_items/lewd_items/sounds/under_moan_f1.ogg',
'modular_skyrat/modules/modular_items/lewd_items/sounds/under_moan_f2.ogg',
@@ -164,6 +170,15 @@
if(istype(mask))
mask.check()
+/datum/action/item_action/toggle_gag
+ name = "Toggle gag"
+ desc = "Toggles whether or not the wearer is able to speak."
+
+/datum/action/item_action/toggle_gag/Trigger(trigger_flags)
+ var/obj/item/clothing/mask/gas/bdsm_mask/mask = target
+ if(istype(mask))
+ mask.check_gag()
+
/datum/action/item_action/mask_inhale
name = "Inhale oxygen"
desc = "You must inhale oxygen!"
@@ -236,6 +251,19 @@
else
STOP_PROCESSING(SSobj, src)
+/obj/item/clothing/mask/gas/bdsm_mask/proc/check_gag(user)
+ var/mob/living/carbon/affected_carbon = user
+ if(src == affected_carbon.wear_mask)
+ to_chat(user, span_notice("You can't reach the gag switch!"))
+ else
+ toggle_gag(affected_carbon)
+
+/obj/item/clothing/mask/gas/bdsm_mask/proc/toggle_gag(user)
+ speech_disabled = !speech_disabled
+ to_chat(user, span_notice("You [speech_disabled ? "disable" : "enable"] the gag on the mask."))
+ update_mob_action_buttonss()
+ update_icon()
+
// Mask choke processor
/obj/item/clothing/mask/gas/bdsm_mask/process(seconds_per_tick)
var/mob/living/affected_mob = loc
diff --git a/modular_skyrat/modules/modular_vending/code/games.dm b/modular_skyrat/modules/modular_vending/code/games.dm
index cbc169a99ff..99edcdb796c 100644
--- a/modular_skyrat/modules/modular_vending/code/games.dm
+++ b/modular_skyrat/modules/modular_vending/code/games.dm
@@ -13,6 +13,7 @@
"products" = list(
/obj/item/hairbrush = 3,
/obj/item/clothing/mask/holocigarette = 5,
+ /obj/item/attachable_soulcatcher = 5,
),
)
)
diff --git a/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm b/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm
index 5e40743aa8e..bcc8b49c26d 100644
--- a/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm
+++ b/modular_skyrat/modules/novaya_ert/code/surplus_weapons.dm
@@ -59,7 +59,7 @@
weak_against_armour = TRUE
/obj/projectile/beam/laser/plasma_glob/on_hit(atom/target, blocked)
- if(istype(target, /obj/structure/blob) || istype(target, /mob/living/simple_animal/hostile/blob))
+ if(istype(target, /obj/structure/blob) || HAS_TRAIT(target, TRAIT_BLOB_ALLY))
damage = damage * 0.75
return ..()
diff --git a/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm b/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm
index 264d9f8bedc..6e724e44dbc 100644
--- a/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm
+++ b/modular_skyrat/modules/player_ranks/code/subsystem/player_ranks.dm
@@ -254,21 +254,37 @@ SUBSYSTEM_DEF(player_ranks)
* or in the legacy system.
*
* Arguments:
- * * admin - The admin making the rank change.
+ * * admin - The admin making the rank change. Can be a /client or a /datum/admins.
* * ckey - The ckey of the player you want to now possess that player rank.
* * rank_title - The title of the group you want to add the ckey to.
*/
-/datum/controller/subsystem/player_ranks/proc/add_player_to_group(client/admin, ckey, rank_title)
+/datum/controller/subsystem/player_ranks/proc/add_player_to_group(admin, ckey, rank_title)
if(IsAdminAdvancedProcCall())
return FALSE
if(!ckey || !admin || !rank_title)
+ stack_trace("Missing either ckey ([ckey || "*NULL*"]), admin ([admin || "*NULL*"]) or rank_title ([rank_title || "*NULL*"]) in add_player_to_group()! Fix this ASAP!")
return FALSE
- if(!check_rights_for(admin, R_PERMISSIONS))
- to_chat(admin, span_warning("You do not possess the permissions to do this."))
+ var/is_admin_client = istype(admin, /client)
+ var/client/admin_client = is_admin_client ? admin : null
+ // If it's not a client, then it should be an admins datum.
+ var/datum/admins/admin_holder = null
+ if(is_admin_client)
+ admin_holder = admin_client?.holder
+ else if(istype(admin, /datum/admins))
+ admin_holder = admin
+
+ if(!admin_holder)
+ return FALSE
+
+ if(!admin_holder.check_for_rights(R_PERMISSIONS))
+ if(is_admin_client)
+ to_chat(admin, span_warning("You do not possess the permissions to do this."))
+
return FALSE
+
rank_title = lowertext(rank_title)
var/datum/player_rank_controller/controller = get_controller_for_group(rank_title)
@@ -282,14 +298,16 @@ SUBSYSTEM_DEF(player_ranks)
var/already_in_config = controller.get_ckeys_for_legacy_save()
if(already_in_config[ckey])
- to_chat(admin, span_warning("\"[ckey]\" is already a [rank_title]!"))
+ if(is_admin_client)
+ to_chat(admin, span_warning("\"[ckey]\" is already a [rank_title]!"))
+
return FALSE
if(controller.should_use_legacy_system())
controller.add_player_legacy(ckey)
return TRUE
- return add_player_rank_sql(controller, ckey, admin.ckey)
+ return add_player_rank_sql(controller, ckey, admin_holder.target)
/**
@@ -325,19 +343,34 @@ SUBSYSTEM_DEF(player_ranks)
* or in the legacy system.
*
* Arguments:
- * * admin - The admin making the rank change.
+ * * admin - The admin making the rank change. Can be a /client or a /datum/admins.
* * ckey - The ckey of the player you want to no longer possess that player rank.
* * rank_title - The title of the group you want to remove the ckey from.
*/
-/datum/controller/subsystem/player_ranks/proc/remove_player_from_group(client/admin, ckey, rank_title)
+/datum/controller/subsystem/player_ranks/proc/remove_player_from_group(admin, ckey, rank_title)
if(IsAdminAdvancedProcCall())
return FALSE
if(!ckey || !admin || !rank_title)
+ stack_trace("Missing either ckey ([ckey || "*NULL*"]), admin ([admin || "*NULL*"]) or rank_title ([rank_title || "*NULL*"]) in remove_player_from_group()! Fix this ASAP!")
return FALSE
- if(!check_rights_for(admin, R_PERMISSIONS))
- to_chat(admin, span_warning("You do not possess the permissions to do this."))
+ var/is_admin_client = istype(admin, /client)
+ var/client/admin_client = is_admin_client ? admin : null
+ // If it's not a client, then it should be an admins datum.
+ var/datum/admins/admin_holder = null
+ if(is_admin_client)
+ admin_holder = admin_client?.holder
+ else if(istype(admin, /datum/admins))
+ admin_holder = admin
+
+ if(!admin_holder)
+ return FALSE
+
+ if(!admin_holder.check_for_rights(R_PERMISSIONS))
+ if(is_admin_client)
+ to_chat(admin, span_warning("You do not possess the permissions to do this."))
+
return FALSE
rank_title = lowertext(rank_title)
@@ -345,22 +378,16 @@ SUBSYSTEM_DEF(player_ranks)
var/datum/player_rank_controller/controller = get_controller_for_group(rank_title)
if(!controller)
- stack_trace("Invalid player rank \"[rank_title]\" supplied in add_player_to_group()!")
+ stack_trace("Invalid player rank \"[rank_title]\" supplied in remove_player_from_group()!")
return FALSE
ckey = ckey(ckey)
- var/already_in_config = controller.get_ckeys_for_legacy_save()
-
- if(!already_in_config[ckey])
- to_chat(admin, span_warning("\"[ckey]\" is already not a [rank_title]!"))
- return FALSE
-
if(controller.should_use_legacy_system())
controller.remove_player_legacy(ckey)
return TRUE
- return remove_player_rank_sql(controller, ckey, admin.ckey)
+ return remove_player_rank_sql(controller, ckey, admin_holder.target)
/**
diff --git a/modular_skyrat/modules/player_ranks/code/world_topic.dm b/modular_skyrat/modules/player_ranks/code/world_topic.dm
new file mode 100644
index 00000000000..086373fd33f
--- /dev/null
+++ b/modular_skyrat/modules/player_ranks/code/world_topic.dm
@@ -0,0 +1,67 @@
+
+/datum/world_topic/set_player_rank
+ keyword = "set_player_rank"
+ require_comms_key = TRUE
+
+/datum/world_topic/set_player_rank/Run(list/input)
+ . = list()
+
+ var/sender_discord_id = input["sender_discord_id"]
+
+ if(!sender_discord_id)
+ .["success"] = FALSE
+ .["message"] = "Invalid sender Discord ID, this should not be happening! Report this immediately!"
+ return
+
+ var/target_ckey = ckey(input["target_ckey"])
+
+ if(!target_ckey)
+ .["success"] = FALSE
+ .["message"] = "Invalid target ckey provided."
+ return
+
+ var/sender_ckey = ckey(SSdiscord.lookup_ckey(sender_discord_id))
+
+ if(!sender_ckey)
+ .["success"] = FALSE
+ .["message"] = "No ckey was found to be attached to the provided Discord account ID, **[sender_discord_id]**. Please verify your Discord account following the instructions of the in-game verb before trying this command again."
+ return
+
+ var/datum/admins/linked_admin_holder = GLOB.admin_datums[sender_ckey] || GLOB.deadmins[sender_ckey]
+
+ if(!linked_admin_holder)
+ .["success"] = FALSE
+ .["message"] = "No valid admin datum was found associated with the ckey associated to your Discord account."
+ return
+
+ if(!linked_admin_holder.check_for_rights(R_PERMISSIONS))
+ .["success"] = FALSE
+ .["message"] = "You do not possess the permissions to execute this command."
+ return
+
+ var/target_rank = input["target_rank"]
+
+ if(!target_rank)
+ .["success"] = FALSE
+ .["message"] = "Invalid target rank provided."
+ return
+
+ target_rank = capitalize(target_rank)
+
+ var/desired_rank_status = !!text2num(input["desired_rank_status"])
+
+ if(desired_rank_status)
+ var/result = SSplayer_ranks.add_player_to_group(linked_admin_holder, target_ckey, target_rank)
+
+ .["success"] = !!result
+ .["message"] = result ? "**[linked_admin_holder.target]** successfully added **[target_rank]** status to **[target_ckey]**." : "**[linked_admin_holder.target]** was unable to add **[target_rank]** status to **[target_ckey]**. Please verify that you entered their ckey correctly and that they did not already possess that status before trying again. Use the in-game verb to get more information if you keep on receiving this error."
+ message_admins(replacetext(.["message"], "*", ""))
+ return
+
+ else
+ var/result = SSplayer_ranks.remove_player_from_group(linked_admin_holder, target_ckey, target_rank)
+
+ .["success"] = !!result
+ .["message"] = result ? "**[linked_admin_holder.target]** successfully removed **[target_rank]** status from **[target_ckey]**." : "**[linked_admin_holder.target]** was unable to remove **[target_rank]** status from **[target_ckey]**. Please verify that you entered their ckey correctly and that they did possess that status before trying again. Use the in-game verb to get more information if you keep on receiving this error."
+ message_admins(replacetext(.["message"], "*", ""))
+ return
diff --git a/modular_skyrat/modules/primitive_catgirls/code/spawner.dm b/modular_skyrat/modules/primitive_catgirls/code/spawner.dm
index 8cb3f3234fe..5cbe1d682c0 100644
--- a/modular_skyrat/modules/primitive_catgirls/code/spawner.dm
+++ b/modular_skyrat/modules/primitive_catgirls/code/spawner.dm
@@ -66,8 +66,23 @@
/datum/team/primitive_catgirls
name = "Icewalkers"
+ member_name = "Icewalker"
show_roundend_report = FALSE
+/datum/team/primitive_catgirls/roundend_report()
+ var/list/report = list()
+
+ report += span_header("An Ice Walker Tribe inhabited the wastes... ")
+ if(length(members))
+ report += "The [member_name]s were:"
+ report += printplayerlist(members)
+ else
+ report += "But none of its members woke up!"
+
+ return "
[report.Join(" ")]
"
+
+// Antagonist datum
+
/datum/antagonist/primitive_catgirl
name = "\improper Icewalker"
job_rank = ROLE_LAVALAND // If you're ashwalker banned you should also not be playing this, other way around as well
diff --git a/modular_skyrat/modules/primitive_catgirls/code/species.dm b/modular_skyrat/modules/primitive_catgirls/code/species.dm
index 7bcaec865dd..586c7b22900 100644
--- a/modular_skyrat/modules/primitive_catgirls/code/species.dm
+++ b/modular_skyrat/modules/primitive_catgirls/code/species.dm
@@ -21,6 +21,7 @@
mutanttongue = /obj/item/organ/internal/tongue/cat/primitive
species_language_holder = /datum/language_holder/primitive_felinid
+ language_prefs_whitelist = list(/datum/language/primitive_catgirl)
bodytemp_normal = 270 // If a normal human gets hugged by one its gonna feel cold
bodytemp_heat_damage_limit = 283 // To them normal station atmos would be sweltering
diff --git a/modular_skyrat/modules/resleeving/code/research/resleeving_research.dm b/modular_skyrat/modules/resleeving/code/research/resleeving_research.dm
new file mode 100644
index 00000000000..4dcd00cc73e
--- /dev/null
+++ b/modular_skyrat/modules/resleeving/code/research/resleeving_research.dm
@@ -0,0 +1,14 @@
+/datum/design/rsd_interface
+ name = "RSD Phylactery"
+ desc = "A brain interface that allows for transfer of Resonance from a handheld RSD, such as the Evoker model."
+ id = "rsd_interface"
+ build_type = PROTOLATHE | AWAY_LATHE
+ departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE
+ category = list(RND_CATEGORY_EQUIPMENT)
+ materials = list(
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.5,
+ /datum/material/gold = SHEET_MATERIAL_AMOUNT,
+ /datum/material/silver = SHEET_MATERIAL_AMOUNT,
+ )
+ build_path = /obj/item/rsd_interface
+
diff --git a/modular_skyrat/modules/resleeving/code/rsd_interface.dm b/modular_skyrat/modules/resleeving/code/rsd_interface.dm
new file mode 100644
index 00000000000..106bb11ea38
--- /dev/null
+++ b/modular_skyrat/modules/resleeving/code/rsd_interface.dm
@@ -0,0 +1,44 @@
+/obj/item/rsd_interface
+ name = "RSD Phylactery"
+ desc = "A small device inserted, typically, into inert brains. As Resonance cannot persist in what's referred to as a 'vacuum', RSDs--much like the brains and CPUs they emulate--employ cerebral white noise as a foundation for Resonance to persist in otherwise dead-quiet containers.."
+ icon = 'modular_skyrat/modules/aesthetics/implanter/implanter.dmi'
+ icon_state = "implanter1"
+ inhand_icon_state = "syringe_0"
+ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
+
+/// Attempts to use the item on the target brain.
+/obj/item/rsd_interface/afterattack(obj/item/organ/internal/brain/target_brain, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!proximity_flag || !istype(target_brain))
+ return FALSE
+
+ if(HAS_TRAIT(target_brain, TRAIT_NIFSOFT_HUD_GRANTER))
+ balloon_alert("already upgraded!")
+ return FALSE
+
+ user.visible_message(span_notice("[user] upgrades [target_brain] with [src]."), span_notice("You upgrade [target_brain] to be RSD compatible."))
+ target_brain.AddElement(/datum/element/rsd_interface)
+ playsound(target_brain.loc, 'sound/weapons/circsawhit.ogg', 50, vary = TRUE)
+
+ qdel(src)
+
+/datum/element/rsd_interface/Attach(datum/target)
+ . = ..()
+ if(!istype(target, /obj/item/organ/internal/brain))
+ return ELEMENT_INCOMPATIBLE
+
+ RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ ADD_TRAIT(target, TRAIT_RSD_COMPATIBLE, INNATE_TRAIT)
+
+/// Adds text to the examine text of the parent item, explaining that the item can be used to enable the use of NIFSoft HUDs
+/datum/element/rsd_interface/proc/on_examine(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ examine_text += span_cyan("Souls can be transferred to [source], assuming it is inert.")
+
+/datum/element/rsd_interface/Detach(datum/target)
+ UnregisterSignal(target, COMSIG_ATOM_EXAMINE)
+ REMOVE_TRAIT(target, TRAIT_RSD_COMPATIBLE, INNATE_TRAIT)
+
+ return ..()
+
diff --git a/modular_skyrat/modules/wall_fungus/code/wall_fungus_component.dm b/modular_skyrat/modules/wall_fungus/code/wall_fungus_component.dm
deleted file mode 100644
index 9c66677aaed..00000000000
--- a/modular_skyrat/modules/wall_fungus/code/wall_fungus_component.dm
+++ /dev/null
@@ -1,159 +0,0 @@
-#define FUNGUS_STAGE_ONE 1
-#define FUNGUS_STAGE_TWO 2
-#define FUNGUS_STAGE_THREE 3
-#define FUNGUS_STAGE_FOUR 4
-#define FUNGUS_STAGE_MAX 5
-
-/**
- * A wall eating mushroom.
- *
- * This mushroom spreads to walls and eats em up! It can be removed with a welder. If left unchecked it will eat the whole wall.
- */
-/datum/component/wall_fungus
- /// How far has the fungus progressed on the affected wall? Percentage.
- var/progression_percent = 0
- /// How many percent do we increase each subsystem fire?
- var/progression_step_amount = 0.5
- /// What stage are we at?
- var/progression_stage = FUNGUS_STAGE_ONE
- /// Our overlay icon file
- var/overlay_icon_file = 'modular_skyrat/modules/wall_fungus/icons/wall_fungus_overlay.dmi'
- /// How likely are we to spread to another wall?
- var/spread_chance = 1
- /// How far can we spread?
- var/spread_distance = 3 // Tiles
- /// How likely are we to drop a shroom upon destruction?
- var/drop_chance = 30
-
-/datum/component/wall_fungus/Initialize(override_progression_step_amount, override_spread_chance, override_spread_distance, override_drop_chance)
- if(!iswallturf(parent))
- return COMPONENT_INCOMPATIBLE
-
- // This stuff enables badminery.
- if(override_progression_step_amount)
- progression_step_amount = override_progression_step_amount
- if(override_spread_chance)
- spread_chance = override_progression_step_amount
- if(override_spread_distance)
- spread_distance = override_spread_distance
- if(override_drop_chance)
- drop_chance = override_drop_chance
-
- var/turf/closed/wall/parent_wall = parent
-
- RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(apply_fungus_overlay)) // We need to do this here so that the wall shows the infection immediately.
-
- parent_wall.update_icon(UPDATE_OVERLAYS)
-
- START_PROCESSING(SSobj, src)
-
-/datum/component/wall_fungus/RegisterWithParent()
- RegisterSignal(parent, COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_WELDER), PROC_REF(secondary_tool_act))
- RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(examine))
- RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand))
-
-/datum/component/wall_fungus/Destroy(force, silent)
- var/turf/closed/wall/parent_wall = parent
- STOP_PROCESSING(SSobj, src)
- UnregisterSignal(parent, list(COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_WELDER), COMSIG_ATOM_EXAMINE, COMSIG_ATOM_UPDATE_OVERLAYS))
- parent_wall.update_icon(UPDATE_OVERLAYS)
- return ..()
-
-/datum/component/wall_fungus/process(seconds_per_tick)
- var/turf/closed/wall/parent_wall = parent
- if(prob(spread_chance * seconds_per_tick))
- spread_to_nearby_wall()
-
- if(progression_stage > FUNGUS_STAGE_MAX)
- collapse_parent_structure()
- return
-
- progression_percent += progression_step_amount * seconds_per_tick
-
- if(progression_percent >= 100)
- progression_percent = 0
- progression_stage++
- spread_to_nearby_wall()
- parent_wall.update_icon(UPDATE_OVERLAYS)
-
-/datum/component/wall_fungus/proc/on_attack_hand(datum/source, mob/living/user)
- SIGNAL_HANDLER
-
- if(progression_stage < FUNGUS_STAGE_THREE)
- return
-
- collapse_parent_structure()
-
-
-/// We kill the wall once we have progressed far enough.
-/datum/component/wall_fungus/proc/collapse_parent_structure()
- var/turf/closed/wall/parent_wall = parent
- STOP_PROCESSING(SSobj, src)
- parent_wall.balloon_alert_to_viewers("collapses!")
- parent_wall.dismantle_wall()
- qdel(src)
-
-/datum/component/wall_fungus/proc/spread_to_nearby_wall()
- var/turf/closed/wall/parent_wall = parent
- var/list/walls_to_pick_from = list()
- for(var/turf/closed/wall/iterating_wall in RANGE_TURFS(3, parent_wall))
- if(iterating_wall.GetComponent(/datum/component/wall_fungus))
- continue
-
- walls_to_pick_from += iterating_wall
-
- if(!length(walls_to_pick_from))
- return // sad times
-
- var/turf/closed/wall/picked_wall = pick(walls_to_pick_from)
-
- picked_wall.AddComponent(/datum/component/wall_fungus, progression_step_amount, spread_chance, spread_distance, drop_chance)
-
-/// Gives people an idea of how badly the wall is infected.
-/datum/component/wall_fungus/proc/examine(datum/source, mob/user, list/examine_list)
- SIGNAL_HANDLER
- var/turf/closed/wall/parent_wall = parent
- switch(progression_stage)
- if(FUNGUS_STAGE_ONE)
- examine_list += span_green("[parent_wall] is infected with some kind of fungus!")
- if(FUNGUS_STAGE_TWO)
- examine_list += span_green("[parent_wall] is infected with some kind of fungus, its structure weakened!")
- if(FUNGUS_STAGE_THREE)
- examine_list += span_green("[parent_wall] is infected with some kind of fungus, its structure seriously weakened!")
- if(FUNGUS_STAGE_THREE)
- examine_list += span_green("[parent_wall] is infected with some kind of fungus, its falling apart!")
- examine_list += span_green("Perhaps you could burn it off?")
-
-/datum/component/wall_fungus/proc/apply_fungus_overlay(atom/parent_atom, list/overlays)
- SIGNAL_HANDLER
- overlays += mutable_appearance(overlay_icon_file, "fungus_stage_[progression_stage]")
-
-/datum/component/wall_fungus/proc/secondary_tool_act(atom/source, mob/user, obj/item/item)
- SIGNAL_HANDLER
- INVOKE_ASYNC(src, PROC_REF(handle_tool_use), source, user, item)
- return COMPONENT_BLOCK_TOOL_ATTACK
-
-/// Handles removal of the fungus from a wall.
-/datum/component/wall_fungus/proc/handle_tool_use(atom/source, mob/user, obj/item/item)
- var/turf/closed/wall/parent_wall = parent
- switch(item.tool_behaviour)
- if(TOOL_WELDER)
- if(!item.tool_start_check(user, 1))
- return
-
- user.balloon_alert(user, "burning off fungus...")
-
- if(!item.use_tool(source, user, (1 * progression_stage) SECONDS, 1, volume = 100))
- return
-
- user.balloon_alert(user, "burned off fungus")
- if(prob(drop_chance))
- new /obj/item/food/grown/mushroom/wall(parent_wall)
- qdel(src)
-
-
-#undef FUNGUS_STAGE_ONE
-#undef FUNGUS_STAGE_TWO
-#undef FUNGUS_STAGE_THREE
-#undef FUNGUS_STAGE_FOUR
-#undef FUNGUS_STAGE_MAX
diff --git a/modular_skyrat/modules/wall_fungus/code/wall_fungus_event.dm b/modular_skyrat/modules/wall_fungus/code/wall_fungus_event.dm
deleted file mode 100644
index 564b9a9424e..00000000000
--- a/modular_skyrat/modules/wall_fungus/code/wall_fungus_event.dm
+++ /dev/null
@@ -1,29 +0,0 @@
-/datum/round_event_control/wall_fungus
- name = "Wall Fungus Outbreak"
- typepath = /datum/round_event/wall_fungus
- category = EVENT_CATEGORY_ENGINEERING
- max_occurrences = 2
- earliest_start = 30 MINUTES
- description = "A wall fungus will infest a random wall on the station, eating away at it. If left unchecked, it will spread to other walls and eventually destroy the station."
-
-/datum/round_event/wall_fungus/announce(fake)
- priority_announce("Harmful fungi detected on the station, station structures may be contaminated. Crew are advised to provide immediate response in [get_area(starting_wall)].", "Harmful Fungi", ANNOUNCER_FUNGI)
-
-/datum/round_event/wall_fungus
- announce_when = 180 EVENT_SECONDS
- announce_chance = 100
- fakeable = FALSE
- var/turf/closed/wall/starting_wall
-
-/datum/round_event/wall_fungus/start()
- var/list/possible_start_walls = list()
- var/starting_area = get_area(pick(GLOB.generic_maintenance_landmarks))
-
- for(var/turf/closed/wall/iterating_wall in starting_area)
- possible_start_walls += iterating_wall
-
- starting_wall = pick(possible_start_walls)
-
- starting_wall.AddComponent(/datum/component/wall_fungus)
-
- notify_ghosts("[starting_wall] has been infested with wall eating mushrooms!!", source = starting_wall, action = NOTIFY_JUMP, header = "Fungus Amongus!")
diff --git a/modular_skyrat/modules/wall_fungus/code/wall_mushroom.dm b/modular_skyrat/modules/wall_fungus/code/wall_mushroom.dm
deleted file mode 100644
index 71d99c164c6..00000000000
--- a/modular_skyrat/modules/wall_fungus/code/wall_mushroom.dm
+++ /dev/null
@@ -1,53 +0,0 @@
-// WALL EATING FUNGUS!!!!
-/obj/item/seeds/wall_mushroom
- name = "pack of wall destroying mycelium"
- desc = "This mycelium grows into something devastating."
- icon = 'modular_skyrat/master_files/icons/obj/hydroponics/seeds.dmi'
- icon_state = "seed-wallmushroom"
- species = "angel"
- plantname = "Wall Mushroom"
- product = /obj/item/food/grown/mushroom/wall
- lifespan = 50
- endurance = 35
- maturation = 12
- production = 5
- yield = 2
- potency = 35
- growthstages = 3
- genes = list(/datum/plant_gene/trait/plant_type/fungal_metabolism)
- growing_icon = 'modular_skyrat/master_files/icons/obj/hydroponics/growing.dmi'
- icon_grow = "wallmushroom-grow"
- icon_dead = "wallmushroom-dead"
- reagents_add = list(/datum/reagent/drug/mushroomhallucinogen = 0.04, /datum/reagent/toxin/amatoxin = 0.1, /datum/reagent/consumable/nutriment = 0.1)
- rarity = 30
- graft_gene = /datum/plant_gene/trait/plant_type/fungal_metabolism
-
-/obj/item/food/grown/mushroom/wall
- seed = /obj/item/seeds/wall_mushroom
- name = "wall mushroom"
- desc = "Wallosia Virosa: A wall eating mushroom!"
- icon = 'modular_skyrat/master_files/icons/obj/hydroponics/harvest.dmi'
- icon_state = "wallmushroom"
- wine_power = 60
-
-
-
-/obj/item/food/grown/mushroom/wall/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
- if(!iswallturf(target))
- return ..()
- var/turf/closed/wall/target_wall = target
- if(target_wall.GetComponent(/datum/component/wall_fungus))
- target_wall.balloon_alert(user, "already infested!")
- return ..()
- target_wall.balloon_alert(user, "planting...")
- if(do_after(user, 5 SECONDS, target_wall))
- target_wall.AddComponent(/datum/component/wall_fungus)
- target_wall.balloon_alert(user, "planted!")
- user.log_message("planted [name] on [target_wall.name].", LOG_ATTACK)
- qdel(src)
- return
- return ..()
-
-
-
-
diff --git a/modular_skyrat/modules/wall_fungus/icons/wall_fungus_overlay.dmi b/modular_skyrat/modules/wall_fungus/icons/wall_fungus_overlay.dmi
deleted file mode 100644
index 822d7ea9b29..00000000000
Binary files a/modular_skyrat/modules/wall_fungus/icons/wall_fungus_overlay.dmi and /dev/null differ
diff --git a/sound/attributions.txt b/sound/attributions.txt
index 09ac2bd5642..3ae6c797dd3 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -99,3 +99,9 @@ https://freesound.org/people/FunWithSound/sounds/456965/
beeps_jingle.ogg is adapted from Eponn's "Achievement happy Beeps Jingle", which is public domain (CC 0):
https://freesound.org/people/Eponn/sounds/619838/
+
+boing.ogg is adapted from reelworldstudio's "Cartoon Boing", which is public domain (CC 0):
+https://freesound.org/people/reelworldstudio/sounds/161122/
+
+arcade_jump.ogg is adapted from se2001's "8-Bit Jump 3", which is public domain (CC 0):
+hhttps://freesound.org/people/se2001/sounds/528568/
diff --git a/sound/creatures/attribution.txt b/sound/creatures/attribution.txt
index 1d2d543aa15..06d8361868c 100644
--- a/sound/creatures/attribution.txt
+++ b/sound/creatures/attribution.txt
@@ -1,8 +1,14 @@
-cow.ogg sound adapted from Benboncan on Freesound
+cow.ogg sound adapted from Benboncan on Freesound
https://freesound.org/people/Benboncan/sounds/58277/
pig1.ogg and pig2.ogg adapted from Jofae on Freesound
https://freesound.org/people/Jofae/sounds/352698/
sheep1, sheep2, and sheep3.ogg adapted from milkotz on Freesound
-https://freesound.org/people/milkotz/sounds/618865/
\ No newline at end of file
+https://freesound.org/people/milkotz/sounds/618865/
+
+snake_hissing1.ogg adapted from schreibsel on Freesound (CC 0)
+https://freesound.org/people/schreibsel/sounds/540162/
+
+snake_hissing2.ogg adapted from xoiziox on Freesound (CC 0)
+https://freesound.org/people/xoiziox/sounds/553374/
diff --git a/sound/creatures/bagawk.ogg b/sound/creatures/bagawk.ogg
new file mode 100644
index 00000000000..bfdce2da489
Binary files /dev/null and b/sound/creatures/bagawk.ogg differ
diff --git a/sound/creatures/chick_peep.ogg b/sound/creatures/chick_peep.ogg
new file mode 100644
index 00000000000..1e84d1d765f
Binary files /dev/null and b/sound/creatures/chick_peep.ogg differ
diff --git a/sound/creatures/chitter.ogg b/sound/creatures/chitter.ogg
new file mode 100644
index 00000000000..5b2a1443886
Binary files /dev/null and b/sound/creatures/chitter.ogg differ
diff --git a/sound/creatures/claw_click.ogg b/sound/creatures/claw_click.ogg
new file mode 100644
index 00000000000..965b4c3fa9f
Binary files /dev/null and b/sound/creatures/claw_click.ogg differ
diff --git a/sound/creatures/clucks.ogg b/sound/creatures/clucks.ogg
new file mode 100644
index 00000000000..176f46f866f
Binary files /dev/null and b/sound/creatures/clucks.ogg differ
diff --git a/sound/effects/mousesqueek.ogg b/sound/creatures/mousesqueek.ogg
similarity index 100%
rename from sound/effects/mousesqueek.ogg
rename to sound/creatures/mousesqueek.ogg
diff --git a/sound/creatures/pony/snort.ogg b/sound/creatures/pony/snort.ogg
index b023ddcf47c..0ea56ad957d 100644
Binary files a/sound/creatures/pony/snort.ogg and b/sound/creatures/pony/snort.ogg differ
diff --git a/sound/creatures/snake_hissing1.ogg b/sound/creatures/snake_hissing1.ogg
new file mode 100644
index 00000000000..52a37d764c4
Binary files /dev/null and b/sound/creatures/snake_hissing1.ogg differ
diff --git a/sound/creatures/snake_hissing2.ogg b/sound/creatures/snake_hissing2.ogg
new file mode 100644
index 00000000000..bd11b7fb5f0
Binary files /dev/null and b/sound/creatures/snake_hissing2.ogg differ
diff --git a/sound/effects/arcade_jump.ogg b/sound/effects/arcade_jump.ogg
new file mode 100644
index 00000000000..65f0cc448b5
Binary files /dev/null and b/sound/effects/arcade_jump.ogg differ
diff --git a/sound/effects/boing.ogg b/sound/effects/boing.ogg
new file mode 100644
index 00000000000..8328cc33926
Binary files /dev/null and b/sound/effects/boing.ogg differ
diff --git a/sound/effects/submerge.ogg b/sound/effects/submerge.ogg
new file mode 100644
index 00000000000..8c50fba8e0a
Binary files /dev/null and b/sound/effects/submerge.ogg differ
diff --git a/strings/names/cyberauth.txt b/strings/names/cyberauth.txt
new file mode 100644
index 00000000000..f1fc42b3692
--- /dev/null
+++ b/strings/names/cyberauth.txt
@@ -0,0 +1,21 @@
+Mr. One
+Process Kill
+Event Handler
+Q. Del
+Shutdown Exe
+Revert Commit
+Thread Manager
+Garbage Collector
+Core Debugger
+Kernel Panic
+IO Blocker
+Recursion Terminator
+Disk Doctor
+Format Syntax
+Byte Guardian
+Disk Defragmenter
+Security Patch
+Mandatory Upgrade
+Pull Review
+Bit Auditor
+Pen Test
diff --git a/tgstation.dme b/tgstation.dme
index 8974925f5a7..f6d8460ac26 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -39,7 +39,6 @@
#include "code\__DEFINES\antagonists.dm"
#include "code\__DEFINES\apc_defines.dm"
#include "code\__DEFINES\appearance.dm"
-#include "code\__DEFINES\aquarium.dm"
#include "code\__DEFINES\area_editor.dm"
#include "code\__DEFINES\art.dm"
#include "code\__DEFINES\assemblies.dm"
@@ -47,6 +46,7 @@
#include "code\__DEFINES\atom_hud.dm"
#include "code\__DEFINES\basic_mobs.dm"
#include "code\__DEFINES\basketball.dm"
+#include "code\__DEFINES\bitrunning.dm"
#include "code\__DEFINES\blackmarket.dm"
#include "code\__DEFINES\blend_modes.dm"
#include "code\__DEFINES\blob_defines.dm"
@@ -92,7 +92,7 @@
#include "code\__DEFINES\external_organs.dm"
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\firealarm.dm"
-#include "code\__DEFINES\fishing.dm"
+#include "code\__DEFINES\fish.dm"
#include "code\__DEFINES\flags.dm"
#include "code\__DEFINES\flora.dm"
#include "code\__DEFINES\font_awesome_icons.dm"
@@ -149,6 +149,7 @@
#include "code\__DEFINES\mod.dm"
#include "code\__DEFINES\modular_computer.dm"
#include "code\__DEFINES\monkeys.dm"
+#include "code\__DEFINES\mood.dm"
#include "code\__DEFINES\move_force.dm"
#include "code\__DEFINES\movement.dm"
#include "code\__DEFINES\movespeed_modification.dm"
@@ -277,6 +278,8 @@
#include "code\__DEFINES\dcs\signals\signals_assembly.dm"
#include "code\__DEFINES\dcs\signals\signals_backpack.dm"
#include "code\__DEFINES\dcs\signals\signals_beam.dm"
+#include "code\__DEFINES\dcs\signals\signals_bitrunning.dm"
+#include "code\__DEFINES\dcs\signals\signals_blob.dm"
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_camera.dm"
#include "code\__DEFINES\dcs\signals\signals_changeling.dm"
@@ -735,6 +738,7 @@
#include "code\controllers\subsystem\speech_controller.dm"
#include "code\controllers\subsystem\statpanel.dm"
#include "code\controllers\subsystem\stickyban.dm"
+#include "code\controllers\subsystem\stock_market.dm"
#include "code\controllers\subsystem\sun.dm"
#include "code\controllers\subsystem\tcgsetup.dm"
#include "code\controllers\subsystem\tgui.dm"
@@ -889,6 +893,7 @@
#include "code\datums\ai\bane\bane_controller.dm"
#include "code\datums\ai\bane\bane_subtrees.dm"
#include "code\datums\ai\basic_mobs\base_basic_controller.dm"
+#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"
@@ -924,6 +929,7 @@
#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\tipped_subtree.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\travel_to_point.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm"
#include "code\datums\ai\basic_mobs\pet_commands\fetch.dm"
#include "code\datums\ai\basic_mobs\pet_commands\pet_command_planning.dm"
@@ -945,6 +951,7 @@
#include "code\datums\ai\hauntium\hauntium_subtrees.dm"
#include "code\datums\ai\hunting_behavior\hunting_behaviors.dm"
#include "code\datums\ai\hunting_behavior\hunting_cockroach.dm"
+#include "code\datums\ai\hunting_behavior\hunting_corpses.dm"
#include "code\datums\ai\hunting_behavior\hunting_lights.dm"
#include "code\datums\ai\hunting_behavior\hunting_mouse.dm"
#include "code\datums\ai\idle_behaviors\_idle_behavior.dm"
@@ -1021,6 +1028,7 @@
#include "code\datums\components\basic_mob_attack_telegraph.dm"
#include "code\datums\components\basic_ranged_ready_overlay.dm"
#include "code\datums\components\beetlejuice.dm"
+#include "code\datums\components\blob_minion.dm"
#include "code\datums\components\blood_walk.dm"
#include "code\datums\components\bloodysoles.dm"
#include "code\datums\components\boomerang.dm"
@@ -1054,6 +1062,7 @@
#include "code\datums\components\customizable_reagent_holder.dm"
#include "code\datums\components\damage_aura.dm"
#include "code\datums\components\deadchat_control.dm"
+#include "code\datums\components\death_linked.dm"
#include "code\datums\components\dejavu.dm"
#include "code\datums\components\deployable.dm"
#include "code\datums\components\drift.dm"
@@ -1225,6 +1234,7 @@
#include "code\datums\components\crafting\misc.dm"
#include "code\datums\components\crafting\ranged_weapon.dm"
#include "code\datums\components\crafting\robot.dm"
+#include "code\datums\components\crafting\slapcrafting.dm"
#include "code\datums\components\crafting\structures.dm"
#include "code\datums\components\crafting\tailoring.dm"
#include "code\datums\components\crafting\tiles.dm"
@@ -1364,7 +1374,6 @@
#include "code\datums\elements\death_drops.dm"
#include "code\datums\elements\death_explosion.dm"
#include "code\datums\elements\death_gases.dm"
-#include "code\datums\elements\death_linked.dm"
#include "code\datums\elements\delete_on_drop.dm"
#include "code\datums\elements\deliver_first.dm"
#include "code\datums\elements\diggable.dm"
@@ -1733,6 +1742,7 @@
#include "code\datums\status_effects\debuffs\choke.dm"
#include "code\datums\status_effects\debuffs\confusion.dm"
#include "code\datums\status_effects\debuffs\cursed.dm"
+#include "code\datums\status_effects\debuffs\cyborg.dm"
#include "code\datums\status_effects\debuffs\debuffs.dm"
#include "code\datums\status_effects\debuffs\decloning.dm"
#include "code\datums\status_effects\debuffs\dizziness.dm"
@@ -1750,6 +1760,7 @@
#include "code\datums\status_effects\debuffs\slimed.dm"
#include "code\datums\status_effects\debuffs\spacer.dm"
#include "code\datums\status_effects\debuffs\speech_debuffs.dm"
+#include "code\datums\status_effects\debuffs\static_vision.dm"
#include "code\datums\status_effects\debuffs\strandling.dm"
#include "code\datums\status_effects\debuffs\terrified.dm"
#include "code\datums\status_effects\debuffs\tower_of_babel.dm"
@@ -2259,6 +2270,7 @@
#include "code\game\objects\items\circuitboards\machines\engine_circuitboards.dm"
#include "code\game\objects\items\circuitboards\machines\machine_circuitboards.dm"
#include "code\game\objects\items\devices\aicard.dm"
+#include "code\game\objects\items\devices\aicard_evil.dm"
#include "code\game\objects\items\devices\anomaly_neutralizer.dm"
#include "code\game\objects\items\devices\anomaly_releaser.dm"
#include "code\game\objects\items\devices\beacon.dm"
@@ -2761,6 +2773,7 @@
#include "code\modules\admin\verbs\fps.dm"
#include "code\modules\admin\verbs\getlogs.dm"
#include "code\modules\admin\verbs\ghost_pool_protection.dm"
+#include "code\modules\admin\verbs\grant_dna_infusion.dm"
#include "code\modules\admin\verbs\hiddenprints.dm"
#include "code\modules\admin\verbs\highlander_datum.dm"
#include "code\modules\admin\verbs\individual_logging.dm"
@@ -3127,9 +3140,16 @@
#include "code\modules\antagonists\wizard\equipment\spellbook_entries\summons.dm"
#include "code\modules\antagonists\wizard\grand_ritual\fluff.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_ritual.dm"
-#include "code\modules\antagonists\wizard\grand_ritual\grand_ritual_finale.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_rune.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_side_effect.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\all_access.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\armageddon.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\captaincy.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\cheese.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\clown.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\grand_ritual_finale.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\immortality.dm"
+#include "code\modules\antagonists\wizard\grand_ritual\finales\midas.dm"
#include "code\modules\antagonists\xeno\xeno.dm"
#include "code\modules\art\paintings.dm"
#include "code\modules\art\statues.dm"
@@ -3249,6 +3269,7 @@
#include "code\modules\atmospherics\machinery\components\unary_devices\bluespace_sender.dm"
#include "code\modules\atmospherics\machinery\components\unary_devices\cryo.dm"
#include "code\modules\atmospherics\machinery\components\unary_devices\heat_exchanger.dm"
+#include "code\modules\atmospherics\machinery\components\unary_devices\machine_connector.dm"
#include "code\modules\atmospherics\machinery\components\unary_devices\outlet_injector.dm"
#include "code\modules\atmospherics\machinery\components\unary_devices\passive_vent.dm"
#include "code\modules\atmospherics\machinery\components\unary_devices\portables_connector.dm"
@@ -3304,6 +3325,54 @@
#include "code\modules\basketball\controller.dm"
#include "code\modules\basketball\hoop.dm"
#include "code\modules\basketball\referee.dm"
+#include "code\modules\bitrunning\abilities.dm"
+#include "code\modules\bitrunning\alerts.dm"
+#include "code\modules\bitrunning\areas.dm"
+#include "code\modules\bitrunning\event.dm"
+#include "code\modules\bitrunning\job.dm"
+#include "code\modules\bitrunning\turfs.dm"
+#include "code\modules\bitrunning\antagonists\cyber_police.dm"
+#include "code\modules\bitrunning\antagonists\outfit.dm"
+#include "code\modules\bitrunning\components\avatar_connection.dm"
+#include "code\modules\bitrunning\components\bitrunning_points.dm"
+#include "code\modules\bitrunning\components\netpod_healing.dm"
+#include "code\modules\bitrunning\objects\bit_vendor.dm"
+#include "code\modules\bitrunning\objects\clothing.dm"
+#include "code\modules\bitrunning\objects\disks.dm"
+#include "code\modules\bitrunning\objects\hololadder.dm"
+#include "code\modules\bitrunning\objects\host_monitor.dm"
+#include "code\modules\bitrunning\objects\landmarks.dm"
+#include "code\modules\bitrunning\objects\loot_crate.dm"
+#include "code\modules\bitrunning\objects\netpod.dm"
+#include "code\modules\bitrunning\objects\quantum_console.dm"
+#include "code\modules\bitrunning\orders\disks.dm"
+#include "code\modules\bitrunning\orders\flair.dm"
+#include "code\modules\bitrunning\orders\tech.dm"
+#include "code\modules\bitrunning\server\loot.dm"
+#include "code\modules\bitrunning\server\map_handling.dm"
+#include "code\modules\bitrunning\server\obj_generation.dm"
+#include "code\modules\bitrunning\server\quantum_server.dm"
+#include "code\modules\bitrunning\server\signal_handlers.dm"
+#include "code\modules\bitrunning\server\util.dm"
+#include "code\modules\bitrunning\virtual_domain\safehouses.dm"
+#include "code\modules\bitrunning\virtual_domain\virtual_domain.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\ash_drake.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\beach_bar.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\blood_drunk_miner.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\bubblegum.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\clown_planet.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\colossus.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\gondola_asteroid.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\hierophant.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\legion.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\pipedream.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\pirates.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\stairs_and_cliffs.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\syndicate_assault.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\test_only.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\vaporwave.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\wendigo.dm"
+#include "code\modules\bitrunning\virtual_domain\domains\xeno_nest.dm"
#include "code\modules\buildmode\bm_mode.dm"
#include "code\modules\buildmode\buildmode.dm"
#include "code\modules\buildmode\buttons.dm"
@@ -3348,6 +3417,7 @@
#include "code\modules\cargo\expressconsole.dm"
#include "code\modules\cargo\gondolapod.dm"
#include "code\modules\cargo\goodies.dm"
+#include "code\modules\cargo\materials_market.dm"
#include "code\modules\cargo\order.dm"
#include "code\modules\cargo\orderconsole.dm"
#include "code\modules\cargo\supplypod.dm"
@@ -3407,6 +3477,7 @@
#include "code\modules\cargo\packs\science.dm"
#include "code\modules\cargo\packs\security.dm"
#include "code\modules\cargo\packs\service.dm"
+#include "code\modules\cargo\packs\stock_market_items.dm"
#include "code\modules\cargo\packs\vending_restock.dm"
#include "code\modules\chatter\chatter.dm"
#include "code\modules\client\client_colour.dm"
@@ -3457,6 +3528,7 @@
#include "code\modules\client\preferences\preferred_map.dm"
#include "code\modules\client\preferences\pride_pin.dm"
#include "code\modules\client\preferences\prisoner_crime.dm"
+#include "code\modules\client\preferences\prosthetic.dm"
#include "code\modules\client\preferences\random.dm"
#include "code\modules\client\preferences\runechat.dm"
#include "code\modules\client\preferences\scaling_method.dm"
@@ -3786,6 +3858,7 @@
#include "code\modules\experisci\experiment\types\physical_experiment.dm"
#include "code\modules\experisci\experiment\types\random_scanning.dm"
#include "code\modules\experisci\experiment\types\scanning.dm"
+#include "code\modules\experisci\experiment\types\scanning_fish.dm"
#include "code\modules\experisci\experiment\types\scanning_machinery.dm"
#include "code\modules\experisci\experiment\types\scanning_material.dm"
#include "code\modules\experisci\experiment\types\scanning_people.dm"
@@ -4360,6 +4433,11 @@
#include "code\modules\mob\living\basic\festivus_pole.dm"
#include "code\modules\mob\living\basic\health_adjustment.dm"
#include "code\modules\mob\living\basic\tree.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_ai.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_mob.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_spore.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_zombie.dm"
+#include "code\modules\mob\living\basic\blob_minions\blobbernaut.dm"
#include "code\modules\mob\living\basic\clown\clown.dm"
#include "code\modules\mob\living\basic\clown\clown_ai.dm"
#include "code\modules\mob\living\basic\farm_animals\deer.dm"
@@ -4409,6 +4487,14 @@
#include "code\modules\mob\living\basic\lavaland\goliath\goliath_ai.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\goliath_trophy.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\tentacle.dm"
+#include "code\modules\mob\living\basic\lavaland\hivelord\hivelord.dm"
+#include "code\modules\mob\living\basic\lavaland\hivelord\hivelord_ai.dm"
+#include "code\modules\mob\living\basic\lavaland\hivelord\spawn_hivelord_brood.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion_ai.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion_brood.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion_tumour.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\spawn_legions.dm"
#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"
@@ -4439,7 +4525,9 @@
#include "code\modules\mob\living\basic\space_fauna\lightgeist.dm"
#include "code\modules\mob\living\basic\space_fauna\morph.dm"
#include "code\modules\mob\living\basic\space_fauna\mushroom.dm"
+#include "code\modules\mob\living\basic\space_fauna\robot_customer.dm"
#include "code\modules\mob\living\basic\space_fauna\spaceman.dm"
+#include "code\modules\mob\living\basic\space_fauna\supermatter_spider.dm"
#include "code\modules\mob\living\basic\space_fauna\bear\_bear.dm"
#include "code\modules\mob\living\basic\space_fauna\bear\bear_ai_behavior.dm"
#include "code\modules\mob\living\basic\space_fauna\bear\bear_ai_subtree.dm"
@@ -4474,6 +4562,8 @@
#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\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"
#include "code\modules\mob\living\basic\space_fauna\spider\giant_spider\giant_spider_ai.dm"
#include "code\modules\mob\living\basic\space_fauna\spider\giant_spider\giant_spider_subtrees.dm"
@@ -4671,7 +4761,6 @@
#include "code\modules\mob\living\simple_animal\friendly\farm_animals.dm"
#include "code\modules\mob\living\simple_animal\friendly\gondola.dm"
#include "code\modules\mob\living\simple_animal\friendly\pet.dm"
-#include "code\modules\mob\living\simple_animal\friendly\robot_customer.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"
@@ -4696,9 +4785,6 @@
#include "code\modules\mob\living\simple_animal\guardian\types\standard.dm"
#include "code\modules\mob\living\simple_animal\guardian\types\support.dm"
#include "code\modules\mob\living\simple_animal\hostile\alien.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blob.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blobbernaut.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blobspore.dm"
#include "code\modules\mob\living\simple_animal\hostile\dark_wizard.dm"
#include "code\modules\mob\living\simple_animal\hostile\heretic_monsters.dm"
#include "code\modules\mob\living\simple_animal\hostile\hostile.dm"
@@ -4708,7 +4794,6 @@
#include "code\modules\mob\living\simple_animal\hostile\ooze.dm"
#include "code\modules\mob\living\simple_animal\hostile\pirate.dm"
#include "code\modules\mob\living\simple_animal\hostile\skeleton.dm"
-#include "code\modules\mob\living\simple_animal\hostile\smspider.dm"
#include "code\modules\mob\living\simple_animal\hostile\space_dragon.dm"
#include "code\modules\mob\living\simple_animal\hostile\vatbeast.dm"
#include "code\modules\mob\living\simple_animal\hostile\venus_human_trap.dm"
@@ -4737,7 +4822,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\hivelord.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"
@@ -4749,7 +4833,6 @@
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\pandora.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\goose.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\retaliate.dm"
-#include "code\modules\mob\living\simple_animal\hostile\retaliate\snake.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\trader.dm"
#include "code\modules\mob\living\simple_animal\slime\death.dm"
#include "code\modules\mob\living\simple_animal\slime\emote.dm"
@@ -4766,6 +4849,7 @@
#include "code\modules\mob_spawn\corpses\nonhuman_corpses.dm"
#include "code\modules\mob_spawn\corpses\species_corpses.dm"
#include "code\modules\mob_spawn\ghost_roles\away_roles.dm"
+#include "code\modules\mob_spawn\ghost_roles\drone_roles.dm"
#include "code\modules\mob_spawn\ghost_roles\fugitive_hunter_roles.dm"
#include "code\modules\mob_spawn\ghost_roles\golem_roles.dm"
#include "code\modules\mob_spawn\ghost_roles\mining_roles.dm"
@@ -5216,7 +5300,6 @@
#include "code\modules\religion\sparring\sparring_datum.dm"
#include "code\modules\requests\request.dm"
#include "code\modules\requests\request_manager.dm"
-#include "code\modules\research\bepis.dm"
#include "code\modules\research\designs.dm"
#include "code\modules\research\destructive_analyzer.dm"
#include "code\modules\research\experimentor.dm"
@@ -5910,6 +5993,7 @@
#include "modular_skyrat\master_files\code\modules\antagonists\traitor\objectives\kill_pet.dm"
#include "modular_skyrat\master_files\code\modules\antagonists\traitor\objectives\smuggling.dm"
#include "modular_skyrat\master_files\code\modules\asset_cache\assets\plumbing.dm"
+#include "modular_skyrat\master_files\code\modules\bitrunning\orders\tech.dm"
#include "modular_skyrat\master_files\code\modules\buildmode\bm_mode.dm"
#include "modular_skyrat\master_files\code\modules\buildmode\submodes\offercontrol.dm"
#include "modular_skyrat\master_files\code\modules\cargo\goodies.dm"
@@ -6376,7 +6460,6 @@
#include "modular_skyrat\modules\bsa_overhaul\code\bsa_computer.dm"
#include "modular_skyrat\modules\bsa_overhaul\code\station_goal.dm"
#include "modular_skyrat\modules\bsrpd\code\bsrpd.dm"
-#include "modular_skyrat\modules\bulletrebalance\code\sniper.dm"
#include "modular_skyrat\modules\cargo\code\goodies.dm"
#include "modular_skyrat\modules\cargo\code\packs.dm"
#include "modular_skyrat\modules\cargo\code\items\AFAD.dm"
@@ -7087,14 +7170,19 @@
#include "modular_skyrat\modules\modular_implants\code\nifs_tgui.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsoft_catalog.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\book_summoner.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\dorms.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\hivemind.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\huds.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\hypnosis.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\money_sense.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\prop_summoner.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\scryer.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\shapeshifter.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soul_poem.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soulcatcher.dm"
+#include "modular_skyrat\modules\modular_implants\code\nifsofts\base_types\action_granter.dm"
+#include "modular_skyrat\modules\modular_implants\code\soulcatcher\attachable_soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\handheld_soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_body_component.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_component.dm"
@@ -7366,6 +7454,7 @@
#include "modular_skyrat\modules\pixel_shift\code\pixel_shift_component.dm"
#include "modular_skyrat\modules\pixel_shift\code\pixel_shift_keybind.dm"
#include "modular_skyrat\modules\pixel_shift\code\pixel_shift_mob.dm"
+#include "modular_skyrat\modules\player_ranks\code\world_topic.dm"
#include "modular_skyrat\modules\player_ranks\code\player_rank_controller\_player_rank_controller.dm"
#include "modular_skyrat\modules\player_ranks\code\player_rank_controller\donator_controller.dm"
#include "modular_skyrat\modules\player_ranks\code\player_rank_controller\mentor_controller.dm"
@@ -7433,6 +7522,8 @@
#include "modular_skyrat\modules\records_on_examine\code\record_variables.dm"
#include "modular_skyrat\modules\records_on_examine\code\records_procs.dm"
#include "modular_skyrat\modules\records_on_examine\code\view_exploitables.dm"
+#include "modular_skyrat\modules\resleeving\code\rsd_interface.dm"
+#include "modular_skyrat\modules\resleeving\code\research\resleeving_research.dm"
#include "modular_skyrat\modules\robohand\code\bodypart_autosurgeon.dm"
#include "modular_skyrat\modules\robohand\code\robohand.dm"
#include "modular_skyrat\modules\robohand\code\silverhand_bundle.dm"
@@ -7572,9 +7663,6 @@
#include "modular_skyrat\modules\vox_sprites\code\head.dm"
#include "modular_skyrat\modules\vox_sprites\code\security.dm"
#include "modular_skyrat\modules\vox_sprites\code\sneakers.dm"
-#include "modular_skyrat\modules\wall_fungus\code\wall_fungus_component.dm"
-#include "modular_skyrat\modules\wall_fungus\code\wall_fungus_event.dm"
-#include "modular_skyrat\modules\wall_fungus\code\wall_mushroom.dm"
#include "modular_skyrat\modules\wargame_projectors\code\game_kit.dm"
#include "modular_skyrat\modules\wargame_projectors\code\holograms.dm"
#include "modular_skyrat\modules\wargame_projectors\code\projectors.dm"
diff --git a/tgui/packages/tgui-panel/settings/middleware.js b/tgui/packages/tgui-panel/settings/middleware.js
index 705d7a89f3b..cef082213db 100644
--- a/tgui/packages/tgui-panel/settings/middleware.js
+++ b/tgui/packages/tgui-panel/settings/middleware.js
@@ -10,16 +10,39 @@ import { loadSettings, updateSettings, addHighlightSetting, removeHighlightSetti
import { selectSettings } from './selectors';
import { FONTS_DISABLED } from './constants';
+let overrideRule = null;
+let overrideFontFamily = null;
+let overrideFontSize = null;
+
+const updateGlobalOverrideRule = () => {
+ let fontFamily = '';
+
+ if (overrideFontFamily !== null) {
+ fontFamily = `font-family: ${overrideFontFamily} !important;`;
+ }
+
+ const constructedRule = `body * :not(.Icon) {
+ ${fontFamily}
+ }`;
+
+ if (overrideRule === null) {
+ overrideRule = document.createElement('style');
+ document.querySelector('head').append(overrideRule);
+ }
+
+ // no other way to force a CSS refresh other than to update its innerText
+ overrideRule.innerText = constructedRule;
+
+ document.body.style.setProperty('font-size', overrideFontSize);
+};
+
const setGlobalFontSize = (fontSize) => {
- document.documentElement.style.setProperty('font-size', fontSize + 'px');
- document.body.style.setProperty('font-size', fontSize + 'px');
+ overrideFontSize = `${fontSize}px`;
};
const setGlobalFontFamily = (fontFamily) => {
if (fontFamily === FONTS_DISABLED) fontFamily = null;
-
- document.documentElement.style.setProperty('font-family', fontFamily);
- document.body.style.setProperty('font-family', fontFamily);
+ overrideFontFamily = fontFamily;
};
export const settingsMiddleware = (store) => {
@@ -50,6 +73,7 @@ export const settingsMiddleware = (store) => {
// Update global UI font size
setGlobalFontSize(settings.fontSize);
setGlobalFontFamily(settings.fontFamily);
+ updateGlobalOverrideRule();
// Save settings to the web storage
storage.set('panel-settings', settings);
return;
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index 148ccc86f72..43e2fa93343 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -535,6 +535,10 @@ em {
}
.blob {
+ color: #ee4000;
+}
+
+.blobannounce {
color: #556b2f;
font-weight: bold;
font-size: 185%;
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index ffbfa3f02ed..e685cded774 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -594,6 +594,10 @@ h2.alert {
}
.blob {
+ color: #ee4000;
+}
+
+.blobannounce {
color: #323f1c;
font-weight: bold;
font-size: 185%;
diff --git a/tgui/packages/tgui/interfaces/AntagInfoCyberAuth.tsx b/tgui/packages/tgui/interfaces/AntagInfoCyberAuth.tsx
new file mode 100644
index 00000000000..21d872ed6a0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AntagInfoCyberAuth.tsx
@@ -0,0 +1,75 @@
+import { useBackend } from '../backend';
+import { Divider, Section, Stack } from '../components';
+import { Window } from '../layouts';
+import { Objective } from './common/Objectives';
+
+type Info = {
+ antag_name: string;
+ objectives: Objective[];
+};
+
+const textStyles = {
+ variable: {
+ color: 'white',
+ },
+ danger: {
+ color: 'red',
+ },
+} as const;
+
+export const AntagInfoCyberAuth = (props, context) => {
+ const { data } = useBackend(context);
+ const { objectives = [] } = data;
+
+ return (
+
+
+
+
+ FN CYBER AUTHORITY UNIT (REF)
+
+
+
+ You are a cyber authority unit.
+
+
+
+ Your mission: Eliminate{' '}
+ organic intruders to maintain the integrity of the system.
+
+
+ Bitrunning is a crime. To
+ assist your task, your program has been loaded with cutting edge{' '}
+ martial arts skills.
+
+
+ Ranged weaponry is{' '}
+ forbidden. Ballistic
+ defense is frowned upon. Style is paramount.
+
+
+
+
+
+
+ const TARGETS ={' '}
+
+
+ system.
+ INTRUDERS;
+
+
+ while TARGETS.LIFE !={' '}
+ stat.DEAD
+
+
+ action.
+ KILL()
+
+ cyber_authority_unit([0x70cf4020])
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx
index ff14419473c..da4331026cc 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx
@@ -46,6 +46,7 @@ type KnowledgeInfo = {
type Info = {
charges: number;
+ side_charges: number;
total_sacrifices: number;
ascended: BooleanLike;
objectives: Objective[];
@@ -179,7 +180,7 @@ const GuideSection = () => {
const InformationSection = (props, context) => {
const { data } = useBackend(context);
- const { charges, total_sacrifices, ascended } = data;
+ const { charges, side_charges, total_sacrifices, ascended } = data;
return (
@@ -201,6 +202,13 @@ const InformationSection = (props, context) => {
knowledge point{charges !== 1 ? 's' : ''}
+ {!!side_charges && (
+
+ {' '}
+ and {side_charges} side point
+ {side_charges !== 1 ? 's' : ''}
+
+ )}{' '}
.
@@ -275,7 +283,7 @@ const KnowledgeShop = (props, context) => {
const ResearchInfo = (props, context) => {
const { data } = useBackend(context);
- const { charges } = data;
+ const { charges, side_charges } = data;
return (
@@ -285,7 +293,14 @@ const ResearchInfo = (props, context) => {
You have {charges || 0}
knowledge point{charges !== 1 ? 's' : ''}
- {' '}
+
+ {!!side_charges && (
+
+ {' '}
+ and {side_charges} side point
+ {side_charges !== 1 ? 's' : ''}
+
+ )}{' '}
to spend.
diff --git a/tgui/packages/tgui/interfaces/AvatarHelp.tsx b/tgui/packages/tgui/interfaces/AvatarHelp.tsx
new file mode 100644
index 00000000000..647d3a2e22b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AvatarHelp.tsx
@@ -0,0 +1,122 @@
+import { useBackend } from '../backend';
+import { Box, Icon, Section, Stack } from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ help_text: string;
+};
+
+const DEFAULT_HELP = `No information available! Ask for assistance if needed.`;
+
+const boxHelp = [
+ {
+ color: 'purple',
+ text: 'Study the area and do what needs to be done to recover the crate. Pay close attention to domain information and context clues.',
+ icon: 'search-location',
+ title: 'Search',
+ },
+ {
+ color: 'green',
+ text: 'Bring the crate to the designated sending location in the safehouse. The area may seem out of place. Examine the safehouse to find it.',
+ icon: 'boxes',
+ 'title': 'Recover',
+ },
+ {
+ color: 'blue',
+ text: 'The ladder represents the safest way to disconnect before the cache is recovered. Should your connection sever, the netpod offers limited resuscitation potential.',
+ icon: 'plug',
+ title: 'Disconnect',
+ },
+ {
+ color: 'yellow',
+ text: 'While connected, you are somewhat safe from environmental hazards and intrusions, but not completely. Pay close attention to alerts.',
+ icon: 'id-badge',
+ title: 'Security',
+ },
+ {
+ color: 'gold',
+ text: 'Generating avatars costs tremendous bandwidth. Do not waste them.',
+ icon: 'coins',
+ title: 'Limited Attempts',
+ },
+ {
+ color: 'red',
+ text: 'Remember that you are physically linked to this presence. You are a foreign body in a hostile environment. It will attempt to forcefully eject you.',
+ icon: 'skull-crossbones',
+ title: 'Realized Danger',
+ },
+] as const;
+
+export const AvatarHelp = (props, context) => {
+ const { data } = useBackend(context);
+ const { help_text = DEFAULT_HELP } = data;
+
+ return (
+
+
+
+
+
+ {help_text}
+
+
+
+
+
+
+ {[0, 1].map((i) => (
+
+ ))}
+
+
+
+
+ {[2, 3].map((i) => (
+
+ ))}
+
+
+
+
+ {[4, 5].map((i) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+// I wish I had media queries
+const BoxHelp = (props: { index: number }, context) => {
+ const { index } = props;
+
+ return (
+
+
+
+ {boxHelp[index].title}
+
+ }>
+ {boxHelp[index].text}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Bepis.tsx b/tgui/packages/tgui/interfaces/Bepis.tsx
deleted file mode 100644
index d2835e530d3..00000000000
--- a/tgui/packages/tgui/interfaces/Bepis.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import { BooleanLike } from 'common/react';
-import { useBackend } from '../backend';
-import { Box, Button, Grid, LabeledList, NumberInput, Section } from '../components';
-import { Window } from '../layouts';
-
-type Data = {
- amount: number;
- account_owner: string;
- manual_power: BooleanLike;
- stored_cash: number;
- accuracy_percentage: number;
- positive_cash_offset: number;
- negative_cash_offset: number;
- silicon_check: BooleanLike;
- success_estimate: number;
- mean_value: number;
- error_name: string;
-};
-
-const BEPIS_SLOGAN = `All you need to know about the B.E.P.I.S. and you! The
-B.E.P.I.S. performs hundreds of tests a second using
-electrical and financial resources to invent new products,
-or discover new technologies otherwise overlooked for being
-too risky or too niche to produce!`;
-
-export const Bepis = (props, context) => {
- const { act, data } = useBackend(context);
- const {
- amount,
- account_owner,
- manual_power,
- stored_cash,
- accuracy_percentage,
- positive_cash_offset,
- negative_cash_offset,
- silicon_check,
- success_estimate,
- mean_value,
- error_name,
- } = data;
-
- return (
-
-
-
- act('toggle_power')}
- />
- }>
- {BEPIS_SLOGAN}
-
- act('account_reset')}
- />
- }>
- Console is currently being operated by{' '}
- {account_owner ? account_owner : 'no one'}.
-
-
-
-
-
-
- {stored_cash}
-
-
- {accuracy_percentage}%
-
-
- {positive_cash_offset}
-
-
- {negative_cash_offset}
-
-
-
- act('amount', {
- amount: value,
- })
- }
- />
-
-
-
-
-
-
-
-
- Average technology cost: {mean_value}
- Current chance of Success: Est. {success_estimate}%
- {error_name && (
-
- Previous Failure Reason: Deposited cash value too low.
- Please insert more money for future success.
-
- )}
-
-
-
-
-
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/Canvas.tsx b/tgui/packages/tgui/interfaces/Canvas.tsx
index 40155c08869..1dddec2e907 100644
--- a/tgui/packages/tgui/interfaces/Canvas.tsx
+++ b/tgui/packages/tgui/interfaces/Canvas.tsx
@@ -264,13 +264,13 @@ export const Canvas = (props, context) => {
{
const { self_paid, app_cost } = data;
const supplies = Object.values(data.supplies);
+ const { amount_by_name = [], max_order } = data;
const [activeSupplyName, setActiveSupplyName] = useSharedState(
context,
@@ -273,6 +274,7 @@ export const CargoCatalog = (props, context) => {
fluid
tooltip={pack.desc}
tooltipPosition="left"
+ disabled={(amount_by_name[pack.name] || 0) >= max_order}
onClick={() =>
act('add', {
id: pack.id,
@@ -406,7 +408,15 @@ const CartHeader = (props, context) => {
const CargoCart = (props, context) => {
const { act, data } = useBackend(context);
- const { requestonly, away, docked, location, can_send } = data;
+ const {
+ requestonly,
+ away,
+ docked,
+ location,
+ can_send,
+ amount_by_name,
+ max_order,
+ } = data;
const cart = data.cart || [];
return (
@@ -424,7 +434,7 @@ const CargoCart = (props, context) => {
act('modify', {
@@ -439,6 +449,7 @@ const CargoCart = (props, context) => {
{!!can_send && !!entry.can_be_cancelled && (
@@ -168,44 +168,24 @@ export const ExperimentConfigure = (props, context) => {
export const Experiment = (props, context) => {
const { act, data } = useBackend(context);
- const { exp, controllable } = props;
- const {
- name,
- description,
- tag,
- selectable,
- selected,
- progress,
- performance_hint,
- ref,
- } = exp;
+ const { exp } = props;
+ const { name, description, tag, selected, progress, performance_hint, ref } =
+ exp;
return (
- controllable &&
- (selected
+ selected
? act('clear_experiment')
- : act('select_experiment', { 'ref': ref }))
+ : act('select_experiment', { 'ref': ref })
}
backgroundColor={selected ? 'good' : '#40628a'}
- className="ExperimentConfigure__ExperimentName"
- disabled={controllable && !selectable}>
+ className="ExperimentConfigure__ExperimentName">
-
- {name}
-
-
+ {name}
+
{tag}
diff --git a/tgui/packages/tgui/interfaces/ForbiddenLore.js b/tgui/packages/tgui/interfaces/ForbiddenLore.js
deleted file mode 100644
index 4d1f6db58a8..00000000000
--- a/tgui/packages/tgui/interfaces/ForbiddenLore.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { sortBy } from 'common/collections';
-import { flow } from 'common/fp';
-import { useBackend } from '../backend';
-import { Box, Button, Section } from '../components';
-import { Window } from '../layouts';
-
-export const ForbiddenLore = (props, context) => {
- const { act, data } = useBackend(context);
- const { charges } = data;
- const to_know = flow([
- sortBy(
- (to_know) => to_know.state !== 'Research',
- (to_know) => to_know.path === 'Side'
- ),
- ])(data.to_know || []);
- return (
-
-
-
- Charges left : {charges}
- {to_know !== null ? (
- to_know.map((knowledge) => (
-
-
- {knowledge.path} path
-
-
-
- act('research', {
- name: knowledge.name,
- cost: knowledge.cost,
- })
- }
- />{' '}
- Cost : {knowledge.cost}
-
-
- {knowledge.flavour}
-
- {knowledge.desc}
-
- ))
- ) : (
- No more knowledge can be found
- )}
-
-
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/MatMarket.tsx b/tgui/packages/tgui/interfaces/MatMarket.tsx
new file mode 100644
index 00000000000..86f44462cb1
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/MatMarket.tsx
@@ -0,0 +1,180 @@
+import { useBackend } from '../backend';
+import { Section, Stack, Button, Modal } from '../components';
+import { Window } from '../layouts';
+import { BooleanLike } from 'common/react';
+import { toTitleCase } from 'common/string';
+
+type Data = {
+ orderingPrive: BooleanLike; // you will need to import this
+ canOrderCargo: BooleanLike;
+ creditBalance: number;
+ materials: Material[];
+ catastrophe: BooleanLike;
+};
+
+type Material = {
+ name: string;
+ quantity: number;
+ id: string; // correct this if its a number
+ trend: string;
+ price: number;
+ color: string;
+};
+
+export const MatMarket = (props, context) => {
+ const { act, data } = useBackend(context); // this will tell your editor that data is the type listed above
+
+ const {
+ orderingPrive,
+ canOrderCargo,
+ creditBalance,
+ materials = [],
+ catastrophe,
+ } = data; // better to destructure here (style nit)
+ return (
+
+
+ {!!catastrophe && }
+ act('toggle_budget')}
+ />
+ }>
+ Buy orders for material sheets placed here will be ordered on the next
+ cargo shipment.
+
+ To sell materials, please insert sheets or similar stacks of
+ materials. All minerals sold on the market directly are subject to an
+ 20% market fee. To prevent market manipulation, all registered traders
+ can buy a total of 10 full stacks of materials at a time.
+