diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm
index a2b3d227d5db..0a526b0f82d5 100644
--- a/_maps/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm
+++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_cultaltar.dmm
@@ -56,10 +56,6 @@
},
/area/ruin/unpowered/cultaltar)
"o" = (
-/obj/effect/rune/narsie{
- color = "#ff0000";
- used = 1
- },
/obj/item/cult_shift,
/obj/effect/decal/remains/human,
/obj/item/melee/cultblade/dagger,
@@ -69,10 +65,7 @@
sound = 'sound/hallucinations/i_see_you1.ogg';
triggerer_only = 1
},
-/obj/effect/step_trigger/message{
- message = "You've made a grave mistake, haven't you?";
- name = "ohfuck"
- },
+/obj/structure/cult/altar,
/turf/open/floor/engine/cult{
initial_gas_mix = "LAVALAND_ATMOS"
},
diff --git a/_maps/RandomRuins/SpaceRuins/the_outlet.dmm b/_maps/RandomRuins/SpaceRuins/the_outlet.dmm
index 12885f167d25..2ec52aba802d 100644
--- a/_maps/RandomRuins/SpaceRuins/the_outlet.dmm
+++ b/_maps/RandomRuins/SpaceRuins/the_outlet.dmm
@@ -104,10 +104,6 @@
/turf/open/floor/iron/white,
/area/ruin/space/has_grav/the_outlet/researchrooms)
"cL" = (
-/obj/effect/rune/apocalypse{
- req_cultists = 999;
- layer = 2
- },
/obj/structure/destructible/cult/pants_altar{
layer = 3
},
diff --git a/_maps/map_files/generic/CentCom.dmm b/_maps/map_files/generic/CentCom.dmm
index 5e908132206a..2254c7ca9f1a 100644
--- a/_maps/map_files/generic/CentCom.dmm
+++ b/_maps/map_files/generic/CentCom.dmm
@@ -5734,7 +5734,6 @@
/turf/open/ai_visible,
/area/centcom/ai_multicam_room)
"apv" = (
-/obj/effect/rune/narsie,
/obj/structure/chair/comfy/carp{
dir = 1
},
@@ -21733,7 +21732,6 @@
/turf/open/floor/carpet/donk,
/area/centcom/central_command_areas/adminroom)
"jCt" = (
-/obj/effect/rune/apocalypse,
/obj/structure/chair/comfy/black{
dir = 1
},
diff --git a/_maps/shuttles/emergency_narnar.dmm b/_maps/shuttles/emergency_narnar.dmm
index d6c48664e08e..1a6e5650eb3a 100644
--- a/_maps/shuttles/emergency_narnar.dmm
+++ b/_maps/shuttles/emergency_narnar.dmm
@@ -132,7 +132,6 @@
/turf/open/floor/cult,
/area/shuttle/escape)
"B" = (
-/obj/effect/rune/convert,
/obj/effect/decal/remains/human,
/obj/effect/decal/cleanable/blood/gibs/body,
/obj/effect/decal/cleanable/blood/gibs/core,
@@ -143,7 +142,6 @@
/turf/open/floor/cult,
/area/shuttle/escape)
"D" = (
-/obj/effect/rune/convert,
/obj/effect/decal/remains/human,
/turf/open/floor/cult,
/area/shuttle/escape)
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 73f2b7ee3c00..26609c21467c 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -35,10 +35,10 @@
*/
//Monkestation EDIT: START - CYBERNETICS
-/proc/ui_hand_position(i,y_offset = 0,y_pixel_offset = 0) //values based on old hand ui positions (CENTER:-/+16,SOUTH:5)
+/proc/ui_hand_position(i,y_offset = 0,y_pixel_offset = 0, x_pixel_offset = 0) //values based on old hand ui positions (CENTER:-/+16,SOUTH:5)
var/x_off = -(!(i % 2))
var/y_off = round((i-1) / 2) + y_offset
- return"CENTER+[x_off]:16,SOUTH+[y_off]:[5 + y_pixel_offset]"
+ return"CENTER+[x_off]:[16 + x_pixel_offset],SOUTH+[y_off]:[5 + y_pixel_offset]"
//Monkestation EDIT: END - CYBERNETICS
/proc/ui_equip_position(mob/M)
@@ -101,6 +101,7 @@
#define ui_mood "EAST-1:28,CENTER:21"
#define ui_spacesuit "EAST-1:28,CENTER-4:14"
#define ui_stamina "EAST-1:28,CENTER-3:14"
+#define ui_more_under_health_and_to_the_left "EAST-2:14,CENTER-5:29"
//Pop-up inventory
#define ui_shoes "WEST+1:8,SOUTH:5"
diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm
index ffac885bf4c8..3ced7ac65365 100644
--- a/code/__DEFINES/text.dm
+++ b/code/__DEFINES/text.dm
@@ -28,6 +28,7 @@
/// Smallest size. (ie: whisper runechat) - Size options: 6pt 12pt 18pt.
#define MAPTEXT_SPESSFONT(text) {"[##text]"}
+#define MAPTEXT_YOU_MURDERER(text) {"[##text]"}
/**
* Prepares a text to be used for maptext, using a variable size font.
*
diff --git a/code/__DEFINES/~monkestation/bloodcult.dm b/code/__DEFINES/~monkestation/bloodcult.dm
new file mode 100644
index 000000000000..daa4482dd23f
--- /dev/null
+++ b/code/__DEFINES/~monkestation/bloodcult.dm
@@ -0,0 +1,202 @@
+
+#define BLOODCULT_STAGE_NORMAL 1 //default
+#define BLOODCULT_STAGE_READY 2 //eclipse timer has reached zero
+#define BLOODCULT_STAGE_ECLIPSE 3 //3 - narsie summoning ritual undergoing
+#define BLOODCULT_STAGE_MISSED 4 //eclipse window has ended
+#define BLOODCULT_STAGE_DEFEATED 5 //5 - narsie summoning ritual failed
+#define BLOODCULT_STAGE_NARSIE 6 //endgame
+
+#define BLOODCOST_TARGET_BLEEDER "bleeder"
+#define BLOODCOST_AMOUNT_BLEEDER "bleeder_amount"
+#define BLOODCOST_TARGET_GRAB "grabbed"
+#define BLOODCOST_AMOUNT_GRAB "grabbed_amount"
+#define BLOODCOST_TARGET_HANDS "hands"
+#define BLOODCOST_AMOUNT_HANDS "hands_amount"
+#define BLOODCOST_TARGET_HELD "held"
+#define BLOODCOST_AMOUNT_HELD "held_amount"
+#define BLOODCOST_LID_HELD "held_lid"
+#define BLOODCOST_TARGET_SPLATTER "splatter"
+#define BLOODCOST_AMOUNT_SPLATTER "splatter_amount"
+#define BLOODCOST_TARGET_BLOODPACK "bloodpack"
+#define BLOODCOST_AMOUNT_BLOODPACK "bloodpack_amount"
+#define BLOODCOST_HOLES_BLOODPACK "bloodpack_noholes"
+#define BLOODCOST_TARGET_CONTAINER "container"
+#define BLOODCOST_AMOUNT_CONTAINER "container_amount"
+#define BLOODCOST_LID_CONTAINER "container_lid"
+#define BLOODCOST_TARGET_USER "user"
+#define BLOODCOST_AMOUNT_USER "user_amount"
+#define BLOODCOST_TOTAL "total"
+#define BLOODCOST_RESULT "result"
+#define BLOODCOST_FAILURE "failure"
+#define BLOODCOST_TRIBUTE "tribute"
+#define BLOODCOST_USER "user"
+#define RITUALABORT_ERASED "erased"
+#define RITUALABORT_STAND "too far"
+#define RITUALABORT_GONE "moved away"
+#define RITUALABORT_BLOCKED "blocked"
+#define RITUALABORT_BLOOD "channel cancel"
+#define RITUALABORT_TOOLS "moved talisman"
+#define RITUALABORT_REMOVED "victim removed"
+#define RITUALABORT_CONVERT "convert success"
+#define RITUALABORT_REFUSED "convert refused"
+#define RITUALABORT_NOCHOICE "convert nochoice"
+#define RITUALABORT_SACRIFICE "convert failure"
+#define RITUALABORT_FULL "no room"
+#define RITUALABORT_CONCEAL "conceal"
+#define RITUALABORT_NEAR "near"
+#define RITUALABORT_MISSING "missing"
+#define RITUALABORT_OVERCROWDED "overcrowded"
+
+#define TATTOO_POOL "Blood Pooling"
+#define TATTOO_SILENT "Silent Casting"
+#define TATTOO_DAGGER "Blood Dagger"
+#define TATTOO_HOLY "Unholy Protection"
+#define TATTOO_FAST "Rapid Tracing"
+#define TATTOO_CHAT "Dark Communication"
+#define TATTOO_MANIFEST "Pale Body"
+#define TATTOO_MEMORIZE "Arcane Dimension"
+#define TATTOO_RUNESTORE "Runic Skin"
+#define TATTOO_SHORTCUT "Shortcut Sigil"
+
+#define TOME_CLOSED 1
+#define TOME_OPEN 2
+#define RUNE_WRITE_CANNOT 0
+#define RUNE_WRITE_COMPLETE 1
+#define RUNE_WRITE_CONTINUE 2
+#define RUNE_CAN_ATTUNE 0
+#define RUNE_CAN_IMBUE 1
+#define RUNE_CANNOT 2
+#define RUNE_STAND 1
+#define MAX_TALISMAN_PER_TOME 5
+#define SACRIFICE_CHANGE_COOLDOWN 30 MINUTES
+#define DEATH_SHADEOUT_TIMER 60 SECONDS
+#define CONVERSION_REFUSE -1
+#define CONVERSION_NOCHOICE 0
+#define CONVERSION_ACCEPT 1
+#define CONVERSION_BANNED 2
+#define CONVERSION_MINDLESS 3
+#define CONVERSION_OVERCROWDED 4
+#define CONVERTIBLE_ALWAYS 1
+#define CONVERTIBLE_CHOICE 2
+#define CONVERTIBLE_NEVER 3
+#define CONVERTIBLE_NOMIND 4
+#define CONVERTIBLE_ALREADY 5
+#define CONVERTIBLE_IMPLANT 6
+#define DECONVERSION_ACCEPT 1
+#define DECONVERSION_REFUSE 2
+#define CULTIST_ROLE_NONE 0
+#define CULTIST_ROLE_ACOLYTE 1
+#define CULTIST_ROLE_HERALD 2
+#define CULTIST_ROLE_MENTOR 3
+
+#define DEVOTION_TIER_0 0
+#define DEVOTION_TIER_1 1
+#define DEVOTION_TIER_2 2
+#define DEVOTION_TIER_3 3
+#define DEVOTION_TIER_4 4
+
+#define RITUAL_CULTIST_1 "first_ritual"
+#define RITUAL_CULTIST_2 "second_ritual"
+
+#define RITUAL_FACTION_1 "first_ritual"
+#define RITUAL_FACTION_2 "second_ritual"
+#define RITUAL_FACTION_3 "third_ritual"
+
+#define HEX_MODE_ROAMING 0
+#define HEX_MODE_GUARD 1
+#define HEX_MODE_ESCORT 2
+
+#define HOSTILE_STANCE_IDLE 1
+#define HOSTILE_STANCE_ALERT 2
+#define HOSTILE_STANCE_ATTACK 3
+#define HOSTILE_STANCE_ATTACKING 4
+#define HOSTILE_STANCE_TIRED 5
+
+//Particles system defines
+#define PS_STEAM "Steam"
+#define PS_SMOKE "Smoke"
+#define PS_TEAR_REALITY "Tear Reality"
+#define PS_CANDLE "Candle"
+#define PS_CANDLE2 "Candle2"
+#define PS_CULT_GAUGE "Cult Gauge"
+#define PS_CULT_SMOKE "Cult Smoke"
+#define PS_CULT_SMOKE2 "Cult Smoke2"
+#define PS_CULT_SMOKE_BOX "Cult Smoke Box"
+#define PS_CULT_HALO "Cult Halo"
+#define PS_SPACE_RUNES "Space Runes"
+#define PS_NARSIEHASRISEN1 "Nar-SieHasRisen1"
+#define PS_NARSIEHASRISEN2 "Nar-SieHasRisen2"
+#define PS_NARSIEHASRISEN3 "Nar-SieHasRisen3"
+#define PS_ZAS_DUST "ZAS Dust"
+#define PS_DANDELIONS "Dandelions"
+#define PS_CROSS_DUST "Cross Dust"
+#define PS_CROSS_ORB "Cross Orb"
+#define PS_SACRED_FLAME "Sacred Flame"
+#define PS_SACRED_FLAME2 "Sacred Flame2"
+#define PS_BIBLE_PAGE "Bible Page"
+
+//Particles variable defines
+#define PVAR_SPAWNING "spawning"
+#define PVAR_POSITION "position"
+#define PVAR_VELOCITY "velocity"
+#define PVAR_ICON_STATE "icon_state"
+#define PVAR_COLOR "color"
+#define PVAR_SCALE "scale"
+#define PVAR_PLANE "plane"
+#define PVAR_LAYER "layer"
+#define PVAR_PIXEL_X "pixel_x"
+#define PVAR_PIXEL_Y "pixel_y"
+#define PVAR_LIFESPAN "lifespan"
+#define PVAR_FADE "fade"
+
+
+GLOBAL_LIST_INIT(particle_string_to_type, list(
+ PS_STEAM = /particles/steam,
+ PS_SMOKE = /particles/smoke,
+ PS_TEAR_REALITY = /particles/tear_reality,
+ PS_CANDLE = /particles/candle,
+ PS_CANDLE2 = /particles/candle_alt,
+ PS_CULT_GAUGE = /particles/cult_gauge,
+ PS_CULT_SMOKE = /particles/cult_smoke,
+ PS_CULT_SMOKE2 = /particles/cult_smoke/alt,
+ PS_CULT_SMOKE_BOX = /particles/cult_smoke/box,
+ PS_CULT_HALO = /particles/cult_halo,
+ PS_SPACE_RUNES = /particles/space_runes,
+ PS_NARSIEHASRISEN1 = /particles/narsie_has_risen,
+ PS_NARSIEHASRISEN2 = /particles/narsie_has_risen/next,
+ PS_NARSIEHASRISEN3 = /particles/narsie_has_risen/last,
+ PS_ZAS_DUST = /particles/zas_dust,
+ PS_DANDELIONS = /particles/dandelions,
+ PS_CROSS_DUST = /particles/cross_dust,
+ PS_CROSS_ORB = /particles/cross_orb,
+ PS_SACRED_FLAME = /particles/sacred_flame,
+ PS_SACRED_FLAME2 = /particles/sacred_flame/alt,
+ PS_BIBLE_PAGE = /particles/bible_page,
+ ))
+
+#define isholyweapon(I) (istype(I, /obj/item/book/bible)\
+ || istype(I, /obj/item/nullrod) \
+ || istype(I, /obj/projectile/boomerang))
+
+#define REVERT_ON_CONTROLLER_DAMAGED 1
+#define LOCK_EYE_TO_CONTROLLED 2
+#define LOCK_MOVEMENT_OF_CONTROLLER 4
+#define REQUIRES_CONTROL 8
+
+#define TRAIT_SEER "seer_trait"
+
+#define MODE_CULT "cult"
+#define MODE_TOKEN_CULT ":x"
+
+#define HOLOMAP_MARKER_TEARREALITY "tearreality"
+#define HOLOMAP_MARKER_BLOODSTONE "bloodstone"
+#define HOLOMAP_MARKER_CULT_ALTAR "altar"
+#define HOLOMAP_MARKER_CULT_FORGE "forge"
+#define HOLOMAP_MARKER_CULT_SPIRE "spire"
+#define HOLOMAP_MARKER_CULT_ENTRANCE "path_entrance"
+#define HOLOMAP_MARKER_CULT_EXIT "path_exit"
+#define HOLOMAP_MARKER_CULT_RUNE "rune"
+
+#define HOLOMAP_FILTER_CULT (1<<0)
+
+#define HOLOMAP_EXTRA_CULTMAP "cultmap"
diff --git a/code/__DEFINES/~monkestation/mind_ui.dm b/code/__DEFINES/~monkestation/mind_ui.dm
new file mode 100644
index 000000000000..c787e1b7d1fe
--- /dev/null
+++ b/code/__DEFINES/~monkestation/mind_ui.dm
@@ -0,0 +1,13 @@
+#define MIND_UI_BACK 10
+#define MIND_UI_BUTTON 11
+#define MIND_UI_FRONT 12
+
+#define MIND_UI_GROUP_A 0
+#define MIND_UI_GROUP_B 3
+#define MIND_UI_GROUP_C 6
+#define MIND_UI_GROUP_D 9
+
+#define MINDUI_FLAG_PROCESSING 1
+#define MINDUI_FLAG_TOOLTIP 2
+
+#define MINDUI_MAX_CULT_SLOTS 14
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index a10213b8eb99..a0139d0a9891 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -549,117 +549,6 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
icon_state = "blobbernaut_nofactory"
alerttooltipstyle = "blob"
-// BLOODCULT
-
-/atom/movable/screen/alert/bloodsense
- name = "Blood Sense"
- desc = "Allows you to sense blood that is manipulated by dark magicks."
- icon_state = "cult_sense"
- alerttooltipstyle = "cult"
- var/static/image/narnar
- var/angle = 0
- var/mob/living/basic/construct/Cviewer
-
-/atom/movable/screen/alert/bloodsense/Initialize(mapload)
- . = ..()
- narnar = new('icons/hud/screen_alert.dmi', "mini_nar")
- START_PROCESSING(SSprocessing, src)
-
-/atom/movable/screen/alert/bloodsense/Destroy()
- Cviewer = null
- STOP_PROCESSING(SSprocessing, src)
- return ..()
-
-/atom/movable/screen/alert/bloodsense/process()
- var/atom/blood_target
-
- if(!owner.mind)
- return
-
- var/datum/antagonist/cult/antag = owner.mind.has_antag_datum(/datum/antagonist/cult,TRUE)
- if(!antag)
- return
- var/datum/objective/sacrifice/sac_objective = locate() in antag.cult_team.objectives
-
- if(antag.cult_team.blood_target)
- if(!get_turf(antag.cult_team.blood_target))
- antag.cult_team.unset_blood_target()
- else
- blood_target = antag.cult_team.blood_target
- if(Cviewer?.seeking && Cviewer.master)
- blood_target = Cviewer.master
- desc = "Your blood sense is leading you to [Cviewer.master]"
- if(!blood_target)
- if(sac_objective && !sac_objective.check_completion())
- if(icon_state == "runed_sense0")
- return
- animate(src, transform = null, time = 1, loop = 0)
- angle = 0
- cut_overlays()
- icon_state = "runed_sense0"
- desc = "Nar'Sie demands that [sac_objective.target] be sacrificed before the summoning ritual can begin."
- add_overlay(sac_objective.sac_image)
- else
- var/datum/objective/eldergod/summon_objective = locate() in antag.cult_team.objectives
- if(!summon_objective)
- return
- var/list/location_list = list()
- for(var/area/area_to_check in summon_objective.summon_spots)
- location_list += area_to_check.get_original_area_name()
- desc = "The sacrifice is complete, summon Nar'Sie! The summoning can only take place in [english_list(location_list)]!"
- if(icon_state == "runed_sense1")
- return
- animate(src, transform = null, time = 1, loop = 0)
- angle = 0
- cut_overlays()
- icon_state = "runed_sense1"
- add_overlay(narnar)
- return
- var/turf/P = get_turf(blood_target)
- var/turf/Q = get_turf(owner)
- if(!P || !Q || (P.z != Q.z)) //The target is on a different Z level, we cannot sense that far.
- icon_state = "runed_sense2"
- desc = "You can no longer sense your target's presence."
- return
- if(isliving(blood_target))
- var/mob/living/real_target = blood_target
- desc = "You are currently tracking [real_target.real_name] in [get_area_name(blood_target)]."
- else
- desc = "You are currently tracking [blood_target] in [get_area_name(blood_target)]."
- var/target_angle = get_angle(Q, P)
- var/target_dist = get_dist(P, Q)
- cut_overlays()
- switch(target_dist)
- if(0 to 1)
- icon_state = "runed_sense2"
- if(2 to 8)
- icon_state = "arrow8"
- if(9 to 15)
- icon_state = "arrow7"
- if(16 to 22)
- icon_state = "arrow6"
- if(23 to 29)
- icon_state = "arrow5"
- if(30 to 36)
- icon_state = "arrow4"
- if(37 to 43)
- icon_state = "arrow3"
- if(44 to 50)
- icon_state = "arrow2"
- if(51 to 57)
- icon_state = "arrow1"
- if(58 to 64)
- icon_state = "arrow0"
- if(65 to 400)
- icon_state = "arrow"
- var/difference = target_angle - angle
- angle = target_angle
- if(!difference)
- return
- var/matrix/final = matrix(transform)
- final.Turn(difference)
- animate(src, transform = final, time = 5, loop = 0)
-
//GUARDIANS
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index b9bd8cee9bdf..823828077cd7 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -20,6 +20,16 @@
return screen
+/mob/proc/update_fullscreen_alpha(category, alpha = 255, time = 1 SECONDS)
+ var/atom/movable/screen/fullscreen/screen = screens[category]
+ if(!screen)
+ screens -= category
+ return
+ if (client)
+ client.screen -= screen
+ animate(screen, alpha = alpha, time = time)
+ client.screen += screen
+
/mob/proc/clear_fullscreen(category, animated = 10)
var/atom/movable/screen/fullscreen/screen = screens[category]
if(!screen)
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index 1ee09a05f3fb..aa7b6b8d8615 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -43,6 +43,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
var/atom/movable/screen/rest_icon
var/atom/movable/screen/throw_icon
var/atom/movable/screen/module_store_icon
+ var/atom/movable/screen/streamer_display
var/list/static_inventory = list() //the screen objects which are static
var/list/toggleable_inventory = list() //the screen objects which can be hidden
diff --git a/code/_onclick/hud/parallax.dm b/code/_onclick/hud/parallax.dm
index ef26ff7b5fff..6b5ae7d18156 100755
--- a/code/_onclick/hud/parallax.dm
+++ b/code/_onclick/hud/parallax.dm
@@ -16,6 +16,16 @@
C.parallax_layers_cached = list()
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_1(null, screenmob)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/stars(null, screenmob) //monkestation edit
+ if(GLOB.eclipse.eclipse_start_time)
+ var/view = C.view || world.view
+ C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/rifts(null, screenmob)
+ for(var/atom/movable/screen/parallax_layer/layer as anything in C.parallax_layers_cached)
+ if(!istype(layer, /atom/movable/screen/parallax_layer/layer_1))
+ continue
+ layer.remove_atom_colour(ADMIN_COLOUR_PRIORITY, GLOB.starlight_color)
+ layer.icon_state = "narsie"
+ layer.update_o(view)
+
/* monkestation removal
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_2(null, screenmob)
C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/planet(null, screenmob)
diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm
index a6c470f29673..712773fe548c 100644
--- a/code/_onclick/hud/radial.dm
+++ b/code/_onclick/hud/radial.dm
@@ -30,6 +30,8 @@ GLOBAL_LIST_EMPTY(radial_menus)
. = ..()
if(!QDELETED(parent))
icon_state = parent.radial_slice_icon
+ if(parent.radial_icon)
+ icon = parent.radial_icon
/atom/movable/screen/radial/slice/MouseEntered(location, control, params)
. = ..()
@@ -118,6 +120,7 @@ GLOBAL_LIST_EMPTY(radial_menus)
///A replacement icon state for the generic radial slice bg icon. Doesn't affect the next page nor the center buttons
var/radial_slice_icon
+ var/radial_icon
//If we swap to vis_contens inventory these will need a redo
/datum/radial_menu/proc/check_screen_border(mob/user)
@@ -358,7 +361,7 @@ GLOBAL_LIST_EMPTY(radial_menus)
Choices should be a list where list keys are movables or text used for element names and return value
and list values are movables/icons/images used for element icons
*/
-/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice", autopick_single_option = TRUE)
+/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice", autopick_single_option = TRUE, radial_icon)
if(!user || !anchor || !length(choices))
return
@@ -382,6 +385,8 @@ GLOBAL_LIST_EMPTY(radial_menus)
menu.custom_check_callback = custom_check
menu.anchor = anchor
menu.radial_slice_icon = radial_slice_icon
+ if(radial_icon)
+ menu.radial_icon = radial_icon
menu.check_screen_border(user) //Do what's needed to make it look good near borders or on hud
menu.set_choices(choices, tooltips)
menu.show_to(user)
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 13c02c7f9533..14a83fb5fc04 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -192,6 +192,8 @@
return
var/image/item_overlay = image(holding)
+ item_overlay.pixel_x = holding.base_pixel_x
+ item_overlay.pixel_y = holding.base_pixel_y
item_overlay.alpha = 92
if(!holding.mob_can_equip(user, slot_id, disable_warning = TRUE, bypass_equip_delay_self = TRUE))
diff --git a/code/datums/actions/items/cult_dagger.dm b/code/datums/actions/items/cult_dagger.dm
deleted file mode 100644
index 76e92c7b2319..000000000000
--- a/code/datums/actions/items/cult_dagger.dm
+++ /dev/null
@@ -1,39 +0,0 @@
-
-/datum/action/item_action/cult_dagger
- name = "Draw Blood Rune"
- desc = "Use the ritual dagger to create a powerful blood rune"
- button_icon = 'icons/mob/actions/actions_cult.dmi'
- button_icon_state = "draw"
- buttontooltipstyle = "cult"
- background_icon_state = "bg_demon"
- overlay_icon_state = "bg_demon_border"
-
- default_button_position = "6:157,4:-2"
-
-/datum/action/item_action/cult_dagger/Grant(mob/grant_to)
- if(!IS_CULTIST(grant_to))
- return
-
- return ..()
-
-/datum/action/item_action/cult_dagger/Trigger(trigger_flags)
- for(var/obj/item/held_item as anything in owner.held_items) // In case we were already holding a dagger
- if(istype(held_item, /obj/item/melee/cultblade/dagger))
- held_item.attack_self(owner)
- return
- var/obj/item/target_item = target
- if(owner.can_equip(target_item, ITEM_SLOT_HANDS))
- owner.temporarilyRemoveItemFromInventory(target_item)
- owner.put_in_hands(target_item)
- target_item.attack_self(owner)
- return
-
- if(!isliving(owner))
- to_chat(owner, span_warning("You lack the necessary living force for this action."))
- return
-
- var/mob/living/living_owner = owner
- if (living_owner.usable_hands <= 0)
- to_chat(living_owner, span_warning("You don't have any usable hands!"))
- else
- to_chat(living_owner, span_warning("Your hands are full!"))
diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm
index 8d2e9fc133e0..cede6862f263 100644
--- a/code/datums/components/cult_ritual_item.dm
+++ b/code/datums/components/cult_ritual_item.dm
@@ -20,7 +20,7 @@
/datum/component/cult_ritual_item/Initialize(
examine_message,
- action = /datum/action/item_action/cult_dagger,
+ action = null,
turfs_that_boost_us = /turf/open/floor/engine/cult,
)
@@ -45,7 +45,6 @@
return ..()
/datum/component/cult_ritual_item/RegisterWithParent()
- RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(try_scribe_rune))
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(try_purge_holywater))
RegisterSignal(parent, COMSIG_ITEM_ATTACK_OBJ, PROC_REF(try_hit_object))
RegisterSignal(parent, COMSIG_ITEM_ATTACK_EFFECT, PROC_REF(try_clear_rune))
@@ -75,27 +74,6 @@
examine_text += examine_message
-/*
- * Signal proc for [COMSIG_ITEM_ATTACK_SELF].
- * Allows the user to begin scribing runes.
- */
-/datum/component/cult_ritual_item/proc/try_scribe_rune(datum/source, mob/user)
- SIGNAL_HANDLER
-
- if(!isliving(user))
- return
-
- if(!can_scribe_rune(source, user))
- return
-
- if(drawing_a_rune)
- to_chat(user, span_warning("You are already drawing a rune."))
- return
-
- INVOKE_ASYNC(src, PROC_REF(start_scribe_rune), source, user)
-
- return COMPONENT_CANCEL_ATTACK_CHAIN
-
/*
* Signal proc for [COMSIG_ITEM_ATTACK].
* Allows for a cultist (user) to hit another cultist (target)
@@ -224,157 +202,6 @@
to_chat(cultist, span_notice("You carefully erase the [lowertext(rune.cultist_name)] rune."))
qdel(rune)
-/*
- * Wraps the entire act of [/proc/do_scribe_rune] to ensure it properly enables or disables [var/drawing_a_rune].)
- *
- * tool - the parent, source of the signal - the item inscribing the rune, casted to item.
- * cultist - the mob scribing the rune
- */
-/datum/component/cult_ritual_item/proc/start_scribe_rune(obj/item/tool, mob/living/cultist)
- drawing_a_rune = TRUE
- do_scribe_rune(tool, cultist)
- drawing_a_rune = FALSE
-
-/*
- * Actually give the user input to begin scribing a rune.
- * Creates the new instance of the rune if successful.
- *
- * tool - the parent, source of the signal - the item inscribing the rune, casted to item.
- * cultist - the mob scribing the rune
- */
-/datum/component/cult_ritual_item/proc/do_scribe_rune(obj/item/tool, mob/living/cultist)
- var/turf/our_turf = get_turf(cultist)
- var/obj/effect/rune/rune_to_scribe
- var/entered_rune_name
- var/chosen_keyword
-
- var/datum/antagonist/cult/user_antag = cultist.mind.has_antag_datum(/datum/antagonist/cult, TRUE)
- var/datum/team/cult/user_team = user_antag?.get_team()
- if(!user_antag || !user_team)
- stack_trace("[type] - [cultist] attempted to scribe a rune, but did not have an associated [user_antag ? "cult team":"cult antag datum"]!")
- return FALSE
-
- if(!LAZYLEN(GLOB.rune_types))
- to_chat(cultist, span_cult("There appears to be no runes to scribe. Contact your god about this!"))
- stack_trace("[type] - [cultist] attempted to scribe a rune, but the global rune list is empty!")
- return FALSE
-
- entered_rune_name = tgui_input_list(cultist, "Choose a rite to scribe", "Sigils of Power", GLOB.rune_types)
- if(isnull(entered_rune_name))
- return FALSE
- if(!can_scribe_rune(tool, cultist))
- return FALSE
-
- rune_to_scribe = GLOB.rune_types[entered_rune_name]
- if(!ispath(rune_to_scribe))
- stack_trace("[type] - [cultist] attempted to scribe a rune, but did not find a path from the global rune list!")
- return FALSE
-
- if(initial(rune_to_scribe.req_keyword))
- chosen_keyword = tgui_input_text(cultist, "Keyword for the new rune", "Words of Power", max_length = MAX_NAME_LEN)
- if(!chosen_keyword)
- drawing_a_rune = FALSE
- start_scribe_rune(tool, cultist)
- return FALSE
-
- our_turf = get_turf(cultist) //we may have moved. adjust as needed...
-
- if(!can_scribe_rune(tool, cultist))
- return FALSE
-
- if(ispath(rune_to_scribe, /obj/effect/rune/summon) && (!is_station_level(our_turf.z) || istype(get_area(cultist), /area/space)))
- to_chat(cultist, span_cultitalic("The veil is not weak enough here to summon a cultist, you must be on station!"))
- return
-
- if(ispath(rune_to_scribe, /obj/effect/rune/apocalypse))
- if((world.time - SSticker.round_start_time) <= 6000)
- var/wait = 6000 - (world.time - SSticker.round_start_time)
- to_chat(cultist, span_cultitalic("The veil is not yet weak enough for this rune - it will be available in [DisplayTimeText(wait)]."))
- return
- if(!check_if_in_ritual_site(cultist, user_team, TRUE))
- return
-
- if(ispath(rune_to_scribe, /obj/effect/rune/narsie))
- if(!scribe_narsie_rune(cultist, user_team))
- return
-
- cultist.visible_message(
- span_warning("[cultist] [cultist.blood_volume ? "cuts open [cultist.p_their()] arm and begins writing in [cultist.p_their()] own blood":"begins sketching out a strange design"]!"),
- span_cult("You [cultist.blood_volume ? "slice open your arm and ":""]begin drawing a sigil of the Geometer.")
- )
-
- if(!HAS_TRAIT(cultist, TRAIT_NOBLOOD)) // Monkestation Edit: BLOOD_DATUM
- cultist.apply_damage(initial(rune_to_scribe.scribe_damage), BRUTE, pick(GLOB.arm_zones), wound_bonus = CANT_WOUND) // *cuts arm* *bone explodes* ever have one of those days?
-
- var/scribe_mod = initial(rune_to_scribe.scribe_delay)
- if(!initial(rune_to_scribe.no_scribe_boost) && (our_turf.type in turfs_that_boost_us))
- scribe_mod *= 0.5
-
- SEND_SOUND(cultist, sound('sound/weapons/slice.ogg', 0, 1, 10))
- if(!do_after(cultist, scribe_mod, target = get_turf(cultist), timed_action_flags = IGNORE_SLOWDOWNS))
- cleanup_shields()
- return FALSE
- if(!can_scribe_rune(tool, cultist))
- cleanup_shields()
- return FALSE
-
- cultist.visible_message(
- span_warning("[cultist] creates a strange circle[cultist.blood_volume ? " in [cultist.p_their()] own blood":""]."),
- span_cult("You finish drawing the arcane markings of the Geometer.")
- )
-
- cleanup_shields()
- var/obj/effect/rune/made_rune = new rune_to_scribe(our_turf, chosen_keyword)
- made_rune.add_mob_blood(cultist)
-
- to_chat(cultist, span_cult("The [lowertext(made_rune.cultist_name)] rune [made_rune.cultist_desc]"))
- cultist.log_message("scribed \a [lowertext(made_rune.cultist_name)] rune using [parent] ([parent.type])", LOG_GAME)
- SSblackbox.record_feedback("tally", "cult_runes_scribed", 1, made_rune.cultist_name)
-
- return TRUE
-
-/*
- * The process of scribing the nar'sie rune.
- *
- * cultist - the mob placing the rune
- * cult_team - the team of the mob placing the rune
- */
-/datum/component/cult_ritual_item/proc/scribe_narsie_rune(mob/living/cultist, datum/team/cult/cult_team)
- var/datum/objective/eldergod/summon_objective = locate() in cult_team.objectives
- var/datum/objective/sacrifice/sac_objective = locate() in cult_team.objectives
- if(!check_if_in_ritual_site(cultist, cult_team))
- return FALSE
- if(sac_objective && !sac_objective.check_completion())
- to_chat(cultist, span_warning("The sacrifice is not complete. The portal would lack the power to open if you tried!"))
- return FALSE
- if(summon_objective.check_completion())
- to_chat(cultist, span_cultlarge("\"I am already here. There is no need to try to summon me now.\""))
- return FALSE
- var/confirm_final = tgui_alert(cultist, "This is the FINAL step to summon Nar'Sie; it is a long, painful ritual and the crew will be alerted to your presence.", "Are you prepared for the final battle?", list("My life for Nar'Sie!", "No"))
- if(confirm_final == "No")
- to_chat(cultist, span_cult("You decide to prepare further before scribing the rune."))
- return
- if(!check_if_in_ritual_site(cultist, cult_team))
- return FALSE
- var/area/summon_location = get_area(cultist)
- priority_announce(
- text = "Figments from an eldritch god are being summoned by [cultist.real_name] into [summon_location.get_original_area_name()] from an unknown dimension. Disrupt the ritual at all costs!",
- sound = 'sound/ambience/antag/bloodcult/bloodcult_scribe.ogg',
- sender_override = "[command_name()] Higher Dimensional Affairs",
- has_important_message = TRUE,
- )
- for(var/shielded_turf in spiral_range_turfs(1, cultist, 1))
- LAZYADD(shields, new /obj/structure/emergency_shield/cult/narsie(shielded_turf))
-
- notify_ghosts(
- "[cultist] has begun scribing a Nar'Sie rune!",
- source = cultist,
- action = NOTIFY_ORBIT,
- header = "Maranax Infirmux!",
- notify_flags = NOTIFY_CATEGORY_NOFLASH,
- )
-
- return TRUE
/*
* Helper to check if a rune can be scraped by a cultist.
@@ -447,30 +274,6 @@
return TRUE
-/*
- * Helper to check a cultist is located in one of the ritual / summoning sites.
- *
- * cultist - the mob making the rune
- * cult_team - the team of the mob making the rune
- * fail_if_last_site - whether the check fails if it's the last site in the summoning list.
- */
-/datum/component/cult_ritual_item/proc/check_if_in_ritual_site(mob/living/cultist, datum/team/cult/cult_team, fail_if_last_site = FALSE)
- var/datum/objective/eldergod/summon_objective = locate() in cult_team.objectives
- var/area/our_area = get_area(cultist)
- if(!summon_objective)
- to_chat(cultist, span_warning("There are no ritual sites on this station to scribe this rune!"))
- return FALSE
-
- if(!(our_area in summon_objective.summon_spots))
- to_chat(cultist, span_warning("This veil is not weak enough here - it can only be scribed in [english_list(summon_objective.summon_spots)]!"))
- return FALSE
-
- if(fail_if_last_site && length(summon_objective.summon_spots) <= 1)
- to_chat(cultist, span_warning("This rune cannot be scribed here - the ritual site must be reserved for the final summoning!"))
- return FALSE
-
- return TRUE
-
/*
* Removes all shields from the shields list.
*/
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index de83c8ae9741..ad6e7f9fd2d0 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -1470,6 +1470,8 @@
animate(src, pixel_x = pixel_x + pixel_x_diff, pixel_y = pixel_y + pixel_y_diff, transform=rotated_transform, time = 1, easing=BACK_EASING|EASE_IN, flags = ANIMATION_PARALLEL)
animate(pixel_x = pixel_x - pixel_x_diff, pixel_y = pixel_y - pixel_y_diff, transform=initial_transform, time = 2, easing=SINE_EASING, flags = ANIMATION_PARALLEL)
+ do_hitmarker(usr)
+
/atom/movable/vv_get_dropdown()
. = ..()
. += ""
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 54e622fb12d3..2f9e1d3072d2 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -8,11 +8,13 @@
circuit = /obj/item/circuitboard/computer/security
light_color = COLOR_SOFT_RED
- var/list/network = list("ss13")
+ var/clears_camera = FALSE
+ var/list/network = list("ss13", "SpessTV")
var/obj/machinery/camera/active_camera
/// The turf where the camera was last updated.
var/turf/last_camera_turf
var/list/concurrent_users = list()
+ var/ui_path = "CameraConsole"
// Stuff needed to render the map
var/atom/movable/screen/map_view/cam_screen
@@ -41,6 +43,11 @@
/obj/machinery/computer/security/Destroy()
QDEL_NULL(cam_screen)
QDEL_NULL(cam_background)
+ if(active_camera)
+ active_camera.on_deactive_camera(src)
+ active_camera = null
+ last_camera_turf = null
+
return ..()
/obj/machinery/computer/security/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
@@ -71,7 +78,7 @@
cam_screen.display_to(user)
user.client.register_map_obj(cam_background)
// Open UI
- ui = new(user, src, "CameraConsole", name)
+ ui = new(user, src, ui_path, name)
ui.open()
/obj/machinery/computer/security/ui_status(mob/user)
@@ -114,6 +121,7 @@
if(action == "switch_camera")
var/obj/machinery/camera/selected_camera = locate(params["camera"]) in GLOB.cameranet.cameras
active_camera = selected_camera
+ active_camera.on_active_camera(src)
playsound(src, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE)
if(isnull(active_camera))
@@ -123,6 +131,24 @@
return TRUE
+ switch(action)
+ if("follow")
+ var/obj/machinery/camera/spesstv/camera = active_camera
+ if(!istype(camera))
+ return
+ var/datum/antagonist/streamer/streamer_role = camera.streamer
+ if(!istype(streamer_role))
+ return
+ streamer_role.try_add_follower(usr.mind)
+ if("subscribe")
+ var/obj/machinery/camera/spesstv/camera = active_camera
+ if(!istype(camera))
+ return
+ var/datum/antagonist/streamer/streamer_role = camera.streamer
+ if(!istype(streamer_role))
+ return
+ streamer_role.try_add_subscription(usr.mind, src)
+
/obj/machinery/computer/security/proc/update_active_camera_screen()
// Show static if can't use the camera
if(!active_camera?.can_use())
@@ -167,7 +193,8 @@
// Unregister map objects
cam_screen.hide_from(user)
// Turn off the console
- if(length(concurrent_users) == 0 && is_living)
+ if(length(concurrent_users) == 0 && is_living && clears_camera)
+ active_camera.on_deactive_camera(src)
active_camera = null
last_camera_turf = null
playsound(src, 'sound/machines/terminal_off.ogg', 25, FALSE)
diff --git a/code/game/machinery/computer/telescreen.dm b/code/game/machinery/computer/telescreen.dm
index 49cbd4a1cf21..42f14f6ef8d4 100644
--- a/code/game/machinery/computer/telescreen.dm
+++ b/code/game/machinery/computer/telescreen.dm
@@ -37,7 +37,7 @@
desc = "Damn, they better have the /tg/ channel on these things."
icon = 'icons/obj/machines/status_display.dmi'
icon_state = "entertainment_blank"
- network = list()
+ network = list("SpessTV")
density = FALSE
circuit = null
interaction_flags_atom = INTERACT_ATOM_UI_INTERACT | INTERACT_ATOM_NO_FINGERPRINT_INTERACT | INTERACT_ATOM_NO_FINGERPRINT_ATTACK_HAND | INTERACT_MACHINE_REQUIRES_SIGHT
diff --git a/code/game/objects/effects/decals/cleanable/humans.dm b/code/game/objects/effects/decals/cleanable/humans.dm
index 0d8f4e26bb9d..26af4fa552c0 100644
--- a/code/game/objects/effects/decals/cleanable/humans.dm
+++ b/code/game/objects/effects/decals/cleanable/humans.dm
@@ -47,8 +47,15 @@
if(color && can_dry && !dried)
update_blood_drying_effect()
+ var/datum/team/cult/cult_team = locate_team(/datum/team/cult)
+ if(cult_team)
+ cult_team.add_bloody_floor(get_turf(src))
+
/obj/effect/decal/cleanable/blood/Destroy()
STOP_PROCESSING(SSblood_drying, src)
+ var/datum/team/cult/cult_team = locate_team(/datum/team/cult)
+ if(cult_team)
+ cult_team.remove_bloody_floor(get_turf(src))
return ..()
/obj/effect/decal/cleanable/blood/on_entered(datum/source, atom/movable/AM)
diff --git a/code/game/objects/effects/particle_holder.dm b/code/game/objects/effects/particle_holder.dm
index 9921e75ec8ab..5a76440f5cb4 100644
--- a/code/game/objects/effects/particle_holder.dm
+++ b/code/game/objects/effects/particle_holder.dm
@@ -8,19 +8,17 @@
/// Holds info about how this particle emitter works
/// See \code\__DEFINES\particles.dm
var/particle_flags = NONE
+ var/atom/main_holder
/obj/effect/abstract/particle_holder/Initialize(mapload, particle_path = /particles/smoke, particle_flags = NONE)
. = ..()
- if(!loc)
- stack_trace("particle holder was created with no loc!")
- return INITIALIZE_HINT_QDEL
// We assert this isn't an /area
src.particle_flags = particle_flags
particles = new particle_path
// /atom doesn't have vis_contents, /turf and /atom/movable do
var/atom/movable/lie_about_areas = loc
- lie_about_areas.vis_contents += src
+ lie_about_areas?.vis_contents += src
if(!ismovable(loc))
RegisterSignal(loc, COMSIG_QDELETING, PROC_REF(immovable_deleted))
@@ -30,6 +28,7 @@
/obj/effect/abstract/particle_holder/Destroy(force)
QDEL_NULL(particles)
+ main_holder = null
return ..()
/// Non movables don't delete contents on destroy, so we gotta do this
diff --git a/code/game/objects/items/knives.dm b/code/game/objects/items/knives.dm
index c8eccd25407b..38be68cb85d7 100644
--- a/code/game/objects/items/knives.dm
+++ b/code/game/objects/items/knives.dm
@@ -50,19 +50,6 @@
span_suicide("[user] is slitting [user.p_their()] stomach open with the [src.name]! It looks like [user.p_theyre()] trying to commit seppuku.")))
return BRUTELOSS
-/obj/item/knife/ritual
- name = "ritual knife"
- desc = "The unearthly energies that once powered this blade are now dormant."
- icon = 'icons/obj/eldritch.dmi'
- icon_state = "bone_blade"
- inhand_icon_state = "bone_blade"
- worn_icon_state = "bone_blade"
- lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/64x64_righthand.dmi'
- inhand_x_dimension = 64
- inhand_y_dimension = 64
- w_class = WEIGHT_CLASS_NORMAL
-
/obj/item/knife/bloodletter
name = "bloodletter"
desc = "An occult looking dagger that is cold to the touch. Somehow, the flawless orb on the pommel is made entirely of liquid blood."
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 4386545f07d3..a2fd7b47c796 100755
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -289,6 +289,29 @@ GLOBAL_LIST_EMPTY(station_turfs)
return TRUE
return FALSE
+/turf/proc/check_blocking_content(exclude_mobs = FALSE, source_atom = null, list/ignore_atoms, type_list = FALSE)
+ if(density)
+ return src
+
+ for(var/atom/movable/movable_content as anything in contents)
+ // We don't want to block ourselves
+ if((movable_content == source_atom))
+ continue
+ // dont consider ignored atoms or their types
+ if(length(ignore_atoms))
+ if(!type_list && (movable_content in ignore_atoms))
+ continue
+ else if(type_list && is_type_in_list(movable_content, ignore_atoms))
+ continue
+
+ // If the thing is dense AND we're including mobs or the thing isn't a mob AND if there's a source atom and
+ // it cannot pass through the thing on the turf, we consider the turf blocked.
+ if(movable_content.density && (!exclude_mobs || !ismob(movable_content)))
+ if(source_atom && movable_content.CanPass(source_atom, get_dir(src, source_atom)))
+ continue
+ return movable_content
+ return null
+
/**
* Checks whether the specified turf is blocked by something dense inside it, but ignores anything with the climbable trait
*
diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm
deleted file mode 100644
index c7bb4e2a820f..000000000000
--- a/code/modules/antagonists/cult/cult.dm
+++ /dev/null
@@ -1,604 +0,0 @@
-#define SUMMON_POSSIBILITIES 3
-#define CULT_VICTORY 1
-#define CULT_LOSS 0
-#define CULT_NARSIE_KILLED -1
-
-/datum/antagonist/cult
- name = "Cultist"
- roundend_category = "cultists"
- antagpanel_category = "Cult"
- antag_moodlet = /datum/mood_event/cult
- suicide_cry = "FOR NAR'SIE!!"
- preview_outfit = /datum/outfit/cultist
- var/datum/action/innate/cult/comm/communion = new
- var/datum/action/innate/cult/mastervote/vote = new
- var/datum/action/innate/cult/blood_magic/magic = new
- job_rank = ROLE_CULTIST
- antag_hud_name = "cult"
- var/ignore_implant = FALSE
- var/give_equipment = FALSE
- var/datum/team/cult/cult_team
-
-
-/datum/antagonist/cult/get_team()
- return cult_team
-
-/datum/antagonist/cult/create_team(datum/team/cult/new_team)
- if(!new_team)
- //todo remove this and allow admin buttons to create more than one cult
- for(var/datum/antagonist/cult/H in GLOB.antagonists)
- if(!H.owner)
- continue
- if(H.cult_team)
- cult_team = H.cult_team
- return
- cult_team = new /datum/team/cult
- cult_team.setup_objectives()
- return
- if(!istype(new_team))
- stack_trace("Wrong team type passed to [type] initialization.")
- cult_team = new_team
-
-/datum/antagonist/cult/proc/add_objectives()
- objectives |= cult_team.objectives
-
-/datum/antagonist/cult/Destroy()
- QDEL_NULL(communion)
- QDEL_NULL(vote)
- return ..()
-
-/datum/antagonist/cult/can_be_owned(datum/mind/new_owner)
- . = ..()
- if(. && !ignore_implant)
- . = is_convertable_to_cult(new_owner.current,cult_team)
-
-/datum/antagonist/cult/greet()
- . = ..()
- owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/bloodcult/bloodcult_gain.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)//subject to change
- owner.announce_objectives()
-
-/datum/antagonist/cult/on_gain()
- add_objectives()
- . = ..()
- var/mob/living/current = owner.current
- if(give_equipment)
- equip_cultist(TRUE)
- current.log_message("has been converted to the cult of Nar'Sie!", LOG_ATTACK, color="#960000")
-
- if(cult_team.blood_target && cult_team.blood_target_image && current.client)
- current.client.images += cult_team.blood_target_image
-
- ADD_TRAIT(current, TRAIT_HEALS_FROM_CULT_PYLONS, CULT_TRAIT)
-
-/datum/antagonist/cult/on_removal()
- REMOVE_TRAIT(owner.current, TRAIT_HEALS_FROM_CULT_PYLONS, CULT_TRAIT)
- if(!silent)
- owner.current.visible_message(span_deconversion_message("[owner.current] looks like [owner.current.p_theyve()] just reverted to [owner.current.p_their()] old faith!"), ignored_mobs = owner.current)
- to_chat(owner.current, span_userdanger("An unfamiliar white light flashes through your mind, cleansing the taint of the Geometer and all your memories as her servant."))
- owner.current.log_message("has renounced the cult of Nar'Sie!", LOG_ATTACK, color="#960000")
- if(cult_team.blood_target && cult_team.blood_target_image && owner.current.client)
- owner.current.client.images -= cult_team.blood_target_image
-
- return ..()
-
-/datum/antagonist/cult/get_preview_icon()
- var/icon/icon = render_preview_outfit(preview_outfit)
-
- // The longsword is 64x64, but getFlatIcon crunches to 32x32.
- // So I'm just going to add it in post, screw it.
-
- // Center the dude, because item icon states start from the center.
- // This makes the image 64x64.
- icon.Crop(-15, -15, 48, 48)
-
- var/obj/item/melee/cultblade/longsword = new
- icon.Blend(icon(longsword.lefthand_file, longsword.inhand_icon_state), ICON_OVERLAY)
- qdel(longsword)
-
- // Move the guy back to the bottom left, 32x32.
- icon.Crop(17, 17, 48, 48)
-
- return finish_preview_icon(icon)
-
-/datum/antagonist/cult/proc/equip_cultist(metal=TRUE)
- var/mob/living/carbon/H = owner.current
- if(!istype(H))
- return
- . += cult_give_item(/obj/item/melee/cultblade/dagger, H)
- if(metal)
- . += cult_give_item(/obj/item/stack/sheet/runed_metal/ten, H)
- to_chat(owner, "These will help you start the cult on this station. Use them well, and remember - you are not the only one.")
-
-///Attempts to make a new item and put it in a potential inventory slot in the provided mob.
-/datum/antagonist/cult/proc/cult_give_item(obj/item/item_path, mob/living/carbon/human/mob)
- var/item = new item_path(mob)
- var/where = mob.equip_conspicuous_item(item)
- if(!where)
- to_chat(mob, span_userdanger("Unfortunately, you weren't able to get [item]. This is very bad and you should adminhelp immediately (press F1)."))
- return FALSE
- else
- to_chat(mob, span_danger("You have [item] in your [where]."))
- if(where == "backpack")
- mob.back.atom_storage?.show_contents(mob)
- return TRUE
-
-/datum/antagonist/cult/apply_innate_effects(mob/living/mob_override)
- . = ..()
- var/mob/living/current = owner.current
- if(mob_override)
- current = mob_override
- handle_clown_mutation(current, mob_override ? null : "Your training has allowed you to overcome your clownish nature, allowing you to wield weapons without harming yourself.")
- current.faction |= FACTION_CULT
- current.grant_language(/datum/language/narsie, TRUE, TRUE, LANGUAGE_CULTIST)
- if(!cult_team.cult_master)
- vote.Grant(current)
- communion.Grant(current)
- if(ishuman(current))
- magic.Grant(current)
- current.throw_alert("bloodsense", /atom/movable/screen/alert/bloodsense)
- if(cult_team.cult_risen)
- current.AddElement(/datum/element/cult_eyes, initial_delay = 0 SECONDS)
- if(cult_team.cult_ascendent)
- current.AddElement(/datum/element/cult_halo, initial_delay = 0 SECONDS)
-
- add_team_hud(current)
-
-/datum/antagonist/cult/remove_innate_effects(mob/living/mob_override)
- . = ..()
- var/mob/living/current = owner.current
- if(mob_override)
- current = mob_override
- handle_clown_mutation(current, removing = FALSE)
- current.faction -= FACTION_CULT
- current.remove_language(/datum/language/narsie, TRUE, TRUE, LANGUAGE_CULTIST)
- vote.Remove(current)
- communion.Remove(current)
- magic.Remove(current)
- current.clear_alert("bloodsense")
- if (HAS_TRAIT(current, TRAIT_UNNATURAL_RED_GLOWY_EYES))
- current.RemoveElement(/datum/element/cult_eyes)
- if (HAS_TRAIT(current, TRAIT_CULT_HALO))
- current.RemoveElement(/datum/element/cult_halo)
-
-/datum/antagonist/cult/on_mindshield(mob/implanter)
- if(!silent)
- to_chat(owner.current, span_warning("You feel something interfering with your mental conditioning, but you resist it!"))
- return
-
-/datum/antagonist/cult/admin_add(datum/mind/new_owner,mob/admin)
- give_equipment = FALSE
- new_owner.add_antag_datum(src)
- message_admins("[key_name_admin(admin)] has cult-ed [key_name_admin(new_owner)].")
- log_admin("[key_name(admin)] has cult-ed [key_name(new_owner)].")
-
-/datum/antagonist/cult/admin_remove(mob/user)
- silent = TRUE
- return ..()
-
-/datum/antagonist/cult/get_admin_commands()
- . = ..()
- .["Dagger"] = CALLBACK(src, PROC_REF(admin_give_dagger))
- .["Dagger and Metal"] = CALLBACK(src, PROC_REF(admin_give_metal))
- .["Remove Dagger and Metal"] = CALLBACK(src, PROC_REF(admin_take_all))
-
-/datum/antagonist/cult/proc/admin_give_dagger(mob/admin)
- if(!equip_cultist(metal=FALSE))
- to_chat(admin, span_danger("Spawning dagger failed!"))
-
-/datum/antagonist/cult/proc/admin_give_metal(mob/admin)
- if (!equip_cultist(metal=TRUE))
- to_chat(admin, span_danger("Spawning runed metal failed!"))
-
-/datum/antagonist/cult/proc/admin_take_all(mob/admin)
- var/mob/living/current = owner.current
- for(var/o in current.get_all_contents())
- if(istype(o, /obj/item/melee/cultblade/dagger) || istype(o, /obj/item/stack/sheet/runed_metal))
- qdel(o)
-
-/datum/antagonist/cult/master
- ignore_implant = TRUE
- show_in_antagpanel = FALSE //Feel free to add this later
- antag_hud_name = "cultmaster"
- var/datum/action/innate/cult/master/finalreck/reckoning = new
- var/datum/action/innate/cult/master/cultmark/bloodmark = new
- var/datum/action/innate/cult/master/pulse/throwing = new
-
-/datum/antagonist/cult/master/Destroy()
- QDEL_NULL(reckoning)
- QDEL_NULL(bloodmark)
- QDEL_NULL(throwing)
- return ..()
-
-/datum/antagonist/cult/master/greet()
- to_chat(owner.current, "You are the cult's Master. As the cult's Master, you have a unique title and loud voice when communicating, are capable of marking \
- targets, such as a location or a noncultist, to direct the cult to them, and, finally, you are capable of summoning the entire living cult to your location once. Use these abilities to direct the cult to victory at any cost.")
-
-/datum/antagonist/cult/master/apply_innate_effects(mob/living/mob_override)
- . = ..()
- var/mob/living/current = owner.current
- if(mob_override)
- current = mob_override
- if(!cult_team.reckoning_complete)
- reckoning.Grant(current)
- bloodmark.Grant(current)
- throwing.Grant(current)
- current.update_mob_action_buttons()
- current.apply_status_effect(/datum/status_effect/cult_master)
- if(cult_team.cult_risen)
- current.AddElement(/datum/element/cult_eyes, initial_delay = 0 SECONDS)
- if(cult_team.cult_ascendent)
- current.AddElement(/datum/element/cult_halo, initial_delay = 0 SECONDS)
- add_team_hud(current, /datum/antagonist/cult)
-
-/datum/antagonist/cult/master/remove_innate_effects(mob/living/mob_override)
- . = ..()
- var/mob/living/current = owner.current
- if(mob_override)
- current = mob_override
- reckoning.Remove(current)
- bloodmark.Remove(current)
- throwing.Remove(current)
- current.update_mob_action_buttons()
- current.remove_status_effect(/datum/status_effect/cult_master)
-
-/datum/team/cult
- name = "\improper Cult"
-
- ///The blood mark target
- var/atom/blood_target
- ///Image of the blood mark target
- var/image/blood_target_image
- ///Timer for the blood mark expiration
- var/blood_target_reset_timer
-
- ///Has a vote been called for a leader?
- var/cult_vote_called = FALSE
- ///The cult leader
- var/mob/living/cult_master
- ///Has the mass teleport been used yet?
- var/reckoning_complete = FALSE
- ///Has the cult risen, and gotten red eyes?
- var/cult_risen = FALSE
- ///Has the cult asceneded, and gotten halos?
- var/cult_ascendent = FALSE
-
- ///Has narsie been summoned yet?
- var/narsie_summoned = FALSE
- ///How large were we at max size.
- var/size_at_maximum = 0
- ///list of cultists just before summoning Narsie
- var/list/true_cultists = list()
-
-/datum/team/cult/proc/check_size()
- if(cult_ascendent)
- return
-
-#ifdef UNIT_TESTS
- // This proc is unnecessary clutter whilst running cult related unit tests
- // Remove this if, at some point, someone decides to test that halos and eyes are added at expected ratios
- return
-#endif
-
- var/alive = 0
- var/cultplayers = 0
- for(var/I in GLOB.player_list)
- var/mob/M = I
- if(M.stat != DEAD)
- if(IS_CULTIST(M))
- ++cultplayers
- else
- ++alive
-
- ASSERT(cultplayers) //we shouldn't be here.
- var/ratio = alive ? cultplayers/alive : 1
- if(ratio > CULT_RISEN && !cult_risen)
- for(var/datum/mind/mind as anything in members)
- if(mind.current)
- SEND_SOUND(mind.current, 'sound/ambience/antag/bloodcult/bloodcult_eyes.ogg')
- to_chat(mind.current, span_cultlarge(span_warning("The veil weakens as your cult grows, your eyes begin to glow...")))
- mind.current.AddElement(/datum/element/cult_eyes)
- cult_risen = TRUE
- log_game("The blood cult has risen with [cultplayers] players.")
-
- if(ratio > CULT_ASCENDENT && !cult_ascendent)
- for(var/datum/mind/mind as anything in members)
- if(mind.current)
- SEND_SOUND(mind.current, 'sound/ambience/antag/bloodcult/bloodcult_halos.ogg')
- to_chat(mind.current, span_cultlarge(span_warning("Your cult is ascendent and the red harvest approaches - you cannot hide your true nature for much longer!!")))
- mind.current.AddElement(/datum/element/cult_halo)
- cult_ascendent = TRUE
- log_game("The blood cult has ascended with [cultplayers] players.")
-
-/datum/team/cult/add_member(datum/mind/new_member)
- . = ..()
- // A little hacky, but this checks that cult ghosts don't contribute to the size at maximum value.
- if(is_unassigned_job(new_member.assigned_role))
- return
- size_at_maximum++
-
-/datum/team/cult/proc/make_image(datum/objective/sacrifice/sac_objective)
- var/datum/job/job_of_sacrifice = sac_objective.target.assigned_role
- var/datum/preferences/prefs_of_sacrifice = sac_objective.target.current.client.prefs
- var/icon/reshape = get_flat_human_icon(null, job_of_sacrifice, prefs_of_sacrifice, list(SOUTH))
- reshape.Shift(SOUTH, 4)
- reshape.Shift(EAST, 1)
- reshape.Crop(7,4,26,31)
- reshape.Crop(-5,-3,26,30)
- sac_objective.sac_image = reshape
-
-/datum/team/cult/proc/setup_objectives()
- var/datum/objective/sacrifice/sacrifice_objective = new
- sacrifice_objective.team = src
- sacrifice_objective.find_target()
- objectives += sacrifice_objective
-
- var/datum/objective/eldergod/summon_objective = new
- summon_objective.team = src
- objectives += summon_objective
-
-/datum/objective/sacrifice
- var/sacced = FALSE
- var/sac_image
-
-/// Unregister signals from the old target so it doesn't cause issues when sacrificed of when a new target is found.
-/datum/objective/sacrifice/proc/clear_sacrifice()
- if(!target)
- return
- UnregisterSignal(target, COMSIG_MIND_TRANSFERRED)
- if(target.current)
- UnregisterSignal(target.current, list(COMSIG_QDELETING, COMSIG_MOB_MIND_TRANSFERRED_INTO))
- target = null
-
-/datum/objective/sacrifice/find_target(dupe_search_range, list/blacklist)
- clear_sacrifice()
- if(!istype(team, /datum/team/cult))
- return
- var/datum/team/cult/cult = team
- var/list/target_candidates = list()
- var/opt_in_disabled = CONFIG_GET(flag/disable_antag_opt_in_preferences)
- for(var/mob/living/carbon/human/player in GLOB.player_list)
- if (!opt_in_disabled && !opt_in_valid(player))
- continue
- if(player.mind && !player.mind.has_antag_datum(/datum/antagonist/cult) && !is_convertable_to_cult(player) && player.stat != DEAD)
- target_candidates += player.mind
-
- if(target_candidates.len == 0)
- message_admins("Cult Sacrifice: Could not find unconvertible target, checking for convertible target, this could be because NO ONE was set to Round Remove forcibly picking target.")
- for(var/mob/living/carbon/human/player in GLOB.player_list)
- if(player.mind && !player.mind.has_antag_datum(/datum/antagonist/cult) && player.stat != DEAD)
- target_candidates += player.mind
- list_clear_nulls(target_candidates)
- if(LAZYLEN(target_candidates))
- target = pick(target_candidates)
- update_explanation_text()
- // Register a bunch of signals to both the target mind and its body
- // to stop cult from softlocking everytime the target is deleted before being actually sacrificed.
- RegisterSignal(target, COMSIG_MIND_TRANSFERRED, PROC_REF(on_mind_transfer))
- RegisterSignal(target.current, COMSIG_QDELETING, PROC_REF(on_target_body_del))
- RegisterSignal(target.current, COMSIG_MOB_MIND_TRANSFERRED_INTO, PROC_REF(on_possible_mindswap))
- else
- message_admins("Cult Sacrifice: Could not find unconvertible or convertible target. WELP!")
- sacced = TRUE // Prevents another hypothetical softlock. This basically means every PC is a cultist.
- if(!sacced)
- cult.make_image(src)
- for(var/datum/mind/mind in cult.members)
- if(mind.current)
- mind.current.clear_alert("bloodsense")
- mind.current.throw_alert("bloodsense", /atom/movable/screen/alert/bloodsense)
-
-/datum/objective/sacrifice/proc/on_target_body_del()
- SIGNAL_HANDLER
- INVOKE_ASYNC(src, PROC_REF(find_target))
-
-/datum/objective/sacrifice/proc/on_mind_transfer(datum/source, mob/previous_body)
- SIGNAL_HANDLER
- //If, for some reason, the mind was transferred to a ghost (better safe than sorry), find a new target.
- if(!isliving(target.current))
- INVOKE_ASYNC(src, PROC_REF(find_target))
- return
- UnregisterSignal(previous_body, list(COMSIG_QDELETING, COMSIG_MOB_MIND_TRANSFERRED_INTO))
- RegisterSignal(target.current, COMSIG_QDELETING, PROC_REF(on_target_body_del))
- RegisterSignal(target.current, COMSIG_MOB_MIND_TRANSFERRED_INTO, PROC_REF(on_possible_mindswap))
-
-/datum/objective/sacrifice/proc/on_possible_mindswap(mob/source)
- SIGNAL_HANDLER
- UnregisterSignal(target.current, list(COMSIG_QDELETING, COMSIG_MOB_MIND_TRANSFERRED_INTO))
- //we check if the mind is bodyless only after mindswap shenanigeans to avoid issues.
- addtimer(CALLBACK(src, PROC_REF(do_we_have_a_body)), 0 SECONDS)
-
-/datum/objective/sacrifice/proc/do_we_have_a_body()
- if(!target.current) //The player was ghosted and the mind isn't probably going to be transferred to another mob at this point.
- find_target()
- return
- RegisterSignal(target.current, COMSIG_QDELETING, PROC_REF(on_target_body_del))
- RegisterSignal(target.current, COMSIG_MOB_MIND_TRANSFERRED_INTO, PROC_REF(on_possible_mindswap))
-
-/datum/objective/sacrifice/check_completion()
- return sacced || completed
-
-/datum/objective/sacrifice/update_explanation_text()
- if(target)
- explanation_text = "Sacrifice [target], the [target.assigned_role.title] via invoking an Offer rune with [target.p_them()] on it and three acolytes around it."
- else
- explanation_text = "The veil has already been weakened here, proceed to the final objective."
-
-/datum/objective/eldergod
- var/summoned = FALSE
- var/killed = FALSE
- var/list/summon_spots = list()
-
-/datum/objective/eldergod/New()
- ..()
- var/sanity = 0
- while(summon_spots.len < SUMMON_POSSIBILITIES && sanity < 100)
- var/area/summon_area = pick(GLOB.areas - summon_spots)
- if(summon_area && is_station_level(summon_area.z) && (summon_area.area_flags & VALID_TERRITORY))
- summon_spots += summon_area
- sanity++
- update_explanation_text()
-
-/datum/objective/eldergod/update_explanation_text()
- explanation_text = "Summon Nar'Sie by invoking the rune 'Summon Nar'Sie'. The summoning can only be accomplished in [english_list(summon_spots)] - where the veil is weak enough for the ritual to begin."
-
-/datum/objective/eldergod/check_completion()
- if(killed)
- return CULT_NARSIE_KILLED // You failed so hard that even the code went backwards.
- return summoned || completed
-
-/datum/team/cult/proc/check_cult_victory()
- for(var/datum/objective/O in objectives)
- if(O.check_completion() == CULT_NARSIE_KILLED)
- return CULT_NARSIE_KILLED
- else if(!O.check_completion())
- return CULT_LOSS
- return CULT_VICTORY
-
-/datum/team/cult/roundend_report()
- var/list/parts = list()
- var/victory = check_cult_victory()
-
- if(victory == CULT_NARSIE_KILLED) // Epic failure, you summoned your god and then someone killed it.
- parts += "Nar'sie has been killed! The cult will haunt the universe no longer!"
- else if(victory)
- parts += "The cult has succeeded! Nar'Sie has snuffed out another torch in the void!"
- else
- parts += "The staff managed to stop the cult! Dark words and heresy are no match for Nanotrasen's finest!"
-
- if(objectives.len)
- parts += "The cultists' objectives were:"
- var/count = 1
- for(var/datum/objective/objective in objectives)
- if(objective.check_completion())
- parts += "Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]"
- else
- parts += "Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]"
- count++
-
- if(members.len)
- parts += "The cultists were:"
- if(length(true_cultists))
- parts += printplayerlist(true_cultists)
- else
- parts += printplayerlist(members)
-
- return "
[parts.Join(" ")]
"
-
-/datum/team/cult/proc/is_sacrifice_target(datum/mind/mind)
- for(var/datum/objective/sacrifice/sac_objective in objectives)
- if(mind == sac_objective.target)
- return TRUE
- return FALSE
-
-/// Returns whether the given mob is convertable to the blood cult, monkestation edit: or clock cult
-/proc/is_convertable_to_cult(mob/living/target, datum/team/cult/specific_cult, for_clock_cult) //monkestation edit: adds for_clock_cult
- if(!istype(target))
- return FALSE
- if(isnull(target.mind) || !GET_CLIENT(target))
- return FALSE
- if(HAS_MIND_TRAIT(target, TRAIT_UNCONVERTABLE)) // monkestation edit: mind.unconvertable -> TRAIT_UNCONVERTABLE
- return FALSE
- if(ishuman(target) && target.mind.holy_role)
- return FALSE
- if(specific_cult?.is_sacrifice_target(target.mind))
- return FALSE
- var/mob/living/master = target.mind.enslaved_to?.resolve()
- if(master && (for_clock_cult ? !IS_CLOCK(master) : !IS_CULTIST(master))) //monkestation edit: master is now checked based off of for_clock_cult
- return FALSE
- if(IS_HERETIC_OR_MONSTER(target))
- return FALSE
- if(HAS_TRAIT(target, TRAIT_MINDSHIELD) || isbot(target)) //monkestation edit: moved isdrone() as well as issilicon() to the next check down
- return FALSE //can't convert machines, shielded, or braindead
- if((isdrone(target) || issilicon(target)) && !for_clock_cult) //monkestation edit: clock cult converts them into cogscarabs and clock borgs
- return FALSE //monkestation edit
- if(for_clock_cult ? IS_CULTIST(target) : IS_CLOCK(target)) //monkestation edit
- return FALSE //monkestation edit
- return TRUE
-
-/// Sets a blood target for the cult.
-/datum/team/cult/proc/set_blood_target(atom/new_target, mob/marker, duration = 90 SECONDS)
- if(QDELETED(new_target))
- CRASH("A null or invalid target was passed to set_blood_target.")
-
- if(duration != INFINITY && blood_target_reset_timer)
- return FALSE
-
- deltimer(blood_target_reset_timer)
- blood_target = new_target
- RegisterSignal(blood_target, COMSIG_QDELETING, PROC_REF(unset_blood_target_and_timer))
- var/area/target_area = get_area(new_target)
-
- blood_target_image = image('icons/effects/mouse_pointers/cult_target.dmi', new_target, "glow", ABOVE_MOB_LAYER)
- blood_target_image.appearance_flags = RESET_COLOR
- blood_target_image.pixel_x = -new_target.pixel_x
- blood_target_image.pixel_y = -new_target.pixel_y
-
- for(var/datum/mind/cultist as anything in members)
- if(!cultist.current)
- continue
- if(cultist.current.stat == DEAD || !cultist.current.client)
- continue
-
- to_chat(cultist.current, span_bold(span_cultlarge("[marker] has marked [blood_target] in the [target_area.name] as the cult's top priority, get there immediately!")))
- SEND_SOUND(cultist.current, sound(pick('sound/hallucinations/over_here2.ogg','sound/hallucinations/over_here3.ogg'), 0, 1, 75))
- cultist.current.client.images += blood_target_image
-
- if(duration != INFINITY)
- blood_target_reset_timer = addtimer(CALLBACK(src, PROC_REF(unset_blood_target)), duration, TIMER_STOPPABLE)
- return TRUE
-
-/// Unsets out blood target, clearing the images from all the cultists.
-/datum/team/cult/proc/unset_blood_target()
- blood_target_reset_timer = null
-
- for(var/datum/mind/cultist as anything in members)
- if(!cultist.current)
- continue
- if(cultist.current.stat == DEAD || !cultist.current.client)
- continue
-
- if(QDELETED(blood_target))
- to_chat(cultist.current, span_bold(span_cultlarge("The blood mark's target is lost!")))
- else
- to_chat(cultist.current, span_bold(span_cultlarge("The blood mark has expired!")))
- cultist.current.client.images -= blood_target_image
-
- UnregisterSignal(blood_target, COMSIG_QDELETING)
- blood_target = null
-
- QDEL_NULL(blood_target_image)
-
-/// Unsets our blood target when they get deleted.
-/datum/team/cult/proc/unset_blood_target_and_timer(datum/source)
- SIGNAL_HANDLER
-
- deltimer(blood_target_reset_timer)
- unset_blood_target()
-
-/datum/outfit/cultist
- name = "Cultist (Preview only)"
-
- uniform = /obj/item/clothing/under/color/black
- suit = /obj/item/clothing/suit/hooded/cultrobes/alt
- shoes = /obj/item/clothing/shoes/cult/alt
- r_hand = /obj/item/melee/blood_magic/stun
-
-/datum/outfit/cultist/post_equip(mob/living/carbon/human/equipped, visualsOnly)
- equipped.eye_color_left = BLOODCULT_EYE
- equipped.eye_color_right = BLOODCULT_EYE
- equipped.update_body()
-
-#undef CULT_LOSS
-#undef CULT_NARSIE_KILLED
-#undef CULT_VICTORY
-#undef SUMMON_POSSIBILITIES
-
-/datum/antagonist/cult/antag_token(datum/mind/hosts_mind, mob/spender)
- var/datum/antagonist/cult/new_cultist = new
- new_cultist.cult_team = get_team()
- new_cultist.give_equipment = TRUE
- if(isobserver(spender))
- var/mob/living/carbon/human/new_mob = spender.change_mob_type( /mob/living/carbon/human, delete_old_mob = TRUE)
- new_mob.equipOutfit(/datum/outfit/job/assistant)
- new_mob.mind.add_antag_datum(new_cultist)
- else
- hosts_mind.add_antag_datum(new_cultist)
diff --git a/code/modules/antagonists/cult/cult_comms.dm b/code/modules/antagonists/cult/cult_comms.dm
deleted file mode 100644
index 19716589e1af..000000000000
--- a/code/modules/antagonists/cult/cult_comms.dm
+++ /dev/null
@@ -1,492 +0,0 @@
-// Contains cult communion, guide, and cult master abilities
-
-/datum/action/innate/cult
- button_icon = 'icons/mob/actions/actions_cult.dmi'
- background_icon_state = "bg_demon"
- overlay_icon_state = "bg_demon_border"
-
- buttontooltipstyle = "cult"
- check_flags = AB_CHECK_INCAPACITATED|AB_CHECK_HANDS_BLOCKED|AB_CHECK_IMMOBILE|AB_CHECK_CONSCIOUS
- ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi'
-
-/datum/action/innate/cult/IsAvailable(feedback = FALSE)
- if(!IS_CULTIST(owner))
- return FALSE
- return ..()
-
-/datum/action/innate/cult/comm
- name = "Communion"
- desc = "Whispered words that all cultists can hear. Warning:Nearby non-cultists can still hear you."
- button_icon_state = "cult_comms"
-
-/datum/action/innate/cult/comm/IsAvailable(feedback = FALSE)
- if(isshade(owner) && IS_CULTIST(owner))
- return TRUE
- return ..()
-
-/datum/action/innate/cult/comm/Activate()
- var/input = tgui_input_text(usr, "Message to tell to the other acolytes", "Voice of Blood")
- if(!input || !IsAvailable(feedback = TRUE))
- return
-
- var/list/filter_result = CAN_BYPASS_FILTER(usr) ? null : is_ic_filtered(input)
- if(filter_result)
- REPORT_CHAT_FILTER_TO_USER(usr, filter_result)
- return
-
- var/list/soft_filter_result = CAN_BYPASS_FILTER(usr) ? null : is_soft_ic_filtered(input)
- if(soft_filter_result)
- if(tgui_alert(usr,"Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to say it?", "Soft Blocked Word", list("Yes", "No")) != "Yes")
- return
- message_admins("[ADMIN_LOOKUPFLW(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term. Message: \"[html_encode(input)]\"")
- log_admin_private("[key_name(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term. Message: \"[input]\"")
- cultist_commune(usr, input)
-
-/datum/action/innate/cult/comm/proc/cultist_commune(mob/living/user, message)
- var/my_message
- if(!message)
- return
- user.whisper("O bidai nabora se[pick("'","`")]sma!", language = /datum/language/common)
- user.whisper(html_decode(message), filterproof = TRUE)
- var/title = "Acolyte"
- var/span = "cult italic"
- if(user.mind && user.mind.has_antag_datum(/datum/antagonist/cult/master))
- span = "cultlarge"
- title = "Master"
- else if(!ishuman(user))
- title = "Construct"
- my_message = "[title] [findtextEx(user.name, user.real_name) ? user.name : "[user.real_name] (as [user.name])"]: [message]"
- for(var/i in GLOB.player_list)
- var/mob/M = i
- if(IS_CULTIST(M))
- to_chat(M, my_message)
- else if(M in GLOB.dead_mob_list)
- var/link = FOLLOW_LINK(M, user)
- to_chat(M, "[link] [my_message]")
-
- user.log_talk(message, LOG_SAY, tag="cult")
-
-/datum/action/innate/cult/comm/spirit
- name = "Spiritual Communion"
- desc = "Conveys a message from the spirit realm that all cultists can hear."
-
-/datum/action/innate/cult/comm/spirit/IsAvailable(feedback = FALSE)
- if(IS_CULTIST(owner.mind.current))
- return TRUE
- return ..()
-
-/datum/action/innate/cult/comm/spirit/cultist_commune(mob/living/user, message)
- var/my_message
- if(!message)
- return
- my_message = span_cultboldtalic("The [user.name]: [message]")
- for(var/mob/player_list as anything in GLOB.player_list)
- if(IS_CULTIST(player_list))
- to_chat(player_list, my_message)
- else if(player_list in GLOB.dead_mob_list)
- var/link = FOLLOW_LINK(player_list, user)
- to_chat(player_list, "[link] [my_message]")
-
-/datum/action/innate/cult/mastervote
- name = "Assert Leadership"
- button_icon_state = "cultvote"
-
-/datum/action/innate/cult/mastervote/IsAvailable(feedback = FALSE)
- var/datum/antagonist/cult/C = owner.mind.has_antag_datum(/datum/antagonist/cult,TRUE)
- if(!C || C.cult_team.cult_vote_called || !ishuman(owner))
- return FALSE
- return ..()
-
-/datum/action/innate/cult/mastervote/Activate()
- var/choice = tgui_alert(owner, "The mantle of leadership is heavy. Success in this role requires an expert level of communication and experience. Are you sure?",, list("Yes", "No"))
- if(choice == "Yes" && IsAvailable())
- var/datum/antagonist/cult/mind_cult_datum = owner.mind.has_antag_datum(/datum/antagonist/cult)
- start_poll_cultists_for_leader(owner, mind_cult_datum.cult_team)
-
-///Start the poll for Cult Leaeder.
-/proc/start_poll_cultists_for_leader(mob/living/nominee, datum/team/cult/team)
- if(world.time < CULT_POLL_WAIT)
- to_chat(nominee, "It would be premature to select a leader while everyone is still settling in, try again in [DisplayTimeText(CULT_POLL_WAIT-world.time)].")
- return
- team.cult_vote_called = TRUE
- for(var/datum/mind/team_member as anything in team.members)
- if(!team_member.current)
- continue
- team_member.current.update_mob_action_buttons()
- if(team_member.current.incapacitated())
- continue
- SEND_SOUND(team_member.current, 'sound/hallucinations/im_here1.ogg')
- to_chat(team_member.current, span_cultlarge("Acolyte [nominee] has asserted that [nominee.p_theyre()] worthy of leading the cult. A vote will be called shortly."))
-
- addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(poll_cultists_for_leader), nominee, team), 10 SECONDS)
-
-///Polls all Cultists on whether the person putting themselves forward should be made the Cult Leader, if they can actually be such.
-/proc/poll_cultists_for_leader(mob/living/nominee, datum/team/cult/team)
- if(QDELETED(nominee) || nominee.incapacitated())
- team.cult_vote_called = FALSE
- for(var/datum/mind/team_member as anything in team.members)
- if(!team_member.current)
- continue
- team_member.current.update_mob_action_buttons()
- if(team_member.current.incapacitated())
- continue
- to_chat(team_member.current,span_cultlarge("[nominee] has died in the process of attempting to start a vote!"))
- return FALSE
- var/list/mob/living/asked_cultists = list()
- for(var/datum/mind/team_member as anything in team.members)
- if(!team_member.current || team_member.current == nominee || team_member.current.incapacitated())
- continue
- SEND_SOUND(team_member.current, 'sound/magic/exit_blood.ogg')
- asked_cultists += team_member.current
-
- var/list/yes_voters = SSpolling.poll_candidates(
- question = "[span_notice(nominee.name)] seeks to lead your cult, do you support [nominee.p_them()]?",
- poll_time = 30 SECONDS,
- group = asked_cultists,
- alert_pic = nominee,
- role_name_text = "cult master nomination",
- custom_response_messages = list(
- POLL_RESPONSE_SIGNUP = "You have pledged your allegience to [nominee].",
- POLL_RESPONSE_ALREADY_SIGNED = "You have already pledged your allegience!",
- POLL_RESPONSE_NOT_SIGNED = "You aren't nominated for this.",
- POLL_RESPONSE_TOO_LATE_TO_UNREGISTER = "It's too late to unregister yourself, voting has already begun!",
- POLL_RESPONSE_UNREGISTERED = "You have been removed your pledge to [nominee].",
- )
- )
- if(QDELETED(nominee) || nominee.incapacitated())
- team.cult_vote_called = FALSE
- for(var/datum/mind/team_member as anything in team.members)
- if(!team_member.current)
- continue
- team_member.current.update_mob_action_buttons()
- if(team_member.current.incapacitated())
- continue
- to_chat(team_member.current,span_cultlarge("[nominee] has died in the process of attempting to win the cult's support!"))
- return FALSE
- if(!nominee.mind)
- team.cult_vote_called = FALSE
- for(var/datum/mind/team_member as anything in team.members)
- if(!team_member.current)
- continue
- team_member.current.update_mob_action_buttons()
- if(team_member.current.incapacitated())
- continue
- to_chat(team_member.current,span_cultlarge("[nominee] has gone catatonic in the process of attempting to win the cult's support!"))
- return FALSE
- if(LAZYLEN(yes_voters) <= LAZYLEN(asked_cultists) * 0.5)
- team.cult_vote_called = FALSE
- for(var/datum/mind/team_member as anything in team.members)
- if(!team_member.current)
- continue
- team_member.current.update_mob_action_buttons()
- if(team_member.current.incapacitated())
- continue
- to_chat(team_member.current, span_cultlarge("[nominee] could not win the cult's support and shall continue to serve as an acolyte."))
- return FALSE
-
- team.cult_vote_called = FALSE
- team.cult_master = nominee
- var/datum/antagonist/cult/cultist = nominee.mind.has_antag_datum(/datum/antagonist/cult)
- cultist?.silent = TRUE
- cultist?.on_removal()
- nominee.mind.add_antag_datum(/datum/antagonist/cult/master)
- return TRUE
-
-/datum/action/innate/cult/master/IsAvailable(feedback = FALSE)
- if(!owner.mind || !owner.mind.has_antag_datum(/datum/antagonist/cult/master) || GLOB.cult_narsie)
- return FALSE
- return ..()
-
-/datum/action/innate/cult/master/finalreck
- name = "Final Reckoning"
- desc = "A single-use spell that brings the entire cult to the master's location."
- button_icon_state = "sintouch"
-
-/datum/action/innate/cult/master/finalreck/Activate()
- var/datum/antagonist/cult/antag = owner.mind.has_antag_datum(/datum/antagonist/cult,TRUE)
- if(!antag)
- return
- var/place = get_area(owner)
- var/datum/objective/eldergod/summon_objective = locate() in antag.cult_team.objectives
- if(place in summon_objective.summon_spots)//cant do final reckoning in the summon area to prevent abuse, you'll need to get everyone to stand on the circle!
- to_chat(owner, span_cultlarge("The veil is too weak here! Move to an area where it is strong enough to support this magic."))
- return
- for(var/i in 1 to 4)
- chant(i)
- var/list/destinations = list()
- for(var/turf/T in orange(1, owner))
- if(!T.is_blocked_turf(TRUE))
- destinations += T
- if(!LAZYLEN(destinations))
- to_chat(owner, span_warning("You need more space to summon your cult!"))
- return
- if(do_after(owner, 30, target = owner))
- for(var/datum/mind/B in antag.cult_team.members)
- if(B.current && B.current.stat != DEAD)
- var/turf/mobloc = get_turf(B.current)
- switch(i)
- if(1)
- new /obj/effect/temp_visual/cult/sparks(mobloc, B.current.dir)
- playsound(mobloc, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- if(2)
- new /obj/effect/temp_visual/dir_setting/cult/phase/out(mobloc, B.current.dir)
- playsound(mobloc, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- if(3)
- new /obj/effect/temp_visual/dir_setting/cult/phase(mobloc, B.current.dir)
- playsound(mobloc, SFX_SPARKS, 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- if(4)
- playsound(mobloc, 'sound/magic/exit_blood.ogg', 100, TRUE)
- if(B.current != owner)
- var/turf/final = pick(destinations)
- if(istype(B.current.loc, /obj/item/soulstone))
- var/obj/item/soulstone/S = B.current.loc
- S.release_shades(owner)
- B.current.setDir(SOUTH)
- new /obj/effect/temp_visual/cult/blood(final)
- addtimer(CALLBACK(B.current, TYPE_PROC_REF(/mob/, reckon), final), 10)
- else
- return
- antag.cult_team.reckoning_complete = TRUE
- Remove(owner)
-
-/mob/proc/reckon(turf/final)
- new /obj/effect/temp_visual/cult/blood/out(get_turf(src))
- forceMove(final)
-
-/datum/action/innate/cult/master/finalreck/proc/chant(chant_number)
- switch(chant_number)
- if(1)
- owner.say("C'arta forbici!", language = /datum/language/common, forced = "cult invocation")
- if(2)
- owner.say("Pleggh e'ntrath!", language = /datum/language/common, forced = "cult invocation")
- playsound(get_turf(owner),'sound/magic/clockwork/narsie_attack.ogg', 50, TRUE)
- if(3)
- owner.say("Barhah hra zar'garis!", language = /datum/language/common, forced = "cult invocation")
- playsound(get_turf(owner),'sound/magic/clockwork/narsie_attack.ogg', 75, TRUE)
- if(4)
- owner.say("N'ath reth sh'yro eth d'rekkathnor!!!", language = /datum/language/common, forced = "cult invocation")
- playsound(get_turf(owner),'sound/magic/clockwork/narsie_attack.ogg', 100, TRUE)
-
-/datum/action/innate/cult/master/cultmark
- name = "Mark Target"
- desc = "Marks a target for the cult."
- button_icon_state = "cult_mark"
- click_action = TRUE
- enable_text = span_cult("You prepare to mark a target for your cult. Click a target to mark them!")
- disable_text = span_cult("You cease the marking ritual.")
- /// The duration of the mark itself
- var/cult_mark_duration = 90 SECONDS
- /// The duration of the cooldown for cult marks
- var/cult_mark_cooldown_duration = 2 MINUTES
- /// The actual cooldown tracked of the action
- COOLDOWN_DECLARE(cult_mark_cooldown)
-
-/datum/action/innate/cult/master/cultmark/IsAvailable(feedback = FALSE)
- return ..() && COOLDOWN_FINISHED(src, cult_mark_cooldown)
-
-/datum/action/innate/cult/master/cultmark/InterceptClickOn(mob/user, params, atom/clicked_on)
- var/turf/caller_turf = get_turf(user)
- if(!isturf(caller_turf))
- return FALSE
-
- if(!(clicked_on in view(7, caller_turf)))
- return FALSE
-
- return ..()
-
-/datum/action/innate/cult/master/cultmark/do_ability(mob/living/user, atom/clicked_on)
- var/datum/antagonist/cult/cultist = user.mind.has_antag_datum(/datum/antagonist/cult, TRUE)
- if(!cultist)
- CRASH("[type] was casted by someone without a cult antag datum.")
-
- var/datum/team/cult/cult_team = cultist.get_team()
- if(!cult_team)
- CRASH("[type] was casted by a cultist without a cult team datum.")
-
- if(cult_team.blood_target)
- to_chat(user, span_cult("The cult has already designated a target!"))
- return FALSE
-
- if(cult_team.set_blood_target(clicked_on, user, cult_mark_duration))
- unset_ranged_ability(user, span_cult("The marking rite is complete! It will last for [DisplayTimeText(cult_mark_duration)] seconds."))
- COOLDOWN_START(src, cult_mark_cooldown, cult_mark_cooldown_duration)
- build_all_button_icons()
- addtimer(CALLBACK(src, PROC_REF(build_all_button_icons)), cult_mark_cooldown_duration + 1)
- return TRUE
-
- unset_ranged_ability(user, span_cult("The marking rite failed!"))
- return TRUE
-
-/datum/action/innate/cult/ghostmark //Ghost version
- name = "Blood Mark your Target"
- desc = "Marks whatever you are orbiting for the entire cult to track."
- button_icon_state = "cult_mark"
- check_flags = NONE
- /// The duration of the mark on the target
- var/cult_mark_duration = 60 SECONDS
- /// The cooldown between marks - the ability can be used in between cooldowns, but can't mark (only clear)
- var/cult_mark_cooldown_duration = 60 SECONDS
- /// The actual cooldown tracked of the action
- COOLDOWN_DECLARE(cult_mark_cooldown)
-
-/datum/action/innate/cult/ghostmark/IsAvailable(feedback = FALSE)
- return ..() && isobserver(owner)
-
-/datum/action/innate/cult/ghostmark/Activate()
- var/datum/antagonist/cult/cultist = owner.mind?.has_antag_datum(/datum/antagonist/cult, TRUE)
- if(!cultist)
- CRASH("[type] was casted by someone without a cult antag datum.")
-
- var/datum/team/cult/cult_team = cultist.get_team()
- if(!cult_team)
- CRASH("[type] was casted by a cultist without a cult team datum.")
-
- if(cult_team.blood_target)
- if(!COOLDOWN_FINISHED(src, cult_mark_cooldown))
- cult_team.unset_blood_target_and_timer()
- to_chat(owner, span_cultbold("You have cleared the cult's blood target!"))
- return TRUE
-
- to_chat(owner, span_cultbold("The cult has already designated a target!"))
- return FALSE
-
- if(!COOLDOWN_FINISHED(src, cult_mark_cooldown))
- to_chat(owner, span_cultbold("You aren't ready to place another blood mark yet!"))
- return FALSE
-
- var/atom/mark_target = owner.orbiting?.parent || get_turf(owner)
- if(!mark_target)
- return FALSE
-
- if(cult_team.set_blood_target(mark_target, owner, 60 SECONDS))
- to_chat(owner, span_cultbold("You have marked [mark_target] for the cult! It will last for [DisplayTimeText(cult_mark_duration)]."))
- COOLDOWN_START(src, cult_mark_cooldown, cult_mark_cooldown_duration)
- build_all_button_icons(UPDATE_BUTTON_NAME|UPDATE_BUTTON_ICON)
- addtimer(CALLBACK(src, PROC_REF(reset_button)), cult_mark_cooldown_duration + 1)
- return TRUE
-
- to_chat(owner, span_cult("The marking failed!"))
- return FALSE
-
-/datum/action/innate/cult/ghostmark/update_button_name(atom/movable/screen/movable/action_button/current_button, force = FALSE)
- if(COOLDOWN_FINISHED(src, cult_mark_duration))
- name = initial(name)
- desc = initial(desc)
- else
- name = "Clear the Blood Mark"
- desc = "Remove the Blood Mark you previously set."
-
- return ..()
-
-/datum/action/innate/cult/ghostmark/apply_button_icon(atom/movable/screen/movable/action_button/current_button, force = FALSE)
- if(COOLDOWN_FINISHED(src, cult_mark_duration))
- button_icon_state = initial(button_icon_state)
- else
- button_icon_state = "emp"
-
- return ..()
-
-/datum/action/innate/cult/ghostmark/proc/reset_button()
- if(QDELETED(owner) || QDELETED(src))
- return
-
- SEND_SOUND(owner, 'sound/magic/enter_blood.ogg')
- to_chat(owner, span_cultbold("Your previous mark is gone - you are now ready to create a new blood mark."))
- build_all_button_icons(UPDATE_BUTTON_NAME|UPDATE_BUTTON_ICON)
-
-//////// ELDRITCH PULSE /////////
-
-/datum/action/innate/cult/master/pulse
- name = "Eldritch Pulse"
- desc = "Seize upon a fellow cultist or cult structure and teleport it to a nearby location."
- button_icon = 'icons/mob/actions/actions_spells.dmi'
- button_icon_state = "arcane_barrage"
- click_action = TRUE
- enable_text = span_cult("You prepare to tear through the fabric of reality... Click a target to sieze them!")
- disable_text = span_cult("You cease your preparations.")
- /// Weakref to whoever we're currently about to toss
- var/datum/weakref/throwee_ref
- /// Cooldown of the ability
- var/pulse_cooldown_duration = 15 SECONDS
- /// The actual cooldown tracked of the action
- COOLDOWN_DECLARE(pulse_cooldown)
-
-/datum/action/innate/cult/master/pulse/IsAvailable(feedback = FALSE)
- return ..() && COOLDOWN_FINISHED(src, pulse_cooldown)
-
-/datum/action/innate/cult/master/pulse/InterceptClickOn(mob/living/user, params, atom/clicked_on)
- var/turf/caller_turf = get_turf(user)
- if(!isturf(caller_turf))
- return FALSE
-
- if(!(clicked_on in view(7, caller_turf)))
- return FALSE
-
- if(clicked_on == user)
- return FALSE
-
- return ..()
-
-/datum/action/innate/cult/master/pulse/do_ability(mob/living/user, atom/clicked_on)
- var/atom/throwee = throwee_ref?.resolve()
-
- if(QDELETED(throwee))
- to_chat(user, span_cult("You lost your target!"))
- throwee = null
- throwee_ref = null
- return FALSE
-
- if(throwee)
- if(get_dist(throwee, clicked_on) >= 16)
- to_chat(user, span_cult("You can't teleport [clicked_on.p_them()] that far!"))
- return FALSE
-
- var/turf/throwee_turf = get_turf(throwee)
-
- playsound(throwee_turf, 'sound/magic/exit_blood.ogg')
- new /obj/effect/temp_visual/cult/sparks(throwee_turf, user.dir)
- throwee.visible_message(
- span_warning("A pulse of magic whisks [throwee] away!"),
- span_cult("A pulse of blood magic whisks you away..."),
- )
-
- if(!do_teleport(throwee, clicked_on, channel = TELEPORT_CHANNEL_CULT))
- to_chat(user, span_cult("The teleport fails!"))
- throwee.visible_message(
- span_warning("...Except they don't go very far"),
- span_cult("...Except you don't appear to have moved very far."),
- )
- return FALSE
-
- throwee_turf.Beam(clicked_on, icon_state = "sendbeam", time = 0.4 SECONDS)
- new /obj/effect/temp_visual/cult/sparks(get_turf(clicked_on), user.dir)
- throwee.visible_message(
- span_warning("[throwee] appears suddenly in a pulse of magic!"),
- span_cult("...And you appear elsewhere."),
- )
-
- COOLDOWN_START(src, pulse_cooldown, pulse_cooldown_duration)
- to_chat(user, span_cult("A pulse of blood magic surges through you as you shift [throwee] through time and space."))
- user.click_intercept = null
- throwee_ref = null
- build_all_button_icons()
- addtimer(CALLBACK(src, PROC_REF(build_all_button_icons)), pulse_cooldown_duration + 1)
-
- return TRUE
-
- else
- if(isliving(clicked_on))
- var/mob/living/living_clicked = clicked_on
- if(!IS_CULTIST(living_clicked))
- return FALSE
- SEND_SOUND(user, sound('sound/weapons/thudswoosh.ogg'))
- to_chat(user, span_cultbold("You reach through the veil with your mind's eye and seize [clicked_on]! Click anywhere nearby to teleport [clicked_on.p_them()]!"))
- throwee_ref = WEAKREF(clicked_on)
- return TRUE
-
- if(istype(clicked_on, /obj/structure/destructible/cult))
- to_chat(user, span_cultbold("You reach through the veil with your mind's eye and lift [clicked_on]! Click anywhere nearby to teleport it!"))
- throwee_ref = WEAKREF(clicked_on)
- return TRUE
-
- return FALSE
diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm
index 97c90bba4945..29ec821901d0 100644
--- a/code/modules/antagonists/cult/cult_items.dm
+++ b/code/modules/antagonists/cult/cult_items.dm
@@ -286,55 +286,6 @@ Striking a noncultist, however, will tear their flesh."}
fire = 10
acid = 10
-/obj/item/clothing/suit/hooded/cultrobes/hardened
- name = "\improper Nar'Sien hardened armor"
- desc = "A heavily-armored exosuit worn by warriors of the Nar'Sien cult. It can withstand hard vacuum."
- icon_state = "cult_armor"
- inhand_icon_state = null
- w_class = WEIGHT_CLASS_BULKY
- allowed = list(/obj/item/tome, /obj/item/melee/cultblade, /obj/item/tank/internals)
- armor_type = /datum/armor/cultrobes_hardened
- hoodtype = /obj/item/clothing/head/hooded/cult_hoodie/hardened
- clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL
- flags_inv = HIDEGLOVES | HIDEJUMPSUIT
- min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
- max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
- resistance_flags = NONE
-
-/datum/armor/cultrobes_hardened
- melee = 50
- bullet = 40
- laser = 50
- energy = 60
- bomb = 50
- bio = 100
- fire = 100
- acid = 100
-
-/obj/item/clothing/head/hooded/cult_hoodie/hardened
- name = "\improper Nar'Sien hardened helmet"
- desc = "A heavily-armored helmet worn by warriors of the Nar'Sien cult. It can withstand hard vacuum."
- icon_state = "cult_helmet"
- inhand_icon_state = null
- armor_type = /datum/armor/cult_hoodie_hardened
- clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL | SNUG_FIT | PLASMAMAN_HELMET_EXEMPT | HEADINTERNALS
- flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDEFACIALHAIR|HIDESNOUT
- min_cold_protection_temperature = SPACE_HELM_MIN_TEMP_PROTECT
- max_heat_protection_temperature = SPACE_HELM_MAX_TEMP_PROTECT
- flash_protect = FLASH_PROTECTION_WELDER
- flags_cover = HEADCOVERSEYES | HEADCOVERSMOUTH | PEPPERPROOF
- resistance_flags = NONE
-
-/datum/armor/cult_hoodie_hardened
- melee = 50
- bullet = 40
- laser = 50
- energy = 60
- bomb = 50
- bio = 100
- fire = 100
- acid = 100
-
/obj/item/sharpener/cult
name = "eldritch whetstone"
desc = "A block, empowered by dark magic. Sharp weapons will be enhanced when used on the stone."
diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm
index 88d010a26e58..3ac7b3002585 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -145,7 +145,7 @@ structure_check() searches for nearby cultist structures required for the invoca
var/list/invokers = list() //people eligible to invoke the rune
if(user)
invokers += user
- if(req_cultists > 1 || istype(src, /obj/effect/rune/convert))
+ if(req_cultists > 1)
for(var/mob/living/cultist in range(1, src))
if(!IS_CULTIST(cultist))
continue
@@ -205,229 +205,6 @@ structure_check() searches for nearby cultist structures required for the invoca
..()
qdel(src)
-//Rite of Offering: Converts or sacrifices a target.
-/obj/effect/rune/convert
- cultist_name = "Offer"
- cultist_desc = "offers a noncultist above it to Nar'Sie, either converting them or sacrificing them."
- req_cultists_text = "2 for conversion, 3 for living sacrifices and sacrifice targets."
- invocation = "Mah'weyh pleggh at e'ntrath!"
- icon_state = "3"
- color = RUNE_COLOR_OFFER
- req_cultists = 1
- rune_in_use = FALSE
-
-/obj/effect/rune/convert/do_invoke_glow()
- return
-
-/obj/effect/rune/convert/invoke(list/invokers)
- if(rune_in_use)
- return
-
- var/list/myriad_targets = list()
- for(var/mob/living/non_cultist in loc)
- if(!IS_CULTIST(non_cultist))
- myriad_targets += non_cultist
-
- if(!length(myriad_targets) && !try_spawn_sword())
- fail_invoke()
- return
-
- rune_in_use = TRUE
- visible_message(span_warning("[src] pulses blood red!"))
- var/oldcolor = color
- color = RUNE_COLOR_DARKRED
-
- if(length(myriad_targets))
- var/mob/living/new_convertee = pick(myriad_targets)
- var/mob/living/first_invoker = invokers[1]
- var/datum/antagonist/cult/first_invoker_datum = first_invoker.mind.has_antag_datum(/datum/antagonist/cult)
- var/datum/team/cult/cult_team = first_invoker_datum.get_team()
-
- var/is_convertable = is_convertable_to_cult(new_convertee, cult_team)
- if(new_convertee.stat != DEAD && is_convertable)
- invocation = "Mah'weyh pleggh at e'ntrath!"
- ..()
- do_convert(new_convertee, invokers, cult_team)
-
- else
- invocation = "Barhah hra zar'garis!"
- ..()
- do_sacrifice(new_convertee, invokers, cult_team)
-
- cult_team.check_size() // Triggers the eye glow or aura effects if the cult has grown large enough relative to the crew
-
- else
- do_invoke_glow()
-
- animate(src, color = oldcolor, time = 0.5 SECONDS)
- addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_atom_colour)), 0.5 SECONDS)
- rune_in_use = FALSE
-
-/obj/effect/rune/convert/proc/do_convert(mob/living/convertee, list/invokers, datum/team/cult/cult_team)
- ASSERT(convertee.mind)
-
- if(length(invokers) < 2)
- for(var/invoker in invokers)
- to_chat(invoker, span_warning("You need at least two invokers to convert [convertee]!"))
- return FALSE
-
- if(convertee.can_block_magic(MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY, charge_cost = 0)) //No charge_cost because it can be spammed
- for(var/invoker in invokers)
- to_chat(invoker, span_warning("Something is shielding [convertee]'s mind!"))
- return FALSE
-
-
- // monke start: old man henderson
- if(convertee.get_drunk_amount() >= OLD_MAN_HENDERSON_DRUNKENNESS)
- convertee.visible_message(span_cultitalic("[convertee] is unfazed by the rune, grumbling with incoherent drunken annoyance instead!"))
- for(var/invoker in invokers)
- to_chat(invoker, span_warning("The rune's blood magic is ineffective on [convertee], unable to pierce the intense alcoholic haze clouding [convertee.p_their()] mind!"))
- return FALSE
- // monke end
-
- var/brutedamage = convertee.getBruteLoss()
- var/burndamage = convertee.getFireLoss()
- if(brutedamage || burndamage)
- convertee.adjustBruteLoss(-(brutedamage * 0.75))
- convertee.adjustFireLoss(-(burndamage * 0.75))
-
- convertee.visible_message(
- span_warning("[convertee] writhes in pain [(brutedamage || burndamage) \
- ? "even as [convertee.p_their()] wounds heal and close" \
- : "as the markings below [convertee.p_them()] glow a bloody red"]!"),
- span_cultlarge("AAAAAAAAAAAAAA-"),
- )
-
- // We're not guaranteed to be a human but we'll cast here since we use it in a few branches
- var/mob/living/carbon/human/human_convertee = convertee
-
- if(check_holidays(APRIL_FOOLS) && prob(10))
- convertee.Paralyze(10 SECONDS)
- if(istype(human_convertee))
- human_convertee.force_say()
- convertee.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in. (April Fools)")
-
- else
- convertee.Unconscious(10 SECONDS)
-
- new /obj/item/melee/cultblade/dagger(get_turf(src))
- convertee.mind.special_role = ROLE_CULTIST
- convertee.mind.add_antag_datum(/datum/antagonist/cult, cult_team)
-
- to_chat(convertee, span_cultitalic("Your blood pulses. Your head throbs. The world goes red. \
- All at once you are aware of a horrible, horrible, truth. The veil of reality has been ripped away \
- and something evil takes root."))
- to_chat(convertee, span_cultitalic("Assist your new compatriots in their dark dealings. \
- Your goal is theirs, and theirs is yours. You serve the Geometer above all else. Bring it back."))
-
- if(istype(human_convertee))
- human_convertee.uncuff()
- human_convertee.remove_status_effect(/datum/status_effect/speech/slurring/cult)
- human_convertee.remove_status_effect(/datum/status_effect/speech/stutter)
- if(isshade(convertee))
- convertee.icon_state = "shade_cult"
- convertee.name = convertee.real_name
- return TRUE
-
-/obj/effect/rune/convert/proc/do_sacrifice(mob/living/sacrificial, list/invokers, datum/team/cult/cult_team)
- var/big_sac = FALSE
- if((((ishuman(sacrificial) || iscyborg(sacrificial)) && sacrificial.stat != DEAD) || cult_team.is_sacrifice_target(sacrificial.mind)) && length(invokers) < 3)
- for(var/invoker in invokers)
- to_chat(invoker, span_cultitalic("[sacrificial] is too greatly linked to the world! You need three acolytes!"))
- return FALSE
-
- var/signal_result = SEND_SIGNAL(sacrificial, COMSIG_LIVING_CULT_SACRIFICED, invokers)
- if(signal_result & STOP_SACRIFICE)
- return FALSE
-
- if(sacrificial.mind)
- LAZYADD(GLOB.sacrificed, WEAKREF(sacrificial.mind))
- for(var/datum/objective/sacrifice/sac_objective in cult_team.objectives)
- if(sac_objective.target == sacrificial.mind)
- sac_objective.sacced = TRUE
- sac_objective.clear_sacrifice()
- sac_objective.update_explanation_text()
- big_sac = TRUE
- else
- LAZYADD(GLOB.sacrificed, WEAKREF(sacrificial))
-
- new /obj/effect/temp_visual/cult/sac(loc)
-
- if(!(signal_result & SILENCE_SACRIFICE_MESSAGE))
- for(var/invoker in invokers)
- if(big_sac)
- to_chat(invoker, span_cultlarge("\"Yes! This is the one I desire! You have done well.\""))
- continue
- if(ishuman(sacrificial) || iscyborg(sacrificial))
- to_chat(invoker, span_cultlarge("\"I accept this sacrifice.\""))
- else
- to_chat(invoker, span_cultlarge("\"I accept this meager sacrifice.\""))
-
- if(iscyborg(sacrificial))
- var/construct_class = show_radial_menu(invokers[1], sacrificial, GLOB.construct_radial_images, require_near = TRUE, tooltips = TRUE)
- if(QDELETED(sacrificial) || !construct_class)
- return FALSE
- sacrificial.grab_ghost()
- make_new_construct_from_class(construct_class, THEME_CULT, sacrificial, invokers[1], TRUE, get_turf(src))
- var/mob/living/silicon/robot/sacriborg = sacrificial
- sacrificial.log_message("was sacrificed as a cyborg.", LOG_GAME)
- sacriborg.mmi = null
- qdel(sacrificial)
- return TRUE
-
- var/obj/item/soulstone/stone = new(loc)
- if(sacrificial.mind && !HAS_TRAIT(sacrificial, TRAIT_SUICIDED))
- stone.capture_soul(sacrificial, invokers[1], forced = TRUE)
-
- if(sacrificial)
- playsound(sacrificial, 'sound/magic/disintegrate.ogg', 100, TRUE)
- sacrificial.investigate_log("has been sacrificially gibbed by the cult.", INVESTIGATE_DEATHS)
- sacrificial.gib()
-
- try_spawn_sword() // after sharding and gibbing, which potentially dropped a null rod
- return TRUE
-
-/// Tries to convert a null rod over the rune to a cult sword
-/obj/effect/rune/convert/proc/try_spawn_sword()
- for(var/obj/item/nullrod/rod in loc)
- if(rod.anchored || (rod.resistance_flags & INDESTRUCTIBLE))
- continue
-
- var/num_slain = LAZYLEN(rod.cultists_slain)
- var/displayed_message = "[rod] glows an unholy red and begins to transform..."
- if(GET_ATOM_BLOOD_DNA_LENGTH(rod))
- displayed_message += " The blood of [num_slain] fallen cultist[num_slain == 1 ? "":"s"] is absorbed into [rod]!"
-
- rod.visible_message(span_cultitalic(displayed_message))
- switch(num_slain)
- if(0, 1)
- animate_spawn_sword(rod, /obj/item/melee/cultblade/dagger)
- if(2)
- animate_spawn_sword(rod, /obj/item/melee/cultblade)
- else
- animate_spawn_sword(rod, /obj/item/cult_bastard)
- return TRUE
-
- return FALSE
-
-/// Does an animation of a null rod transforming into a cult sword
-/obj/effect/rune/convert/proc/animate_spawn_sword(obj/item/nullrod/former_rod, new_blade_typepath)
- playsound(src, 'sound/effects/magic.ogg', 33, vary = TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, frequency = 0.66)
- former_rod.anchored = TRUE
- former_rod.Shake()
- animate(former_rod, alpha = 0, transform = matrix(former_rod.transform).Scale(0.01), time = 2 SECONDS, easing = BOUNCE_EASING, flags = ANIMATION_PARALLEL)
- QDEL_IN(former_rod, 2 SECONDS)
-
- var/obj/item/new_blade = new new_blade_typepath(loc)
- var/matrix/blade_matrix_on_spawn = matrix(new_blade.transform)
- new_blade.name = "converted [new_blade.name]"
- new_blade.anchored = TRUE
- new_blade.alpha = 0
- new_blade.transform = matrix(new_blade.transform).Scale(0.01)
- new_blade.Shake()
- animate(new_blade, alpha = 255, transform = blade_matrix_on_spawn, time = 2 SECONDS, easing = BOUNCE_EASING, flags = ANIMATION_PARALLEL)
- addtimer(VARSET_CALLBACK(new_blade, anchored, FALSE), 2 SECONDS)
-
/obj/effect/rune/empower
cultist_name = "Empower"
cultist_desc = "allows cultists to prepare greater amounts of blood magic at far less of a cost."
@@ -573,89 +350,6 @@ structure_check() searches for nearby cultist structures required for the invoca
set_light_range(0)
update_light()
-//Ritual of Dimensional Rending: Calls forth the avatar of Nar'Sie upon the station.
-/obj/effect/rune/narsie
- cultist_name = "Nar'Sie"
- cultist_desc = "tears apart dimensional barriers, calling forth the Geometer. Requires 9 invokers."
- invocation = "TOK-LYR RQA-NAP G'OLT-ULOFT!!"
- req_cultists = 9
- icon = 'icons/effects/96x96.dmi'
- color = RUNE_COLOR_DARKRED
- icon_state = "rune_large"
- pixel_x = -32 //So the big ol' 96x96 sprite shows up right
- pixel_y = -32
- scribe_delay = 50 SECONDS //how long the rune takes to create
- scribe_damage = 40.1 //how much damage you take doing it
- log_when_erased = TRUE
- no_scribe_boost = TRUE
- erase_time = 5 SECONDS
- ///Has the rune been used already?
- var/used = FALSE
-
-/obj/effect/rune/narsie/Initialize(mapload, set_keyword)
- . = ..()
- SSpoints_of_interest.make_point_of_interest(src)
-
-/obj/effect/rune/narsie/conceal() //can't hide this, and you wouldn't want to
- return
-
-/obj/effect/rune/narsie/invoke(list/invokers)
- if(used)
- return
- if(!is_station_level(z))
- return
- var/mob/living/user = invokers[1]
- var/datum/antagonist/cult/user_antag = user.mind.has_antag_datum(/datum/antagonist/cult, TRUE)
- var/datum/objective/eldergod/summon_objective = locate() in user_antag.cult_team.objectives
- var/area/place = get_area(src)
- if(!(place in summon_objective.summon_spots))
- to_chat(user, span_cultlarge("The Geometer can only be summoned where the veil is weak - in [english_list(summon_objective.summon_spots)]!"))
- return
- if(locate(/obj/narsie) in SSpoints_of_interest.narsies)
- for(var/invoker in invokers)
- to_chat(invoker, span_warning("Nar'Sie is already on this plane!"))
- log_game("Nar'Sie rune activated by [user] at [COORD(src)] failed - already summoned.")
- return
-//monkestation edit start
- used = TRUE
- var/datum/team/cult/cult_team = user_antag.cult_team
- if (cult_team.narsie_summoned)
- for (var/datum/mind/cultist_mind in cult_team.members)
- var/mob/living/cultist_mob = cultist_mind.current
- cultist_mob.client?.give_award(/datum/award/achievement/misc/narsupreme, cultist_mob)
-
- cult_team.narsie_summoned = TRUE
-
- if(GLOB.clock_ark) //might bump this up to need the ark to be active in some form
- if(!GLOB.narsie_breaching_rune)
- GLOB.narsie_breaching_rune = src
-
- for(var/invoker in invokers)
- to_chat(invoker, span_bigbrass("A vile light prvents you from saying the invocation! \
- It looks like you will have to destroy whatever is causing this before Nar'sie may be summoned."))
- return
-//monkestation edit end
-
- //BEGIN THE SUMMONING
-//monkestation removal start
-/* used = TRUE
- var/datum/team/cult/cult_team = user_antag.cult_team
- if (cult_team.narsie_summoned)
- for (var/datum/mind/cultist_mind in cult_team.members)
- var/mob/living/cultist_mob = cultist_mind.current
- cultist_mob.client?.give_award(/datum/award/achievement/misc/narsupreme, cultist_mob)
-
- cult_team.narsie_summoned = TRUE */
-//monkestation removal end
- ..()
- sound_to_playing_players('sound/effects/dimensional_rend.ogg')
- var/turf/rune_turf = get_turf(src)
- for(var/datum/mind/cult_mind as anything in cult_team.members)
- cult_team.true_cultists += cult_mind
- sleep(4 SECONDS)
- if(src)
- color = RUNE_COLOR_RED
- new /obj/narsie(rune_turf) //Causes Nar'Sie to spawn even if the rune has been removed
//Rite of Resurrection: Requires a dead or inactive cultist. When reviving the dead, you can only perform one revival for every three sacrifices your cult has carried out.
/obj/effect/rune/raise_dead
@@ -1001,12 +695,8 @@ structure_check() searches for nearby cultist structures required for the invoca
affecting.visible_message(span_warning("[affecting] freezes statue-still, glowing an unearthly red."), \
span_cult("You see what lies beyond. All is revealed. In this form you find that your voice booms louder and you can mark targets for the entire cult"))
var/mob/dead/observer/G = affecting.ghostize(TRUE)
- var/datum/action/innate/cult/comm/spirit/CM = new
- var/datum/action/innate/cult/ghostmark/GM = new
G.name = "Dark Spirit of [G.name]"
G.color = "red"
- CM.Grant(G)
- GM.Grant(G)
while(!QDELETED(affecting))
if(!(affecting in T))
user.visible_message(span_warning("A spectral tendril wraps around [affecting] and pulls [affecting.p_them()] back to the rune!"))
@@ -1021,8 +711,6 @@ structure_check() searches for nearby cultist structures required for the invoca
to_chat(G, span_cultitalic("Your body can no longer sustain the connection!"))
break
sleep(0.5 SECONDS)
- CM.Remove(G)
- GM.Remove(G)
affecting.remove_atom_colour(ADMIN_COLOUR_PRIORITY, RUNE_COLOR_DARKRED)
affecting.grab_ghost()
affecting = null
@@ -1038,141 +726,6 @@ structure_check() searches for nearby cultist structures required for the invoca
. -= B
-/obj/effect/rune/apocalypse
- cultist_name = "Apocalypse"
- cultist_desc = "a harbinger of the end times. Grows in strength with the cult's desperation - but at the risk of... side effects."
- invocation = "Ta'gh fara'qha fel d'amar det!"
- icon = 'icons/effects/96x96.dmi'
- icon_state = "apoc"
- pixel_x = -32
- pixel_y = -32
- color = RUNE_COLOR_DARKRED
- req_cultists = 3
- scribe_delay = 100
-
-/obj/effect/rune/apocalypse/invoke(list/invokers)
- if(rune_in_use)
- return
- . = ..()
-
- var/area/place = get_area(src)
- var/mob/living/user = invokers[1]
- var/datum/antagonist/cult/user_antag = user.mind.has_antag_datum(/datum/antagonist/cult,TRUE)
- var/datum/objective/eldergod/summon_objective = locate() in user_antag.cult_team.objectives
- if(length(summon_objective.summon_spots) <= 1)
- to_chat(user, span_cultlarge("Only one ritual site remains - it must be reserved for the final summoning!"))
- return
- if(!(place in summon_objective.summon_spots))
- to_chat(user, span_cultlarge("The Apocalypse rune will remove a ritual site, where Nar'Sie can be summoned, it can only be scribed in [english_list(summon_objective.summon_spots)]!"))
- return
-
- summon_objective.summon_spots -= place
- rune_in_use = TRUE
-
- var/turf/T = get_turf(src)
- new /obj/effect/temp_visual/dir_setting/curse/grasp_portal/fading(T)
- var/intensity = 0
- for(var/mob/living/M in GLOB.player_list)
- if(IS_CULTIST(M))
- intensity++
- intensity = max(60, 360 - (360*(intensity/length(GLOB.player_list) + 0.3)**2)) //significantly lower intensity for "winning" cults
- var/duration = intensity*10
-
- playsound(T, 'sound/magic/enter_blood.ogg', 100, TRUE)
- visible_message(span_warning("A colossal shockwave of energy bursts from the rune, disintegrating it in the process!"))
-
- for(var/mob/living/target in range(src, 3))
- target.Paralyze(30)
- if(!GLOB.clock_ark) //monkestation edit: this does a little too much damage to the clock cult due to killing their cam consoles with no counterplay
- empulse(T, 0.42*(intensity), 1)
-
- var/list/images = list()
- var/datum/atom_hud/sec_hud = GLOB.huds[DATA_HUD_SECURITY_ADVANCED]
- for(var/mob/living/M in GLOB.alive_mob_list)
- if(!is_valid_z_level(T, get_turf(M)))
- continue
- if(ishuman(M))
- if(!IS_CULTIST(M))
- sec_hud.hide_from(M)
- addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(hudFix), M), duration)
- var/image/A = image('icons/mob/nonhuman-player/cult.dmi',M,"cultist", ABOVE_MOB_LAYER)
- A.override = 1
- add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/noncult, "human_apoc", A, NONE)
- addtimer(CALLBACK(M, TYPE_PROC_REF(/atom/, remove_alt_appearance),"human_apoc",TRUE), duration)
- images += A
- SEND_SOUND(M, pick(sound('sound/ambience/antag/bloodcult/bloodcult_gain.ogg'),sound('sound/voice/ghost_whisper.ogg'),sound('sound/misc/ghosty_wind.ogg')))
- else
- var/construct = pick("floater","artificer","behemoth")
- var/image/B = image('icons/mob/simple/mob.dmi',M,construct, ABOVE_MOB_LAYER)
- B.override = 1
- add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/noncult, "mob_apoc", B, NONE)
- addtimer(CALLBACK(M, TYPE_PROC_REF(/atom/, remove_alt_appearance),"mob_apoc",TRUE), duration)
- images += B
- if(!IS_CULTIST(M))
- if(M.client)
- var/image/C = image('icons/effects/cult/effects.dmi',M,"bloodsparkles", ABOVE_MOB_LAYER)
- add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/cult, "cult_apoc", C, NONE)
- addtimer(CALLBACK(M, TYPE_PROC_REF(/atom/, remove_alt_appearance),"cult_apoc",TRUE), duration)
- images += C
- else
- to_chat(M, span_cultlarge("An Apocalypse Rune was invoked in the [place.name], it is no longer available as a summoning site!"))
- SEND_SOUND(M, 'sound/effects/pope_entry.ogg')
- image_handler(images, duration)
- if(intensity >= 285) // Based on the prior formula, this means the cult makes up <15% of current players
- var/outcome = rand(1,100)
- switch(outcome)
- if(1 to 10)
- force_event_async(/datum/round_event_control/disease_outbreak, "an apocalypse rune")
- force_event_async(/datum/round_event_control/mice_migration, "an apocalypse rune")
- if(11 to 20)
- force_event_async(/datum/round_event_control/radiation_storm, "an apocalypse rune")
-
- if(21 to 30)
- force_event_async(/datum/round_event_control/brand_intelligence, "an apocalypse rune")
-
- if(31 to 40)
- force_event_async(/datum/round_event_control/immovable_rod, "an apocalypse rune")
- force_event_async(/datum/round_event_control/immovable_rod, "an apocalypse rune")
- force_event_async(/datum/round_event_control/immovable_rod, "an apocalypse rune")
-
- if(41 to 50)
- force_event_async(/datum/round_event_control/meteor_wave, "an apocalypse rune")
-
- if(51 to 60)
- force_event_async(/datum/round_event_control/spider_infestation, "an apocalypse rune")
-
- if(61 to 70)
- force_event_async(/datum/round_event_control/anomaly/anomaly_flux, "an apocalypse rune")
- force_event_async(/datum/round_event_control/anomaly/anomaly_grav, "an apocalypse rune")
- force_event_async(/datum/round_event_control/anomaly/anomaly_pyro, "an apocalypse rune")
- force_event_async(/datum/round_event_control/anomaly/anomaly_vortex, "an apocalypse rune")
-
- if(71 to 80)
- force_event_async(/datum/round_event_control/spacevine, "an apocalypse rune")
- force_event_async(/datum/round_event_control/grey_tide, "an apocalypse rune")
-
- if(81 to 100)
- force_event_async(/datum/round_event_control/portal_storm_narsie, "an apocalypse rune")
-
- qdel(src)
-
-/obj/effect/rune/apocalypse/proc/image_handler(list/images, duration)
- var/end = world.time + duration
- set waitfor = 0
- while(end>world.time)
- for(var/image/I in images)
- I.override = FALSE
- animate(I, alpha = 0, time = 25, flags = ANIMATION_PARALLEL)
- sleep(3.5 SECONDS)
- for(var/image/I in images)
- animate(I, alpha = 255, time = 25, flags = ANIMATION_PARALLEL)
- sleep(2.5 SECONDS)
- for(var/image/I in images)
- if(I.icon_state != "bloodsparkles")
- I.override = TRUE
- sleep(19 SECONDS)
-
-
/proc/hudFix(mob/living/carbon/human/target)
if(!target || !target.client)
diff --git a/code/modules/antagonists/wizard/equipment/soulstone.dm b/code/modules/antagonists/wizard/equipment/soulstone.dm
index 71a4204a942d..43faacc1bd66 100644
--- a/code/modules/antagonists/wizard/equipment/soulstone.dm
+++ b/code/modules/antagonists/wizard/equipment/soulstone.dm
@@ -25,6 +25,7 @@
/// Role check, if any needed
var/required_role = /datum/antagonist/cult
grind_results = list(/datum/reagent/hauntium = 25, /datum/reagent/silicon = 10) //can be ground into hauntium
+ var/perfect = FALSE
/obj/item/soulstone/Initialize(mapload)
. = ..()
@@ -295,13 +296,6 @@
return FALSE
if(!forced)
- var/datum/antagonist/cult/cultist = IS_CULTIST(user)
- if(cultist)
- var/datum/team/cult/cult_team = cultist.get_team()
- if(victim.mind && cult_team.is_sacrifice_target(victim.mind))
- to_chat(user, span_cult("\"This soul is mine.SACRIFICE THEM!\""))
- return FALSE
-
if(grab_sleeping ? victim.stat == CONSCIOUS : victim.stat != DEAD)
to_chat(user, "[span_userdanger("Capture failed!")]: Kill or maim the victim first!")
return FALSE
@@ -356,7 +350,7 @@
var/construct_class = show_radial_menu(user, src, GLOB.construct_radial_images, custom_check = CALLBACK(src, PROC_REF(check_menu), user, shell), require_near = TRUE, tooltips = TRUE)
if(QDELETED(shell) || !construct_class)
return FALSE
- make_new_construct_from_class(construct_class, theme, shade, user, FALSE, shell.loc)
+ make_new_construct_from_class(construct_class, theme, shade, user, FALSE, shell.loc, perfect)
shade.mind?.remove_antag_datum(/datum/antagonist/cult)
qdel(shell)
qdel(src)
@@ -450,11 +444,14 @@
return TRUE
-/proc/make_new_construct_from_class(construct_class, theme, mob/target, mob/creator, cultoverride, loc_override)
+/proc/make_new_construct_from_class(construct_class, theme, mob/target, mob/creator, cultoverride, loc_override, perfect = FALSE)
switch(construct_class)
if(CONSTRUCT_JUGGERNAUT)
if(IS_CULTIST(creator))
- make_new_construct(/mob/living/basic/construct/juggernaut, target, creator, cultoverride, loc_override) // ignore themes, the actual giving of cult info is in the make_new_construct proc
+ if(perfect)
+ make_new_construct(/mob/living/basic/construct/juggernaut/perfect, target, creator, cultoverride, loc_override)
+ else
+ make_new_construct(/mob/living/basic/construct/juggernaut, target, creator, cultoverride, loc_override) // ignore themes, the actual giving of cult info is in the make_new_construct proc
return
switch(theme)
if(THEME_WIZARD)
@@ -476,7 +473,10 @@
make_new_construct(/mob/living/basic/construct/wraith, target, creator, cultoverride, loc_override)
if(CONSTRUCT_ARTIFICER)
if(IS_CULTIST(creator))
- make_new_construct(/mob/living/basic/construct/artificer, target, creator, cultoverride, loc_override) // ignore themes, the actual giving of cult info is in the make_new_construct proc
+ if(perfect)
+ make_new_construct(/mob/living/basic/construct/artificer/perfect, target, creator, cultoverride, loc_override)
+ else
+ make_new_construct(/mob/living/basic/construct/artificer, target, creator, cultoverride, loc_override) // ignore themes, the actual giving of cult info is in the make_new_construct proc
return
switch(theme)
if(THEME_WIZARD)
@@ -500,17 +500,16 @@
var/datum/action/innate/seek_master/seek_master = new
seek_master.Grant(newstruct)
target.mind?.transfer_to(newstruct, force_key_move = TRUE)
- var/atom/movable/screen/alert/bloodsense/sense_alert
if(newstruct.mind && !IS_CULTIST(newstruct) && ((stoner && IS_CULTIST(stoner)) || cultoverride) && SSticker.HasRoundStarted())
- newstruct.mind.add_antag_datum(/datum/antagonist/cult/construct)
+ var/datum/team/cult/cult_team = locate_team(/datum/team/cult)
+
+ if(cult_team.CanConvert(newstruct.construct_type))
+ newstruct.mind.add_antag_datum(/datum/antagonist/cult)
+
if(IS_CULTIST(stoner) || cultoverride)
to_chat(newstruct, span_cultbold("You are still bound to serve the cult[stoner ? " and [stoner]" : ""], follow [stoner?.p_their() || "their"] orders and help [stoner?.p_them() || "them"] complete [stoner?.p_their() || "their"] goals at all costs."))
else if(stoner)
to_chat(newstruct, span_boldwarning("You are still bound to serve your creator, [stoner], follow [stoner.p_their()] orders and help [stoner.p_them()] complete [stoner.p_their()] goals at all costs."))
- newstruct.clear_alert("bloodsense")
- sense_alert = newstruct.throw_alert("bloodsense", /atom/movable/screen/alert/bloodsense)
- if(sense_alert)
- sense_alert.Cviewer = newstruct
newstruct.cancel_camera()
/obj/item/soulstone/anybody
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 42d1e5e67ad1..1ed9c36f2db7 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -1195,6 +1195,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
M.update_damage_hud()
attempt_auto_fit_viewport()
+ if(mob)
+ mob.UpdateUIScreenLoc()
+
/client/proc/generate_clickcatcher()
if(!void)
void = new()
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index fe32f251f9bb..bc540f53ed4e 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -2,6 +2,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
/datum/preferences
var/client/parent
+ var/parent_key
/// The path to the general savefile for this datum
var/path
/// Whether or not we allow saving/loading. Used for guests, if they're enabled
@@ -98,6 +99,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
/datum/preferences/New(client/parent)
src.parent = parent
+ src.parent_key = parent?.key
for (var/middleware_type in subtypesof(/datum/preference_middleware))
middleware += new middleware_type(src)
diff --git a/code/modules/client/preferences/ghost.dm b/code/modules/client/preferences/ghost.dm
index ae84249507dd..045e4a25cd54 100644
--- a/code/modules/client/preferences/ghost.dm
+++ b/code/modules/client/preferences/ghost.dm
@@ -89,7 +89,9 @@
if (!client.is_content_unlocked())
return
- ghost.update_icon(ALL, value)
+ if(!GLOB.eclipse?.eclipse_start_time)
+ ghost.icon = initial(ghost.icon)
+ ghost.update_icon(ALL, value)
/datum/preference/choiced/ghost_form/compile_constant_data()
var/list/data = ..()
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 95ba23548e1d..a4d7e760b215 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -193,8 +193,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
parsed_favs += path
favorite_outfits = unique_list(parsed_favs)
- load_metacoins(parent.ckey)
- load_inventory(parent.ckey)
+ load_metacoins(ckey(parent_key))
+ load_inventory(ckey(parent_key))
load_preferences_monkestation()
@@ -237,7 +237,6 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
default_slot = old_default_slot
max_save_slots = old_max_save_slots
save_preferences()
-
return TRUE
/datum/preferences/proc/save_preferences()
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 849468a93b4a..67559879db4c 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -157,12 +157,6 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
if(!invisibility || camera.see_ghosts)
return "You can also see a g-g-g-g-ghooooost!"
-/mob/dead/observer/narsie_act()
- var/old_color = color
- color = "#960000"
- animate(src, color = old_color, time = 10, flags = ANIMATION_PARALLEL)
- addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_atom_colour)), 10)
-
/mob/dead/observer/Destroy()
if(data_huds_on)
remove_data_huds()
diff --git a/code/modules/mob/living/basic/cult/constructs/_construct.dm b/code/modules/mob/living/basic/cult/constructs/_construct.dm
index 162c6fe39df5..36e4551b630c 100644
--- a/code/modules/mob/living/basic/cult/constructs/_construct.dm
+++ b/code/modules/mob/living/basic/cult/constructs/_construct.dm
@@ -52,6 +52,8 @@
THEME_WIZARD = list(/obj/item/ectoplasm/mystic),
)
+ var/purge = FALSE
+
/mob/living/basic/construct/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_flying)
@@ -96,14 +98,16 @@
spell_count++
update_action_buttons()
- if(icon_state)
+ if(icon_state && !new_glow)
add_overlay("glow_[icon_state]_[theme]")
+ update_appearance()
/mob/living/basic/construct/Login()
. = ..()
if(!. || !client)
return FALSE
to_chat(src, span_bold(playstyle_string))
+ update_appearance()
/mob/living/basic/construct/examine(mob/user)
var/text_span
diff --git a/code/modules/mob/living/basic/cult/constructs/artificer.dm b/code/modules/mob/living/basic/cult/constructs/artificer.dm
index bf4a086bcdb2..3868a6fa6e1f 100644
--- a/code/modules/mob/living/basic/cult/constructs/artificer.dm
+++ b/code/modules/mob/living/basic/cult/constructs/artificer.dm
@@ -30,6 +30,7 @@
can_repair = TRUE
can_repair_self = TRUE
smashes_walls = TRUE
+ construct_type = "Artificer"
///The health HUD applied to this mob.
var/health_hud = DATA_HUD_MEDICAL_ADVANCED
diff --git a/code/modules/mob/living/basic/cult/constructs/harvester.dm b/code/modules/mob/living/basic/cult/constructs/harvester.dm
index 30b309948728..f275e0bb3e6c 100644
--- a/code/modules/mob/living/basic/cult/constructs/harvester.dm
+++ b/code/modules/mob/living/basic/cult/constructs/harvester.dm
@@ -74,10 +74,6 @@
var/datum/antagonist/cult/cult_status = owner.mind.has_antag_datum(/datum/antagonist/cult)
if(!cult_status)
return
- var/datum/objective/eldergod/summon_objective = locate() in cult_status.cult_team.objectives
-
- if(summon_objective.check_completion())
- the_construct.master = cult_status.cult_team.blood_target
if(!the_construct.master)
to_chat(the_construct, span_cultitalic("You have no master to seek!"))
diff --git a/code/modules/mob/living/basic/cult/constructs/juggernaut.dm b/code/modules/mob/living/basic/cult/constructs/juggernaut.dm
index 2b8bb7e293d8..66780720ffb3 100644
--- a/code/modules/mob/living/basic/cult/constructs/juggernaut.dm
+++ b/code/modules/mob/living/basic/cult/constructs/juggernaut.dm
@@ -26,6 +26,7 @@
playstyle_string = span_bold("You are a Juggernaut. Though slow, your shell can withstand heavy punishment, create shield walls, rip apart enemies and walls alike, and even deflect energy weapons.")
smashes_walls = TRUE
+ construct_type = "Juggernaut"
/// Hostile NPC version. Pretty dumb, just attacks whoever is near.
/mob/living/basic/construct/juggernaut/hostile
diff --git a/code/modules/mob/living/basic/cult/constructs/wraith.dm b/code/modules/mob/living/basic/cult/constructs/wraith.dm
index 06a09b6446ed..598b80c7c3a4 100644
--- a/code/modules/mob/living/basic/cult/constructs/wraith.dm
+++ b/code/modules/mob/living/basic/cult/constructs/wraith.dm
@@ -14,9 +14,9 @@
attack_vis_effect = ATTACK_EFFECT_SLASH
construct_spells = list(
/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift,
- /datum/action/innate/cult/create_rune/tele,
)
playstyle_string = span_bold("You are a Wraith. Though relatively fragile, you are fast, deadly, and can phase through walls. Your attacks will lower the cooldown on phasing, moreso for fatal blows.")
+ construct_type = "Wraith"
/mob/living/basic/construct/wraith/Initialize(mapload)
. = ..()
diff --git a/code/modules/mob/living/basic/cult/shade.dm b/code/modules/mob/living/basic/cult/shade.dm
index fac1d347665e..3cc20d4b67b4 100644
--- a/code/modules/mob/living/basic/cult/shade.dm
+++ b/code/modules/mob/living/basic/cult/shade.dm
@@ -1,3 +1,4 @@
+/*
/mob/living/basic/shade
name = "Shade"
real_name = "Shade"
@@ -35,6 +36,7 @@
THEME_HOLY = list(/obj/item/ectoplasm/angelic),
THEME_WIZARD = list(/obj/item/ectoplasm/mystic),
)
+ var/soulblade_ritual = FALSE
/mob/living/basic/shade/Initialize(mapload)
. = ..()
@@ -69,3 +71,4 @@
stone.capture_shade(src, user)
else
. = ..()
+*/
diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm
index 38422a58979b..cc60aaabb832 100644
--- a/code/modules/mob/living/carbon/carbon_update_icons.dm
+++ b/code/modules/mob/living/carbon/carbon_update_icons.dm
@@ -257,7 +257,7 @@
var/list/hands = list()
for(var/obj/item/I in held_items)
if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- I.screen_loc = ui_hand_position(get_held_index_of_item(I))
+ I.screen_loc = ui_hand_position(get_held_index_of_item(I), y_pixel_offset = I.base_pixel_y, x_pixel_offset = I.base_pixel_x)
client.screen += I
if(length(observers))
for(var/mob/dead/observe as anything in observers)
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index c3ee8c1eda32..bdf6ad4477c3 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -592,7 +592,7 @@ There are several things that need to be remembered:
var/list/hands = list()
for(var/obj/item/worn_item in held_items)
if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
- worn_item.screen_loc = ui_hand_position(get_held_index_of_item(worn_item))
+ worn_item.screen_loc = ui_hand_position(get_held_index_of_item(worn_item), y_pixel_offset = worn_item.base_pixel_y, x_pixel_offset = worn_item.base_pixel_x)
client.screen += worn_item
if(observers?.len)
for(var/M in observers)
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index 7025795385d9..7ce93a7a7f02 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -66,6 +66,7 @@ GLOBAL_LIST_INIT(department_radio_keys, list(
GLOBAL_LIST_INIT(message_modes_stat_limits, list(
MODE_INTERCOM = HARD_CRIT,
MODE_CHANGELING = HARD_CRIT,
+ MODE_CULT = HARD_CRIT,
MODE_ALIEN = HARD_CRIT,
MODE_BINARY = HARD_CRIT, //extra stat check on human/binarycheck()
MODE_MONKEY = HARD_CRIT,
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index c6d6b6a5e9d7..5bdcba646cb7 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -21,3 +21,5 @@
med_hud_set_status()
update_fov_client()
+
+ ResendAllUIs()
diff --git a/code/modules/mob/living/logout.dm b/code/modules/mob/living/logout.dm
index 1b21d7f676be..1f3da3eb44f8 100644
--- a/code/modules/mob/living/logout.dm
+++ b/code/modules/mob/living/logout.dm
@@ -4,3 +4,5 @@
if(!key && mind) //key and mind have become separated.
mind.active = FALSE //This is to stop say, a mind.transfer_to call on a corpse causing a ghost to re-enter its body.
med_hud_set_status()
+
+ RemoveAllUIs()
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 8102b105741d..06b31bdfe917 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -3,6 +3,7 @@
stop_automated_movement_when_pulled = 0
obj_damage = 40
environment_smash = ENVIRONMENT_SMASH_STRUCTURES // Set to ENVIRONMENT_SMASH_STRUCTURES to break closets,tables,racks, etc; ENVIRONMENT_SMASH_WALLS for walls; ENVIRONMENT_SMASH_RWALLS for rwalls
+
///The current target of our attacks, use GiveTarget and LoseTarget to set this var
var/atom/target
///Does this mob use ranged attacks?
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 2f08bbc8ab2f..669f2789762c 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -19,13 +19,17 @@
*/
/client/proc/Move_object(direct)
if(mob?.control_object)
- if(mob.control_object.density)
- step(mob.control_object,direct)
- if(!mob.control_object)
- return
- mob.control_object.setDir(direct)
+ if(istype(mob.control_object, /datum/control))
+ var/datum/control/control = mob.control_object
+ control.Move_object(direct)
else
- mob.control_object.forceMove(get_step(mob.control_object,direct))
+ if(mob.control_object.density)
+ step(mob.control_object,direct)
+ if(!mob.control_object)
+ return
+ mob.control_object.setDir(direct)
+ else
+ mob.control_object.forceMove(get_step(mob.control_object,direct))
/**
* Move a client in a direction
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index 49bae89b563c..157d6e0c9f1a 100644
--- a/code/modules/power/singularity/narsie.dm
+++ b/code/modules/power/singularity/narsie.dm
@@ -78,9 +78,6 @@
for (var/_cult_team in all_cults)
var/datum/team/cult/cult_team = _cult_team
cult_team.set_blood_target(src, duration = INFINITY)
- var/datum/objective/eldergod/summon_objective = locate() in cult_team.objectives
- if(summon_objective)
- summon_objective.summoned = TRUE
for (var/datum/mind/cult_mind as anything in get_antag_minds(/datum/antagonist/cult))
if (isliving(cult_mind.current))
@@ -107,13 +104,6 @@
continue
all_cults |= cultist.cult_team
- for(var/_cult_team in all_cults)
- var/datum/team/cult/cult_team = _cult_team
- var/datum/objective/eldergod/summon_objective = locate() in cult_team.objectives
- if (summon_objective)
- summon_objective.summoned = FALSE
- summon_objective.killed = TRUE
-
if (GLOB.cult_narsie == src)
GLOB.cult_narsie = null
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 4d3625b7a5ad..7e160eb382ab 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -215,6 +215,8 @@
dissipate_delay = 10
time_since_last_dissipiation = 0
dissipate_strength = 1
+ if(chained)
+ overlays += image(icon = icon, icon_state = "chain_s1")
if(STAGE_TWO)
if(check_cardinals_range(1, TRUE))
current_size = STAGE_TWO
@@ -227,6 +229,8 @@
dissipate_delay = 5
time_since_last_dissipiation = 0
dissipate_strength = 5
+ if(chained)
+ overlays += image(icon = icon, icon_state = "chain_s3")
if(STAGE_THREE)
if(check_cardinals_range(2, TRUE))
current_size = STAGE_THREE
@@ -239,6 +243,8 @@
dissipate_delay = 4
time_since_last_dissipiation = 0
dissipate_strength = 20
+ if(chained)
+ overlays += image(icon = icon, icon_state = "chain_s5")
if(STAGE_FOUR)
if(check_cardinals_range(3, TRUE))
current_size = STAGE_FOUR
@@ -251,6 +257,8 @@
dissipate_delay = 10
time_since_last_dissipiation = 0
dissipate_strength = 10
+ if(chained)
+ overlays += image(icon = icon, icon_state = "chain_s7")
if(STAGE_FIVE)//this one also lacks a check for gens because it eats everything
current_size = STAGE_FIVE
icon = 'icons/effects/288x288.dmi'
@@ -260,6 +268,8 @@
new_grav_pull = 10
new_consume_range = 4
dissipate = FALSE //It cant go smaller due to e loss
+ if(chained)
+ overlays += image(icon = icon, icon_state = "chain_s9")
if(STAGE_SIX) //This only happens if a stage 5 singulo consumes a supermatter shard.
current_size = STAGE_SIX
icon = 'icons/effects/352x352.dmi'
@@ -270,6 +280,7 @@
new_consume_range = 5
dissipate = FALSE
+
var/datum/component/singularity/resolved_singularity = singularity_component.resolve()
if (!isnull(resolved_singularity))
resolved_singularity.consume_range = new_consume_range
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index c3f0a45e2aa4..d70d3d50c658 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -211,6 +211,8 @@
var/parried = FALSE
///how long we paralyze for as this is a disorient
var/paralyze_timer = 0
+ ///the angle we add when rotating via matrix (used by projectiles that are drawn diagonally)
+ var/extra_rotation = 0
/obj/projectile/Initialize(mapload)
. = ..()
@@ -805,7 +807,7 @@
original_angle = Angle
if(!nondirectional_sprite)
var/matrix/matrix = new
- matrix.Turn(Angle)
+ matrix.Turn(Angle + extra_rotation)
transform = matrix
trajectory_ignore_forcemove = TRUE
forceMove(starting)
@@ -826,7 +828,7 @@
Angle = new_angle
if(!nondirectional_sprite)
var/matrix/matrix = new
- matrix.Turn(Angle)
+ matrix.Turn(Angle + extra_rotation)
transform = matrix
if(trajectory)
trajectory.set_angle(new_angle)
@@ -842,7 +844,7 @@
Angle = new_angle
if(!nondirectional_sprite)
var/matrix/matrix = new
- matrix.Turn(Angle)
+ matrix.Turn(Angle + extra_rotation)
transform = matrix
if(trajectory)
trajectory.set_angle(new_angle)
@@ -923,7 +925,7 @@
last_projectile_move = world.time
if(!nondirectional_sprite && !hitscanning)
var/matrix/matrix = new
- matrix.Turn(Angle)
+ matrix.Turn(Angle + extra_rotation)
transform = matrix
if(homing)
process_homing()
@@ -1111,7 +1113,7 @@
var/atom/movable/thing = new muzzle_type
p.move_atom_to_src(thing)
var/matrix/matrix = new
- matrix.Turn(original_angle)
+ matrix.Turn(original_angle + extra_rotation)
thing.transform = matrix
thing.color = color
thing.set_light(l_outer_range = muzzle_flash_range, l_power = muzzle_flash_intensity, l_color = muzzle_flash_color_override? muzzle_flash_color_override : color)
@@ -1121,7 +1123,7 @@
var/atom/movable/thing = new impact_type
p.move_atom_to_src(thing)
var/matrix/matrix = new
- matrix.Turn(Angle)
+ matrix.Turn(Angle + extra_rotation)
thing.transform = matrix
thing.color = color
thing.set_light(l_outer_range = impact_light_outer_range, l_power = impact_light_intensity, l_color = impact_light_color_override? impact_light_color_override : color)
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 17ec5771f854..7e5c89a16def 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -409,8 +409,6 @@
to_chat(affected_mob, "[pick(phrase_list["seizure"])].")
if(data["misc"] >= (1 MINUTES)) // 24 units
- if(IS_CULTIST(affected_mob))
- affected_mob.mind.remove_antag_datum(/datum/antagonist/cult)
if(IS_CLOCK(affected_mob))
affected_mob.mind.remove_antag_datum(/datum/antagonist/clock_cultist)
affected_mob.Unconscious(10 SECONDS)
@@ -2702,7 +2700,7 @@
..()
/datum/reagent/determination/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
- if(!significant && volume >= WOUND_DETERMINATION_SEVERE)
+ if(!significant && volume >= 3)
significant = TRUE
affected_mob.apply_status_effect(/datum/status_effect/determined) // in addition to the slight healing, limping cooldowns are divided by 4 during the combat high
diff --git a/code/modules/spells/spell_types/conjure/cult_turfs.dm b/code/modules/spells/spell_types/conjure/cult_turfs.dm
index faaac4728406..5df00a6a99e1 100644
--- a/code/modules/spells/spell_types/conjure/cult_turfs.dm
+++ b/code/modules/spells/spell_types/conjure/cult_turfs.dm
@@ -15,6 +15,11 @@
summon_radius = 0
summon_type = list(/turf/open/floor/engine/cult)
+/datum/action/cooldown/spell/conjure/cult_floor/post_summon(atom/summoned_object, atom/cast_on)
+ . = ..()
+ var/datum/antagonist/cult/cultist = owner.mind?.has_antag_datum(/datum/antagonist/cult)
+ cultist?.gain_devotion(0, DEVOTION_TIER_0, "convert_floor")
+
/datum/action/cooldown/spell/conjure/cult_wall
name = "Summon Cult Wall"
desc = "This spell constructs a cult wall."
@@ -31,3 +36,8 @@
summon_radius = 0
summon_type = list(/turf/closed/wall/mineral/cult/artificer) // We don't want artificer-based runed metal farms.
+
+/datum/action/cooldown/spell/conjure/cult_wall/post_summon(atom/summoned_object, atom/cast_on)
+ . = ..()
+ var/datum/antagonist/cult/cultist = owner.mind?.has_antag_datum(/datum/antagonist/cult)
+ cultist?.gain_devotion(0, DEVOTION_TIER_0, "convert_wall")
diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm
index 2b9d1bd5c96e..de8fad3bf3fa 100644
--- a/code/modules/surgery/bodyparts/helpers.dm
+++ b/code/modules/surgery/bodyparts/helpers.dm
@@ -38,6 +38,17 @@
return get_bodypart(check_zone(which_hand))
+///Get the bodypart for whatever hand we have active, Only relevant for carbons
+/mob/proc/get_inactive_hand()
+ return FALSE
+
+/mob/living/carbon/get_inactive_hand()
+ var/which_hand = BODY_ZONE_PRECISE_L_HAND
+ if((active_hand_index % 2))
+ which_hand = BODY_ZONE_PRECISE_R_HAND
+ return get_bodypart(check_zone(which_hand))
+
+
/mob/proc/has_left_hand(check_disabled = TRUE)
return TRUE
diff --git a/code/modules/unit_tests/antag_conversion.dm b/code/modules/unit_tests/antag_conversion.dm
index 01c05671ef72..f52b356e7660 100644
--- a/code/modules/unit_tests/antag_conversion.dm
+++ b/code/modules/unit_tests/antag_conversion.dm
@@ -40,36 +40,3 @@
TEST_ASSERT(peasant.IsStun(), "Peasant was not stunned after being converted by the leader.") // Conversion stun
TEST_ASSERT(IS_REVOLUTIONARY(peasant), "Peasant did not gain revolution antag datum on conversion.")
TEST_ASSERT_EQUAL(length(revolution.members), 2, "Expected revolution to have 2 members after the leader flashes the peasant.")
-
-/// Tests that cults can convert people with their rune
-/datum/unit_test/cult_conversion
-
-/datum/unit_test/cult_conversion/Run()
- var/mob/living/carbon/human/cult_a = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
- var/mob/living/carbon/human/cult_b = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
- var/mob/living/carbon/human/new_cultist = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
-
- cult_a.mind_initialize()
- cult_a.mock_client = new()
- cult_b.mind_initialize()
- cult_b.mock_client = new()
- new_cultist.mind_initialize()
- new_cultist.mock_client = new()
-
- var/datum/antagonist/cult/a_cult_datum = cult_a.mind.add_antag_datum(/datum/antagonist/cult)
- var/datum/team/cult/cult_team = a_cult_datum.get_team()
-
- var/obj/effect/rune/convert/convert_rune = allocate(/obj/effect/rune/convert, run_loc_floor_bottom_left)
-
- // Fail case
- cult_a.ClickOn(convert_rune)
- TEST_ASSERT(!IS_CULTIST(new_cultist), "New cultist became a cultist with only 1 person converting them.")
- TEST_ASSERT_EQUAL(length(cult_team.members), 1, "Expected cult to have 1 member after the cultist failed to convert anyone.")
- cult_a.next_move = 0
- cult_a.next_click = 0
-
- // Success case
- cult_b.mind.add_antag_datum(/datum/antagonist/cult)
- cult_a.ClickOn(convert_rune)
- TEST_ASSERT(IS_CULTIST(new_cultist), "New cultist did not become a cultist after being converted by two people.")
- TEST_ASSERT_EQUAL(length(cult_team.members), 3, "Expected cult to have 3 members after the cultists convert the new cultist.")
diff --git a/icons/obj/clothing/accessories.dmi b/icons/obj/clothing/accessories.dmi
index b3ca92160cce..5f9aa90d9669 100644
Binary files a/icons/obj/clothing/accessories.dmi and b/icons/obj/clothing/accessories.dmi differ
diff --git a/icons/obj/wizard.dmi b/icons/obj/wizard.dmi
index 6b8309b90382..ae6f9cdba2ca 100644
Binary files a/icons/obj/wizard.dmi and b/icons/obj/wizard.dmi differ
diff --git a/interface/fonts/YouMurdererBB12pt.dm b/interface/fonts/YouMurdererBB12pt.dm
new file mode 100644
index 000000000000..03d379034bca
--- /dev/null
+++ b/interface/fonts/YouMurdererBB12pt.dm
@@ -0,0 +1,255 @@
+/// Base font
+/datum/font/YouMurdererBB
+ name = "YouMurderer BB"
+ font_family = 'interface/fonts/youmurdererbb_reg.ttf'
+
+/*
+ YouMurderer BB 12pt
+
+ For use with the DmiFontsPlus library (LummoxJR.DmiFontsPlus)
+
+ Generated by DmiFontsPlus version 1
+ */
+
+/datum/font/YouMurdererBB/size_12pt
+ name = "YouMurderer BB 12pt"
+ height = 14
+ ascent = 11
+ descent = 3
+ average_width = 5
+ max_width = 16
+ overhang = 0
+ in_leading = -2
+ ex_leading = 0
+ default_character = 31
+ start = 30
+ end = 255
+
+ metrics = list(\
+ 1, 6, 1, /* char 30 */
+ 1, 6, 1, /* char 31 */
+ 0, 1, 3, /* char 32 */
+ 0, 3, 0, /* char 33 */
+ 0, 4, 0, /* char 34 */
+ 0, 7, 0, /* char 35 */
+ 0, 7, -1, /* char 36 */
+ 0, 4, 0, /* char 37 */
+ 0, 6, 0, /* char 38 */
+ 0, 4, -1, /* char 39 */
+ -2, 6, 0, /* char 40 */
+ 0, 3, 1, /* char 41 */
+ 0, 5, 0, /* char 42 */
+ 0, 5, 0, /* char 43 */
+ 0, 3, 0, /* char 44 */
+ 0, 9, -3, /* char 45 */
+ 1, 1, 1, /* char 46 */
+ 0, 4, 1, /* char 47 */
+ 0, 7, -1, /* char 48 */
+ 0, 6, -1, /* char 49 */
+ 0, 6, -1, /* char 50 */
+ 0, 5, 0, /* char 51 */
+ 0, 6, -1, /* char 52 */
+ 0, 5, 1, /* char 53 */
+ 0, 5, 0, /* char 54 */
+ 0, 5, -1, /* char 55 */
+ 0, 6, 0, /* char 56 */
+ 0, 7, -2, /* char 57 */
+ 0, 2, 1, /* char 58 */
+ 0, 3, 0, /* char 59 */
+ 0, 5, 0, /* char 60 */
+ 0, 6, 0, /* char 61 */
+ 0, 4, 0, /* char 62 */
+ 0, 5, 0, /* char 63 */
+ 0, 7, 1, /* char 64 */
+ 0, 6, 0, /* char 65 */
+ -1, 6, 0, /* char 66 */
+ -1, 8, -2, /* char 67 */
+ -1, 7, 0, /* char 68 */
+ 0, 5, 0, /* char 69 */
+ 0, 6, 0, /* char 70 */
+ 0, 7, 0, /* char 71 */
+ -1, 7, 0, /* char 72 */
+ 0, 9, -3, /* char 73 */
+ 0, 5, 0, /* char 74 */
+ 0, 6, 0, /* char 75 */
+ 0, 7, -2, /* char 76 */
+ -1, 8, -1, /* char 77 */
+ 0, 5, 0, /* char 78 */
+ 0, 8, 0, /* char 79 */
+ 0, 6, -1, /* char 80 */
+ 0, 8, 0, /* char 81 */
+ -1, 7, 0, /* char 82 */
+ 0, 5, 0, /* char 83 */
+ -1, 8, -2, /* char 84 */
+ 0, 7, -1, /* char 85 */
+ 0, 4, 0, /* char 86 */
+ 0, 8, -1, /* char 87 */
+ 0, 5, 0, /* char 88 */
+ 0, 5, -1, /* char 89 */
+ -1, 7, 0, /* char 90 */
+ 1, 4, 0, /* char 91 */
+ 0, 4, 0, /* char 92 */
+ 0, 4, 0, /* char 93 */
+ 0, 4, 1, /* char 94 */
+ 1, 7, 0, /* char 95 */
+ 0, 4, 0, /* char 96 */
+ 0, 6, -2, /* char 97 */
+ -1, 7, 0, /* char 98 */
+ -1, 8, -1, /* char 99 */
+ 0, 6, 0, /* char 100 */
+ 0, 7, -1, /* char 101 */
+ 0, 6, 0, /* char 102 */
+ -1, 8, -1, /* char 103 */
+ 0, 7, -1, /* char 104 */
+ 0, 3, 0, /* char 105 */
+ 0, 5, 0, /* char 106 */
+ -1, 8, -1, /* char 107 */
+ 0, 6, -1, /* char 108 */
+ 0, 7, 0, /* char 109 */
+ 0, 5, 0, /* char 110 */
+ 0, 7, -1, /* char 111 */
+ -1, 7, -1, /* char 112 */
+ -1, 10, -1, /* char 113 */
+ 0, 6, -1, /* char 114 */
+ 0, 6, 0, /* char 115 */
+ -1, 7, -2, /* char 116 */
+ 0, 6, 0, /* char 117 */
+ 0, 5, 0, /* char 118 */
+ 0, 7, 0, /* char 119 */
+ 0, 6, 0, /* char 120 */
+ 0, 6, -1, /* char 121 */
+ -2, 8, 0, /* char 122 */
+ 1, 7, 1, /* char 123 */
+ 1, 2, 0, /* char 124 */
+ 0, 8, 1, /* char 125 */
+ 1, 5, 0, /* char 126 */
+ 1, 6, 1, /* char 127 */
+ 1, 6, 1, /* char 128 */
+ 1, 6, 1, /* char 129 */
+ 1, 6, 1, /* char 130 */
+ 1, 6, 1, /* char 131 */
+ 1, 6, 1, /* char 132 */
+ 1, 5, 1, /* char 133 */
+ 1, 6, 1, /* char 134 */
+ 1, 6, 1, /* char 135 */
+ 1, 6, 1, /* char 136 */
+ 1, 6, 1, /* char 137 */
+ 1, 6, 1, /* char 138 */
+ 1, 6, 1, /* char 139 */
+ 1, 6, 1, /* char 140 */
+ 1, 6, 1, /* char 141 */
+ 1, 6, 1, /* char 142 */
+ 1, 6, 1, /* char 143 */
+ 1, 6, 1, /* char 144 */
+ 0, 4, -1, /* char 145 */
+ 0, 3, 0, /* char 146 */
+ 0, 6, -2, /* char 147 */
+ 0, 5, 0, /* char 148 */
+ 1, 6, 1, /* char 149 */
+ 1, 6, 1, /* char 150 */
+ 1, 6, 1, /* char 151 */
+ 1, 6, 1, /* char 152 */
+ 0, 7, 0, /* char 153 */
+ 1, 6, 1, /* char 154 */
+ 1, 6, 1, /* char 155 */
+ 1, 6, 1, /* char 156 */
+ 1, 6, 1, /* char 157 */
+ 1, 6, 1, /* char 158 */
+ 1, 6, 1, /* char 159 */
+ 1, 6, 1, /* char 160 */
+ 1, 6, 1, /* char 161 */
+ -1, 8, -1, /* char 162 */
+ 0, 7, 0, /* char 163 */
+ 1, 6, 1, /* char 164 */
+ 1, 6, 1, /* char 165 */
+ 1, 6, 1, /* char 166 */
+ 1, 6, 1, /* char 167 */
+ 1, 6, 1, /* char 168 */
+ 0, 9, 0, /* char 169 */
+ 1, 6, 1, /* char 170 */
+ 1, 6, 1, /* char 171 */
+ 1, 6, 1, /* char 172 */
+ 1, 6, 1, /* char 173 */
+ 0, 9, 0, /* char 174 */
+ 1, 6, 1, /* char 175 */
+ 1, 6, 1, /* char 176 */
+ 1, 6, 1, /* char 177 */
+ 1, 6, 1, /* char 178 */
+ 1, 6, 1, /* char 179 */
+ 1, 6, 1, /* char 180 */
+ 1, 6, 1, /* char 181 */
+ 1, 6, 1, /* char 182 */
+ 1, 6, 1, /* char 183 */
+ 1, 6, 1, /* char 184 */
+ 1, 6, 1, /* char 185 */
+ 1, 6, 1, /* char 186 */
+ 1, 6, 1, /* char 187 */
+ 1, 6, 1, /* char 188 */
+ 1, 6, 1, /* char 189 */
+ 1, 6, 1, /* char 190 */
+ 1, 6, 1, /* char 191 */
+ 1, 6, 1, /* char 192 */
+ 1, 6, 1, /* char 193 */
+ 1, 6, 1, /* char 194 */
+ 1, 6, 1, /* char 195 */
+ 1, 6, 1, /* char 196 */
+ 1, 6, 1, /* char 197 */
+ 1, 6, 1, /* char 198 */
+ 1, 6, 1, /* char 199 */
+ 1, 6, 1, /* char 200 */
+ 1, 6, 1, /* char 201 */
+ 1, 6, 1, /* char 202 */
+ 1, 6, 1, /* char 203 */
+ 1, 6, 1, /* char 204 */
+ 1, 6, 1, /* char 205 */
+ 1, 6, 1, /* char 206 */
+ 1, 6, 1, /* char 207 */
+ 1, 6, 1, /* char 208 */
+ 1, 6, 1, /* char 209 */
+ 1, 6, 1, /* char 210 */
+ 1, 6, 1, /* char 211 */
+ 0, 1, 15, /* char 212 */
+ 0, 1, 15, /* char 213 */
+ 1, 6, 1, /* char 214 */
+ 1, 6, 1, /* char 215 */
+ 1, 6, 1, /* char 216 */
+ 1, 6, 1, /* char 217 */
+ 1, 6, 1, /* char 218 */
+ 1, 6, 1, /* char 219 */
+ 1, 6, 1, /* char 220 */
+ 1, 6, 1, /* char 221 */
+ 1, 6, 1, /* char 222 */
+ 1, 6, 1, /* char 223 */
+ 1, 6, 1, /* char 224 */
+ 1, 6, 1, /* char 225 */
+ 1, 6, 1, /* char 226 */
+ 1, 6, 1, /* char 227 */
+ 1, 6, 1, /* char 228 */
+ 1, 6, 1, /* char 229 */
+ 1, 6, 1, /* char 230 */
+ 1, 6, 1, /* char 231 */
+ 1, 6, 1, /* char 232 */
+ 1, 6, 1, /* char 233 */
+ 1, 6, 1, /* char 234 */
+ 1, 6, 1, /* char 235 */
+ 1, 6, 1, /* char 236 */
+ 1, 6, 1, /* char 237 */
+ 1, 6, 1, /* char 238 */
+ 1, 6, 1, /* char 239 */
+ 1, 6, 1, /* char 240 */
+ 1, 6, 1, /* char 241 */
+ 1, 6, 1, /* char 242 */
+ 1, 6, 1, /* char 243 */
+ 1, 6, 1, /* char 244 */
+ 1, 6, 1, /* char 245 */
+ 1, 6, 1, /* char 246 */
+ 1, 6, 1, /* char 247 */
+ 1, 6, 1, /* char 248 */
+ 1, 6, 1, /* char 249 */
+ 1, 6, 1, /* char 250 */
+ 1, 6, 1, /* char 251 */
+ 1, 6, 1, /* char 252 */
+ 1, 6, 1, /* char 253 */
+ 1, 6, 1, /* char 254 */
+ 1, 6, 1, /* char 255 */
+ 226)
diff --git a/interface/fonts/youmurdererbb_reg.ttf b/interface/fonts/youmurdererbb_reg.ttf
new file mode 100644
index 000000000000..91e402bc3596
Binary files /dev/null and b/interface/fonts/youmurdererbb_reg.ttf differ
diff --git a/monkestation/code/modules/antagonists/cult/blood_magic.dm b/monkestation/code/modules/antagonists/cult/blood_magic.dm
index 051bf95ca9b1..23cb5b691ad0 100644
--- a/monkestation/code/modules/antagonists/cult/blood_magic.dm
+++ b/monkestation/code/modules/antagonists/cult/blood_magic.dm
@@ -16,7 +16,7 @@
effect_magic_resist(target, user)
else if(target.get_drunk_amount() >= OLD_MAN_HENDERSON_DRUNKENNESS)
effect_henderson(target, user)
- else if(HAS_TRAIT(target, TRAIT_MINDSHIELD) || HAS_MIND_TRAIT(target, TRAIT_OCCULTIST) || HAS_MIND_TRAIT(target, TRAIT_UNCONVERTABLE) || cult.cult_team.cult_ascendent || cult.cult_team.is_sacrifice_target(target.mind))
+ else if(HAS_TRAIT(target, TRAIT_MINDSHIELD) || HAS_MIND_TRAIT(target, TRAIT_OCCULTIST) || HAS_MIND_TRAIT(target, TRAIT_UNCONVERTABLE) || cult.cult_team.cult_ascendent)
effect_weakened(target, user)
else
effect_full(target, user)
diff --git a/monkestation/code/modules/bloodsuckers/monster_hunters/events/wonderland_apocalypse.dm b/monkestation/code/modules/bloodsuckers/monster_hunters/events/wonderland_apocalypse.dm
index 2d2efeb4a8bf..afdc2950d75a 100644
--- a/monkestation/code/modules/bloodsuckers/monster_hunters/events/wonderland_apocalypse.dm
+++ b/monkestation/code/modules/bloodsuckers/monster_hunters/events/wonderland_apocalypse.dm
@@ -163,7 +163,6 @@
trigger_recoil_typecache = typecacheof(list(
/datum/action/innate/cult/blood_spell,
/datum/action/innate/cult/blood_magic,
- /datum/action/innate/cult/master,
/datum/action/innate/clockcult/quick_bind,
/datum/action/cooldown/bloodsucker
))
diff --git a/monkestation/code/modules/bloody_cult/adminbus/_bus.dm b/monkestation/code/modules/bloody_cult/adminbus/_bus.dm
new file mode 100644
index 000000000000..0cae0662f5e8
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/adminbus/_bus.dm
@@ -0,0 +1,584 @@
+///////////////////////////////////////////////////////////////
+//Deity Link, giving a new meaning to the Adminbus since 2014//
+///////////////////////////////////////////////////////////////
+
+//RELEASE PASSENGERS
+
+/obj/vehicle/ridden/adminbus/proc/release_passengers(mob/bususer)
+
+
+ unloading = 1
+
+ for(var/i = passengers.len;i>0;i--)
+ var/atom/A = passengers[i]
+ if(isliving(A))
+ var/mob/living/L = A
+ freed(L)
+ sleep(3)
+
+ unloading = 0
+
+ return
+
+/obj/vehicle/ridden/adminbus/proc/freed(var/mob/living/L)
+ L.forceMove(get_step(src, turn(src.dir, -90)))
+ L.anchored = 0
+ L.pixel_x = 0
+ L.pixel_y = 0
+ to_chat(L, span_notice("Thank you for riding with \the [src], have a secure day.") )
+ passengers -= L
+ update_rearview()
+
+//MOB SPAWNING
+/obj/vehicle/ridden/adminbus/proc/spawn_mob(mob/bususer, var/mob_type, var/count)
+ var/turflist[] = list()
+ for(var/turf/T in orange(src, 1))
+ if((T.density == 0) && (T!= src.loc))
+ turflist += T
+
+ var/invocnum = min(count, turflist.len)
+
+ for(var/i = 0;i 0) //it then travels toward the singulo while creating chains on its path,
+ A.forceMove(get_step_towards(A, S)) //and parenting them together
+ var/obj/structure/singulo_chain/C = new /obj/structure/singulo_chain(A.loc)
+ chain += C
+ C.dir = get_dir(src, S)
+ if(!parentchain)
+ chain_base = C
+ else
+ parentchain.child = C
+ parentchain = C
+ if(!parentchain)
+ chain_base = A
+ else
+ parentchain.child = A
+ chain += A //once the anchor has reached the singulo, it parents itself to the last element in the chain
+ A.target = singulo //and stays on top of the singulo.
+
+/obj/vehicle/ridden/adminbus/proc/throw_hookshot(mob/bususer)
+
+
+ if(!hook && !singulo)
+ return
+
+ if(singulo)
+ var/obj/structure/singulo_chain/anchor/A = locate(/obj/structure/singulo_chain/anchor) in chain
+ if(A)
+ qdel(A)//so we don't drag the singulo back to us along with the rest of the chain.
+ singulo.on_release()
+ singulo = null
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/hoverable/adminbus_hook)
+ while(chain_base)
+ var/obj/structure/singulo_chain/C = chain_base
+ C.move_child(get_turf(src))
+ chain_base = C.child
+ qdel(C)
+ sleep(2)
+
+ for(var/obj/structure/singulo_chain/N in chain)//Just in case some bits of the chain were detached from the bus for whatever reason
+ qdel(N)
+ chain.len = 0
+
+ if(!singulo)
+ hook = 1
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/hoverable/adminbus_hook)
+ else if(hook)
+ hook = 0
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/hoverable/adminbus_hook)
+ var/obj/structure/hookshot/claw/C = new/obj/structure/hookshot/claw(get_step(src, src.dir)) //First we spawn the claw
+ hookshot += C
+ C.abus = src
+
+ var/obj/singularity/S = C.hook_throw(src.dir) //The claw moves forward, spawning hookshot-chains on its path
+ if(S)
+ capture_singulo(S)
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/hoverable/adminbus_hook) //If the claw hits a singulo, we remove the hookshot-chains and replace them with singulo-chains
+ else
+ for(var/obj/structure/hookshot/A in hookshot) //If it doesn't hit anything, all the elements of the chain come back toward the bus,
+ spawn()//so they all return at once //deleting themselves when they reach it.
+ A.hook_back()
+
+/////////////////
+
+/obj/vehicle/ridden/adminbus/proc/mass_rejuvenate(mob/bususer)
+ for(var/mob/living/M in orange(src, 3))
+ M.revive(1)
+ M.set_suicide(0)
+ to_chat(M, span_notice("THE ADMINBUS IS LOVE. THE ADMINBUS IS LIFE.") )
+ sleep(2)
+ update_rearview()
+
+/obj/vehicle/ridden/adminbus/proc/toggle_lights(mob/bususer, var/lightpower = 0)
+
+
+ if(lightpower == roadlights)
+ return
+ var/image/roadlights_image = image(icon, "roadlights")
+ roadlights_image.plane = ABOVE_LIGHTING_PLANE
+ roadlights = lightpower
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_roadlights_low)
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_roadlights_mid)
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_roadlights_high)
+ switch(lightpower)
+ if(0)
+ lightsource.set_light(0)
+ if(roadlights == 1 || roadlights == 2)
+ overlays["roadlights"] = null
+ if(1)
+ lightsource.set_light(2)
+ if(roadlights == 0)
+ overlays["roadlights"] = roadlights_image
+ if(2)
+ lightsource.set_light(3)
+ if(roadlights == 0)
+ overlays["roadlights"] = roadlights_image
+
+ update_lightsource()
+
+/obj/vehicle/ridden/adminbus/proc/toggle_bumpers(mob/bususer, var/bumperpower = 1)
+
+
+ if(bumperpower == bumpers)
+ return
+
+ bumpers = bumperpower
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_bumpers_low)
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_bumpers_mid)
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_bumpers_high)
+
+
+/obj/vehicle/ridden/adminbus/proc/toggle_door(mob/bususer, var/doorstate = 0)
+
+
+ if(doorstate == door_mode)
+ return
+
+ door_mode = doorstate
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_door_closed)
+ bususer.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_door_open)
+ if (door_mode)
+ overlays += image(icon, "opendoor")
+ else
+ overlays -= image(icon, "opendoor")
+
+/obj/vehicle/ridden/adminbus/proc/loadsa_goodies(mob/bususer, var/goodie_type)
+ switch(goodie_type)
+ if(1)
+ visible_message(span_notice("All Access for Everyone!") )
+ if(2)
+ visible_message(span_notice("Loads of Money!") )
+
+ var/joy_sound = list('monkestation/code/modules/bloody_cult/sound/SC4Mayor1.ogg', 'monkestation/code/modules/bloody_cult/sound/SC4Mayor2.ogg', 'monkestation/code/modules/bloody_cult/sound/SC4Mayor3.ogg')
+ playsound(src, pick(joy_sound), 50, 0, 0)
+ var/throwzone = list()
+ for(var/i = 1;i<= 5;i++)
+ throwzone = list()
+ for(var/turf/T in range(src, 5))
+ throwzone += T
+ switch(goodie_type)
+ if(1)
+ var/obj/item/card/id/advanced/gold/captains_spare/S = new/obj/item/card/id/advanced/gold/captains_spare(src.loc)
+ S.throw_at(pick(throwzone), rand(2, 5), 5)
+ if(2)
+ var/obj/item/fuckingmoney = null
+ fuckingmoney = pick(
+ 50;/obj/item/coin/gold,
+ 50;/obj/item/coin/silver,
+ 50;/obj/item/coin/diamond,
+ 40;/obj/item/coin/iron,
+ 50;/obj/item/coin/plasma,
+ 40;/obj/item/coin/uranium,
+ 30;/obj/item/coin/adamantine,
+ 30;/obj/item/coin/mythril,
+ 200;/obj/item/stack/spacecash,
+ 200;/obj/item/stack/spacecash/c10,
+ 200;/obj/item/stack/spacecash/c100,
+ 300;/obj/item/stack/spacecash/c1000
+ )
+ var/obj/item/C = new fuckingmoney(src.loc)
+ C.throw_at(pick(throwzone), rand(2, 5), 5)
+
+/obj/vehicle/ridden/adminbus/proc/give_bombs(mob/bususer)
+
+ var/distributed = 0
+
+ if(length(occupants))
+ var/mob/living/M = occupants[1]
+ if(iscarbon(M))
+ for(var/i = 1 to M.held_items.len)
+ if(M.held_items[i] == null)
+ var/obj/item/grenade/B = new /obj/item/grenade(M)
+ spawnedbombs += B
+ if(!M.put_in_hands(B))
+ qdel(B)
+
+ to_chat(M, span_warning("Lit and throw!") )
+ break
+
+ for(var/mob/living/carbon/C in passengers)
+ for(var/i = 1 to C.held_items.len)
+ if(C.held_items[i] == null)
+ var/obj/item/grenade/B = new /obj/item/grenade(C)
+ spawnedbombs += B
+ if(!C.put_in_hands(B))
+ qdel(B)
+
+ to_chat(C, span_warning("Our benefactors have provided you with a bomb. Lit and throw!") )
+ distributed++
+ break
+
+ update_rearview()
+ to_chat(bususer, "[distributed] bombs distributed to passengers.")
+
+/obj/vehicle/ridden/adminbus/proc/delete_bombs(mob/bususer)
+
+ if(spawnedbombs.len == 0)
+ to_chat(bususer, "No bombs to delete.")
+ return
+
+ var/distributed = 0
+
+ for(var/i = spawnedbombs.len;i>0;i--)
+ var/obj/item/grenade/B = spawnedbombs[i]
+ if(B)
+ if(istype(B.loc, /mob/living/carbon))
+ var/mob/living/carbon/C = B.loc
+ qdel(B)
+ C.regenerate_icons()
+ else
+ qdel(B)
+ distributed++
+ spawnedbombs -= spawnedbombs[i]
+
+ update_rearview()
+ to_chat(bususer, "Deleted all [distributed] bombs.")
+
+
+/obj/vehicle/ridden/adminbus/proc/give_lasers(mob/bususer)
+
+ var/distributed = 0
+
+ if(length(occupants))
+ var/mob/living/M = occupants[1]
+ if(iscarbon(M))
+ var/obj/item/gun/energy/laser/hellgun/L = new /obj/item/gun/energy/laser/hellgun(M)
+
+ if(M.put_in_hands(L))
+ spawnedlasers += L
+ to_chat(M, span_warning("Spray and /pray!") )
+ else
+ qdel(L)
+
+ for(var/mob/living/carbon/C in passengers)
+ var/obj/item/gun/energy/laser/hellgun/L = new /obj/item/gun/energy/laser/hellgun(C)
+
+ if(C.put_in_hands(L))
+ spawnedlasers += L
+ to_chat(C, span_warning("Our benefactors have provided you with an infinite laser gun. Spray and /pray!") )
+ distributed++
+ else
+ qdel(L)
+
+ update_rearview()
+ to_chat(bususer, "[distributed] infinite laser guns distributed to passengers.")
+
+/obj/vehicle/ridden/adminbus/proc/delete_lasers(mob/bususer)
+
+ if(spawnedlasers.len == 0)
+ to_chat(bususer, "No laser guns to delete.")
+ return
+
+ var/distributed = 0
+
+ for(var/i = spawnedlasers.len;i>0;i--)
+ var/obj/item/gun/energy/laser/hellgun/L = spawnedlasers[i]
+ if(L)
+ if(istype(L.loc, /mob/living/carbon))
+ var/mob/living/carbon/C = L.loc
+ qdel(L)
+ C.regenerate_icons()
+ else
+ qdel(L)
+ distributed++
+ spawnedlasers -= spawnedlasers[i]
+
+ update_rearview()
+ to_chat(bususer, "Deleted all [distributed] laser guns.")
+
+/obj/vehicle/ridden/adminbus/proc/Mass_Repair(mob/bususer, var/turf/centerloc = null, var/repair_range = 3)//the proc can be called by others, doing (null,
, )
+
+ visible_message(span_notice("WE BUILD!") )
+
+ if(!centerloc)
+ centerloc = src.loc
+
+ for(var/obj/machinery/M in range(centerloc, repair_range))
+ if(istype(M, /obj/machinery/door/window))//for some reason it makes the windoors' sprite disapear (until you bump into it)
+ continue
+ if(istype(M, /obj/machinery/light))
+ var/obj/machinery/light/L = M
+ L.fix()
+ continue
+ M.update_icon()
+
+
+ for(var/turf/T in range(centerloc, repair_range))
+ if(istype(T, /turf/open/space))
+ if(isspaceturf(T.loc))
+ continue
+ var/obj/item/stack/tile/iron/P = new /obj/item/stack/tile/iron
+ P.place_tile(T)
+ else if(istype(T, /turf/open/floor))
+ var/turf/open/floor/F = T
+ if(F.broken || F.burnt)
+ if(istype(F, /turf/open/floor/plating))
+ F.icon_state = "plating"
+ F.burnt = 0
+ F.broken = 0
+ else
+ F.make_plating()
+
+ for(var/obj/structure/girder/G in range(centerloc, repair_range))
+ var/turf/T = get_turf(G)
+ if(istype(G, /obj/structure/girder/reinforced))
+ T.ChangeTurf(/turf/closed/wall/r_wall)
+ else
+ T.ChangeTurf(/turf/closed/wall)
+ qdel(G)
+
+ for(var/obj/item/shard/S in range(centerloc, repair_range))
+ if(istype(S, /obj/item/shard/plasma))
+ new/obj/item/stack/sheet/plasmaglass(S.loc)
+ else
+ new/obj/item/stack/sheet/glass(S.loc)
+ qdel(S)
+
+/obj/vehicle/ridden/adminbus/proc/Teleportation(mob/bususer)
+
+
+ if(warp.icon_state == "warp_activated")
+ return
+
+ warp.icon_state = "warp_activated"
+
+ var/A
+ A = input(bususer, "Area to jump to", "Teleportation Warp", A) as null|anything in get_sorted_areas()
+ var/area/thearea = A
+ if(!thearea)
+ warp.icon_state = ""
+ return
+
+ var/list/L = list()
+
+ for(var/turf/T in get_area_turfs(thearea.type))
+ L+= T
+
+ if(!L || !L.len)
+ to_chat(bususer, "No area available.")
+ warp.icon_state = ""
+ return
+
+ var/turf/T1 = get_turf(src)
+ var/turf/T2 = pick(L)
+ warp.icon_state = ""
+ forceMove(T2)
+ T1.turf_animation('monkestation/code/modules/bloody_cult/icons/160x160.dmi', "busteleport", -32*2, -32, MOB_LAYER+1, 'monkestation/code/modules/bloody_cult/sound/busteleport.ogg', anim_plane = ABOVE_GAME_PLANE)
+ T2.turf_animation('monkestation/code/modules/bloody_cult/icons/160x160.dmi', "busteleport", -32*2, -32, MOB_LAYER+1, 'monkestation/code/modules/bloody_cult/sound/busteleport.ogg', anim_plane = ABOVE_GAME_PLANE)
+
+
+
+
+/obj/item/packobelongings
+ name = "Unknown's belongings"
+ desc = "Full of stuff."
+ icon = 'icons/obj/storage/storage.dmi'
+ icon_state = "belongings"
+ w_class = WEIGHT_CLASS_NORMAL
+
+/obj/item/packobelongings/New()
+ ..()
+ src.pixel_x = rand(-5, 5)
+ src.pixel_y = rand(-5, 5)
+
+/obj/item/packobelongings/attack_self(mob/user as mob)
+ var/turf/T = get_turf(user)
+ for(var/obj/O in src)
+ O.forceMove(T)
+ qdel(src)
+
+/obj/item/packobelongings/green
+ icon_state = "belongings-green"
+ desc = "Items belonging to one of the Thunderdome contestants."
+
+/obj/item/packobelongings/red
+ icon_state = "belongings-red"
+ desc = "Items belonging to one of the Thunderdome contestants."
+
+/obj/vehicle/ridden/adminbus/proc/Send_Home(mob/bususer)
+
+
+ if(passengers.len == 0)
+ to_chat(bususer, span_warning("There are no passengers to send.") )
+ return
+
+ if(alert(bususer, "Send all mobs among the passengers back where they first appeared? (Risky: This sends them back where their \"object\" was created. If they were cloned they will teleport back at genetics, If they had their species changed they'll spawn back where it happenned, etc...)", "Adminbus", "Yes", "No") != "Yes")
+ return
+
+ var/turf/T1 = get_turf(src)
+ if(T1)
+ T1.turf_animation('monkestation/code/modules/bloody_cult/icons/96x96.dmi', "beamin", -32, 0, MOB_LAYER+1, 'sound/weapons/emitter2.ogg', anim_plane = ABOVE_GAME_PLANE)
+
+ for(var/mob/M in passengers)
+ unbuckle_mob(M, TRUE)
+ freed(M)
+ M.send_back()
+
+ var/turf/T2 = get_turf(M)
+ if(T2)
+ T2.turf_animation('monkestation/code/modules/bloody_cult/icons/96x96.dmi', "beamin", -32, 0, MOB_LAYER+1, 'sound/weapons/emitter2.ogg', anim_plane = ABOVE_GAME_PLANE)
+/*
+/obj/vehicle/ridden/adminbus/proc/Make_Antag(mob/bususer)
+
+
+ if(passengers.len == 0)
+ to_chat(bususer, span_warning("There are no passengers to make antag.") )
+ return
+
+ var/list/delays = list("CANCEL", "No Delay", "10 seconds", "30 seconds", "1 minute", "5 minutes", "15 minutes")
+ var/delay = input("How much delay before the transformation occurs?", "Antag Madness") in delays
+
+ switch(delay)
+ if("CANCEL")
+ return
+ if("No Delay")
+ for(var/mob/M in passengers)
+ spawn()
+ to_chat(M, span_danger("YOU JUST REMEMBERED SOMETHING IMPORTANT!") )
+ sleep(20)
+ antag_madness_adminbus(M)
+ if("10 seconds")
+ antagify_passengers(100)
+ if("30 seconds")
+ antagify_passengers(300)
+ if("1 minute")
+ antagify_passengers(600)
+ if("5 minutes")
+ antagify_passengers(3000)
+ if("15 minutes")
+ antagify_passengers(9000)
+
+
+/obj/vehicle/ridden/adminbus/proc/antagify_passengers(var/delay)
+ for(var/mob/M in passengers)
+ spawn()
+ Delay_Antag(M, delay)
+
+/obj/vehicle/ridden/adminbus/proc/Delay_Antag(var/mob/M, var/delay = 100)
+ if(!M.mind)
+ return
+ if(!ishuman(M) && !ismonkey(M))
+ return
+
+ to_chat(M, span_rose("You feel like you forgot something important!") )
+
+ sleep(delay/2)
+
+ to_chat(M, span_rose("You're starting to remember...") )
+
+ sleep(delay/2)
+
+ to_chat(M, span_danger("OH THAT'S RIGHT!") )
+
+ sleep(20)
+
+ antag_madness_adminbus(M)
+*/
+/obj/vehicle/ridden/adminbus/proc/Mounted_Jukebox(mob/bususer)
+ busjuke.attack_hand(bususer)
+
+/obj/vehicle/ridden/adminbus/proc/Adminbus_Deletion(mob/bususer)//make sure to always use this proc when deleting an adminbus
+ if(bususer)
+ if(alert(bususer, "This will free all passengers, remove any spawned mobs/laserguns/bombs, [singulo ? "free the captured singularity" : ""], and remove all the entities associated with the bus(chains, roadlights, jukebox, ...) Are you sure?", "Adminbus Deletion", "Yes", "No") != "Yes")
+ return
+
+ qdel(src)//RIP ADMINBUS
+
+/mob
+ //Keeps track of where the mob was spawned. Mostly for teleportation purposes. and no, using initial() doesn't work.
+ var/origin_x = 0
+ var/origin_y = 0
+ var/origin_z = 0
+
+/mob/proc/store_position()
+ //updates the players' origin_ vars so they retain their location when the round starts.
+ origin_x = x
+ origin_y = y
+ origin_z = z
+
+/mob/proc/send_back()
+ x = origin_x
+ y = origin_y
+ z = origin_z
diff --git a/monkestation/code/modules/bloody_cult/adminbus/powers.dm b/monkestation/code/modules/bloody_cult/adminbus/powers.dm
new file mode 100644
index 000000000000..88dba7930a6d
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/adminbus/powers.dm
@@ -0,0 +1,611 @@
+///////////////////////////////////////////////////////////////
+//Deity Link, giving a new meaning to the Adminbus since 2014//
+///////////////////////////////////////////////////////////////
+
+/obj/vehicle/ridden/adminbus//Fucking release the passengers and unbuckle yourself from the bus before you delete it.
+ name = "\improper Adminbus"
+ desc = "Shit just got fucking real."
+ icon = 'monkestation/code/modules/bloody_cult/icons/bus.dmi'
+ icon_state = "adminbus"
+ plane = ABOVE_GAME_PLANE
+ pass_flags = PASSMOB
+ pixel_x = -32
+ pixel_y = -32
+ max_buckled_mobs = 16
+ max_occupants = 16
+ var/can_move = 1
+ var/list/passengers = list()
+ var/unloading = 0
+ var/bumpers = 1//1 = capture mobs 2 = roll over mobs(deals light brute damage and push them down) 3 = gib mobs
+ var/door_mode = 0//0 = closed door, players cannot climb or leave on their own 1 = openned door, players can climb and leave on their own
+ var/list/spawned_mobs = list()//keeps track of every mobs spawned by the bus, so we can remove them all with the push of a button in needed
+ var/hook = 1
+ var/list/hookshot = list()
+ var/obj/structure/singulo_chain/chain_base = null
+ var/list/chain = list()
+ var/obj/singularity/singulo = null
+ var/roadlights = 0
+ var/obj/structure/buslight/lightsource = null
+ var/list/spawnedbombs = list()
+ var/list/spawnedlasers = list()
+ var/obj/structure/teleportwarp/warp = null
+ var/obj/machinery/media/jukebox/busjuke = null
+
+/obj/vehicle/ridden/adminbus/New()
+ ..()
+ AddElement(/datum/element/ridable, /datum/component/riding/vehicle/adminbus)
+ var/turf/T = get_turf(src)
+ T.turf_animation('monkestation/code/modules/bloody_cult/icons/160x160.dmi', "busteleport", -64, -32, MOB_LAYER+1, 'monkestation/code/modules/bloody_cult/sound/busteleport.ogg', anim_plane = ABOVE_GAME_PLANE)
+ var/image/underbus = image(icon, "underbus", MOB_LAYER-1)
+ underbus.plane = GAME_PLANE
+ overlays += underbus
+ overlays += image(icon, "ad")
+ src.dir = EAST
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/adminbus.ogg', 50, 0, 0)
+ lightsource = new/obj/structure/buslight(src.loc)
+ update_lightsource()
+ warp = new/obj/structure/teleportwarp(src.loc)
+ busjuke = new/obj/machinery/media/jukebox(src.loc)
+ busjuke.plane = ABOVE_GAME_PLANE
+ busjuke.dir = EAST
+ busjuke.density = FALSE
+ busjuke.alpha = 0
+
+/obj/vehicle/ridden/adminbus/Destroy()
+ for(var/i = passengers.len;i>0;i--)
+ var/atom/A = passengers[i]
+ if(isliving(A))
+ var/mob/living/L = A
+ freed(L)
+
+ delete_bombs()
+ delete_lasers()
+ remove_mobs()
+
+ for(var/obj/structure/singulo_chain/N in chain)
+ chain -= N
+ qdel(N)
+
+ for(var/obj/structure/hookshot/H in hookshot)
+ hookshot -= H
+ qdel(H)
+
+ if (busjuke)
+ busjuke.disconnect_media_source()
+ QDEL_NULL(busjuke)
+ QDEL_NULL(warp)
+ QDEL_NULL(lightsource)
+ QDEL_NULL(singulo)
+
+ var/turf/T = get_turf(src)
+ T.turf_animation('monkestation/code/modules/bloody_cult/icons/160x160.dmi', "busteleport", -32*2, -32, MOB_LAYER+1, 'monkestation/code/modules/bloody_cult/sound/busteleport.ogg', anim_plane = ABOVE_GAME_PLANE)
+
+ ..()
+
+/*
+/obj/vehicle/ridden/adminbus/update_mob()
+ if(occupant)
+ if(iscorgi(occupant))//Hail Ian
+ switch(dir)
+ if(SOUTH)
+ occupant.pixel_x = 6 * 1
+ occupant.pixel_y = -4 * 1
+ if(WEST)
+ occupant.pixel_x = -16 * 1
+ occupant.pixel_y = 9 * 1
+ if(NORTH)
+ occupant.pixel_x = 0
+ occupant.pixel_y = 0
+ if(EAST)
+ occupant.pixel_x = 16 * 1
+ occupant.pixel_y = 9 * 1
+ else
+ switch(dir)
+ if(SOUTH)
+ occupant.pixel_x = 7 * 1
+ occupant.pixel_y = -12 * 1
+ if(WEST)
+ occupant.pixel_x = -25 * 1
+ occupant.pixel_y = 1 * 1
+ if(NORTH)
+ occupant.pixel_x = 0
+ occupant.pixel_y = 0
+ if(EAST)
+ occupant.pixel_x = 25 * 1
+ occupant.pixel_y = 1 * 1
+
+ for(var/i = 1;i< = passengers.len;i++)
+ var/atom/A = passengers[i]
+ if(isliving(A))
+ var/mob/living/L = A
+ switch(i)
+ if(1, 5, 9, 13)
+ switch(dir)
+ if(SOUTH)
+ L.pixel_x = -6 * 1
+ L.pixel_y = 0
+ if(WEST)
+ L.pixel_x = -13 * 1
+ L.pixel_y = 4 * 1
+ if(NORTH)
+ L.pixel_x = -6 * 1
+ L.pixel_y = 0
+ if(EAST)
+ L.pixel_x = 12 * 1
+ L.pixel_y = 4 * 1
+ if(2, 6, 10, 14)
+ switch(dir)
+ if(SOUTH)
+ L.pixel_x = 6 * 1
+ L.pixel_y = 0
+ if(WEST)
+ L.pixel_x = -1 * 1
+ L.pixel_y = 4 * 1
+ if(NORTH)
+ L.pixel_x = 6 * 1
+ L.pixel_y = 0
+ if(EAST)
+ L.pixel_x = 1 * 1
+ L.pixel_y = 4 * 1
+ if(3, 7, 11, 15)
+ switch(dir)
+ if(SOUTH)
+ L.pixel_x = -3 * 1
+ L.pixel_y = 8 * 1
+ if(WEST)
+ L.pixel_x = 11 * 1
+ L.pixel_y = 4 * 1
+ if(NORTH)
+ L.pixel_x = -3 * 1
+ L.pixel_y = 8 * 1
+ if(EAST)
+ L.pixel_x = -11 * 1
+ L.pixel_y = 4 * 1
+ if(4, 8, 12, 16)
+ switch(dir)
+ if(SOUTH)
+ L.pixel_x = 7 * 1
+ L.pixel_y = -12 * 1
+ if(WEST)
+ L.pixel_x = 22 * 1
+ L.pixel_y = 4 * 1
+ if(NORTH)
+ L.pixel_x = -3 * 1
+ L.pixel_y = 8 * 1
+ if(EAST)
+ L.pixel_x = -22 * 1
+ L.pixel_y = 4 * 1
+ L.dir = dir
+*/
+
+/obj/vehicle/ridden/adminbus/Move(atom/newloc, direction, glide_size_override = 0, update_dir = TRUE)
+ var/turf/T = get_turf(src)
+ . = ..()
+ update_lightsource()
+ handle_mob_bumping()
+ if(warp)
+ warp.forceMove(loc)
+ if(busjuke)
+ busjuke.forceMove(loc)
+ busjuke.setDir(dir)
+
+ if(chain_base)
+ chain_base.move_child(T)
+ for(var/obj/structure/hookshot/H in hookshot)
+ H.forceMove(get_step(H, src.dir))
+
+/obj/vehicle/ridden/adminbus/proc/update_lightsource()
+ var/turf/T = get_step(src, src.dir)
+ if(T.opacity)
+ lightsource.forceMove(T)
+ switch(roadlights) //if the bus is right against a wall, only the wall's tile is lit
+ if(0)
+ if(lightsource.light_outer_range != 0)
+ lightsource.set_light(0)
+ if(1, 2)
+ if(lightsource.light_outer_range != 1)
+ lightsource.set_light(1)
+ else
+ T = get_step(T, src.dir) //if there is a wall two tiles in front of the bus, the lightsource is right in front of the bus, though weaker
+ if(T.opacity)
+ lightsource.forceMove(get_step(src, src.dir))
+ switch(roadlights)
+ if(0)
+ if(lightsource.light_outer_range != 0)
+ lightsource.set_light(0)
+ if(1)
+ if(lightsource.light_outer_range != 1)
+ lightsource.set_light(1)
+ if(2)
+ if(lightsource.light_outer_range != 2)
+ lightsource.set_light(2)
+ else
+ lightsource.forceMove(T)
+ switch(roadlights) //otherwise, the lightsource position itself two tiles in front of the bus and with regular light_range
+ if(0)
+ if(lightsource.light_outer_range != 0)
+ lightsource.set_light(0)
+ if(1)
+ if(lightsource.light_outer_range != 2)
+ lightsource.set_light(2)
+ if(2)
+ if(lightsource.light_outer_range != 3)
+ lightsource.set_light(3)
+
+
+/obj/vehicle/ridden/adminbus/proc/handle_mob_bumping()
+ var/turf/S = get_turf(src)
+ switch(bumpers)
+ if(1)
+ for(var/mob/living/L in S)
+ if(!ishuman(L))
+ continue
+ if(L in occupants)
+ continue
+ if(!L.client)
+ continue
+ if(passengers.len < 16)
+ capture_mob(L)
+ else
+ var/mob/living/occupant = occupants[1]
+ if(occupant)
+ to_chat(occupant, span_warning("There is no place in the bus for any additional passenger.") )
+ if(2)
+ var/hit_sound = list('sound/weapons/genhit1.ogg', 'sound/weapons/genhit2.ogg', 'sound/weapons/genhit3.ogg')
+ for(var/mob/living/L in S)
+ if(L in occupants)
+ continue
+ L.take_overall_damage(5, 0)
+ if(L.buckled)
+ L.buckled = 0
+ L.Stun(5)
+ L.Knockdown(5)
+ playsound(src, pick(hit_sound), 50, 0, 0)
+ if(3)
+ for(var/mob/living/L in S)
+ if(L in occupants)
+ continue
+ L.gib()
+ //playsound(src, 'monkestation/code/modules/bloody_cult/sound/bloodyslice.ogg', 50, 0, 0)
+
+/obj/vehicle/ridden/adminbus/proc/capture_mob(atom/A, var/selfclimb = 0)
+ if(passengers.len >= 16)
+ to_chat(A, span_warning("\The [src] is full!") )
+ return
+ if(unloading)
+ return
+ if(isliving(A))
+ var/mob/living/M = A
+ if(M.faction == "adminbus mob")
+ return
+ M.forceMove(loc)
+ M.setDir(dir)
+ passengers += M
+ buckle_mob(M, TRUE)
+ if(!selfclimb)
+ to_chat(M, span_warning("\The [src] picks you up!") )
+ var/mob/living/occupant = occupants[1]
+ if(occupant)
+ to_chat(occupant, "[M.name] captured!")
+ to_chat(M, span_notice("Welcome aboard \the [src]. Please keep your hands and arms inside the bus at all times.") )
+ src.add_fingerprint(M)
+ update_rearview()
+
+/obj/vehicle/ridden/adminbus/buckle_mob(mob/living/M, force = FALSE, check_loc = TRUE, buckle_mob_flags= NONE)
+ if(get_dist(src, M) > 1|| M.buckled || M.stat|| M.buckled|| istype(M, /mob/living/silicon))
+ return
+ var/list/drivers = return_drivers()
+ if(!(check_rights_for(M.client, R_ADMIN)) && !length(drivers))
+ to_chat(M, span_notice("You're a god alright, but you don't seem to have your Adminbus driver license!") )
+ return
+ . = ..()
+/*
+/obj/vehicle/ridden/adminbus/manual_unbuckle(mob/user, var/resisting = FALSE)
+ if(occupant && occupant == user) //Are you the driver?
+ var/mob/living/M = occupant
+ M.visible_message(
+ span_notice("[M.name] unbuckles \himself!") ,
+ "You unbuckle yourself from \the [src].")
+ unlock_atom(M)
+ src.add_fingerprint(user)
+ else
+ if(door_mode)
+ if(locate(user) in passengers)
+ freed(user)
+ return
+ else
+ capture_mob(user, 1)
+ return
+ else
+ if(istype(user, /mob/living/carbon/human/dummy) || istype(user, /mob/living/simple_animal/corgi/Ian))
+ if(locate(user) in passengers)
+ freed(user)
+ return
+ else
+ capture_mob(user, 1)
+ return
+ else
+ if(locate(user) in passengers)
+ to_chat(user, span_notice("You may not leave the Adminbus at the current time.") )
+ return
+ else
+ to_chat(user, span_notice("You may not climb into \the [src] while its door is closed.") )
+ return
+*/
+
+/obj/vehicle/ridden/adminbus/proc/add_HUD(var/mob/user)
+ user.DisplayUI("Adminbus")
+
+/obj/vehicle/ridden/adminbus/proc/remove_HUD(var/mob/M)
+ M.HideUI("Adminbus")
+
+/obj/vehicle/ridden/adminbus/proc/update_rearview()
+ var/mob/living/occupant = occupants[1]
+ if(occupant)
+ occupant.UpdateUIElementIcon(/obj/abstract/mind_ui_element/adminbus_top_panel)
+
+/obj/vehicle/ridden/adminbus/emp_act(severity)
+ return
+
+/obj/vehicle/ridden/adminbus/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
+ visible_message(span_warning("The projectile harmlessly bounces off the bus.") )
+ return ..()
+
+/obj/vehicle/ridden/adminbus/ex_act(severity)
+ visible_message(span_warning("The bus withstands the explosion with no damage.") )
+ return
+
+/obj/vehicle/ridden/adminbus/blob_act()
+ return
+
+/obj/vehicle/ridden/adminbus/singularity_act()
+ return 0
+
+/obj/vehicle/ridden/adminbus/singularity_pull()
+ return 0
+
+/////HOOKSHOT/////////
+
+/obj/structure/hookshot
+ name = "admin chain"
+ desc = "Who knows what these chains can hold..."
+ icon = 'monkestation/code/modules/bloody_cult/icons/singulo_chain.dmi'
+ icon_state = "chain"
+ pixel_x = -32
+ pixel_y = -32
+ density = 0
+ plane = ABOVE_LIGHTING_PLANE
+ var/max_distance = 7
+ var/obj/vehicle/ridden/adminbus/abus = null
+ var/dropped = 0
+
+/obj/structure/hookshot/claw
+ name = "admin claw"
+ icon = 'monkestation/code/modules/bloody_cult/icons/96x96.dmi'
+ icon_state = "singulo_catcher"
+ pixel_x = -32
+ pixel_y = -32
+
+/obj/structure/hookshot/claw/proc/hook_throw(var/toward)
+ max_distance--
+ var/obj/singularity/S = locate(/obj/singularity) in src.loc
+ if(S)
+ return S
+ else
+ var/obj/structure/hookshot/H = new/obj/structure/hookshot(src.loc)
+ abus.hookshot += H
+ H.dir = toward
+ H.max_distance = max_distance
+ H.abus = abus
+ if(max_distance > 0)
+ forceMove(get_step(src, toward))
+ sleep(2)
+ var/obj/singularity/S2 = hook_throw(toward)
+ if(S2)
+ return S2
+ else
+ return null
+ else
+ return null
+
+/obj/structure/hookshot/proc/hook_back()
+ forceMove(get_step_towards(src, abus))
+ max_distance++
+ if(max_distance >= 7)
+ abus.hookshot -= src
+ qdel(src)
+ return
+ sleep(2)
+ .()
+
+/obj/structure/hookshot/claw/hook_back()
+ if(!dropped)
+ var/obj/singularity/S = locate(/obj/singularity) in src.loc
+ if(S)
+ abus.capture_singulo(S)
+ if(length(abus.occupants))
+ var/mob/living/M = abus.occupants[1]
+ M.UpdateUIElementIcon(/obj/abstract/mind_ui_element/hoverable/adminbus_hook)
+ return
+ forceMove(get_step_towards(src, abus))
+ max_distance++
+ if(max_distance >= 7)
+ abus.hookshot -= src
+ abus.hook = 1
+ if(length(abus.occupants))
+ var/mob/living/M = abus.occupants[1]
+ M.UpdateUIElementIcon(/obj/abstract/mind_ui_element/hoverable/adminbus_hook)
+ qdel(src)
+ return
+ sleep(2)
+ .()
+
+/obj/structure/hookshot/ex_act(severity)
+ return
+
+/obj/structure/hookshot/singularity_act()
+ return 0
+
+/obj/structure/hookshot/singularity_pull()
+ return 0
+
+/////SINGULO CHAIN/////////
+
+/obj/structure/singulo_chain
+ name = "singularity chain"
+ desc = "Admins are above all logic."
+ icon = 'monkestation/code/modules/bloody_cult/icons/singulo_chain.dmi'
+ icon_state = "chain"
+ pixel_x = -32
+ pixel_y = -32
+ density = 0
+ var/obj/structure/singulo_chain/child = null
+
+/obj/structure/singulo_chain/anchor
+ icon_state = ""
+ var/obj/singularity/target = null
+
+/obj/structure/singulo_chain/ex_act(severity)
+ return
+
+/obj/structure/singulo_chain/proc/move_child(var/turf/parent)
+ var/turf/T = get_turf(src)
+ if(parent)//I don't see how this could be null but a sanity check won't hurt
+ forceMove(parent)
+ if(child)
+ if(get_dist(src, child) > 1)
+ child.move_child(T)
+ dir = get_dir(child, src)
+ else
+ dir = get_dir(T, src)
+
+/obj/structure/singulo_chain/anchor/move_child(var/turf/parent)
+ var/turf/T = get_turf(src)
+ if(parent)
+ forceMove(parent)
+ else
+ dir = get_dir(T, src)
+ if(target)
+ target.forceMove(loc)
+
+
+/obj/structure/singulo_chain/singularity_act()
+ return 0
+
+/obj/structure/singulo_chain/singularity_pull()
+ return 0
+
+/////ROADLIGHTS/////////
+
+/obj/structure/buslight//the things you have to do to pretend that your bus has directional lights...
+ name = ""
+ desc = ""
+ icon = null
+ icon_state = null
+ anchored = 1
+ density = 0
+ opacity = 0
+ mouse_opacity = 0
+
+/obj/structure/buslight/ex_act(severity)
+ return
+
+/obj/structure/buslight/singularity_act()
+ return 0
+
+/obj/structure/buslight/singularity_pull()
+ return 0
+
+
+/////TELEPORT WARP/////////
+
+/obj/structure/teleportwarp
+ name = "teleportation warp"
+ desc = "The bus is about to jump..."
+ icon = 'monkestation/code/modules/bloody_cult/icons/160x160.dmi'
+ icon_state = ""
+ pixel_x = -64
+ pixel_y = -64
+ anchored = 1
+ density = 0
+ mouse_opacity = 0
+
+/obj/structure/teleportwarp/ex_act(severity)
+ return
+
+/obj/structure/teleportwarp/singularity_pull()
+ return 0
+
+/*
+/datum/locking_category/adminbus/lock(var/atom/movable/AM)
+ . = ..()
+ if (isliving(AM))
+ var/mob/living/M = AM
+ var/obj/vehicle/ridden/adminbus/bus = owner
+ M.flags |= INDESTRUCTIBLE
+ bus.add_HUD(M)
+ M.register_event(/event/living_login, bus, /obj/vehicle/ridden/adminbus/proc/add_HUD)
+
+/datum/locking_category/adminbus/unlock(var/atom/movable/AM)
+ . = ..()
+ if (isliving(AM))
+ var/mob/living/M = AM
+ var/obj/vehicle/ridden/adminbus/bus = owner
+ M.flags &= ~INDESTRUCTIBLE
+ bus.remove_HUD(M)
+ M.unregister_event(/event/living_login, bus, /obj/vehicle/ridden/adminbus/proc/add_HUD)
+
+/obj/vehicle/ridden/adminbus/dissolvable()
+ return 0
+*/
+
+/obj/vehicle/ridden/adminbus/add_occupant(mob/M, control_flags)
+ . = ..()
+ M.store_position()
+ M.status_flags |= GODMODE
+ if(!(M in passengers))
+ capture_mob(M)
+ var/list/drivers = return_drivers()
+ for(var/mob/living/driver as anything in drivers)
+ if(driver.mind)
+ if("Adminbus" in driver.mind.active_uis)
+ continue
+ add_HUD(driver)
+
+/obj/vehicle/ridden/adminbus/remove_occupant(mob/M)
+ . = ..()
+ M.status_flags &= ~GODMODE
+ freed(M)
+ var/list/drivers = return_drivers()
+ if(!(M in drivers))
+ return
+ remove_HUD(M)
+
+/datum/component/riding/vehicle/adminbus
+ vehicle_move_delay = 1
+ override_allow_spacemove = TRUE
+
+
+/datum/component/riding/vehicle/adminbus/handle_specials()
+ . = ..()
+ set_vehicle_offsets(list(TEXT_NORTH = list(-32, -32), TEXT_SOUTH = list(-32, -32), TEXT_EAST = list(-32, -32), TEXT_WEST = list(-32, -32)))
+ set_riding_offsets(1, list(TEXT_NORTH = list(-0, -0), TEXT_SOUTH = list(7, -12), TEXT_EAST = list(25, 1), TEXT_WEST = list(-25, 1)))
+
+ set_riding_offsets(2, list(TEXT_NORTH = list(-6, 0), TEXT_SOUTH = list(-6, 0), TEXT_EAST = list(12, 4), TEXT_WEST = list(-13, 4)))
+ set_riding_offsets(6, list(TEXT_NORTH = list(-6, 0), TEXT_SOUTH = list(-6, 0), TEXT_EAST = list(12, 4), TEXT_WEST = list(-13, 4)))
+ set_riding_offsets(10, list(TEXT_NORTH = list(-6, 0), TEXT_SOUTH = list(-6, 0), TEXT_EAST = list(12, 4), TEXT_WEST = list(-13, 4)))
+ set_riding_offsets(14, list(TEXT_NORTH = list(-6, 0), TEXT_SOUTH = list(-6, 0), TEXT_EAST = list(12, 4), TEXT_WEST = list(-13, 4)))
+
+ set_riding_offsets(3, list(TEXT_NORTH = list(6, 0), TEXT_SOUTH = list(6, 0), TEXT_EAST = list(1, 4), TEXT_WEST = list(-1, 4)))
+ set_riding_offsets(7, list(TEXT_NORTH = list(6, 0), TEXT_SOUTH = list(6, 0), TEXT_EAST = list(1, 4), TEXT_WEST = list(-1, 4)))
+ set_riding_offsets(11, list(TEXT_NORTH = list(6, 0), TEXT_SOUTH = list(6, 0), TEXT_EAST = list(1, 4), TEXT_WEST = list(-1, 4)))
+ set_riding_offsets(15, list(TEXT_NORTH = list(6, 0), TEXT_SOUTH = list(6, 0), TEXT_EAST = list(1, 4), TEXT_WEST = list(-1, 4)))
+
+ set_riding_offsets(4, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(-3, 8), TEXT_EAST = list(-11, 4), TEXT_WEST = list(11, 4)))
+ set_riding_offsets(8, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(-3, 8), TEXT_EAST = list(-11, 4), TEXT_WEST = list(11, 4)))
+ set_riding_offsets(12, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(-3, 8), TEXT_EAST = list(-11, 4), TEXT_WEST = list(11, 4)))
+ set_riding_offsets(16, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(-3, 8), TEXT_EAST = list(-11, 4), TEXT_WEST = list(11, 4)))
+
+ set_riding_offsets(5, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(7, -12), TEXT_EAST = list(-22, 4), TEXT_WEST = list(22, 4)))
+ set_riding_offsets(9, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(7, -12), TEXT_EAST = list(-22, 4), TEXT_WEST = list(22, 4)))
+ set_riding_offsets(13, list(TEXT_NORTH = list(-3, 8), TEXT_SOUTH = list(7, -12), TEXT_EAST = list(-22, 4), TEXT_WEST = list(22, 4)))
diff --git a/monkestation/code/modules/bloody_cult/adminbus/singularity.dm b/monkestation/code/modules/bloody_cult/adminbus/singularity.dm
new file mode 100644
index 000000000000..17aa47d6a996
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/adminbus/singularity.dm
@@ -0,0 +1,23 @@
+/obj/singularity
+ var/chained = FALSE
+
+/obj/singularity/proc/on_capture()
+ chained = TRUE
+ overlays = 0
+ move_self = FALSE
+ switch(current_size)
+ if(1)
+ overlays += image('icons/obj/engine/singularity.dmi', "chain_s1")
+ if(3)
+ overlays += image('icons/effects/96x96.dmi', "chain_s3")
+ if(5)
+ overlays += image('icons/effects/160x160.dmi', "chain_s5")
+ if(7)
+ overlays += image('icons/effects/224x224.dmi', "chain_s7")
+ if(9)
+ overlays += image('icons/effects/288x288.dmi', "chain_s9")
+
+/obj/singularity/proc/on_release()
+ chained = FALSE
+ overlays = 0
+ move_self = TRUE
diff --git a/monkestation/code/modules/bloody_cult/cult/blood_tattoos.dm b/monkestation/code/modules/bloody_cult/cult/blood_tattoos.dm
new file mode 100644
index 000000000000..389156e953f1
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/blood_tattoos.dm
@@ -0,0 +1,126 @@
+// Tattoos are currently unobtainable and being reworked. Blood Daggers and Cult Chat will be available to cultists in other ways until then.
+
+/datum/cult_tattoo
+ var/name = "cult tattoo"
+ var/desc = ""
+ var/tier = 0//1, 2 or 3
+ var/icon_state = ""
+ var/mob/bearer = null
+ var/blood_cost = 0
+
+/datum/cult_tattoo/proc/getTattoo(var/mob/M)
+ bearer = M
+
+/datum/cult_tattoo/proc/Display()
+ return TRUE
+
+/mob/proc/checkTattoo(var/tattoo_name)
+ if (!tattoo_name)
+ return
+ if (!IS_CULTIST(src))
+ return
+ var/datum/antagonist/cult/cult_datum = mind?.has_antag_datum(/datum/antagonist/cult)
+ for (var/tattoo in cult_datum.tattoos)
+ var/datum/cult_tattoo/CT = cult_datum.tattoos[tattoo]
+ if (CT.name == tattoo_name)
+ return CT
+ return null
+
+///////////////////////////
+// //
+// TIER 1 //
+// //
+///////////////////////////
+
+/datum/cult_tattoo/bloodpool
+ name = TATTOO_POOL
+ desc = "All blood costs reduced by 20%. Tributes are split with other bearers of this mark."
+ icon_state = "bloodpool"
+ tier = 1
+
+/datum/cult_tattoo/bloodpool/getTattoo(var/mob/M)
+ ..()
+ var/datum/antagonist/cult/cult_datum = M.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ GLOB.blood_communion.Add(cult_datum)
+ cult_datum.blood_pool = TRUE
+
+/datum/cult_tattoo/bloodpool/Display()//Since that tattoo is now unlocked fairly early, better let cultists hide it easily by leaving the pool
+ var/datum/antagonist/cult/cult_datum = bearer.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ return cult_datum.blood_pool
+
+/datum/cult_tattoo/silent
+ name = TATTOO_SILENT
+ desc = "Cast runes and talismans without having to mouth the invocation."
+ icon_state = "silent"
+ tier = 1
+
+/datum/cult_tattoo/dagger
+ name = TATTOO_DAGGER
+ desc = "Materialize a sharp dagger in your hand for a small cost in blood. Use to retrieve."
+ icon_state = "dagger"
+ tier = 1
+
+///////////////////////////
+// //
+// TIER 2 //
+// //
+///////////////////////////
+
+/datum/cult_tattoo/holy // doesn't actually do anything right now beside give you a cool tattoo
+ name = TATTOO_HOLY
+ desc = "Holy water will now only slow you down a bit, and no longer prevent you from casting."
+ icon_state = "holy"
+ tier = 2
+
+/datum/cult_tattoo/memorize
+ name = TATTOO_MEMORIZE//Arcane Dimension
+ desc = "Allows you to hide a tome into thin air, and pull it out whenever you want."
+ icon_state = "memorize"
+ tier = 2
+
+/datum/cult_tattoo/memorize/getTattoo(mob/M)
+ ..()
+ if (IS_CULTIST(M))
+ var/datum/action/cooldown/spell/cult/arcane_dimension/new_spell = new
+ new_spell.Grant(M)
+
+
+/datum/cult_tattoo/rune_store
+ name = TATTOO_RUNESTORE
+ desc = "Allows you to trace a rune onto your skin and activate it at will."
+ icon_state = "rune"
+ tier = 2
+
+
+///////////////////////////
+// //
+// TIER 3 //
+// //
+///////////////////////////
+
+/datum/cult_tattoo/manifest
+ name = TATTOO_MANIFEST
+ desc = "Acquire a new, fully healed body that cannot feel pain."
+ icon_state = "manifest"
+ tier = 3
+
+
+/datum/cult_tattoo/manifest/getTattoo(var/mob/M)
+ ..()
+ var/mob/living/carbon/human/H = bearer
+ if (!istype(H))
+ return
+ H.revive(0)
+ H.status_flags &= ~GODMODE
+ H.status_flags &= ~CANSTUN
+ H.status_flags &= ~CANKNOCKDOWN
+ H.regenerate_icons()
+
+/datum/cult_tattoo/shortcut
+ name = TATTOO_SHORTCUT
+ desc = "Place sigils on walls that allows cultists to jump right through."
+ icon_state = "shortcut"
+ tier = 3
+ blood_cost = 5
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/_base.dm b/monkestation/code/modules/bloody_cult/cult/buildings/_base.dm
new file mode 100644
index 000000000000..220181d34f61
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/_base.dm
@@ -0,0 +1,247 @@
+
+/obj/structure/cult
+ density = 1
+ anchored = 1
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ max_integrity = 50
+ var/sound_damaged = null
+ var/sound_destroyed = null
+ var/conceal_cooldown = 0
+ var/timeleft = 0
+ var/timetotal = 0
+ var/list/contributors = list()// list of cultists currently participating in the ritual
+ var/min_contributors = 1 // how many cultists we need for the current ritual
+ var/image/progbar = null//progress bar
+ var/cancelling = 3//check to abort the ritual if interrupted
+ var/custom_process = 0
+ //if we have a map id we create a holomap marker
+ var/map_id
+ var/marker_icon = 'monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi'
+ var/marker_icon_state
+
+/obj/structure/cult/Initialize(mapload)
+ . = ..()
+ if(map_id)
+ var/datum/holomap_marker/holomarker = new(src)
+ holomarker.id = map_id
+ holomarker.filter = HOLOMAP_FILTER_CULT
+ holomarker.x = src.x
+ holomarker.y = src.y
+ holomarker.z = src.z
+ holomarker.icon = marker_icon
+ holomarker.icon_state = marker_icon_state
+
+/obj/structure/cult/get_cult_power()
+ return 1//light emitted by those won't be reduced during the eclipse
+
+/obj/structure/cult/proc/conceal()
+ var/obj/structure/cult/concealed/C = new(loc)
+ C.pixel_x = pixel_x
+ C.pixel_y = pixel_y
+ forceMove(C)
+ C.held = src
+ C.icon = icon
+ C.icon_state = icon_state
+
+/obj/structure/cult/proc/reveal()
+ conceal_cooldown = 1
+ spawn (100)
+ if(src && loc)
+ conceal_cooldown = 0
+
+/obj/structure/cult/concealed
+ density = 0
+ anchored = 1
+ alpha = 127
+ invisibility = INVISIBILITY_OBSERVER
+ var/obj/structure/cult/held = null
+
+/obj/structure/cult/concealed/reveal()
+ if(held)
+ held.forceMove(loc)
+ held.reveal()
+ held = null
+ qdel(src)
+
+/obj/structure/cult/concealed/conceal()
+ return
+
+/obj/structure/cult/concealed/takeDamage(damage)
+ return
+
+//if you want indestructible buildings, just make a custom takeDamage() proc
+/obj/structure/cult/proc/takeDamage(damage)
+ atom_integrity -= damage
+ if(atom_integrity <= 0)
+ if(sound_destroyed)
+ playsound(src, sound_destroyed, 100, 1)
+ qdel(src)
+ else
+ update_appearance()
+
+//duh
+/obj/structure/cult/narsie_act()
+ . = ..()
+ return
+
+/obj/structure/cult/ex_act(severity)
+ switch(severity)
+ if(1)
+ takeDamage(100)
+ if(2)
+ takeDamage(20)
+ if(3)
+ takeDamage(4)
+
+/obj/structure/cult/blob_act()
+ playsound(src, sound_damaged, 75, 1)
+ takeDamage(20)
+
+/obj/structure/cult/bullet_act(obj/projectile/Proj)
+ takeDamage(Proj.damage)
+ return ..()
+
+/obj/structure/cult/attackby(obj/item/weapon/weapon, mob/user, params)
+ if(istype(weapon))
+ if(!(user.istate & ISTATE_HARM )|| weapon.force == 0)
+ visible_message(span_warning("\The [user] gently taps \the [src] with \the [weapon].") )
+ else
+ user.do_attack_animation(src, weapon)
+ if(sound_damaged)
+ playsound(src, sound_damaged, 75, 1)
+ if(isholyweapon(weapon))
+ takeDamage(weapon.force*2)
+ else
+ takeDamage(weapon.force)
+ visible_message(span_warning("\The [user] hits \the [src] with \the [weapon].") )
+ ..()
+
+
+/obj/structure/cult/attack_paw(mob/user)
+ return attack_hand(user)
+
+
+/obj/structure/cult/attack_hand(mob/living/user)
+ if(user.istate & ISTATE_HARM)
+ user.visible_message(span_danger("[user.name] [pick("kicks", "punches")] \the [src]!") , \
+ span_danger("You strike at \the [src]!") , \
+ "You hear stone cracking.")
+ user.adjustBruteLoss(3)
+ if(sound_damaged)
+ playsound(src, sound_damaged, 75, 1)
+ else if(IS_CULTIST(user))
+ cultist_act(user)
+ else
+ noncultist_act(user)
+
+/obj/structure/cult/proc/cultist_act(mob/user)
+ return 1
+
+/obj/structure/cult/proc/noncultist_act(mob/user)
+ to_chat(user, span_cult("You feel madness taking its toll, trying to figure out \the [name]'s purpose") )
+ //might add some hallucinations or brain damage later, checks for cultist chaplains, etc
+ return 1
+
+/obj/structure/cult/proc/safe_space()
+ for(var/turf/turf in range(5, src))
+ var/dist = cheap_pythag(turf.x - src.x, turf.y - src.y)
+ if(dist <= 2.5)
+ turf.ChangeTurf(/turf/open/floor/engine/cult)
+ turf.turf_animation('monkestation/code/modules/bloody_cult/icons/effects.dmi', "cultfloor", 0, 0, MOB_LAYER-1, anim_plane = GAME_PLANE)
+ for(var/obj/structure/structure in turf)
+ if(!istype(structure, /obj/structure/cult))
+ qdel(structure)
+ for(var/obj/machinery/machine in turf)
+ qdel(machine)
+ else if(dist <= 4.5)
+ if(istype(turf, /turf/open/space))
+ turf.ChangeTurf(/turf/open/floor/engine/cult)
+ turf.turf_animation('monkestation/code/modules/bloody_cult/icons/effects.dmi', "cultfloor", 0, 0, MOB_LAYER-1, anim_plane = GAME_PLANE)
+ else
+ turf.narsie_act()
+ else if(dist <= 5.5)
+ if(istype(turf, /turf/open/space))
+ turf.ChangeTurf(/turf/closed/wall/mineral/cult)
+ turf.turf_animation('monkestation/code/modules/bloody_cult/icons/effects.dmi', "cultwall", 0, 0, MOB_LAYER-1, anim_plane = GAME_PLANE)
+ else
+ turf.narsie_act()
+
+//inspired from LoZ:Oracle of Seasons
+/obj/structure/cult/proc/dance_start()
+ while(timeleft > 0)
+ for(var/mob/contributor in contributors)
+ if(!IS_CULTIST(contributor) || get_dist(src, contributor) > 1 || contributor.incapacitated() || contributor.occult_muted())
+ if(contributor.client)
+ contributor.client.images -= progbar
+ contributors.Remove(contributor)
+ continue
+ if(contributors.len <= 0)
+ return 0
+ if(contributors.len < min_contributors)
+ sleep(10)
+ continue
+ timeleft -= 1 + round(contributors.len/2)//Additional dancers will complete the ritual faster
+ update_progbar()
+ dance_step()
+ sleep(3)
+ dance_step()
+ sleep(3)
+ dance_step()
+ sleep(6)
+ for(var/mob/contributor in contributors)
+ if(contributor.client)
+ contributor.client.images -= progbar
+ ritual_reward(contributor)
+ contributors.Remove(contributor)
+ return 1
+
+/obj/structure/cult/proc/ritual_reward(mob/contributor)
+ return
+
+/obj/structure/cult/proc/dance_step()
+ var/dance_move = pick("clock", "counter", "spin")
+
+ switch(dance_move)
+ if("clock")
+ for(var/mob/contributor in contributors)
+ switch(get_dir(src, contributor))
+ if(NORTHWEST, NORTH)
+ contributor.forceMove(get_step(contributor, EAST))
+ contributor.dir = EAST
+ if(NORTHEAST, EAST)
+ contributor.forceMove(get_step(contributor, SOUTH))
+ contributor.dir = SOUTH
+ if(SOUTHEAST, SOUTH)
+ contributor.forceMove(get_step(contributor, WEST))
+ contributor.dir = WEST
+ if(SOUTHWEST, WEST)
+ contributor.forceMove(get_step(contributor, NORTH))
+ contributor.dir = NORTH
+ if("counter")
+ for(var/mob/contributor in contributors)
+ switch(get_dir(src, contributor))
+ if(NORTHEAST, NORTH)
+ contributor.forceMove(get_step(contributor, WEST))
+ contributor.dir = WEST
+ if(SOUTHEAST, EAST)
+ contributor.forceMove(get_step(contributor, NORTH))
+ contributor.dir = NORTH
+ if(SOUTHWEST, SOUTH)
+ contributor.forceMove(get_step(contributor, EAST))
+ contributor.dir = EAST
+ if(NORTHWEST, WEST)
+ contributor.forceMove(get_step(contributor, SOUTH))
+ contributor.dir = SOUTH
+ if("spin")
+ for(var/mob/contributor in contributors)
+ spawn()
+ contributor.dir = SOUTH
+ sleep(0.75)
+ contributor.dir = EAST
+ sleep(0.75)
+ contributor.dir = NORTH
+ sleep(0.75)
+ contributor.dir = WEST
+ sleep(0.75)
+ contributor.dir = SOUTH
+
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/altar.dm b/monkestation/code/modules/bloody_cult/cult/buildings/altar.dm
new file mode 100644
index 000000000000..a6c5f0d4ebc8
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/altar.dm
@@ -0,0 +1,678 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Spawned from the Raise Structure rune. Available from the beginning. Trigger progress to ACT I
+// CULT ALTAR //Allows communication with Nar-Sie for advice and info on the Cult's current objective.
+// //ACT II : Allows Soulstone crafting, Used to sacrifice the target on the Station
+///////////////////////////ACT III : Can plant an empty Soul Blade in it to prompt observers to become the blade's shade
+#define ALTARTASK_NONE 0
+#define ALTARTASK_GEM 1
+#define ALTARTASK_SACRIFICE_HUMAN 2
+#define ALTARTASK_SACRIFICE_ANIMAL 3
+
+/obj/structure/cult/altar
+ name = "altar"
+ desc = "A bloodstained altar dedicated to Nar-Sie."
+ icon_state = "altar"
+ max_integrity = 100
+ layer = TABLE_LAYER
+ pass_flags_self = PASSTABLE
+ can_buckle = TRUE
+ map_id = HOLOMAP_MARKER_CULT_ALTAR
+ marker_icon_state = "altar"
+ var/obj/item/weapon/melee/soulblade/blade = null
+ var/altar_task = ALTARTASK_NONE
+ var/gem_delay = 30 SECONDS
+ var/narsie_message_cooldown = 0
+
+ var/mob/sacrificer // who started the sacrifice ritual
+ var/mutable_appearance/build
+
+ var/list/watching_mobs = list()
+
+ var/list/can_plant = list(
+ /obj/item/knife/ritual,
+ /obj/item/weapon/melee/soulblade,
+ /obj/item/weapon/melee/cultblade,
+ /obj/item/weapon/melee/cultblade/nocult,
+ )
+
+ var/trapped = FALSE
+
+/obj/structure/cult/altar/New()
+ ..()
+ flick("[icon_state]-spawn", src)
+ var/image/I = image(icon, "altar_overlay")
+ SET_PLANE_EXPLICIT(I, GAME_PLANE, src)
+
+/obj/structure/cult/altar/Initialize()
+ . = ..()
+ //mostly for mappers
+ for (var/obj/item/I in loc)
+ if (is_type_in_list(I, can_plant))
+ I.forceMove(src)
+ blade = I
+ update_appearance()
+
+/obj/structure/cult/altar/Destroy()
+
+ for(var/mob/mob as anything in watching_mobs)
+ SSholomaps.hide_cult_map(mob)
+ if (blade)
+ if (loc)
+ blade.forceMove(loc)
+ else
+ qdel(blade)
+ blade = null
+ flick("[icon_state]-break", src)
+ ..()
+
+/obj/structure/cult/altar/attackby(var/obj/item/I, var/mob/user, params)
+ if (altar_task)
+ return ..()
+ if(is_type_in_list(I, can_plant))
+ if (blade)
+ to_chat(user, span_warning("You must remove \the [blade] planted into \the [src] first.") )
+ return 1
+ var/turf/T = get_turf(user)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/bloodyslice.ogg', 50, 1)
+ user.dropItemToGround(I)
+ I.forceMove(src)
+ blade = I
+ update_appearance()
+ var/mob/living/carbon/C = locate() in loc
+ var/mob/living/basic/S = locate() in loc
+ if (C && C.body_position == LYING_DOWN)
+ C.buckled.unbuckle_mob(C)
+ buckle_mob(C)
+ C.apply_damage(blade.force, BRUTE, BODY_ZONE_CHEST)
+ var/datum/antagonist/cult/cul = user.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cul)
+ cul.gain_devotion(0, DEVOTION_TIER_0, "altar_plant", src)
+ if (C == user)
+ user.visible_message(span_danger("\The [user] holds \the [I] above their stomach and impales themselves on \the [src]!") , span_danger("You hold \the [I] above your stomach and impale yourself on \the [src]!") )
+ else
+ user.visible_message(span_danger("\The [user] holds \the [I] above \the [C]'s stomach and impales them on \the [src]!") , span_danger("You hold \the [I] above \the [C]'s stomach and impale them on \the [src]!") )
+ else if(S)
+ S.buckled.unbuckle_mob(S)
+ S.pixel_y = 6
+ buckle_mob(S)
+ if(S.stat != DEAD)
+ S.death()
+ var/datum/antagonist/cult/cul = user.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cul)
+ cul.gain_devotion(0, DEVOTION_TIER_0, "altar_plant", src)
+ user.visible_message(span_danger("\The [user] holds \the [I] above \the [S] and impales it on \the [src]!") , span_danger("You hold \the [I] above \the [S] and impale it on \the [src]!") )
+ else
+ to_chat(user, "You plant \the [blade] on top of \the [src]")
+ START_PROCESSING(SSobj, src)
+ var/datum/antagonist/cult/cul = user.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cul)
+ cul.gain_devotion(0, DEVOTION_TIER_0, "altar_plant", src)
+ if (istype(blade) && !blade.shade)
+ for(var/mob/dead/observer/M in GLOB.player_list)
+ if(!M.client || is_banned_from(M.key, ROLE_CULTIST) || M.client.is_afk())
+ continue
+ if (IS_CULTIST(M))
+ var/datum/antagonist/cult/cultist = M.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cultist.second_chance)
+ to_chat(M, span_bolddanger("\The [user] has planted a Soul Blade on an altar, opening a small crack in the veil that allows you to become the blade's resident shade. (Possess now!)") )
+ return 1
+ if (user.pulling)
+ if (blade)
+ to_chat(user, span_warning("You must remove \the [blade] planted on \the [src] first.") )
+ return 1
+ if(iscarbon(pulling))
+ if (blade)
+ to_chat(user, span_warning("You must remove \the [blade] planted on \the [src] first.") )
+ return 1
+ var/mob/living/carbon/C = user.pulling
+ C.buckled.unbuckle_mob(C)
+ if (!do_after(user, 1.5 SECONDS, C))
+ return
+ if (ishuman(C))
+ C.resting = 1
+ C.forceMove(loc)
+ to_chat(user, span_warning("You move \the [C] on top of \the [src]") )
+ return 1
+ if(!(user.istate & ISTATE_HARM))
+ if(user.dropItemToGround(I, loc))
+ return TRUE
+ ..()
+
+/obj/structure/cult/altar/update_icon_state()
+ . = ..()
+ icon_state = "altar"
+
+/obj/structure/cult/altar/update_overlays()
+ . = ..()
+ if (blade)
+ var/image/I
+ if(istype(blade, /obj/item/knife/ritual))
+ I = image(icon, "altar-ritualknife")
+ else if (!istype(blade))
+ I = image(icon, "altar-cultblade")
+ else if (blade.shade)
+ I = image(icon, "altar-soulblade-full")
+ else
+ I = image(icon, "altar-soulblade")
+ SET_PLANE_EXPLICIT(I, GAME_PLANE_UPPER, src)
+ I.pixel_y = 3
+ . += I
+ var/image/I = image(icon, "altar_overlay")
+ SET_PLANE_EXPLICIT(I, GAME_PLANE, src)
+ . += I
+
+ if (atom_integrity < max_integrity/3)
+ . += "altar_damage2"
+ else if (atom_integrity < 2*max_integrity/3)
+ . += "altar_damage1"
+
+//We want people on top of the altar to appear slightly higher
+/obj/structure/cult/altar/Entered(var/atom/movable/mover)
+ if (iscarbon(mover))
+ mover.pixel_y += 7 * 1
+
+/obj/structure/cult/altar/Exited(var/atom/movable/mover)
+ if (iscarbon(mover))
+ mover.pixel_y -= 7 * 1
+
+//They're basically the height of regular tables
+/obj/structure/cult/altar/Cross(var/atom/movable/mover, var/turf/target, var/height = 1.5, var/air_group = 0)
+ if(air_group || (height == 0))
+ return 1
+
+ if(ismob(mover))
+ var/mob/M = mover
+ if(M.movement_type & FLYING)
+ return TRUE
+
+ if(istype(mover) && CanPass(mover))
+ return 1
+ else
+ return 0
+
+/obj/structure/cult/altar/MouseDrop_T(var/atom/movable/O, var/mob/living/user)
+ if (altar_task)
+ return
+ if (!istype(O))
+ return
+ if(user.incapacitated() || user.body_position == LYING_DOWN)
+ return
+ if(O.anchored || !Adjacent(user) || !user.Adjacent(O))
+ return
+ if (user.get_active_held_item() == O)
+ if(!user.dropItemToGround(O))
+ return
+ else
+ if(!ismob(O))
+ return
+ if(O.loc == user || !isturf(O.loc) || !isturf(user.loc))
+ return
+
+ var/mob/living/L = O
+ if(!istype(L) || L.buckled)
+ return
+ if (blade)
+ to_chat(user, span_warning("You must remove \the [blade] planted on \the [src] first.") )
+ return 1
+
+ if (!do_after(user, 15, L))
+ return
+ L.buckled?.unbuckle_mob(L)
+
+ if (ishuman(L) && L != user)
+ L.resting = TRUE
+
+ add_fingerprint(L)
+
+ O.forceMove(loc)
+ if(O == user)
+ to_chat(user, span_warning("You climb on top of \the [src].") )
+ user.set_resting(TRUE)
+ else
+ to_chat(user, span_warning("You move \the [O] on top of \the [src].") )
+ buckle_mob(O)
+
+ return 1
+
+/obj/structure/cult/altar/unbuckle_mob(mob/living/buckled_mob, force, can_fall)
+ if(blade)
+ return FALSE
+ . = ..()
+
+/obj/structure/cult/altar/conceal()
+ if (blade || altar_task)
+ return
+ anim(location = loc, target = loc, a_icon = icon, flick_anim = "[icon_state]-conceal")
+ for (var/mob/living/carbon/C in loc)
+ Uncrossed(C)
+ ..()
+
+/obj/structure/cult/altar/reveal()
+ flick("[icon_state]-spawn", src)
+ ..()
+ for (var/mob/living/carbon/C in loc)
+ Crossed(C)
+
+/obj/structure/cult/altar/cultist_act(var/mob/user, var/menu = "default")
+ . = ..()
+ if (!.)
+ return
+ if (altar_task)
+ switch (altar_task)
+ if (ALTARTASK_GEM)
+ to_chat(user, span_warning("You must wait before the Altar's current task is over.") )
+ if (ALTARTASK_SACRIFICE_HUMAN to ALTARTASK_SACRIFICE_ANIMAL)
+ if (user in contributors)
+ return
+ if (!user.checkTattoo(TATTOO_SILENT))
+ if (prob(5))
+ user.say("Let me show you the dance of my people!", "C")
+ else
+ user.say("Barhah hra zar'garis!", "C")
+ contributors.Add(user)
+ if (user.client)
+ user.client.images |= progbar
+ return
+ if(length(buckled_mobs) || blade)
+ var/mob/M
+ if(length(buckled_mobs))
+ M = buckled_mobs[1]
+ if(M && M != user)
+ var/list/choices = list()
+
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', icon_state = "radial_altar_remove")
+ option.info = span_boldnotice("Pull the blade off, freeing the victim.")
+ choices["Remove Blade"] = option
+
+ var/datum/radial_menu_choice/option2 = new
+ option2.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', icon_state = "radial_altar_sacrifice")
+ option2.info = span_boldnotice("Initiate the sacrifice ritual. The ritual can only proceed if the proper victim has been nailed to the altar.")
+ choices["Sacrifice"] = option2
+
+ var/task = show_radial_menu(user, src, choices, tooltips = TRUE, radial_icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi')
+ if (!Adjacent(user) || !task)
+ return
+ switch (task)
+ if ("Remove Blade")
+ if (do_after(user, 2 SECONDS, src))
+ M.visible_message(span_notice("\The [M] was freed from \the [src] by \the [user]!") , "You were freed from \the [src] by \the [user].")
+ unbuckle_mob(M)
+ if(istype(M, /mob/living/simple_animal))
+ M.pixel_y = 0
+ if (blade)
+ blade.forceMove(loc)
+ blade.attack_hand(user)
+ to_chat(user, span_warning("You remove \the [blade] from \the [src]") )
+ STOP_PROCESSING(SSobj, src)
+ blade = null
+ playsound(loc, 'sound/weapons/blade1.ogg', 50, 1)
+ update_appearance()
+ if ("Sacrifice")
+ // First we'll check for any blockers around it since we'll dance using forceMove to allow up to 8 dancers without them bumping into each others
+ // Of course this means that walls and objects placed AFTER the start of the dance can be crossed by dancing but that's good enough.
+ for (var/turf/T in orange(1, src))
+ if (T.density)
+ to_chat(user, span_warning("\The [T] would hinder the ritual. Either dismantle it or use an altar located in a more spacious area.") )
+ return
+ var/atom/A = T.check_blocking_content(TRUE)
+ if (A && (A != src) && !ismob(A)) // mobs get a free pass
+ to_chat(user, span_warning("\The [A] would hinder the ritual. Either move it or use an altar located in a more spacious area.") )
+ return
+ if(ishuman(M))
+ altar_task = ALTARTASK_SACRIFICE_HUMAN
+ else
+ altar_task = ALTARTASK_SACRIFICE_ANIMAL
+ StartSacrifice(user)
+ return
+ else if (blade)
+ if(length(buckled_mobs))
+ if (do_after(user, 2 SECONDS, src))
+ unbuckle_all_mobs()
+ blade.forceMove(loc)
+ blade.attack_hand(user)
+ to_chat(user, span_notice("You remove \the [blade] from \the [src]") )
+ STOP_PROCESSING(SSobj, src)
+ blade = null
+ playsound(loc, 'sound/weapons/blade1.ogg', 50, 1)
+ update_appearance()
+ else
+ blade.forceMove(loc)
+ blade.attack_hand(user)
+ to_chat(user, span_notice("You remove \the [blade] from \the [src]") )
+ STOP_PROCESSING(SSobj, src)
+ blade = null
+ playsound(loc, 'sound/weapons/blade1.ogg', 50, 1)
+ update_appearance()
+ return
+ else
+ var/list/choices = list(
+ list("Consult Roster", "radial_altar_roster", "Check the names and status of all of the cult's members."),
+ list("Commune with Nar-Sie", "radial_altar_commune", "Make contact with Nar-Sie."),
+ list("Look through Veil", "radial_altar_map", "Check the veil for tears to locate other occult constructions."),
+ list("Conjure Soul Gem", "radial_altar_gem", "Order the altar to sculpt you a Soul Gem, to capture the soul of your enemies."),
+ )
+ var/list/made_choices = list()
+ for(var/list/choice in choices)
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', icon_state = choice[2])
+ option.info = span_boldnotice(choice[3])
+ made_choices[choice[1]] = option
+
+ var/task = show_radial_menu(user, src, made_choices, tooltips = TRUE, radial_icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi')
+ if (buckled_mobs || !Adjacent(user) || !task)
+ return
+ switch (task)
+ if ("Consult Roster")
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!cult)
+ return
+ var/dat = {""}
+ dat += "Our cult can currently grow up to [cult.cultist_cap] members."
+ dat += "
"
+ for (var/datum/mind/mind in cult.members)
+ var/datum/antagonist/cult/cult_datum = mind.has_antag_datum(/datum/antagonist/cult)
+ var/conversion = ""
+ var/cult_role = ""
+ switch (cult_datum.cultist_role)
+ if (CULTIST_ROLE_ACOLYTE)
+ cult_role = "Acolyte"
+ if (CULTIST_ROLE_MENTOR)
+ cult_role = "Mentor"
+ else
+ cult_role = "Herald"
+ if (cult_datum.conversion.len > 0)
+ conversion = pick(cult_datum.conversion)
+ var/origin_text = ""
+ switch (conversion)
+ if ("converted")
+ origin_text = "Converted by [cult_datum.conversion[conversion]]"
+ if ("resurrected")
+ origin_text = "Resurrected by [cult_datum.conversion[conversion]]"
+ if ("soulstone")
+ origin_text = "Soul captured by [cult_datum.conversion[conversion]]"
+ if ("altar")
+ origin_text = "Volunteer shade"
+ if ("sacrifice")
+ origin_text = "Sacrifice"
+ else
+ origin_text = "Founder"
+ var/mob/living/carbon/H = mind.current
+ var/extra = ""
+ if (H && istype(H))
+ if (H.stat == DEAD)
+ extra = " - DEAD"
+ dat += "
[H.name] ([cult_role])
- [origin_text][extra]"
+ for(var/obj/item/restraints/handcuffs/cult/cuffs in cult.bindings)
+ if (iscarbon(cuffs.loc))
+ var/mob/living/carbon/C = cuffs.loc
+ if (C.handcuffed == cuffs && cuffs.gaoler && cuffs.gaoler.owner)
+ var/datum/mind/gaoler = cuffs.gaoler.owner
+ var/extra = ""
+ if (C && istype(C))
+ if (C.stat == DEAD)
+ extra = " - DEAD"
+ dat += "
[C.real_name]
- Prisoner of [gaoler.name][extra]"
+ dat += {"
"}
+ user << browse("Cult Roster[dat]", "window = cultroster;size = 600x400")
+ onclose(user, "cultroster")
+ if ("Commune with Nar-Sie")
+ if(narsie_message_cooldown)
+ to_chat(user, span_warning("This altar has already sent a message in the past 30 seconds, wait a moment.") )
+ return
+ var/input = stripped_input(user, "Please choose a message to transmit to Nar-Sie through the veil. Know that he can be fickle, and abuse of this ritual will leave your body asunder. Communion does not guarantee a response. There is a 30 second delay before you may commune again, be clear, full and concise.", "To abort, send an empty message.", "")
+ if(!input || !Adjacent(user))
+ return
+ usr.pray(input)
+ to_chat(usr, span_notice("Your communion has been received.") )
+ var/turf/T = get_turf(usr)
+ log_say("[key_name(usr)] (@[T.x], [T.y], [T.z]) has communed with Nar-Sie: [input]")
+ narsie_message_cooldown = 1
+ spawn(30 SECONDS)
+ narsie_message_cooldown = 0
+ if ("Conjure Soul Gem")
+ altar_task = ALTARTASK_GEM
+ update_appearance()
+ overlays += "altar-soulstone1"
+ spawn (gem_delay/3)
+ update_appearance()
+ overlays += "altar-soulstone2"
+ sleep (gem_delay/3)
+ update_appearance()
+ overlays += "altar-soulstone3"
+ sleep (gem_delay/3)
+ altar_task = ALTARTASK_NONE
+ update_appearance()
+ var/obj/item/soulstone/gem/gem = new (loc)
+ gem.pixel_y = 4
+ if ("Look through Veil")
+ SSholomaps.show_cult_map(user, src)
+ watching_mobs |= user
+ RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(remove_watching))
+
+/obj/structure/cult/altar/proc/remove_watching(mob/user)
+ watching_mobs -= user
+
+/obj/structure/cult/altar/proc/StartSacrifice(var/mob/user)
+ var/mob/M = buckled_mobs[1]
+ switch(altar_task)
+ if(ALTARTASK_SACRIFICE_HUMAN)
+ if((!istype(blade, /obj/item/weapon/melee/cultblade) && !istype(blade, /obj/item/weapon/melee/soulblade)) || istype(blade, /obj/item/weapon/melee/cultblade/nocult))
+ to_chat(user, span_warning("\The [blade] is too weak to perform such a sacrifice. Forge a stronger blade.") )
+ altar_task = ALTARTASK_NONE
+ return
+ timeleft = 30
+ timetotal = timeleft
+ min_contributors = 1//monkey, or other carbon lifeforms
+ if (ishuman(M))
+ if (M.mind)
+ min_contributors = 3
+ to_chat(user, span_cult("You need 3 cultists to partake in the ritual for the sacrifice to proceed.") )
+ if(ALTARTASK_SACRIFICE_ANIMAL)
+ timeleft = 15
+ timetotal = timeleft
+ min_contributors = 1
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult)
+ if (buckled_mobs)
+ sacrificer = user
+ update_appearance()
+ contributors.Add(user)
+ update_progbar()
+ if (user.client)
+ user.client.images |= progbar
+
+ if(!build)
+ build = mutable_appearance('monkestation/code/modules/bloody_cult/icons/cult.dmi', "build", layer = MOB_SHIELD_LAYER)
+ build.pixel_y = 8
+
+ add_overlay(build)
+ if (!user.checkTattoo(TATTOO_SILENT))
+ if (prob(5))
+ user.say("Let me show you the dance of my people!", "C")
+ else
+ user.say("Barhah hra zar'garis!", "C")
+ if (user.client)
+ user.client.images |= progbar
+ spawn()
+ dance_start()
+
+/obj/structure/cult/altar/noncultist_act(var/mob/user)//Non-cultists can still remove blades planted on altars.
+ if(buckled_mobs)
+ var/mob/M = buckled_mobs[1]
+ if(M != user)
+ if (do_after(user, 2 SECONDS, src))
+ M.visible_message(span_notice("\The [M] was freed from \the [src] by \the [user]!") , "You were freed from \the [src] by \the [user].")
+ unbuckle_mob(M)
+ if (blade)
+ blade.forceMove(loc)
+ blade.attack_hand(user)
+ to_chat(user, "You remove \the [blade] from \the [src]")
+ STOP_PROCESSING(SSobj, src)
+ blade = null
+ playsound(loc, 'sound/weapons/blade1.ogg', 50, 1)
+ update_appearance()
+ else if (blade)
+ blade.forceMove(loc)
+ blade.attack_hand(user)
+ to_chat(user, "You remove \the [blade] from \the [src]")
+ STOP_PROCESSING(SSobj, src)
+ blade = null
+ playsound(loc, 'sound/weapons/blade1.ogg', 50, 1)
+ update_appearance()
+ if (trapped) //soulblade sanctum trapped altar
+ trapped = FALSE
+ var/list/possible_floors = list()
+ for (var/turf/open/floor/F in orange(1, get_turf(src)))
+ possible_floors.Add(F)
+ for (var/i = 1 to 4)
+ if (possible_floors.len <= 0)
+ break
+ var/turf/T = pick(possible_floors)
+ if (T)
+ possible_floors.Remove(T)
+ new /obj/effect/cult_ritual/backup_spawn(T)
+ return
+ else
+ to_chat(user, span_cult("You feel madness taking its toll, trying to figure out \the [name]'s purpose.") )
+ return 1
+
+/obj/structure/cult/altar/process()
+ if (istype(blade))
+ blade.blood = min(blade.maxblood, blade.blood+10)
+ if (blade.blood == blade.maxblood)
+ STOP_PROCESSING(SSobj, src)
+ else
+ STOP_PROCESSING(SSobj, src)
+
+/obj/structure/cult/altar/Topic(href, href_list)
+ if(href_list["signup"])
+ var/mob/M = usr
+ if(!isobserver(M) || !IS_CULTIST(M))
+ return
+ var/mob/dead/observer/O = M
+ var/obj/item/weapon/melee/soulblade/blade = locate() in src
+ if (!istype(blade))
+ to_chat(usr, span_warning("The blade was removed from \the [src].") )
+ return
+ if (blade.shade)
+ to_chat(usr, span_warning("Another shade was faster, and is currently possessing \the [blade].") )
+ return
+ var/mob/living/basic/shade/shadeMob = new(blade)
+ blade.shade = shadeMob
+ shadeMob.status_flags |= GODMODE
+ ADD_TRAIT(shadeMob, TRAIT_IMMOBILIZED, REF(src))
+ var/datum/antagonist/cult/cultist = IS_CULTIST(M)
+ cultist.second_chance = 0
+ shadeMob.real_name = M.mind.name
+ shadeMob.name = "[shadeMob.real_name] the Shade"
+ M.mind.transfer_to(shadeMob)
+ O.can_reenter_corpse = 1
+ O.reenter_corpse()
+
+ /* Only cultists get brought back this way now, so let's assume they kept their identity.
+ spawn()
+ var/list/shade_names = list("Orenmir", "Felthorn", "Sparda", "Vengeance", "Klinge")
+ shadeMob.real_name = pick(shade_names)
+ shadeMob.real_name = copytext(sanitize(input(shadeMob, "You have no memories of your previous life, if you even had one. What name will you give yourself?", "Give yourself a new name", "[shadeMob.real_name]") as null|text), 1, MAX_NAME_LEN)
+ shadeMob.name = "[shadeMob.real_name] the Shade"
+ if (shadeMob.mind)
+ shadeMob.mind.name = shadeMob.real_name
+ */
+ shadeMob.cancel_camera()
+ //shadeMob.give_blade_powers()
+ blade.dir = NORTH
+ blade.update_appearance()
+ update_appearance()
+ //Automatically makes them cultists
+ var/datum/antagonist/cult/newCultist = new/datum/antagonist/cult(shadeMob.mind)
+ newCultist.conversion.Add("altar")
+
+
+/obj/structure/cult/altar/dance_start()//This is executed at the end of the sacrifice ritual
+ . = ..()//true if the ritual was successful
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ overlays -= build
+ min_contributors = initial(min_contributors)
+ if(!.)
+ altar_task = ALTARTASK_NONE
+ return
+ if(!buckled_mobs)
+ return
+ switch(altar_task)
+ if(ALTARTASK_SACRIFICE_HUMAN)
+ altar_task = ALTARTASK_NONE
+ update_appearance()
+ var/mob/M = buckled_mobs[1]
+ if (istype(blade) && !blade.shade && M.mind)//If an empty soul blade was the tool used for the ritual, let's make them its shade.
+ var/mob/living/basic/shade/new_shade = M.change_mob_type( /mob/living/basic/shade , null, null, 1 )
+ blade.forceMove(loc)
+ blade.blood = blade.maxblood
+ new_shade.forceMove(blade)
+ blade.shade = new_shade
+ blade.update_appearance()
+ blade = null
+ for(var/mob/living/L in dview(world.view, loc, INVISIBILITY_MAXIMUM))
+ if (L.client)
+ L.playsound_local(loc, 'monkestation/code/modules/bloody_cult/sound/convert_failure.ogg', 75, 0, -4)
+ playsound(loc, get_sfx("soulstone"), 50, 1)
+ var/obj/effect/cult_ritual/conversion/anim = new(loc)
+ anim.icon_state = ""
+ flick("rune_convert_refused", anim)
+ anim.Die()
+
+ if (!IS_CULTIST(new_shade))
+ var/datum/antagonist/cult/newCultist = new(new_shade.mind)
+ cult.HandleRecruitedRole(newCultist)
+ newCultist.conversion.Add("sacrifice")
+
+ new_shade.status_flags |= GODMODE
+ ADD_TRAIT(new_shade, TRAIT_IMMOBILIZED, REF(src))
+ new_shade.soulblade_ritual = TRUE
+ new_shade.name = "[M.real_name] the Shade"
+ new_shade.real_name = "[M.real_name]"
+ //new_shade.give_blade_powers()
+ playsound(src, get_sfx("soulstone"), 50, 1)
+ else
+ anim(target = src, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_sac", plane = ABOVE_GAME_PLANE)
+
+ if(ALTARTASK_SACRIFICE_ANIMAL)
+ altar_task = ALTARTASK_NONE
+ var/mob/living/M = buckled_mobs[1]
+ anim(target = src, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_sac", plane = ABOVE_GAME_PLANE)
+ var/turf/TU = get_turf(src)
+ spawn(5)
+ var/obj/item/reagent_containers/R = locate(/obj/item/reagent_containers) in TU.contents
+ if(R)
+ var/remaining = R.volume - R.reagents.total_volume
+ if(R && R.is_open_container())
+ if(istype(M, /mob/living/basic/mouse))
+ M.transfer_blood_to(R, min(remaining, 30))
+ else
+ M.transfer_blood_to(R, min(remaining, 60))
+ R.on_reagent_change()
+ qdel(M)
+ //bloodmess_splatter(TU)
+ playsound(src, "gib", 30, 0, -3)
+
+/obj/structure/cult/altar/ritual_reward(var/mob/M)
+ var/datum/antagonist/cult/cult_datum = M.mind.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ switch(altar_task)
+ if(ALTARTASK_SACRIFICE_HUMAN)
+ var/mob/O = buckled_mobs[1]
+ if (O.mind)
+ cult_datum.gain_devotion(500, DEVOTION_TIER_4, "altar_sacrifice_human", O)
+ else//monkey-human
+ cult_datum.gain_devotion(200, DEVOTION_TIER_4, "altar_sacrifice_human_nomind", O)
+ if(ALTARTASK_SACRIFICE_ANIMAL)
+ var/mob/O = buckled_mobs[1]
+ if (ismonkey(O))
+ cult_datum.gain_devotion(200, DEVOTION_TIER_3, "altar_sacrifice_monkey", O)
+ else
+ cult_datum.gain_devotion(200, DEVOTION_TIER_3, "altar_sacrifice_animal", O)
+
+#undef ALTARTASK_NONE
+#undef ALTARTASK_GEM
+#undef ALTARTASK_SACRIFICE_HUMAN
+#undef ALTARTASK_SACRIFICE_ANIMAL
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/blood_stone.dm b/monkestation/code/modules/bloody_cult/cult/buildings/blood_stone.dm
new file mode 100644
index 000000000000..6795612255f1
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/blood_stone.dm
@@ -0,0 +1,206 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Re-added as a cosmetic structure by admin request
+// BLOOD STONE //
+// //
+///////////////////////////
+
+/obj/structure/cult/bloodstone
+ name = "blood stone"
+ icon_state = "bloodstone-enter1"
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ pixel_x = -16 * 1
+ max_integrity = 1800
+ plane = GAME_PLANE_UPPER
+ light_color = "#FF0000"
+
+ var/ready = FALSE
+ var/image/image_base
+ var/image/image_circle
+ var/image/image_stones
+ var/image/image_lights
+ var/image/image_damage
+ var/datum/team/cult/cult
+ var/list/pillars = list()
+
+/obj/structure/cult/bloodstone/New()
+ ..()
+ set_light(3)
+ cult = locate_team(/datum/team/cult)
+ image_base = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "bloodstone-base-old")
+ image_base.appearance_flags |= RESET_COLOR
+ image_damage = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "bloodstone_damage0")
+
+ image_circle = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "large_circle")
+ SET_PLANE_EXPLICIT(image_circle, GAME_PLANE_UPPER, src)
+ image_circle.appearance_flags |= RESET_COLOR
+ image_circle.pixel_y = -16
+ image_stones = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "tear_stones")
+ SET_PLANE_EXPLICIT(image_stones, GAME_PLANE, src)
+ image_stones.appearance_flags |= RESET_COLOR
+ image_stones.pixel_y = -16
+ image_lights = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "tear_stones_light")
+ SET_PLANE_EXPLICIT(image_lights, GAME_PLANE_UPPER, src)
+ image_lights.pixel_y = -16
+
+/obj/structure/cult/bloodstone/proc/overlays_pre()
+ overlays += image_base
+ overlays += image_circle
+ overlays += image_stones
+ overlays += image_lights
+
+/obj/structure/cult/bloodstone/admin/overlays_pre()
+ overlays += image_base
+
+/obj/structure/cult/bloodstone/proc/overlays_post()
+ overlays -= image_base
+ image_base.icon_state = "bloodstone-base"
+ overlays += image_base
+
+/obj/structure/cult/bloodstone/admin/overlays_post()
+ return
+
+/obj/structure/cult/bloodstone/proc/flashy_entrance(var/datum/rune_spell/tearreality/TR)
+ for (var/obj/O in loc)
+ if (O != src && !istype(O, /obj/item/weapon/melee/soulblade))
+ O.ex_act(2)
+ safe_space()
+ overlays_pre()
+ explosion_sound(TR)
+ TR?.pillar_update(1)
+
+ spawn(10)
+ pillars = list()
+ icon_state = "bloodstone-enter2"
+ explosion_sound(TR)
+ TR?.pillar_update(2)
+ var/turf/T1 = locate(x-2, y-2, z)
+ pillars += new /obj/structure/cult/pillar(T1)
+ var/turf/T2 = locate(x+2, y-2, z)
+ pillars += new /obj/structure/cult/pillar/alt(T2)
+ var/turf/T3 = locate(x-2, y+2, z)
+ pillars += new /obj/structure/cult/pillar(T3)
+ var/turf/T4 = locate(x+2, y+2, z)
+ pillars += new /obj/structure/cult/pillar/alt(T4)
+ sleep(10)
+ icon_state = "bloodstone-enter3"
+ explosion_sound(TR)
+ TR?.pillar_update(3)
+ for (var/obj/structure/cult/pillar/P in pillars)
+ P.update_icon()
+ sleep(10)
+ ready = TRUE
+ overlays_post()
+ set_animate()
+
+/obj/structure/cult/bloodstone/proc/explosion_sound(var/datum/rune_spell/tearreality/TR)
+ for(var/mob/M in GLOB.player_list)
+ if (M.z == z && M.client)
+ if (TR || (get_dist(M, src)<= 20))//If there's a tear reality rune, then spires should be appearing all over the station, so no point not having it be loud
+ M.playsound_local(src, get_sfx("explosion"), 50, 1)
+ shake_camera(M, 4, 1)
+ else
+ M.playsound_local(src, 'sound/effects/explosionfar.ogg', 50, 1)
+ shake_camera(M, 1, 1)
+
+
+/obj/structure/cult/bloodstone/Destroy()
+ new /obj/effect/decal/cleanable/ash(loc)
+ if (cult && (cult.bloodstone == src))
+ cult.bloodstone = null
+ spawn()
+ cult.stage(BLOODCULT_STAGE_DEFEATED)
+ for (var/obj/effect/new_rune/R in src)
+ R.active_spell?.abort()
+ ..()
+
+/*
+/obj/structure/cult/bloodstone/cultist_act(var/mob/user)
+ . = ..()
+ if (!.)
+ return
+ if(isliving(user))
+ var/obj/effect/cult_ritual/dance/dance_center = locate() in loc
+ if (dance_center)
+ dance_center.add_dancer(user)
+ else
+ dance_center = new(loc, user)
+
+ if (prob(5))
+ user.say("Let me show you the dance of my people!", "C")
+ else
+ user.say("Tok-lyr rqa'nap g'lt-ulotf!", "C")
+*/
+
+/obj/structure/cult/bloodstone/conceal()
+ return
+
+/obj/structure/cult/bloodstone/takeDamage(var/damage)
+ if (cult && (cult.stage == BLOODCULT_STAGE_NARSIE))
+ return
+ atom_integrity -= damage
+ if (atom_integrity <= 0)
+ if (sound_destroyed)
+ playsound(src, sound_destroyed, 100, 1)
+ qdel(src)
+ else
+ update_icon()
+
+/obj/structure/cult/bloodstone/ex_act(var/severity)
+ switch(severity)
+ if (1)
+ takeDamage(250)
+ if (2)
+ takeDamage(50)
+ if (3)
+ takeDamage(10)
+
+/obj/structure/cult/bloodstone/singularity_pull(S, current_size, repel = FALSE)//we don't want that one to come unanchored
+ return
+
+/obj/structure/cult/bloodstone/update_icon()
+ . = ..()
+ if (!ready)
+ return
+ icon_state = "bloodstone-0"
+ if (cult)
+ icon_state = "bloodstone-[clamp(round(9*(world.time - cult.bloodstone_rising_time) / (cult.bloodstone_target_time - cult.bloodstone_rising_time)), 0, 9)]"
+ overlays -= image_damage
+ if (atom_integrity < max_integrity/3)
+ image_damage.icon_state = "bloodstone_damage2"
+ else if (atom_integrity < 2*max_integrity/3)
+ image_damage.icon_state = "bloodstone_damage1"
+ else
+ image_damage.icon_state = "bloodstone_damage0"
+ overlays += image_damage
+
+/obj/structure/cult/bloodstone/admin/update_icon()
+ . = ..()
+ icon_state = "bloodstone-9-old"
+ overlays -= image_damage
+ if (atom_integrity < max_integrity/3)
+ image_damage.icon_state = "bloodstone_damage2"
+ else if (atom_integrity < 2*max_integrity/3)
+ image_damage.icon_state = "bloodstone_damage1"
+ else
+ image_damage.icon_state = "bloodstone_damage0"
+ overlays += image_damage
+
+
+/obj/structure/cult/bloodstone/proc/set_animate()
+ animate(src, color = list(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 10, loop = -1)
+ animate(color = list(1.125, 0.06, 0, 0, 0, 1.125, 0.06, 0, 0.06, 0, 1.125, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 2)
+ animate(color = list(1.25, 0.12, 0, 0, 0, 1.25, 0.12, 0, 0.12, 0, 1.25, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 2)
+ animate(color = list(1.375, 0.19, 0, 0, 0, 1.375, 0.19, 0, 0.19, 0, 1.375, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.5)
+ animate(color = list(1.5, 0.27, 0, 0, 0, 1.5, 0.27, 0, 0.27, 0, 1.5, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.5)
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(2, 0.67, 0.27, 0, 0.27, 2, 0.67, 0, 0.67, 0.27, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 5)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.5, 0.27, 0, 0, 0, 1.5, 0.27, 0, 0.27, 0, 1.5, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.375, 0.19, 0, 0, 0, 1.375, 0.19, 0, 0.19, 0, 1.375, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.25, 0.12, 0, 0, 0, 1.25, 0.12, 0, 0.12, 0, 1.25, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.125, 0.06, 0, 0, 0, 1.125, 0.06, 0, 0.06, 0, 1.125, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ update_icon()
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/forge.dm b/monkestation/code/modules/bloody_cult/cult/buildings/forge.dm
new file mode 100644
index 000000000000..42132eb0ca98
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/forge.dm
@@ -0,0 +1,256 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Spawned from the Raise Structure rune
+// CULT FORGE //Also a source of heat
+// // Add /obj/item/melee/cultblade/halberd
+///////////////////////////
+
+
+/obj/structure/cult/forge
+ name = "forge"
+ desc = "Molten rocks flow down its cracks producing a searing heat, better not stand too close for long."
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ icon_state = ""
+ max_integrity = 100
+ pixel_x = -16 * 1
+ pixel_y = -16 * 1
+ plane = GAME_PLANE
+ light_color = LIGHT_COLOR_ORANGE
+ custom_process = 1
+ map_id = HOLOMAP_MARKER_CULT_ALTAR
+ marker_icon_state = "forge"
+
+ var/heating_power = 40000
+ var/set_temperature = 50
+ var/mob/forger = null
+ var/template = null
+ var/forge_icon = ""
+ var/obj/effect/cult_ritual/forge/forging = null
+
+
+/obj/structure/cult/forge/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+ set_light(2)
+ flick("forge-spawn", src)
+ spawn(10)
+ setup_overlays()
+
+/obj/structure/cult/forge/Destroy()
+ if (forging)
+ qdel(forging)
+ forging = null
+ forger = null
+ STOP_PROCESSING(SSobj, src)
+ ..()
+
+/obj/structure/cult/forge/proc/setup_overlays()
+ animate(src, alpha = 255, time = 10, loop = -1)
+ animate(alpha = 240, time = 2)
+ animate(alpha = 224, time = 2)
+ animate(alpha = 208, time = 1.5)
+ animate(alpha = 192, time = 1.5)
+ animate(alpha = 176, time = 1)
+ animate(alpha = 160, time = 1)
+ animate(alpha = 144, time = 1)
+ animate(alpha = 128, time = 3)
+ animate(alpha = 144, time = 1)
+ animate(alpha = 160, time = 1)
+ animate(alpha = 176, time = 1)
+ animate(alpha = 192, time = 1.5)
+ animate(alpha = 208, time = 1.5)
+ animate(alpha = 224, time = 2)
+ animate(alpha = 240, time = 2)
+ overlays.len = 0
+ var/image/I_base = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "forge")
+ SET_PLANE_EXPLICIT(I_base, GAME_PLANE, src)
+ I_base.appearance_flags |= RESET_ALPHA //we don't want the stone to pulse
+ var/image/I_lave = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "forge-lightmask")
+ I_lave.plane = ABOVE_LIGHTING_PLANE
+ I_lave.blend_mode = BLEND_ADD
+ overlays += I_base
+ overlays += I_lave
+
+/obj/structure/cult/forge/process()
+ ..()
+ if (isturf(loc))
+ var/turf/L = loc
+ if(!isspaceturf(loc))
+ for (var/mob/living/carbon/M in view(src, 3))
+ M.bodytemperature += (6-round(M.get_cult_power()/30))/((get_dist(src, M)+1))//cult gear reduces the heat buildup
+ if (forging)
+ if (forger)
+ if (!Adjacent(forger) || forger.incapacitated())
+ if (forger.client)
+ forger.client.images -= progbar
+ forger = null
+ return
+ else
+ timeleft--
+ update_progbar()
+ var/datum/antagonist/cult/cult_datum = IS_CULTIST(forger)
+ if (cult_datum)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_2, "[forge_icon]", timeleft)
+ if (timeleft <= 0)
+ playsound(L, 'monkestation/code/modules/bloody_cult/sound/forge_over.ogg', 50, 0, -3)
+ if (forger.client)
+ forger.client.images -= progbar
+ QDEL_NULL(forging)
+ var/obj/item/I = new template(L)
+ if (istype(I))
+ SET_PLANE_EXPLICIT(I, GAME_PLANE, src)
+ I.pixel_y = 12
+ else
+ I.forceMove(get_turf(forger))
+ forger = null
+ template = null
+ else
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', flick_anim = "forge-work", offX = pixel_x, offY = pixel_y, plane = ABOVE_LIGHTING_PLANE)
+ playsound(L, 'monkestation/code/modules/bloody_cult/sound/forge.ogg', 50, 0, -4)
+ forging.overlays.len = 0
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "[forging.icon_state]-mask")
+ I.plane = ABOVE_LIGHTING_PLANE
+ I.blend_mode = BLEND_ADD
+ I.alpha = (timeleft/timetotal)*255
+ forging.overlays += I
+
+
+
+/obj/structure/cult/forge/conceal()
+ overlays.len = 0
+ set_light(0)
+ anim(location = loc, target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', flick_anim = "forge-conceal", offX = pixel_x, offY = pixel_y, plane = GAME_PLANE)
+ ..()
+ var/obj/structure/cult/concealed/C = loc
+ if (istype(C))
+ C.icon_state = "forge"
+
+/obj/structure/cult/forge/reveal()
+ ..()
+ animate(src)
+ alpha = 255
+ set_light(2)
+ flick("forge-spawn", src)
+ spawn(10)
+ animate(src, alpha = 255, time = 10, loop = -1)
+ animate(alpha = 240, time = 2)
+ animate(alpha = 224, time = 2)
+ animate(alpha = 208, time = 1.5)
+ animate(alpha = 192, time = 1.5)
+ animate(alpha = 176, time = 1)
+ animate(alpha = 160, time = 1)
+ animate(alpha = 144, time = 1)
+ animate(alpha = 128, time = 3)
+ animate(alpha = 144, time = 1)
+ animate(alpha = 160, time = 1)
+ animate(alpha = 176, time = 1)
+ animate(alpha = 192, time = 1.5)
+ animate(alpha = 208, time = 1.5)
+ animate(alpha = 224, time = 2)
+ animate(alpha = 240, time = 2)
+ var/image/I_base = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "forge")
+ SET_PLANE_EXPLICIT(I_base, GAME_PLANE, src)
+ I_base.appearance_flags |= RESET_ALPHA //we don't want the stone to pulse
+ var/image/I_lave = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "forge-lightmask")
+ I_lave.plane = ABOVE_LIGHTING_PLANE
+ I_lave.blend_mode = BLEND_ADD
+ overlays += I_base
+ overlays += I_lave
+
+/obj/structure/cult/forge/attackby(var/obj/item/I, var/mob/user)
+ if(istype(I, /obj/item/clothing/mask/cigarette))
+ var/obj/item/clothing/mask/cigarette/fag = I
+ fag.light(span_notice("\The [user] lights \the [fag] by bringing its tip close to \the [src]'s molten flow.") )
+ return 1
+ if(istype(I, /obj/item/candle))
+ var/obj/item/candle/stick = I
+ stick.light(span_notice("\The [user] lights \the [stick] by bringing its wick close to \the [src]'s molten flow.") )
+ return 1
+ ..()
+
+/obj/structure/cult/proc/update_progbar()
+ if (!progbar)
+ progbar = image("icon" = 'monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi', "loc" = src, "icon_state" = "prog_bar_0")
+ progbar.pixel_z = 32
+ progbar.plane = HUD_PLANE
+ progbar.pixel_x = 16 * 1
+ progbar.pixel_y = 16 * 1
+ progbar.appearance_flags = RESET_ALPHA|RESET_COLOR
+ progbar.icon_state = "prog_bar_[round((100 - min(1, timeleft / timetotal) * 100), 10)]"
+
+/obj/structure/cult/altar/update_progbar()
+ if (!progbar)
+ progbar = image("icon" = 'monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi', "loc" = src, "icon_state" = "prog_bar_0")
+ progbar.pixel_z = 32
+ progbar.plane = HUD_PLANE
+ progbar.icon_state = "prog_bar_[round((100 - min(1, timeleft / timetotal) * 100), 10)]"
+
+/obj/structure/cult/forge/cultist_act(var/mob/user, var/menu = "default")
+ . = ..()
+ if (!.)
+ return
+
+ if (template)
+ if (forger)
+ if (forger == user)
+ to_chat(user, "You are already working at this forge.")
+ else
+ to_chat(user, "\The [forger] is currently working at this forge already.")
+ else
+ to_chat(user, "You resume working at the forge.")
+ forger = user
+ if (forger.client)
+ forger.client.images |= progbar
+ return
+
+
+ var/list/choices = list(
+ list("Forge Blade", "radial_blade", "A powerful ritual blade, the signature weapon of the bloodthirsty cultists. Features a notch in which a Soul Gem can fit."),
+ list("Forge Construct Shell", "radial_constructshell", "A polymorphic sculpture that can be shaped into a powerful ally by inserting a full Soul Gem or Shard."),
+ list("Forge Armor", "radial_armor", "This protective armor offers the same enhancing powers that Cult Robes provide, on top of being space proof."),
+ )
+
+ var/list/made_choices = list()
+ for(var/list/choice in choices)
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial.dmi', icon_state = choice[2])
+ option.info = span_boldnotice(choice[3])
+ made_choices[choice[1]] = option
+
+ var/task = show_radial_menu(user, loc, made_choices, tooltips = TRUE, radial_icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial.dmi')//spawning on loc so we aren't offset by pixel_x/pixel_y, or affected by animate()
+ if (template || !Adjacent(user) || !task )
+ return
+ forge_icon = ""
+ switch (task)
+ if ("Forge Blade")
+ template = /obj/item/weapon/melee/cultblade
+ timeleft = 10
+ forge_icon = "forge_blade"
+ if ("Forge Armor")
+ template = /obj/item/clothing/suit/hooded/cultrobes/hardened
+ timeleft = 23
+ forge_icon = "forge_armor"
+ if ("Forge Construct Shell")
+ template = /obj/structure/constructshell
+ timeleft = 25
+ forge_icon = "forge_shell"
+ timetotal = timeleft
+ forger = user
+ update_progbar()
+ if (forger.client)
+ forger.client.images |= progbar
+ forging = new (loc, forge_icon)
+
+/obj/effect/cult_ritual/forge
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ icon_state = ""
+ pixel_x = -16 * 1
+ pixel_y = -16 * 1
+ plane = GAME_PLANE
+
+/obj/effect/cult_ritual/forge/New(var/turf/loc, var/i_forge = "")
+ ..()
+ icon_state = i_forge
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', "[i_forge]-mask")
+ I.plane = ABOVE_LIGHTING_PLANE
+ I.blend_mode = BLEND_ADD
+ overlays += I
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/obsidian_pillar.dm b/monkestation/code/modules/bloody_cult/cult/buildings/obsidian_pillar.dm
new file mode 100644
index 000000000000..999c5d62eff9
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/obsidian_pillar.dm
@@ -0,0 +1,64 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Spawns next to blood stones
+// OBSIDIAN PILLAR //
+// //
+///////////////////////////
+
+/obj/structure/cult/pillar
+ name = "obsidian pillar"
+ icon_state = "pillar-enter"
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ pixel_x = -16 * 1
+ max_integrity = 200
+ plane = GAME_PLANE
+ var/alt = 0
+
+/obj/structure/cult/pillar/New()
+ ..()
+ var/turf/T = loc
+ if (!T)
+ qdel(src)
+ return
+ for (var/obj/O in loc)
+ if (istype(O, /obj/structure/window))
+ var/obj/structure/window/W = O
+ if (!W.fulltile)//reduces breaches ever so slightly
+ continue
+ if(O == src)
+ continue
+ O.ex_act(2)
+ if(!QDELETED(O) && (istype(O, /obj/structure) || istype(O, /obj/machinery)))
+ qdel(O)
+ T.ChangeTurf(/turf/open/floor/engine/cult)
+ T.turf_animation('monkestation/code/modules/bloody_cult/icons/effects.dmi', "cultfloor", 0, 0, MOB_LAYER-1, anim_plane = GAME_PLANE)
+
+/obj/structure/cult/pillar/Destroy()
+ new /obj/effect/decal/cleanable/ash(loc)
+ ..()
+
+
+/obj/structure/cult/pillar/alt
+ icon_state = "pillaralt-enter"
+ alt = 1
+
+/obj/structure/cult/pillar/update_icon()
+ . = ..()
+ icon_state = "pillar[alt ? "alt": ""]2"
+ set_light(1.5, 2.5, COLOR_FIRE_LIGHT_RED)
+ overlays.len = 0
+ if (atom_integrity < max_integrity/3)
+ icon_state = "pillar[alt ? "alt": ""]0"
+ else if (atom_integrity < 2*max_integrity/3)
+ icon_state = "pillar[alt ? "alt": ""]1"
+
+/obj/structure/cult/pillar/conceal()
+ return
+
+/obj/structure/cult/pillar/ex_act(var/severity)
+ switch(severity)
+ if (1)
+ takeDamage(200)
+ if (2)
+ takeDamage(100)
+ if (3)
+ takeDamage(20)
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/pylon.dm b/monkestation/code/modules/bloody_cult/cult/buildings/pylon.dm
new file mode 100644
index 000000000000..dc82540ff1a1
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/pylon.dm
@@ -0,0 +1,150 @@
+/obj/structure/cult/pylon
+ name = "pylon"
+ desc = "A floating crystal that hums with an unearthly energy."
+ icon_state = "pylon"
+ light_outer_range = 5
+ light_color = COLOR_FIRE_LIGHT_RED
+ max_integrity = 50
+ sound_damaged = 'sound/effects/Glasshit.ogg'
+ plane = GAME_PLANE_UPPER
+ /// Length of the cooldown in between tile corruptions. Doubled if no turfs are found.
+ var/corruption_cooldown_duration = 5 SECONDS
+ /// The cooldown for corruptions.
+ COOLDOWN_DECLARE(corruption_cooldown)
+
+/obj/structure/cult/pylon/attack_hand(var/mob/M)
+ attackpylon(M, 5)
+
+/*
+/obj/structure/cult/pylon/attack_basic_mob(mob/user, list/modifiers)
+ . = ..()
+ if(istype(user, /mob/living/basic/construct/artificer))
+ if(broken)
+ repair(user)
+ return
+ attackpylon(user, user.melee_damage_upper)
+*/
+
+/obj/structure/cult/pylon/attackby(var/obj/item/W, var/mob/user)
+ attackpylon(user, W.force)
+
+/obj/structure/cult/pylon/proc/attackpylon(mob/user as mob, var/damage)
+ if(!broken)
+ if(prob(1+ damage * 5))
+ to_chat(user, "You hit the pylon, and its crystal breaks apart!")
+ for(var/mob/M in viewers(src))
+ if(M == user)
+ continue
+ M.show_message("[user.name] smashed the pylon!", 1, "You hear a tinkle of crystal shards.", 2)
+ playsound(src, 'sound/effects/Glassbr3.ogg', 75, 1)
+ broken = TRUE
+ density = FALSE
+ icon_state = "pylon-broken"
+ set_light(0)
+ else
+ to_chat(user, "You hit the pylon!")
+ playsound(src, 'sound/effects/Glasshit.ogg', 75, 1)
+ else
+ playsound(src, 'sound/effects/Glasshit.ogg', 75, 1)
+ if(prob(damage * 2))
+ to_chat(user, "You pulverize what was left of the pylon!")
+ qdel(src)
+ else
+ to_chat(user, "You hit the pylon!")
+
+/obj/structure/cult/pylon/proc/repair(var/mob/user)
+ if(broken)
+ to_chat(user, "You repair the pylon.")
+ broken = FALSE
+ density = TRUE
+ icon_state = "pylon"
+ sound_damaged = 'sound/effects/Glasshit.ogg'
+ set_light(5)
+
+/obj/structure/cult/pylon/takeDamage()
+ ..()
+ if(atom_integrity <= 20 && !broken)
+ playsound(src, 'sound/effects/Glassbr3.ogg', 75, 1)
+ visible_message(span_warning("\The [src] breaks apart!") )
+ icon_state = "pylon-broken"
+ set_light(0)
+ density = FALSE
+ broken = TRUE
+
+/obj/structure/cult/pylon/New()
+ ..()
+ flick("[icon_state]-spawn", src)
+
+/obj/structure/cult/pylon/Initialize(mapload)
+ . = ..()
+
+ AddComponent( \
+ /datum/component/aura_healing, \
+ range = 5, \
+ brute_heal = 0.4, \
+ burn_heal = 0.4, \
+ blood_heal = 0.4, \
+ simple_heal = 1.2, \
+ requires_visibility = FALSE, \
+ limit_to_trait = TRAIT_HEALS_FROM_CULT_PYLONS, \
+ healing_color = COLOR_CULT_RED, \
+ )
+
+ START_PROCESSING(SSfastprocess, src)
+
+/obj/structure/cult/pylon/Destroy()
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
+
+/obj/structure/cult/pylon/process()
+ if(!anchored)
+ return
+ if(!COOLDOWN_FINISHED(src, corruption_cooldown))
+ return
+
+ var/list/validturfs = list()
+ var/list/cultturfs = list()
+ for(var/nearby_turf in circle_view_turfs(src, 5))
+ if(istype(nearby_turf, /turf/open/floor/engine/cult))
+ cultturfs |= nearby_turf
+ continue
+ var/static/list/blacklisted_pylon_turfs = typecacheof(list(
+ /turf/closed,
+ /turf/open/floor/engine/cult,
+ /turf/open/space,
+ /turf/open/lava,
+ /turf/open/chasm,
+ /turf/open/misc/asteroid,
+ ))
+ if(is_type_in_typecache(nearby_turf, blacklisted_pylon_turfs))
+ continue
+ validturfs |= nearby_turf
+
+ if(length(validturfs))
+ var/turf/converted_turf = pick(validturfs)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ var/datum/mind/mind = pick(cult.members)
+ var/datum/antagonist/cult/cultist = mind?.has_antag_datum(/datum/antagonist/cult)
+ if(isplatingturf(converted_turf))
+ converted_turf.PlaceOnTop(/turf/open/floor/engine/cult, flags = CHANGETURF_INHERIT_AIR)
+ else
+ converted_turf.ChangeTurf(/turf/open/floor/engine/cult, flags = CHANGETURF_INHERIT_AIR)
+ cultist?.gain_devotion(0, DEVOTION_TIER_0, "convert_floor")
+ else if (length(cultturfs))
+ var/turf/open/floor/engine/cult/cult_turf = pick(cultturfs)
+ new /obj/effect/temp_visual/cult/turf/floor(cult_turf)
+
+ else
+ // Are we in space or something? No cult turfs or convertable turfs? Double the cooldown
+ COOLDOWN_START(src, corruption_cooldown, corruption_cooldown_duration * 2)
+ return
+
+ COOLDOWN_START(src, corruption_cooldown, corruption_cooldown_duration)
+
+/obj/structure/cult/pylon/conceal()
+ . = ..()
+ STOP_PROCESSING(SSfastprocess, src)
+
+/obj/structure/cult/pylon/reveal()
+ . = ..()
+ START_PROCESSING(SSfastprocess, src)
diff --git a/monkestation/code/modules/bloody_cult/cult/buildings/spire.dm b/monkestation/code/modules/bloody_cult/cult/buildings/spire.dm
new file mode 100644
index 000000000000..5dd3dd4929cb
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/buildings/spire.dm
@@ -0,0 +1,241 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Spawned from the Raise Structure rune.
+// CULT SPIRE //Enables rune-less cult comms for cultists on their current Z-Level
+// //
+///////////////////////////
+GLOBAL_LIST_INIT(cult_spires, list())
+
+/obj/structure/cult/spire
+ name = "spire"
+ desc = "A blood-red needle surrounded by dangerous looking...teeth?."
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ icon_state = ""
+ max_integrity = 100
+ pixel_x = -16 * 1
+ pixel_y = -4 * 1
+ plane = GAME_PLANE
+ map_id = HOLOMAP_MARKER_CULT_SPIRE
+ marker_icon_state = "spire"
+ light_color = "#FF0000"
+ var/stage = 1
+
+/obj/structure/cult/spire/New()
+ ..()
+ GLOB.cult_spires += src
+ set_light(1)
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult)
+ switch(cult.stage)
+ if (BLOODCULT_STAGE_MISSED, BLOODCULT_STAGE_DEFEATED)
+ stage = 1
+ if (BLOODCULT_STAGE_NORMAL)
+ stage = 2
+ if (BLOODCULT_STAGE_READY, BLOODCULT_STAGE_ECLIPSE, BLOODCULT_STAGE_NARSIE)
+ stage = 3
+ flick("spire[stage]-spawn", src)
+ spawn(10)
+ update_stage()
+
+/obj/structure/cult/spire/Destroy()
+ GLOB.cult_spires -= src
+ ..()
+
+/obj/structure/cult/spire/proc/upgrade(var/new_stage)
+ new_stage = clamp(new_stage, 1, 3)
+ if (new_stage>stage)
+ alpha = 255
+ overlays.len = 0
+ color = null
+ flick("spire[new_stage]-morph", src)
+ spawn(3)
+ update_stage()
+ else if (new_stage= tattoo_tier)
+ for (var/subtype in subtypesof(/datum/cult_tattoo))
+ var/datum/cult_tattoo/T = new subtype
+ if (T.tier == tattoo_tier)
+ choices += list(list(T.name, "radial_[T.icon_state]", T.desc)) //According to BYOND docs, when adding to a list, "If an argument is itself a list, each item in the list will be added." My solution to that, because I am a genius, is to add a list within a list.
+ to_chat(H, "[T.name]: [T.desc]")
+ else
+ to_chat(user, span_warning("Come back to acquire another mark once your cult is a step closer to its goal.") )
+ return
+
+ var/tattoo = show_radial_menu(user, loc, choices, 'monkestation/code/modules/bloody_cult/icons/cult_radial2.dmi', "radial-cult2")//spawning on loc so we aren't offset by pixel_x/pixel_y, or affected by animate()
+
+ for (var/tat in C.tattoos)
+ var/datum/cult_tattoo/CT = C.tattoos[tat]
+ if (CT.tier == tattoo_tier)//the spire won't let cultists get multiple tattoos of the same tier.
+ return
+
+ if (!Adjacent(user))//stay here you bloke!
+ return
+
+ for (var/subtype in subtypesof(/datum/cult_tattoo))
+ var/datum/cult_tattoo/T = new subtype
+ if (T.name == tattoo)
+ var/datum/cult_tattoo/new_tattoo = T
+ C.tattoos[new_tattoo.name] = new_tattoo
+
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/32x96.dmi', flick_anim = "tattoo_send", lay = NARSIE_GLOW, plane = ABOVE_LIGHTING_PLANE)
+ spawn (3)
+ C.update_cult_hud()
+ new_tattoo.getTattoo(H)
+ anim(target = H, a_icon = 'monkestation/code/modules/bloody_cult/icons/32x96.dmi', flick_anim = "tattoo_receive", lay = NARSIE_GLOW, plane = ABOVE_LIGHTING_PLANE)
+ sleep(1)
+ H.update_mutations()
+ var/atom/movable/overlay/tattoo_markings = anim(target = H, a_icon = 'icons/mob/cult_tattoos.dmi', flick_anim = "[new_tattoo.icon_state]_mark", sleeptime = 30, lay = NARSIE_GLOW, plane = ABOVE_LIGHTING_PLANE)
+ animate(tattoo_markings, alpha = 0, time = 30)
+
+ available_tattoos -= "tier[new_tattoo.tier]"
+ if (available_tattoos.len > 0)
+ cultist_act(user)
+ break
+ */
+
+
+/datum/saymode/cult
+ key = "x"
+ mode = MODE_CULT
+
+/datum/saymode/cult/handle_message(mob/living/user, message, datum/language/language)
+ //we can send the message
+ if(!length(GLOB.cult_spires))
+ return FALSE
+ var/located_z = FALSE
+ for(var/obj/structure/cult/spire/spire as anything in GLOB.cult_spires)
+ if(spire.z == user.z)
+ located_z = TRUE
+ break
+
+ if(!located_z)
+ return FALSE
+ if(!user.mind)
+ return FALSE
+ if(user.occult_muted())
+ return
+ if(!user.mind.has_antag_datum(/datum/antagonist/cult) && !istype(user, /mob/living/basic/shade) && !istype(user, /mob/living/basic/astral_projection))
+ return
+
+ if(istype(user, /mob/living/basic/construct))
+ var/mob/living/basic/construct/construct = user
+ if(construct.theme != THEME_CULT)
+ return
+
+
+ user.log_talk(message, LOG_SAY, tag = "cult member [user.name]")
+ var/msg = span_cult("[user.name]: [message]")
+
+ //the recipients can recieve the message
+ var/datum/team/cult/cult_team = locate_team(/datum/team/cult)
+ for(var/datum/mind/mind in cult_team.members)
+ if(QDELETED(mind))
+ continue
+ var/mob/living/cult_member = mind.current
+ // can't recieve messages on the hivemind right now
+ if(cult_member.occult_muted())
+ continue
+
+ var/found_z = FALSE
+ for(var/obj/structure/cult/spire/spire as anything in GLOB.cult_spires)
+ if(spire.z == user.z)
+ found_z = TRUE
+ break
+ if(!found_z)
+ continue
+
+ to_chat(cult_member, msg)
+
+ for(var/mob/dead/ghost as anything in GLOB.dead_mob_list)
+ to_chat(ghost, "[FOLLOW_LINK(ghost, user)] [msg]")
+ return FALSE
diff --git a/monkestation/code/modules/bloody_cult/cult/clothing/_cult_power_additions.dm b/monkestation/code/modules/bloody_cult/cult/clothing/_cult_power_additions.dm
new file mode 100644
index 000000000000..79c1151b9b19
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/clothing/_cult_power_additions.dm
@@ -0,0 +1,5 @@
+/obj/item/storage/backpack/cultpack/get_cult_power()
+ return 30
+
+/obj/item/clothing/suit/hooded/cultrobes/hardened/get_cult_power()
+ return 60
diff --git a/monkestation/code/modules/bloody_cult/cult/clothing/cult_gloves.dm b/monkestation/code/modules/bloody_cult/cult/clothing/cult_gloves.dm
new file mode 100644
index 000000000000..6d03214ef0f5
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/clothing/cult_gloves.dm
@@ -0,0 +1,10 @@
+/obj/item/clothing/gloves/color/black/cult
+ name = "cult gloves"
+ desc = "These gloves are quite comfortable, and will keep you warm!"
+ siemens_coefficient = 0.7
+
+/obj/item/clothing/gloves/color/black/cult/get_cult_power()
+ return 10
+
+/obj/item/clothing/gloves/color/black/cult/narsie_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/clothing/cult_hardened.dm b/monkestation/code/modules/bloody_cult/cult/clothing/cult_hardened.dm
new file mode 100644
index 000000000000..d733e9adecec
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/clothing/cult_hardened.dm
@@ -0,0 +1,58 @@
+/obj/item/clothing/suit/hooded/cultrobes/hardened
+ name = "\improper Nar'Sien hardened armor"
+ desc = "A heavily-armored exosuit worn by warriors of the Nar'Sien cult. It can withstand hard vacuum."
+ icon = 'monkestation/code/modules/bloody_cult/icons/worn/suit.dmi'
+ worn_icon = 'monkestation/code/modules/bloody_cult/icons/worn/suit.dmi'
+ icon_state = "cultarmor"
+ hood_up_affix = null
+
+ inhand_icon_state = null
+ w_class = WEIGHT_CLASS_BULKY
+ allowed = list(/obj/item/weapon/tome, /obj/item/weapon/melee/cultblade, /obj/item/weapon/melee/soulblade, /obj/item/tank, /obj/item/weapon/tome, /obj/item/weapon/talisman, /obj/item/weapon/blood_tesseract)
+ armor_type = /datum/armor/cultrobes_hardened
+ hoodtype = /obj/item/clothing/head/hooded/cult_hoodie/hardened
+ clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL
+ flags_inv = HIDEGLOVES | HIDEJUMPSUIT
+ min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
+ max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
+ resistance_flags = NONE
+
+/obj/item/clothing/suit/hooded/cultrobes/hardened/salt_act()
+ acid_melt()
+
+/datum/armor/cultrobes_hardened
+ melee = 50
+ bullet = 40
+ laser = 50
+ energy = 60
+ bomb = 50
+ bio = 100
+ fire = 100
+ acid = 100
+
+/obj/item/clothing/head/hooded/cult_hoodie/hardened
+ name = "\improper Nar'Sien hardened helmet"
+ desc = "A heavily-armored helmet worn by warriors of the Nar'Sien cult. It can withstand hard vacuum."
+ icon_state = "culthelmet"
+ icon = 'monkestation/code/modules/bloody_cult/icons/worn/head.dmi'
+ worn_icon = 'monkestation/code/modules/bloody_cult/icons/worn/head.dmi'
+
+ inhand_icon_state = null
+ armor_type = /datum/armor/cult_hoodie_hardened
+ clothing_flags = STOPSPRESSUREDAMAGE | THICKMATERIAL | SNUG_FIT | PLASMAMAN_HELMET_EXEMPT | HEADINTERNALS
+ flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDEFACIALHAIR|HIDESNOUT
+ min_cold_protection_temperature = SPACE_HELM_MIN_TEMP_PROTECT
+ max_heat_protection_temperature = SPACE_HELM_MAX_TEMP_PROTECT
+ flash_protect = FLASH_PROTECTION_WELDER
+ flags_cover = HEADCOVERSEYES | HEADCOVERSMOUTH | PEPPERPROOF
+ resistance_flags = NONE
+
+/datum/armor/cult_hoodie_hardened
+ melee = 50
+ bullet = 40
+ laser = 50
+ energy = 60
+ bomb = 50
+ bio = 100
+ fire = 100
+ acid = 100
diff --git a/monkestation/code/modules/bloody_cult/cult/clothing/cult_restraints.dm b/monkestation/code/modules/bloody_cult/cult/clothing/cult_restraints.dm
new file mode 100644
index 000000000000..660831a6dc79
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/clothing/cult_restraints.dm
@@ -0,0 +1,39 @@
+/obj/item/restraints/handcuffs/cult
+ name = "ghastly bindings"
+ desc = ""
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "cultcuff"
+ breakouttime = 60 SECONDS
+ var/datum/antagonist/cult/gaoler
+
+/obj/item/restraints/handcuffs/cult/New()
+ ..()
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!cult)
+ return
+
+ cult.bindings += src
+
+/obj/item/restraints/handcuffs/cult/Destroy()
+ ..()
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!cult)
+ return
+
+ cult.bindings -= src
+
+/obj/item/restraints/handcuffs/cult/examine(var/mob/user)
+ ..()
+ if (!isliving(loc))//shouldn't happen unless they get admin spawned
+ to_chat(user, span_info("The tentacles flailing out of this egg-like object seem like they're trying to grasp at their surroundings.") )
+ else
+ var/mob/living/carbon/C = loc
+ if (C.handcuffed == src)
+ to_chat(user, span_info("These restrict your arms and inflict tremendous pain upon both your body and psyche. But given some time you should be able to break them.") )
+ else
+ to_chat(user, span_info("\The [C] seems to be in pain as these restrict their arms.") )
+
+/obj/item/restraints/handcuffs/cult/narsie_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/cult_antagonist.dm b/monkestation/code/modules/bloody_cult/cult/cult_antagonist.dm
new file mode 100644
index 000000000000..aa1d6698f8f2
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/cult_antagonist.dm
@@ -0,0 +1,613 @@
+/datum/antagonist/cult
+ name = "Cultist"
+ roundend_category = "cultists"
+ antagpanel_category = "Cult"
+ antag_moodlet = /datum/mood_event/cult
+ suicide_cry = "FOR NAR'SIE!!"
+ preview_outfit = /datum/outfit/cultist
+ job_rank = ROLE_CULTIST
+ antag_hud_name = "cult"
+ var/ignore_implant = FALSE
+ var/give_equipment = FALSE
+ var/datum/team/cult/cult_team
+
+ ///NEW VARS HERE
+ var/list/tattoos = list()
+ var/holywarning_cooldown = 0
+ var/list/conversion = list()
+ var/second_chance = 1
+ var/datum/deconversion_ritual/deconversion = null
+
+ //writing runes
+ var/rune_blood_cost = 1 // How much blood spent per rune word written
+ var/verbose = FALSE // Used by the rune writing UI to avoid message spam
+
+ var/cultist_role = CULTIST_ROLE_NONE // Because the role might change on the fly and we don't want to set everything again each time, better not start dealing with subtypes
+ var/arch_cultist = FALSE // same as above
+
+ var/time_role_changed_last = 0
+
+ var/datum/antagonist/cult/mentor = null
+ var/list/acolytes = list()
+
+ var/devotion = 0
+ var/rank = DEVOTION_TIER_0
+
+ var/blood_pool = FALSE
+
+ var/initial_rituals = FALSE
+ var/list/possible_rituals = list()
+ var/list/rituals = list(RITUAL_CULTIST_1, RITUAL_CULTIST_2)
+ var/logo_state = "cult-logo"
+
+/datum/antagonist/cult/get_team()
+ return cult_team
+
+/datum/antagonist/cult/create_team(datum/team/cult/new_team)
+ if(!new_team)
+ //todo remove this and allow admin buttons to create more than one cult
+ for(var/datum/antagonist/cult/H in GLOB.antagonists)
+ if(!H.owner)
+ continue
+ if(H.cult_team)
+ cult_team = H.cult_team
+ return
+ cult_team = new /datum/team/cult
+ cult_team.setup_objectives()
+ return
+ if(!istype(new_team))
+ stack_trace("Wrong team type passed to [type] initialization.")
+ cult_team = new_team
+
+/datum/antagonist/cult/proc/add_objectives()
+ objectives |= cult_team.objectives
+
+/datum/antagonist/cult/can_be_owned(datum/mind/new_owner)
+ . = ..()
+ if(. && !ignore_implant)
+ . = is_convertable_to_cult(new_owner.current, cult_team)
+
+/datum/antagonist/cult/greet()
+ . = ..()
+ owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/bloodcult/bloodcult_gain.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)//subject to change
+ owner.announce_objectives()
+
+/datum/antagonist/cult/on_gain()
+ add_objectives()
+ START_PROCESSING(SSobj, src)
+ owner.current.DisplayUI("Cultist")
+ for (var/ritual_type in GLOB.bloodcult_personal_rituals)
+ possible_rituals += new ritual_type()
+ . = ..()
+ var/mob/living/current = owner.current
+ if(give_equipment)
+ equip_cultist(TRUE)
+ current.log_message("has been converted to the cult of Nar'Sie!", LOG_ATTACK, color = "#960000")
+
+ if(cult_team.blood_target && cult_team.blood_target_image && current.client)
+ current.client.images += cult_team.blood_target_image
+
+ ADD_TRAIT(current, TRAIT_HEALS_FROM_CULT_PYLONS, CULT_TRAIT)
+
+/datum/antagonist/cult/on_removal()
+ REMOVE_TRAIT(owner.current, TRAIT_HEALS_FROM_CULT_PYLONS, CULT_TRAIT)
+ owner.current.HideUI("Cultist")
+ STOP_PROCESSING(SSobj, src)
+ if(!silent)
+ owner.current.visible_message(span_deconversion_message("[owner.current] looks like [owner.current.p_theyve()] just reverted to [owner.current.p_their()] old faith!"), ignored_mobs = owner.current)
+ to_chat(owner.current, span_userdanger("An unfamiliar white light flashes through your mind, cleansing the taint of the Geometer and all your memories as her servant."))
+ owner.current.log_message("has renounced the cult of Nar'Sie!", LOG_ATTACK, color = "#960000")
+ if(cult_team.blood_target && cult_team.blood_target_image && owner.current.client)
+ owner.current.client.images -= cult_team.blood_target_image
+
+ return ..()
+
+/datum/antagonist/cult/get_preview_icon()
+ var/icon/icon = render_preview_outfit(preview_outfit)
+
+ // The longsword is 64x64, but getFlatIcon crunches to 32x32.
+ // So I'm just going to add it in post, screw it.
+
+ // Center the dude, because item icon states start from the center.
+ // This makes the image 64x64.
+ icon.Crop(-15, -15, 48, 48)
+
+ var/obj/item/melee/cultblade/longsword = new
+ icon.Blend(icon(longsword.lefthand_file, longsword.inhand_icon_state), ICON_OVERLAY)
+ qdel(longsword)
+
+ // Move the guy back to the bottom left, 32x32.
+ icon.Crop(17, 17, 48, 48)
+
+ return finish_preview_icon(icon)
+
+/datum/antagonist/cult/proc/equip_cultist(metal = TRUE)
+ var/mob/living/carbon/H = owner.current
+ if(!istype(H))
+ return
+ . += cult_give_item(/obj/item/melee/cultblade/dagger, H)
+ if(metal)
+ . += cult_give_item(/obj/item/stack/sheet/runed_metal/ten, H)
+ to_chat(owner, "These will help you start the cult on this station. Use them well, and remember - you are not the only one.")
+
+///Attempts to make a new item and put it in a potential inventory slot in the provided mob.
+/datum/antagonist/cult/proc/cult_give_item(obj/item/item_path, mob/living/carbon/human/mob)
+ var/item = new item_path(mob)
+ var/where = mob.equip_conspicuous_item(item)
+ if(!where)
+ to_chat(mob, span_userdanger("Unfortunately, you weren't able to get [item]. This is very bad and you should adminhelp immediately (press F1)."))
+ return FALSE
+ else
+ to_chat(mob, span_danger("You have [item] in your [where]."))
+ if(where == "backpack")
+ mob.back.atom_storage?.show_contents(mob)
+ return TRUE
+
+/datum/antagonist/cult/apply_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/current = owner.current
+ if(mob_override)
+ current = mob_override
+ handle_clown_mutation(current, mob_override ? null : "Your training has allowed you to overcome your clownish nature, allowing you to wield weapons without harming yourself.")
+ current.faction |= FACTION_CULT
+ current.grant_language(/datum/language/narsie, TRUE, TRUE, LANGUAGE_CULTIST)
+ if(cult_team.cult_risen)
+ current.AddElement(/datum/element/cult_eyes, initial_delay = 0 SECONDS)
+ if(cult_team.cult_ascendent)
+ current.AddElement(/datum/element/cult_halo, initial_delay = 0 SECONDS)
+
+ add_team_hud(current)
+
+/datum/antagonist/cult/remove_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/current = owner.current
+ if(mob_override)
+ current = mob_override
+ handle_clown_mutation(current, removing = FALSE)
+ current.faction -= FACTION_CULT
+ current.remove_language(/datum/language/narsie, TRUE, TRUE, LANGUAGE_CULTIST)
+ current.clear_alert("bloodsense")
+ if (HAS_TRAIT(current, TRAIT_UNNATURAL_RED_GLOWY_EYES))
+ current.RemoveElement(/datum/element/cult_eyes)
+ if (HAS_TRAIT(current, TRAIT_CULT_HALO))
+ current.RemoveElement(/datum/element/cult_halo)
+
+/datum/antagonist/cult/on_mindshield(mob/implanter)
+ if(!silent)
+ to_chat(owner.current, span_warning("You feel something interfering with your mental conditioning, but you resist it!"))
+ return
+
+/datum/antagonist/cult/admin_add(datum/mind/new_owner, mob/admin)
+ give_equipment = FALSE
+ new_owner.add_antag_datum(src)
+ message_admins("[key_name_admin(admin)] has cult-ed [key_name_admin(new_owner)].")
+ log_admin("[key_name(admin)] has cult-ed [key_name(new_owner)].")
+
+/datum/antagonist/cult/admin_remove(mob/user)
+ silent = TRUE
+ return ..()
+
+/datum/antagonist/cult/get_admin_commands()
+ . = ..()
+ .["Dagger"] = CALLBACK(src, PROC_REF(admin_give_dagger))
+ .["Dagger and Metal"] = CALLBACK(src, PROC_REF(admin_give_metal))
+ .["Remove Dagger and Metal"] = CALLBACK(src, PROC_REF(admin_take_all))
+
+/datum/antagonist/cult/proc/admin_give_dagger(mob/admin)
+ if(!equip_cultist(metal = FALSE))
+ to_chat(admin, span_danger("Spawning dagger failed!"))
+
+/datum/antagonist/cult/proc/admin_give_metal(mob/admin)
+ if (!equip_cultist(metal = TRUE))
+ to_chat(admin, span_danger("Spawning runed metal failed!"))
+
+/datum/antagonist/cult/proc/admin_take_all(mob/admin)
+ var/mob/living/current = owner.current
+ for(var/o in current.get_all_contents())
+ if(istype(o, /obj/item/melee/cultblade/dagger) || istype(o, /obj/item/stack/sheet/runed_metal))
+ qdel(o)
+
+/datum/antagonist/cult/proc/erase_rune()
+ var/mob/living/user = owner.current
+ if (!istype(user))
+ return
+
+ if (user.incapacitated())
+ return
+
+ var/turf/T = get_turf(user)
+ var/obj/effect/rune/rune = locate() in T
+
+ if (rune && rune.invisibility == INVISIBILITY_OBSERVER)
+ to_chat(user, span_warning("You can feel the presence of a concealed rune here, you have to reveal it before you can erase words from it.") )
+ return
+
+ var/removed_word = erase_rune_word(get_turf(user))
+ if (removed_word)
+ to_chat(user, span_notice("You retrace your steps, carefully undoing the lines of the [removed_word] rune.") )
+ else
+ to_chat(user, span_warning("There aren't any rune words left to erase.") )
+
+/datum/antagonist/cult/proc/gain_devotion(var/acquired_devotion = 0, var/tier = DEVOTION_TIER_0, var/key, var/extra)
+ if (cult_team)
+ switch(cult_team.stage)
+ if (BLOODCULT_STAGE_DEFEATED)//no more devotion gains if the bloodstone has been destroyed
+ return
+ if (BLOODCULT_STAGE_NARSIE)//or narsie has risen
+ return
+
+ if (key && (!cult_team || (cult_team.stage != BLOODCULT_STAGE_ECLIPSE)))
+ for (var/ritual_slot in rituals)
+ if (rituals[ritual_slot])
+ var/datum/bloodcult_ritual/my_ritual = rituals[ritual_slot]
+ if (key in my_ritual.keys)
+ if (my_ritual.key_found(extra))
+ my_ritual.complete()
+ if (!my_ritual.only_once)
+ possible_rituals += my_ritual
+ rituals[ritual_slot] = null
+ var/mob/M = owner.current
+ if (M)
+ to_chat(M, span_cult("You have completed a ritual and been reward for your devotion...soon another ritual will take its place.") )
+ spawn(5 MINUTES)
+ if (!rituals[ritual_slot])
+ replace_rituals(ritual_slot)
+ if (cult_team && (cult_team.stage != BLOODCULT_STAGE_ECLIPSE))
+ var/datum/team/cult/cult = cult_team
+ for (var/ritual_slot in cult.rituals)
+ if (cult.rituals[ritual_slot])
+ var/datum/bloodcult_ritual/faction_ritual = cult.rituals[ritual_slot]
+ if (key in faction_ritual.keys)
+ if (faction_ritual.key_found(extra))
+ faction_ritual.complete()
+ if (!faction_ritual.only_once)
+ cult.possible_rituals += faction_ritual
+ cult.rituals[ritual_slot] = null
+ for (var/datum/mind/mind in cult.members)
+ var/mob/M = mind.current
+ if (M)
+ if (M == owner.current)
+ to_chat(M, span_cult("You have completed a ritual, and rewarded the entire cult...soon another ritual will take its place.") )
+ else
+ to_chat(M, span_cult("Someone has completed a ritual, rewarding the entire cult...soon another ritual will take its place.") )
+ spawn(10 MINUTES)
+ if (!cult.rituals[ritual_slot])
+ cult.replace_rituals(ritual_slot)
+
+ //The more devotion the cultist has acquired, the less devotion they obtain from lesser rituals
+ switch (get_devotion_rank() - tier)
+ if (3 to INFINITY)
+ return//until they just don't get any devotion anymore
+ if (2)
+ acquired_devotion /= 10
+ if (1)
+ acquired_devotion /= 2
+ devotion += acquired_devotion
+ check_rank_upgrade()
+
+ if (cult_team)
+ var/datum/team/cult/cult = cult_team
+ cult.total_devotion += acquired_devotion
+
+/datum/antagonist/cult/proc/check_rank_upgrade()
+ var/new_rank = get_devotion_rank()
+ while (new_rank > rank)
+ rank++
+ if (iscarbon(owner.current))//constructs and shades cannot make use of those powers so no point informing them.
+ to_chat(owner.current, span_cultlarge("As your devotion to the cult increases, a new power awakens inside you.") )
+ switch(rank)
+ if (DEVOTION_TIER_1)
+ to_chat(owner.current, span_danger("Blood Pooling") )
+ to_chat(owner.current, "Any blood cost required by a cult rune or ritual will now be reduced and split with other cult members that have attained this power. You can toggle blood pooling as needed.")
+ GiveTattoo(/datum/cult_tattoo/bloodpool)
+ if (DEVOTION_TIER_2)
+ to_chat(owner.current, span_danger("Blood Dagger") )
+ to_chat(owner.current, "You can now form a dagger using your own blood (or pooled blood, any blood that you can get your hands on). Hitting someone will let the dagger steal some of their blood, while sheathing the dagger will let you recover all the stolen blood. Throwing the dagger deals damage based on how much blood it carries, and nails the victim down, forcing them to pull the dagger out to move away.")
+ GiveTattoo(/datum/cult_tattoo/dagger)
+ if (DEVOTION_TIER_3)
+ to_chat(owner.current, span_danger("Runic Skin") )
+ to_chat(owner.current, "You can now fuse a talisman that has a rune imbued or attuned to it with your skin, granting you the ability to cast this talisman hands free, as long as you are conscious and not under the effects of Holy Water.")
+ GiveTattoo(/datum/cult_tattoo/rune_store)
+ if (DEVOTION_TIER_4)
+ to_chat(owner.current, span_danger("Shortcut Sigil") )
+ to_chat(owner.current, "Apply your palms on a wall to draw a sigil on it that lets you and any ally pass through it.")
+ GiveTattoo(/datum/cult_tattoo/shortcut)
+
+/datum/antagonist/cult/proc/get_devotion_rank()
+ switch(devotion)
+ if (2000 to INFINITY)
+ return DEVOTION_TIER_4
+ if (1000 to 2000)
+ return DEVOTION_TIER_3
+ if (500 to 1000)
+ return DEVOTION_TIER_2
+ if (100 to 500)
+ return DEVOTION_TIER_1
+ if (0 to 100)
+ return DEVOTION_TIER_0
+
+/datum/antagonist/cult/proc/FindMentor()
+ var/datum/team/cult/cult = cult_team
+ if (!cult || !cult.mentor_count)
+ return
+ var/datum/antagonist/cult/potential_mentor
+ var/min_acolytes = 10000
+ for (var/datum/mind/mind in cult.members)
+ var/datum/antagonist/cult/cult_datum = mind.has_antag_datum(/datum/antagonist/cult)
+ if (!mind.current || mind.current.stat == DEAD)
+ continue
+ if (cult_datum.cultist_role == CULTIST_ROLE_MENTOR)
+ if (cult_datum.acolytes.len < min_acolytes || (cult_datum.acolytes.len == min_acolytes && prob(50)))
+ min_acolytes = cult_datum.acolytes.len
+ potential_mentor = cult_datum
+
+ if (potential_mentor)
+ mentor = potential_mentor
+ potential_mentor.acolytes |= src
+ to_chat(owner.current, span_cult("You are now in a mentorship under [mentor.owner.name], the [mentor.owner.assigned_role == "MODE" ? (mentor.owner.special_role) : (mentor.owner.assigned_role)]. Seek their help to learn the ways of our cult.") )
+ to_chat(mentor.owner.current, span_cult("You are now mentoring [owner.name], the [owner.assigned_role == "MODE" ? (owner.special_role) : (owner.assigned_role)]. ") )
+ message_admins("[mentor.owner.key]/([mentor.owner.name]) is now mentoring [owner.name]")
+ log_admin("[mentor.owner.key]/([mentor.owner.name]) is now mentoring [owner.name]")
+
+/datum/antagonist/cult/proc/GiveTattoo(var/type)
+ if(locate(type) in tattoos)
+ return
+ var/datum/cult_tattoo/T = new type
+ tattoos[T.name] = T
+ update_cult_hud()
+ T.getTattoo(owner.current)
+ //anim(target = owner.current, a_icon = 'icons/effects/32x96.dmi', flick_anim = "tattoo_receive", lay = NARSIE_GLOW, plane = ABOVE_LIGHTING_PLANE)
+ sleep(1)
+ //a bit too visible now that those may be unlocked at any time and no longer just in front of a spire
+ //var/atom/movable/overlay/tattoo_markings = anim(target = owner.current, a_icon = 'icons/mob/cult_tattoos.dmi', flick_anim = "[T.icon_state]_mark", sleeptime = 30, lay = NARSIE_GLOW, plane = ABOVE_LIGHTING_PLANE)
+ //animate(tattoo_markings, alpha = 0, time = 30)
+
+/datum/antagonist/cult/proc/update_cult_hud()
+ var/mob/M = owner?.current
+ if(M)
+ if (M.client && M.hud_used)
+ if (isshade(M))
+ if (istype(M.loc, /obj/item/weapon/melee/soulblade))
+ M.DisplayUI("Soulblade")
+
+
+/datum/antagonist/cult/proc/replace_rituals(var/slot)
+ if (!slot)
+ return
+
+ var/list/valid_rituals = list()
+
+ for (var/datum/bloodcult_ritual/R in possible_rituals)
+ if (R.pre_conditions(src))
+ valid_rituals += R
+
+ if (valid_rituals.len < 1)
+ return
+
+ var/datum/bloodcult_ritual/BR = pick(valid_rituals)
+ rituals[slot] = BR
+ possible_rituals -= BR
+ BR.init_ritual()
+
+ var/mob/O = owner.current
+ if (O)
+ to_chat(O, span_cult("A new ritual is available...") )
+ var/datum/mind/M = owner
+ if ("Cult Panel" in M.active_uis)
+ var/datum/mind_ui/m_ui = M.active_uis["Cult Panel"]
+ if (m_ui.active)
+ m_ui.Display()
+
+/datum/antagonist/cult/proc/ChangeCultistRole(var/new_role)
+ if (!new_role)
+ return
+ var/datum/team/cult/cult = cult_team
+ if ((cultist_role == CULTIST_ROLE_MENTOR) && cult)
+ cult.mentor_count--
+
+ cultist_role = new_role
+
+ DropMentorship()
+
+ switch(cultist_role)
+ if (CULTIST_ROLE_ACOLYTE)
+ message_admins("BLOODCULT: [owner.key]/([owner.name]) has become a cultist acolyte.")
+ log_admin("BLOODCULT: [owner.key]/([owner.name]) has become a cultist acolyte.")
+ logo_state = "cult-apprentice-logo"
+ FindMentor()
+ if (!mentor)
+ message_admins("BLOODCULT: [owner.key]/([owner.name]) couldn't find a mentor.")
+ log_admin("BLOODCULT: [owner.key]/([owner.name]) couldn't find a mentor.")
+ if (CULTIST_ROLE_HERALD)
+ message_admins("BLOODCULT: [owner.key]/([owner.name]) has become a cultist herald.")
+ log_admin("BLOODCULT: [owner.key]/([owner.name]) has become a cultist herald.")
+ logo_state = "cult-logo"
+ if (CULTIST_ROLE_MENTOR)
+ message_admins("BLOODCULT: [owner.key]/([owner.name]) has become a cultist mentor.")
+ log_admin("BLOODCULT: [owner.key]/([owner.name]) has become a cultist mentor.")
+ logo_state = "cult-master-logo"
+ if (cult)
+ cult.mentor_count++
+ else
+ logo_state = "cult-logo"
+ cultist_role = CULTIST_ROLE_NONE
+ if (owner.current)//refreshing the UI so the current role icon appears on the cult panel button and role change button.
+ owner.current.DisplayUI("Cultist Left Panel")
+ owner.current.DisplayUI("Cult Panel")
+ time_role_changed_last = world.time
+
+/datum/antagonist/cult/proc/DropMentorship()
+ if (mentor)
+ to_chat(owner.current, span_warning("You have ended your mentorship under [mentor.owner.name].") )
+ to_chat(mentor.owner.current, span_warning("[owner.name] has ended their mentorship under you.") )
+ message_admins("[owner.key]/([owner.name]) has ended their mentorship under [mentor.owner.name]")
+ log_admin("[owner.key]/([owner.name]) has ended their mentorship under [mentor.owner.name]")
+ mentor.acolytes -= src
+ mentor = null
+ if (acolytes.len > 0)
+ for (var/datum/antagonist/cult/acolyte in acolytes)
+ to_chat(owner.current, span_warning("You have ended your mentorship of [acolyte.owner.name].") )
+ to_chat(acolyte.owner.current, span_warning("[owner.name] has ended their mentorship.") )
+ message_admins("[owner.key]/([owner.name]) has ended their mentorship of [acolyte.owner.name]")
+ log_admin("[owner.key]/([owner.name]) has ended their mentorship of [acolyte.owner.name]")
+ acolyte.mentor = null
+ acolytes = list()
+
+/datum/antagonist/cult/proc/write_rune(var/word_to_draw)
+ var/mob/living/user = owner.current
+
+ if (user.incapacitated())
+ return
+
+ var/muted = user.occult_muted()
+ if (muted)
+ to_chat(user, span_danger("You find yourself unable to focus your mind on the words of Nar-Sie.") )
+ return
+
+ if(!istype(user.loc, /turf))
+ to_chat(user, span_warning("You do not have enough space to write a proper rune.") )
+ return
+
+ if(istype(user.loc, /turf/open/space))
+ to_chat(user, span_warning("Get over a solid surface first!") )
+ return
+
+ var/turf/T = get_turf(user)
+ var/obj/effect/new_rune/rune = locate() in T
+
+ if(rune)
+ if (rune.invisibility == INVISIBILITY_OBSERVER)
+ to_chat(user, span_warning("You can feel the presence of a concealed rune here. You have to reveal it before you can add more words to it.") )
+ return
+ else if (rune.word1 && rune.word2 && rune.word3)
+ to_chat(user, span_warning("You cannot add more than 3 words to a rune.") )
+ return
+
+ var/datum/rune_word/word = GLOB.rune_words[word_to_draw]
+ var/list/rune_blood_data = use_available_blood(user, rune_blood_cost, feedback = verbose)
+ if (rune_blood_data[BLOODCOST_RESULT] == BLOODCOST_FAILURE)
+ return
+
+ if (verbose)
+ if(rune)
+ user.visible_message(span_warning("\The [user] chants and paints more symbols on the floor.") , \
+ span_warning("You add another word to the rune.") , \
+ span_warning("You hear chanting.") )
+ else
+ user.visible_message(span_warning("\The [user] begins to chant and paint symbols on the floor.") , \
+ span_warning("You begin drawing a rune on the floor.") , \
+ span_warning("You hear some chanting.") )
+
+ if(!user.checkTattoo(TATTOO_SILENT))
+ user.whisper("...[word.rune]...")
+
+ if(rune)
+ if(rune.word1 && rune.word2 && rune.word3)
+ to_chat(user, span_warning("You cannot add more than 3 words to a rune.") )
+ return
+ gain_devotion(10, DEVOTION_TIER_0, "write_rune", word.english)
+ write_rune_word(get_turf(user), word, rune_blood_data["blood"], caster = user)
+ verbose = FALSE
+
+
+/datum/antagonist/cult/proc/assign_rituals()
+ initial_rituals = TRUE
+ var/list/valid_rituals = list()
+
+ for (var/datum/bloodcult_ritual/R in possible_rituals)
+ if (R.pre_conditions(src))
+ valid_rituals += R
+
+ if (valid_rituals.len < 2)
+ return
+
+ var/datum/bloodcult_ritual/previous_ritual
+ for (var/ritual_slot in rituals)
+ var/datum/bloodcult_ritual/BR = pick(valid_rituals)
+ if ((previous_ritual) && (previous_ritual.ritual_type == BR.ritual_type))
+ BR = pick(valid_rituals)//slightly reducing chances of having several rituals of the same type
+ else
+ previous_ritual = BR
+ rituals[ritual_slot] = BR
+ possible_rituals -= BR
+ valid_rituals -= BR
+ BR.init_ritual()
+
+ var/datum/mind/M = owner
+
+ if ("Cult Panel" in M.active_uis)
+ var/datum/mind_ui/m_ui = M.active_uis["Cult Panel"]
+ if (m_ui.active)
+ m_ui.Display()
+
+/datum/antagonist/cult/process()
+ ..()
+ if (holywarning_cooldown > 0)
+ holywarning_cooldown--
+ if ((cultist_role == CULTIST_ROLE_ACOLYTE) && !mentor)
+ FindMentor()
+
+ if (cult_team)
+ var/datum/team/cult/cult = cult_team
+ if (!initial_rituals && cult.countdown_to_first_rituals <= 0)
+ assign_rituals()
+ var/mob/M = owner.current
+ if (M)
+ to_chat(M, span_cult("Although you can generate devotion by performing most cult activities, a couple rituals for you to perform are now available. Check the cult panel.") )
+ if (!owner.current)
+ return
+ switch(cult.stage)
+ if (BLOODCULT_STAGE_READY)
+ owner.current.add_particles(PS_CULT_SMOKE)
+ owner.current.add_particles(PS_CULT_SMOKE2)
+ if (cult.tear_ritual && cult.tear_ritual.dance_count)
+ var/count = clamp(cult.tear_ritual.dance_count / 400, 0.01, 0.6)
+ owner.current.adjust_particles(PVAR_SPAWNING, count, PS_CULT_SMOKE)
+ owner.current.adjust_particles(PVAR_SPAWNING, count, PS_CULT_SMOKE2)
+ else
+ if (prob(1))
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.05, PS_CULT_SMOKE)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.05, PS_CULT_SMOKE2)
+ else
+ owner.current.adjust_particles(PVAR_SPAWNING, 0, PS_CULT_SMOKE)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0, PS_CULT_SMOKE2)
+ if (BLOODCULT_STAGE_MISSED)
+ owner.current.remove_particles(PS_CULT_SMOKE)
+ owner.current.remove_particles(PS_CULT_SMOKE2)
+ if (BLOODCULT_STAGE_ECLIPSE)
+ if(!HasElement(owner.current, /datum/element/cult_eyes)) // look into moving this into a single run stage check like teams
+ owner.current.AddElement(/datum/element/cult_eyes)
+ owner.current.add_particles(PS_CULT_SMOKE)
+ owner.current.add_particles(PS_CULT_SMOKE2)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.6, PS_CULT_SMOKE)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.6, PS_CULT_SMOKE2)
+ owner.current.add_particles(PS_CULT_HALO)
+ owner.current.adjust_particles(PVAR_ICON_STATE, "cult_halo[get_devotion_rank()]", PS_CULT_HALO)
+ if (BLOODCULT_STAGE_DEFEATED)
+ owner.current.add_particles(PS_CULT_SMOKE)
+ owner.current.add_particles(PS_CULT_SMOKE2)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.19, PS_CULT_SMOKE)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.21, PS_CULT_SMOKE2)
+ owner.current.add_particles(PS_CULT_HALO)
+ owner.current.adjust_particles(PVAR_COLOR, "#00000066", PS_CULT_HALO)
+ owner.current.adjust_particles(PVAR_ICON_STATE, "cult_halo[get_devotion_rank()]", PS_CULT_HALO)
+ if (BLOODCULT_STAGE_NARSIE)
+ owner.current.add_particles(PS_CULT_SMOKE)
+ owner.current.add_particles(PS_CULT_SMOKE2)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.6, PS_CULT_SMOKE)
+ owner.current.adjust_particles(PVAR_SPAWNING, 0.6, PS_CULT_SMOKE2)
+ owner.current.add_particles(PS_CULT_HALO)
+ owner.current.adjust_particles(PVAR_ICON_STATE, "cult_halo[get_devotion_rank()]", PS_CULT_HALO)
+
+/datum/antagonist/cult/proc/get_eclipse_increment()
+ switch(get_devotion_rank())
+ if (DEVOTION_TIER_0)
+ return 0.10
+ if (DEVOTION_TIER_1)
+ return 0.10 + (devotion-100)*0.000375
+ if (DEVOTION_TIER_2)
+ return 0.25 + (devotion-500)*0.0003
+ if (DEVOTION_TIER_3)
+ return 0.40 + (devotion-1000)*0.0001
+ if (DEVOTION_TIER_4)
+ return 0.50 + (devotion-2000)*0.00005
diff --git a/monkestation/code/modules/bloody_cult/cult/cult_spells.dm b/monkestation/code/modules/bloody_cult/cult/cult_spells.dm
new file mode 100644
index 000000000000..639253bb8715
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/cult_spells.dm
@@ -0,0 +1,102 @@
+
+/datum/action/cooldown/spell/cult
+ panel = "Cult"
+ overlay_icon_state = "cult"
+ button_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ spell_requirements = NONE
+ var/pointed = FALSE
+
+/datum/action/cooldown/spell/cult/is_valid_target(atom/cast_on)
+ . = ..()
+ if(cast_on != owner)
+ return FALSE
+
+// Not sure what to do with this spell really, it always kinda sucked and tomes as a whole need an overhaul. Runic Skin is a better power.
+var/list/arcane_pockets = list()
+
+/datum/action/cooldown/spell/cult/arcane_dimension
+ name = "Arcane Dimension (empty)"
+ desc = "Cast while holding an Arcane Tome to discretly store it through the veil."
+ button_icon_state = "cult_pocket_empty"
+ button_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon_state = "const_spell_base"
+ invocation_type = INVOCATION_NONE
+ var/obj/item/weapon/tome/stored_tome = null
+
+/datum/action/cooldown/spell/cult/arcane_dimension/New()
+ ..()
+ arcane_pockets.Add(src)
+
+/datum/action/cooldown/spell/cult/arcane_dimension/Destroy()
+ arcane_pockets.Remove(src)
+ ..()
+
+/datum/action/cooldown/spell/cult/arcane_dimension/cast(mob/living/user)
+ ..()
+ if (user.occult_muted())
+ to_chat(user, span_warning("You can't seem to remember how to access your arcane dimension right now.") )
+ return 0
+ if (stored_tome)
+ stored_tome.forceMove(get_turf(user))
+ if (user.get_inactive_held_item() && user.get_active_held_item())//full hands
+ to_chat(user, span_warning("Your hands being full, your [stored_tome] had nowhere to fall but on the ground.") )
+ else
+ to_chat(user, span_notice("You hold your hand palm up, and your [stored_tome] drops in it from thin air.") )
+ user.put_in_hands(stored_tome)
+ stored_tome = null
+ name = "Arcane Dimension (empty)"
+ desc = "Cast while holding an Arcane Tome to discretly store it through the veil."
+ button_icon_state = "cult_pocket_empty"
+ return
+
+ var/obj/item/weapon/tome/held_tome = locate() in user.held_items
+ if (held_tome)
+ if (held_tome.state == TOME_OPEN)
+ held_tome.icon_state = "tome"
+ held_tome.state = TOME_CLOSED
+ stored_tome = held_tome
+ user.dropItemToGround(held_tome)
+ held_tome.loc = null
+ to_chat(user, span_notice("With a swift movement of your arm, you drop \the [held_tome] that disappears into thin air before touching the ground.") )
+ name = "Arcane Dimension (full)"
+ desc = "Cast to pick up your Arcane Tome back from the veil. You should preferably have a free hand."
+ button_icon_state = "cult_pocket_full"
+
+
+///////////////////////////////ASTRAL PROJECTION SPELLS/////////////////////////////////////
+
+
+/datum/action/cooldown/spell/astral_return
+ name = "Re-enter Body"
+ desc = "End your astral projection and re-awaken inside your body. If used while tangible you might spook on-lookers, so be mindful."
+ button_icon_state = "astral_return"
+ button_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon_state = "const_spell_base"
+ spell_requirements = NONE
+
+
+/datum/action/cooldown/spell/astral_return/cast(mob/living/user)
+ . = ..()
+ var/mob/living/basic/astral_projection/astral = user
+ if (istype(astral))
+ astral.death()//pretty straightforward isn't it?
+
+/datum/action/cooldown/spell/astral_toggle
+ name = "Toggle Tangibility"
+ desc = "Turn into a visible copy of your body, able to speak and bump into doors. But note that the slightest source of damage will dispel your astral projection altogether."
+ button_icon_state = "astral_toggle"
+ button_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon_state = "const_spell_base"
+ spell_requirements = NONE
+
+/datum/action/cooldown/spell/astral_toggle/cast(mob/living/user)
+ . = ..()
+ var/mob/living/basic/astral_projection/astral = user
+ astral.toggle_tangibility()
+ if (astral.tangibility)
+ desc = "Turn back into an invisible projection of your soul."
+ else
+ desc = "Turn into a visible copy of your body, able to speak and bump into doors. But note that the slightest source of damage will dispel your astral projection altogether."
diff --git a/monkestation/code/modules/bloody_cult/cult/cult_team.dm b/monkestation/code/modules/bloody_cult/cult/cult_team.dm
new file mode 100644
index 000000000000..312181fe6f3a
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/cult_team.dm
@@ -0,0 +1,504 @@
+#define CULT_VICTORY 1
+#define CULT_LOSS 0
+#define CULT_NARSIE_KILLED -1
+
+/datum/team/cult
+ name = "\improper Cult"
+
+ ///The blood mark target
+ var/atom/blood_target
+ ///Image of the blood mark target
+ var/image/blood_target_image
+ ///Timer for the blood mark expiration
+ var/blood_target_reset_timer
+
+ ///Has a vote been called for a leader?
+ var/cult_vote_called = FALSE
+ ///The cult leader
+ var/mob/living/cult_master
+ ///Has the mass teleport been used yet?
+ var/reckoning_complete = FALSE
+ ///Has the cult risen, and gotten red eyes?
+ var/cult_risen = FALSE
+ ///Has the cult asceneded, and gotten halos?
+ var/cult_ascendent = FALSE
+
+ ///Has narsie been summoned yet?
+ var/narsie_summoned = FALSE
+ ///How large were we at max size.
+ var/size_at_maximum = 0
+ ///list of cultists just before summoning Narsie
+ var/list/true_cultists = list()
+
+
+ //////NEW STUFF HERE
+ var/stage = BLOODCULT_STAGE_NORMAL
+ var/list/bloody_floors = list()
+ var/cult_win = FALSE
+
+ var/list/cult_reminders = list()
+
+ var/list/bindings = list()
+
+ var/cultist_cap = 1 //clamped between 5 and 9 depending on crew size. once the cap goes up it cannot go down.
+ var/min_cultist_cap = 5
+ var/max_cultist_cap = 9
+
+ var/mentor_count = 0 //so we don't loop through the member list if we already know there are no mentors in there
+
+ var/cult_founding_time = 0
+ var/last_process_time = 0
+ var/delta = 1
+
+ var/eclipse_progress = 0
+ var/eclipse_target = 1800
+ var/eclipse_window = 15 MINUTES
+ var/eclipse_increments = 0
+ var/eclipse_contributors = list()//associative list: /mind = score
+
+ var/soon_announcement = FALSE
+ var/overtime_announcement = FALSE
+
+ var/bloodstone_rising_time = 0
+ var/bloodstone_duration = 430 SECONDS
+ var/bloodstone_target_time = 0
+
+ var/datum/rune_spell/tearreality/tear_ritual = null
+ var/obj/structure/cult/bloodstone/bloodstone = null //we track the one spawned by the Tear Reality rune
+ var/obj/narsie/narsie = null
+
+ //we track the mind of anyone that has been converted or made prisoner at least once.
+ var/previously_made_prisoner = list()
+ var/previously_converted = list()
+
+ var/total_devotion = 0
+
+ var/twister = FALSE
+
+ var/list/deconverted = list()//tracking for scoreboard purposes
+
+ var/datum/bloodcult_ritual/bloodspill_ritual = null
+
+ var/list/possible_rituals = list()
+ var/list/rituals = list(RITUAL_FACTION_1, RITUAL_FACTION_2, RITUAL_FACTION_3)
+
+ var/countdown_to_first_rituals = 5
+
+
+/datum/team/cult/add_member(datum/mind/new_member)
+ . = ..()
+ // A little hacky, but this checks that cult ghosts don't contribute to the size at maximum value.
+ if(is_unassigned_job(new_member.assigned_role))
+ return
+ size_at_maximum++
+
+/datum/team/cult/proc/setup_objectives()
+ START_PROCESSING(SSobj, src)
+ for (var/ritual_type in GLOB.bloodcult_faction_rituals)
+ possible_rituals += new ritual_type()
+ cult_founding_time = world.time
+ initialize_rune_words()
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ to_chat(M, span_cult("Our communion must remain small and secretive until we are confident enough.") )
+ previously_converted |= mind
+
+
+/datum/team/cult/proc/replace_rituals(var/slot)
+ if (!slot)
+ return
+
+ var/list/valid_rituals = list()
+
+ for (var/datum/bloodcult_ritual/R in possible_rituals)
+ if (R.pre_conditions())
+ valid_rituals += R
+
+ if (valid_rituals.len < 1)
+ return
+
+ var/datum/bloodcult_ritual/BR = pick(valid_rituals)
+ rituals[slot] = BR
+ possible_rituals -= BR
+ BR.init_ritual()
+
+ for (var/datum/antagonist/cult/cultist in members)
+ var/mob/O = cultist.owner.current
+ if (O)
+ to_chat(O, span_cult("A new ritual is available...") )
+ var/datum/mind/M = cultist.owner
+ if ("Cult Panel" in M.active_uis)
+ var/datum/mind_ui/m_ui = M.active_uis["Cult Panel"]
+ if (m_ui.active)
+ m_ui.Display()
+
+/datum/team/cult/proc/UpdateCap()
+ if (stage == BLOODCULT_STAGE_DEFEATED)
+ cultist_cap = 0
+ return
+ if (stage == BLOODCULT_STAGE_NARSIE)
+ cultist_cap = 666
+ return
+ var/living_players = 0
+ var/new_cap = 0
+ for (var/mob/M in GLOB.player_list)
+ if (!M.client)
+ continue
+ if (istype(M, /mob/dead/new_player))
+ continue
+ if (M.stat != DEAD)
+ living_players++
+ new_cap = clamp(round(living_players / 3), min_cultist_cap, max_cultist_cap)
+ if (new_cap > cultist_cap)
+ cultist_cap = new_cap
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ to_chat(M, span_cult("The station population is now large enough for [cultist_cap] cultists, plus one of each construct types.") )
+
+/datum/team/cult/proc/CanConvert(construct_type)
+ var/list/free_construct_slots = list()
+ var/cultist_count = 0
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ //The first construct of each type doesn't take up a slot.
+ if (istype(M, /mob/living/basic/construct))
+ var/mob/living/basic/construct/C = M
+ if (!(C.construct_type in free_construct_slots))
+ free_construct_slots += C.construct_type
+ continue
+ //Living Humans, Shades and extra Constructs all count.
+ if (isliving(M))
+ if (M.stat != DEAD)
+ cultist_count += 1
+
+ if(construct_type && (!(construct_type in free_construct_slots)))
+ return TRUE
+
+ return (cultist_count < cultist_cap)
+
+
+/datum/team/cult/proc/check_ritual(var/key, var/extra)
+ switch(stage)
+ if (BLOODCULT_STAGE_DEFEATED)//no more devotion gains if the bloodstone has been destroyed
+ return
+ if (BLOODCULT_STAGE_NARSIE)//or narsie has risen
+ return
+
+ if (key && (stage != BLOODCULT_STAGE_ECLIPSE))
+ for (var/ritual_slot in rituals)
+ if (rituals[ritual_slot])
+ var/datum/bloodcult_ritual/faction_ritual = rituals[ritual_slot]
+ if (key in faction_ritual.keys)
+ if (faction_ritual.key_found(extra))
+ faction_ritual.complete()
+ if (!faction_ritual.only_once)
+ possible_rituals += faction_ritual
+ rituals[ritual_slot] = null
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ if (M)
+ to_chat(M, span_cult("Someone has completed a ritual, rewarding the entire cult...soon another ritual will take its place.") )
+ spawn(10 MINUTES)
+ if (!rituals[ritual_slot])
+ replace_rituals(ritual_slot)
+
+/datum/team/cult/proc/stage(var/value)
+ stage = value
+ switch(stage)
+ if (BLOODCULT_STAGE_READY)
+ eclipse_trigger_cult()
+ addtimer(CALLBACK(src, PROC_REF(stage), BLOODCULT_STAGE_ECLIPSE), 5 SECONDS)
+ for(var/obj/structure/cult/spire/S in GLOB.cult_spires)
+ S.upgrade(3)
+ if (BLOODCULT_STAGE_MISSED)
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ if (M)
+ to_chat(M, span_cult("The Eclipse has passed. You won't be able to tear reality aboard this station anymore. Escape the station alive with your fellow cultists so you may try again another day.") )
+ for(var/obj/structure/cult/spire/S in GLOB.cult_spires)
+ S.upgrade(1)
+ if (BLOODCULT_STAGE_ECLIPSE)
+ setup_hell()
+ var/list/station_zs = SSmapping.levels_by_trait(ZTRAIT_STATION)
+ for(var/datum/space_level/level as anything in SSmapping.z_list)
+ if(!(level.z_value in station_zs))
+ continue
+ level.set_linkage(SELFLOOPING)
+
+ INVOKE_ASYNC(src, PROC_REF(narnar_ghosts))
+ bloodstone_rising_time = world.time
+ bloodstone_target_time = world.time + bloodstone_duration
+ spawn (3 SECONDS)//leaving just a moment for the blood stone to rise.
+ var/sec_change = TRUE
+ priority_announce("Bluespace fluctuation patterns match those observed during past incursions by the Cult of Nar-Sie, which means a Blood Stone has risen. Find and destroy it at all costs or this station will be lost. Be careful of the eldritch entities that may manifest across the station.", "Cult Activity Detected")
+ if (sec_change)
+ sleep(2 SECONDS)
+ SSsecurity_level.set_level(SEC_LEVEL_RED, announce = FALSE)
+
+ if (BLOODCULT_STAGE_DEFEATED)
+ GLOB.eclipse.eclipse_end()
+ for (var/obj/effect/new_rune/R in runes)
+ qdel(R)//new runes can be written, but any pre-existing one gets nuked.
+ cultist_cap = 0
+ spawn()
+ for(var/mob/living/simple_animal/M in GLOB.mob_list)
+ if(!M.client && (M.faction == "cult"))
+ M.death()
+ CHECK_TICK
+ spawn()
+ for(var/obj/structure/cult/spire/S in GLOB.cult_spires)
+ S.upgrade(1)
+ spawn(5 SECONDS)
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ to_chat(M, span_cult("With the blood stone destroyed, the tear through the veil has been mended, and a great deal of occult energies have been purged from the Station.") )
+ sleep(3 SECONDS)
+ to_chat(M, span_cult("Your connection to the Geometer of Blood has grown weaker and you can no longer recall the runes as easily as you did before. Maybe an Arcane Tome can alleviate the problem.") )
+ sleep(3 SECONDS)
+ to_chat(M, span_cult("Lastly it seems that the toll of the ritual on your body hasn't gone away. Going unnoticed will be a lot harder.") )
+ if (BLOODCULT_STAGE_NARSIE)
+ if (bloodstone)
+ anim(target = bloodstone.loc, a_icon = 'icons/obj/cult/narsie.dmi', flick_anim = "narsie_spawn_anim_start", offX = -236, offY = -256, plane = MASSIVE_OBJ_PLANE)
+ sleep(5)
+ narsie = new(bloodstone.loc)
+ for (var/datum/mind/M in members)
+ if ("Cult Panel" in M.active_uis)
+ var/datum/mind_ui/m_ui = M.active_uis["Cult Panel"]
+ if (m_ui.active)
+ m_ui.Display()
+ if ("Cultist Panel" in M.active_uis)
+ var/datum/mind_ui/m_ui = M.active_uis["Cultist Panel"]
+ if (m_ui.active)
+ m_ui.Display()
+
+/datum/team/cult/proc/narnar_ghosts()
+ for (var/mob/dead/observer/O in GLOB.player_list)
+ O.narsie_act()
+ sleep(rand(1, 5))
+
+/datum/team/cult/proc/HandleRecruitedRole(datum/antagonist/R)
+ if (cult_reminders.len)
+ to_chat(R.owner.current, span_notice("Other cultists have shared some of their knowledge. It will be stored in your memory (check your Notes under the IC tab).") )
+ /*
+ for (var/reminder in cult_reminders)
+ R.antag.store_memory("Shared Cultist Knowledge: [reminder].")
+ */
+ previously_converted |= R.owner
+ if (R.owner.name in deconverted)
+ deconverted -= R.owner.name
+
+/datum/team/cult/proc/assign_rituals()
+ var/list/valid_rituals = list()
+
+ for (var/datum/bloodcult_ritual/R in possible_rituals)
+ if (R.pre_conditions())
+ valid_rituals += R
+
+ if (valid_rituals.len < 3)
+ return
+
+ for (var/ritual_slot in rituals)
+ var/datum/bloodcult_ritual/BR = pick(valid_rituals)
+ rituals[ritual_slot] = BR
+ possible_rituals -= BR
+ valid_rituals -= BR
+ BR.init_ritual()
+
+ for (var/datum/mind/M in members)
+ if ("Cult Panel" in M.active_uis)
+ var/datum/mind_ui/m_ui = M.active_uis["Cult Panel"]
+ if (m_ui.active)
+ m_ui.Display()
+
+/datum/team/cult/process()
+ ..()
+ if (cultist_cap >= 1) //The first call occurs in OnPostSetup()
+ UpdateCap()
+
+ switch(stage)
+ if (BLOODCULT_STAGE_NORMAL)
+ if (bloodspill_ritual)
+ check_ritual("bloodspill", bloody_floors.len)
+ //if there is at least one cultist alive, the eclipse comes forward
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ calculate_eclipse_rate()
+ if (isliving(M) && M.stat != DEAD)
+ //we calculate the progress relative to the time since the last process so the overall time is independant from server lag and shit
+ delta = 1
+ if (last_process_time && (last_process_time < world.time))//carefully dealing with midnight rollover
+ delta = (world.time - last_process_time)
+ if(SSticker.initialized)
+ delta /= SSticker.wait
+ last_process_time = world.time
+
+ eclipse_progress += max(0.1, eclipse_increments) * delta
+ if (eclipse_progress >= eclipse_target)
+ stage(BLOODCULT_STAGE_READY)
+ break
+ if (countdown_to_first_rituals)
+ countdown_to_first_rituals--
+ if (countdown_to_first_rituals <= 0)
+ assign_rituals()
+ for (var/datum/mind/mind in members)
+ var/datum/antagonist/cult/cult_datum = mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.assign_rituals()
+ var/mob/M = mind.current
+ if (M)
+ to_chat(M, span_cult("Although you can generate devotion by performing most cult activities, a couple rituals for you to perform are now available. Check the cult panel.") )
+
+
+ if (BLOODCULT_STAGE_MISSED)
+ if (bloodspill_ritual)
+ check_ritual("bloodspill", bloody_floors.len)
+ if (BLOODCULT_STAGE_READY)
+ if (GLOB.eclipse.eclipse_finished)
+ stage(BLOODCULT_STAGE_MISSED)
+ if (BLOODCULT_STAGE_ECLIPSE)
+ bloodstone.update_icon()
+ if (world.time >= bloodstone_target_time)
+ stage(BLOODCULT_STAGE_NARSIE)
+
+/datum/team/cult/proc/calculate_eclipse_rate()
+ eclipse_increments = 0
+ for (var/datum/mind/mind in members)
+ var/mob/M = mind.current
+ var/datum/antagonist/cult/R = mind.has_antag_datum(/datum/antagonist/cult)
+ if (isliving(M) && M.stat != DEAD)
+ if (M.occult_muted())
+ eclipse_increments -= R.get_eclipse_increment()
+ else
+ eclipse_increments += R.get_eclipse_increment()
+
+
+/datum/team/cult/proc/setup_hell()
+ SShell_universe.start_hell()
+
+/datum/team/cult/proc/add_bloody_floor(turf/T)
+ if (!istype(T))
+ return
+ if(T && (is_station_level(T.z)))
+ if(!(locate("\ref[T]") in bloody_floors))
+ bloody_floors[T] = T
+
+
+/datum/team/cult/proc/remove_bloody_floor(turf/T)
+ if (!istype(T))
+ return
+ bloody_floors -= T
+
+/datum/team/cult/proc/check_cult_victory()
+ for(var/datum/objective/O in objectives)
+ if(O.check_completion() == CULT_NARSIE_KILLED)
+ return CULT_NARSIE_KILLED
+ else if(!O.check_completion())
+ return CULT_LOSS
+ return CULT_VICTORY
+
+/datum/team/cult/roundend_report()
+ var/list/parts = list()
+
+ switch(stage)
+ if(BLOODCULT_STAGE_MISSED)
+ parts += "The cult missed the chance to summon Nar'Sie. They have failed her!"
+ if(BLOODCULT_STAGE_DEFEATED)
+ parts += "The crew has destroyed the bloodstone preventing Nar'Sie from destroying the station."
+ if(BLOODCULT_STAGE_NARSIE)
+ parts += "The cult has succeeded! Nar'Sie has snuffed out another torch in the void!"
+
+ if(members.len)
+ parts += span_header("The cultists were:")
+ if(length(true_cultists))
+ parts += printplayerlist(true_cultists)
+ else
+ parts += printplayerlist(members)
+
+ return "
[parts.Join(" ")]
"
+
+/// Sets a blood target for the cult.
+/datum/team/cult/proc/set_blood_target(atom/new_target, mob/marker, duration = 90 SECONDS)
+ if(QDELETED(new_target))
+ CRASH("A null or invalid target was passed to set_blood_target.")
+
+ if(duration != INFINITY && blood_target_reset_timer)
+ return FALSE
+
+ deltimer(blood_target_reset_timer)
+ blood_target = new_target
+ RegisterSignal(blood_target, COMSIG_QDELETING, PROC_REF(unset_blood_target_and_timer))
+ var/area/target_area = get_area(new_target)
+
+ blood_target_image = image('icons/effects/mouse_pointers/cult_target.dmi', new_target, "glow", ABOVE_MOB_LAYER)
+ blood_target_image.appearance_flags = RESET_COLOR
+ blood_target_image.pixel_x = -new_target.pixel_x
+ blood_target_image.pixel_y = -new_target.pixel_y
+
+ for(var/datum/mind/cultist as anything in members)
+ if(!cultist.current)
+ continue
+ if(cultist.current.stat == DEAD || !cultist.current.client)
+ continue
+
+ to_chat(cultist.current, span_bold(span_cultlarge("[marker] has marked [blood_target] in the [target_area.name] as the cult's top priority, get there immediately!")))
+ SEND_SOUND(cultist.current, sound(pick('sound/hallucinations/over_here2.ogg', 'sound/hallucinations/over_here3.ogg'), 0, 1, 75))
+ cultist.current.client.images += blood_target_image
+
+ if(duration != INFINITY)
+ blood_target_reset_timer = addtimer(CALLBACK(src, PROC_REF(unset_blood_target)), duration, TIMER_STOPPABLE)
+ return TRUE
+
+/// Unsets our blood target when they get deleted.
+/datum/team/cult/proc/unset_blood_target_and_timer(datum/source)
+ SIGNAL_HANDLER
+
+ deltimer(blood_target_reset_timer)
+ unset_blood_target()
+
+/// Unsets out blood target, clearing the images from all the cultists.
+/datum/team/cult/proc/unset_blood_target()
+ blood_target_reset_timer = null
+
+ for(var/datum/mind/cultist as anything in members)
+ if(!cultist.current)
+ continue
+ if(cultist.current.stat == DEAD || !cultist.current.client)
+ continue
+
+ if(QDELETED(blood_target))
+ to_chat(cultist.current, span_bold(span_cultlarge("The blood mark's target is lost!")))
+ else
+ to_chat(cultist.current, span_bold(span_cultlarge("The blood mark has expired!")))
+ cultist.current.client.images -= blood_target_image
+
+ UnregisterSignal(blood_target, COMSIG_QDELETING)
+ blood_target = null
+
+ QDEL_NULL(blood_target_image)
+
+/datum/antagonist/cult/antag_token(datum/mind/hosts_mind, mob/spender)
+ var/datum/antagonist/cult/new_cultist = new
+ new_cultist.cult_team = get_team()
+ new_cultist.give_equipment = TRUE
+ if(isobserver(spender))
+ var/mob/living/carbon/human/new_mob = spender.change_mob_type( /mob/living/carbon/human, delete_old_mob = TRUE)
+ new_mob.equipOutfit(/datum/outfit/job/assistant)
+ new_mob.mind.add_antag_datum(new_cultist)
+ else
+ hosts_mind.add_antag_datum(new_cultist)
+
+/datum/outfit/cultist
+ name = "Cultist (Preview only)"
+
+ uniform = /obj/item/clothing/under/color/black
+ suit = /obj/item/clothing/suit/hooded/cultrobes/alt
+ shoes = /obj/item/clothing/shoes/cult/alt
+ r_hand = /obj/item/melee/blood_magic/stun
+
+/datum/outfit/cultist/post_equip(mob/living/carbon/human/equipped, visualsOnly)
+ equipped.eye_color_left = BLOODCULT_EYE
+ equipped.eye_color_right = BLOODCULT_EYE
+ equipped.update_body()
+
+#undef CULT_LOSS
+#undef CULT_NARSIE_KILLED
+#undef CULT_VICTORY
diff --git a/monkestation/code/modules/bloody_cult/cult/cultify_exceptions.dm b/monkestation/code/modules/bloody_cult/cult/cultify_exceptions.dm
new file mode 100644
index 000000000000..d055cfed5268
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/cultify_exceptions.dm
@@ -0,0 +1,96 @@
+/turf/proc/decultify()
+ update_icon()
+
+//Machinery that isn't replaced with cult structures by narsie_act()
+/obj/machinery/camera/narsie_act()
+ qdel(src)
+
+/obj/machinery/power/narsie_act()
+ qdel(src)
+
+/obj/machinery/light_switch/narsie_act()
+ qdel(src)
+
+/obj/machinery/firealarm/narsie_act()
+ qdel(src)
+
+/obj/machinery/alarm/narsie_act()
+ qdel(src)
+
+/obj/machinery/atm/narsie_act()
+ qdel(src)
+
+/obj/machinery/hologram/narsie_act()
+ qdel(src)
+
+/obj/machinery/status_display/narsie_act()
+ qdel(src)
+
+/obj/machinery/newscaster/narsie_act()
+ qdel(src)
+
+/obj/machinery/media/narsie_act()
+ qdel(src)
+
+/obj/machinery/door_control/narsie_act()
+ qdel(src)
+
+/obj/machinery/access_button/narsie_act()
+ qdel(src)
+
+/obj/machinery/embedded_controller/narsie_act()
+ qdel(src)
+
+/obj/machinery/navbeacon/narsie_act()
+ qdel(src)
+
+/obj/machinery/gateway/narsie_act()
+ qdel(src)
+
+/obj/machinery/space_heater/narsie_act()
+ qdel(src)
+
+/obj/machinery/crema_switch/narsie_act()
+ qdel(src)
+
+/obj/machinery/portable_atmospherics/narsie_act()
+ qdel(src)
+
+/obj/machinery/pos/narsie_act()
+ qdel(src)
+
+/obj/machinery/requests_console/narsie_act()
+ qdel(src)
+
+/obj/machinery/computer/security/telescreen/narsie_act()
+ qdel(src)
+
+/obj/machinery/conveyor_switch/narsie_act()
+ qdel(src)
+
+/obj/machinery/conveyor/narsie_act()
+ qdel(src)
+
+/obj/machinery/vending/wallmed1/narsie_act()
+ qdel(src)
+
+/obj/machinery/flasher/narsie_act()
+ qdel(src)
+
+/obj/machinery/flasher_button/narsie_act()
+ qdel(src)
+
+/obj/machinery/cell_charger/narsie_act()
+ qdel(src)
+
+/obj/machinery/meter/narsie_act()
+ qdel(src)
+
+/obj/machinery/keycard_auth/narsie_act()
+ qdel(src)
+
+/obj/machinery/airlock_sensor/narsie_act()
+ qdel(src)
+
+/obj/machinery/turretid/narsie_act()
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/eclipse_manager.dm b/monkestation/code/modules/bloody_cult/cult/eclipse_manager.dm
new file mode 100644
index 000000000000..b6b55a6a8fe7
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/eclipse_manager.dm
@@ -0,0 +1,85 @@
+GLOBAL_DATUM_INIT(eclipse, /datum/eclipse_manager, new)
+
+/datum/eclipse_manager
+ var/eclipse_start_time = 0
+ var/eclipse_end_time = 0
+ var/eclipse_duration = 0
+ var/eclipse_problem_announcement //set on eclipse_start()
+
+ //light dimming
+ var/light_reduction = 0.5
+
+ var/timestopped //sigh
+
+ var/delay_first_announcement = 10 SECONDS //time after the eclipse starts before it gets announced
+ var/delay_end_announcement = 5 SECONDS //time after the eclipse end before an announcement confirms it has ended
+ var/delay_problem_announcement = 3 MINUTES //how long after the eclipse's supposed end will the crew be warned (in case the cult is extending the eclipse's duration)
+
+ var/problem_announcement = FALSE
+ var/eclipse_finished = FALSE
+
+/proc/eclipse_trigger_cult()
+ if (!GLOB.eclipse)
+ return
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!cult)
+ return
+ GLOB.eclipse.eclipse_start(cult.eclipse_window)
+
+/proc/eclipse_trigger_random()
+ if (!GLOB.eclipse)
+ return
+ GLOB.eclipse.eclipse_start(rand(12 MINUTES, 20 MINUTES))
+
+/datum/eclipse_manager/proc/eclipse_start(var/duration)
+ eclipse_start_time = world.time
+ eclipse_duration = duration
+ eclipse_end_time = eclipse_start_time + eclipse_duration
+ eclipse_problem_announcement = eclipse_end_time + delay_problem_announcement
+
+ START_PROCESSING(SSobj, src)
+ update_station_lights()
+
+ /*
+ for (var/mob/M in GLOB.player_list)
+ M.playsound_local(get_turf(M), 'sound/effects/wind/wind_5_1.ogg', 100, 0)
+
+ spawn (delay_first_announcement)
+ command_alert(/datum/command_alert/eclipse_start)
+ */
+
+/datum/eclipse_manager/process()
+ if (world.time >= eclipse_end_time)
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!cult || (!cult.tear_ritual && !cult.bloodstone))
+ eclipse_end()
+ else if (!cult.overtime_announcement)
+ cult.overtime_announcement = TRUE
+ for (var/datum/mind/mind in cult.members)
+ var/mob/M = mind.current
+ to_chat(M, span_cult("The Eclipse is entering overtime. Even though its time as run out, Nar-Sie won't let it end as long as the Tear Reality rune is still active, or the Blood Stone is still standing.") )
+ else if (!problem_announcement && (world.time >= eclipse_problem_announcement))
+ problem_announcement = TRUE
+ //command_alert(/datum/command_alert/eclipse_too_long)
+
+/datum/eclipse_manager/proc/eclipse_end()
+ STOP_PROCESSING(SSobj, src)
+
+ update_station_lights()
+ eclipse_finished = TRUE
+
+ /*
+ spawn(delay_end_announcement)
+ command_alert(/datum/command_alert/eclipse_end)
+ */
+
+/datum/eclipse_manager/proc/update_station_lights()
+ return
+ /*
+ var/list/station_zs = levels_by_trait(ZTRAIT_STATION)
+ for (var/datum/light_source/LS in locate())
+ if (LS.top_atom.z )
+ LS.force_update()
+ CHECK_TICK
+ */
diff --git a/monkestation/code/modules/bloody_cult/cult/effects.dm b/monkestation/code/modules/bloody_cult/cult/effects.dm
new file mode 100644
index 000000000000..cb19531e54f3
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/effects.dm
@@ -0,0 +1,1312 @@
+
+///////////////////////////////////////VISUAL EFFECTS//////////////////////////////////////////////
+
+// Based on holopad rays. Causes a Shadow to move from T to C
+// "sprite" var can be replaced to use another icon_state from icons/effects/96x96.dmi
+/proc/shadow(var/atom/C, var/turf/T, var/sprite = "rune_blind")
+ var/disty = C.y - T.y
+ var/distx = C.x - T.x
+ var/newangle
+ if(!disty)
+ if(distx >= 0)
+ newangle = 90
+ else
+ newangle = 270
+ else
+ newangle = arctan(distx/disty)
+ if(disty < 0)
+ newangle += 180
+ else if(distx < 0)
+ newangle += 360
+ var/matrix/M1 = matrix()
+ var/matrix/M2 = turn(M1.Scale(1, sqrt(distx*distx+disty*disty)), newangle)
+ return anim(target = C, a_icon = 'monkestation/code/modules/bloody_cult/icons/96x96.dmi', flick_anim = sprite, offX = -32, offY = -32, plane = ABOVE_LIGHTING_PLANE, trans = M2)
+
+
+///////////////////////////////////////CULT RITUALS////////////////////////////////////////////////
+//Effects spawned by rune spells
+
+/obj/effect/cult_ritual
+ icon_state = ""
+ icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi'
+ anchored = 1
+
+/obj/effect/cult_ritual/get_cult_power()
+ return 1
+
+/obj/effect/cult_ritual/narsie_act()
+ return
+
+/obj/effect/cult_ritual/ex_act()
+ return
+
+/obj/effect/cult_ritual/emp_act()
+ return
+
+/obj/effect/cult_ritual/blob_act()
+ return
+
+/obj/effect/cult_ritual/singularity_act()
+ return
+
+///////////////////////////////////////SHORTCUT////////////////////////////////////////////////
+/obj/effect/cult_shortcut
+ name = "sigil"
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "sigil"
+ anchored = 1
+ mouse_opacity = 1
+ plane = ABOVE_LIGHTING_PLANE
+ var/persist = 0//so mappers can make permanent sigils
+
+/obj/effect/cult_shortcut/New(var/turf/loc, var/atom/model)
+ ..()
+ if (!persist)
+ spawn (60 SECONDS)
+ qdel(src)
+
+/obj/effect/cult_shortcut/attack_hand(var/mob/living/user)
+ if (!IS_CULTIST(user))
+ to_chat(user, span_warning("The markings on this wall are peculiar. You don't feel comfortable staring at them.") )
+ return
+ var/turf/T = get_turf(user)
+ if (T == loc)
+ return
+ var/jump_dir = get_dir(T, loc)
+ shadow(loc, T, "sigil_jaunt")
+ spawn(1)
+ new /obj/effect/afterimage/red(T, user)
+ user.forceMove(loc)
+ sleep(1)
+ new /obj/effect/afterimage/red(loc, user)
+ user.forceMove(get_step(loc, jump_dir))
+
+/obj/effect/cult_shortcut/narsie_act()
+ return
+
+/obj/effect/cult_shortcut/ex_act()
+ return
+
+/obj/effect/cult_shortcut/emp_act()
+ return
+
+/obj/effect/cult_shortcut/blob_act()
+ return
+
+/obj/effect/cult_shortcut/singularity_act()
+ return
+
+
+/obj/effect/afterimage
+ icon = null
+ icon_state = null
+ anchored = 1
+ mouse_opacity = 0
+ var/image_color
+
+/obj/effect/afterimage/red
+ image_color = "red"
+
+/obj/effect/afterimage/black
+ image_color = "black"
+
+/obj/effect/afterimage/richter_tackle/New()
+ ..()
+ transform = matrix()
+ pixel_x = 0
+ pixel_y = 0
+
+/obj/effect/afterimage/New(var/turf/loc, var/atom/model, var/fadout = 5, var/initial_alpha = 255, var/lay = CULT_OVERLAY_LAYER, var/pla = ABOVE_LIGHTING_PLANE)
+ ..()
+ if(model)
+ appearance = model.appearance
+ invisibility = 0
+ alpha = initial_alpha
+ dir = model.dir
+ if (image_color)
+ color = image_color
+ layer = lay
+ plane = pla
+ animate(src, alpha = 0, time = fadout)
+ spawn(fadout)
+ qdel(src)
+
+/obj/effect/afterimage/narsie_act()
+ return
+
+/obj/effect/afterimage/ex_act()
+ return
+
+/obj/effect/afterimage/emp_act()
+ return
+
+/obj/effect/afterimage/blob_act()
+ return
+
+/obj/effect/afterimage/singularity_act()
+ return
+
+
+///////////////////////////////////////JAUNT////////////////////////////////////////////////
+//Cultists ride in those when teleporting
+
+/obj/effect/bloodcult_jaunt
+ mouse_opacity = 0
+ icon = 'monkestation/code/modules/bloody_cult/icons/96x96.dmi'
+ icon_state ="cult_jaunt"
+ invisibility = SEE_INVISIBLE_LIVING
+ alpha = 127
+ plane = ABOVE_LIGHTING_PLANE
+ pixel_x = -32
+ pixel_y = -32
+ animate_movement = 0
+ var/atom/movable/rider = null//lone user?
+ var/list/packed = list()//moving a lot of stuff?
+
+ var/turf/starting = null
+ var/turf/target = null
+
+ var/dist_x = 0
+ var/dist_y = 0
+ var/dx = 0
+ var/dy = 0
+ var/error = 0
+ var/target_angle = 0
+
+ var/override_starting_X = 0
+ var/override_starting_Y = 0
+ var/override_target_X = 0
+ var/override_target_Y = 0
+
+ //update_pixel stuff
+ var/PixelX = 0
+ var/PixelY = 0
+
+ var/initial_pixel_x = 0
+ var/initial_pixel_y = 0
+
+ var/obj/effect/abstract/landing_animation = null
+ var/landing = 0
+
+ var/force_jaunt = FALSE
+
+ var/failsafe = 100
+
+/obj/effect/bloodcult_jaunt/New(var/turf/loc, var/mob/user, var/turf/destination, var/turf/packup, var/mob/activator)
+ ..()
+ if (!user && !packup && !force_jaunt)
+ qdel(src)
+ return
+ if (user)
+ var/muted = FALSE
+ if (user.anchored)
+ to_chat(user, span_warning("The blood jaunt fails to grasp you as you are currently anchored.") )
+ if (iscarbon(user))
+ var/mob/living/carbon/C = user
+ if (C.occult_muted())
+ muted = TRUE
+ to_chat(C, span_warning("The holy energies upon your body repel the blood jaunt.") )
+ if (!muted && !user.anchored)
+ user.forceMove(src)
+ rider = user
+ if (ismob(rider))
+ var/mob/M = rider
+ M.see_invisible = SEE_INVISIBLE_LIVING
+ if (packup)
+ var/list/noncult_victims = list()
+ for (var/atom/movable/AM in packup)
+ if (AM.anchored)
+ if (ismob(AM))
+ var/mob/M = AM
+ to_chat(M, span_warning("The blood jaunt fails to grasp you as you are currently anchored.") )
+ continue
+ var/muted = FALSE
+ if (iscarbon(AM))
+ var/mob/living/carbon/C = AM
+ if (C.occult_muted())
+ muted = TRUE
+ to_chat(C, span_warning("The holy energies upon your body repel the blood jaunt.") )
+ if(!IS_CULTIST(C))
+ noncult_victims += C
+ if (!AM.anchored && !muted)
+ AM.forceMove(src)
+ packed.Add(AM)
+ if (ismob(AM))
+ var/mob/M = AM
+ M.see_invisible = SEE_INVISIBLE_LIVING
+ starting = loc
+ target = destination
+ initial_pixel_x = pixel_x
+ initial_pixel_y = pixel_y
+ //first of all, if our target is off Z-Level, we're immediately teleporting to the edge of the map closest to the target
+ if (target?.z != z)
+ move_to_edge()
+ //quickly making sure that we're not jaunting to where we are
+ bump_target_check()
+ if (!src||!loc)
+ return
+ //calculating how many tiles we should have to cross so we can abort the jaunt if we go off-track
+ failsafe = abs(starting.x - target.x) + abs(starting.y - target.y)
+ //next, let's rotate the jaunter's sprite to face our destination
+ init_angle()
+ //now, let's launch the jaunter at our target
+ init_jaunt()
+
+/obj/effect/bloodcult_jaunt/Destroy()
+ if (rider)
+ QDEL_NULL(rider)
+ if (packed.len > 0)
+ for(var/atom/A in packed)
+ qdel(A)
+ packed = list()
+ ..()
+
+/obj/effect/bloodcult_jaunt/narsie_act()
+ return
+
+/obj/effect/bloodcult_jaunt/ex_act()
+ return
+
+/obj/effect/bloodcult_jaunt/emp_act()
+ return
+
+/obj/effect/bloodcult_jaunt/blob_act()
+ return
+
+/obj/effect/bloodcult_jaunt/singularity_act()
+ return
+
+/obj/effect/bloodcult_jaunt/Bump(atom/bumped_atom)
+ . = ..()
+ forceMove(get_step(loc, dir))
+ bump_target_check()
+
+/obj/effect/bloodcult_jaunt/proc/move_to_edge()
+ var/target_x
+ var/target_y
+ var/dx = abs(target.x - world.maxx/2)
+ var/dy = abs(target.y - world.maxy/2)
+ if (dx > dy)
+ target_y = world.maxy/2 + rand(-4, 4)
+ if (target.x > world.maxx/2)
+ target_x = world.maxx - TRANSITIONEDGE - rand(16, 20)
+ else
+ target_x = TRANSITIONEDGE + rand(16, 20)
+ else
+ target_x = world.maxx/2 + rand(-4, 4)
+ if (target.y > world.maxy/2)
+ target_y = world.maxy - TRANSITIONEDGE - rand(16, 20)
+ else
+ target_y = TRANSITIONEDGE + rand(16, 20)
+
+ var/turf/T = locate(target_x, target_y, target.z)
+ starting = T
+ forceMove(T)
+
+/obj/effect/bloodcult_jaunt/proc/init_angle()
+ dist_x = abs(target.x - starting.x)
+ dist_y = abs(target.y - starting.y)
+
+ override_starting_X = starting.x
+ override_starting_Y = starting.y
+ override_target_X = target.x
+ override_target_Y = target.y
+
+ if (target.x > starting.x)
+ dx = EAST
+ else
+ dx = WEST
+
+ if (target.y > starting.y)
+ dy = NORTH
+ else
+ dy = SOUTH
+
+ if(dist_x > dist_y)
+ error = dist_x/2 - dist_y
+ else
+ error = dist_y/2 - dist_x
+
+ target_angle = round(get_angle(starting, target))
+ var/transform_matrix = turn(matrix(), target_angle+45)
+ transform = transform_matrix
+
+/obj/effect/bloodcult_jaunt/proc/update_pixel()
+ if(src && starting && target)
+ var/AX = (override_starting_X - src.x)*32
+ var/AY = (override_starting_Y - src.y)*32
+ var/BX = (override_target_X - src.x)*32
+ var/BY = (override_target_Y - src.y)*32
+ var/XXcheck = ((BX-AX)*(BX-AX))+((BY-AY)*(BY-AY))
+ if(!XXcheck)
+ return
+ var/XX = (((BX-AX)*(-BX))+((BY-AY)*(-BY)))/XXcheck
+
+ PixelX = round(BX+((BX-AX)*XX))
+ PixelY = round(BY+((BY-AY)*XX))
+
+ PixelX += initial_pixel_x
+ PixelY += initial_pixel_y
+
+ pixel_x = PixelX
+ pixel_y = PixelY
+
+/obj/effect/bloodcult_jaunt/proc/bresenham_step(var/distA, var/distB, var/dA, var/dB)
+ var/dist = get_dist(src, target)
+ if (dist > 135)
+ make_bresenham_step(distA, distB, dA, dB)
+ if (dist > 45)
+ make_bresenham_step(distA, distB, dA, dB)
+ if (dist > 15)
+ make_bresenham_step(distA, distB, dA, dB)
+ if (dist < 10 && !landing)
+ landing = 1
+ playsound(src.target, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_prepare.ogg', 75, 0, -3)
+ landing_animation = anim(target = src.target, a_icon = 'icons/effects/effects.dmi', flick_anim = "cult_jaunt_prepare", plane = GAME_PLANE_UPPER)
+ return make_bresenham_step(distA, distB, dA, dB)
+
+/obj/effect/bloodcult_jaunt/proc/make_bresenham_step(var/distA, var/distB, var/dA, var/dB)
+ if(error < 0)
+ var/atom/step = get_step(src, dB)
+ if(!step)
+ qdel(src)
+ src.Move(step)
+ failsafe--
+ error += distA
+ bump_target_check()
+ return 0//so that we don't move twice slower in diagonals
+ else
+ var/atom/step = get_step(src, dA)
+ if(!step)
+ qdel(src)
+ src.Move(step)
+ failsafe--
+ error -= distB
+ dir = dA
+ if(error < 0)
+ dir = dA + dB
+ bump_target_check()
+ return 1
+
+/obj/effect/bloodcult_jaunt/proc/process_step()
+ var/sleeptime = 1
+ if(src.loc)
+ if(dist_x > dist_y)
+ sleeptime = bresenham_step(dist_x, dist_y, dx, dy)
+ else
+ sleeptime = bresenham_step(dist_y, dist_x, dy, dx)
+ update_pixel()
+ sleep(sleeptime)
+
+/obj/effect/bloodcult_jaunt/proc/init_jaunt()
+ set waitfor = 0
+ if (!rider && packed.len <= 0 && !force_jaunt)
+ qdel(src)
+ return
+ spawn while(loc)
+ if (ismob(rider))
+ var/mob/M = rider
+ M.next_click += 3
+ for(var/mob/M in packed)
+ M.next_click += 3
+ process_step()
+
+/obj/effect/bloodcult_jaunt/proc/bump_target_check()
+ if (loc == target || failsafe <= 0)
+ playsound(target, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_land.ogg', 30, 0, -3)
+ if (force_jaunt)
+ playsound(target, 'monkestation/code/modules/bloody_cult/sound/convert_failure.ogg', 30, 0, -1)
+ if (rider)
+ rider.forceMove(target)
+ if (ismob(rider))
+ var/mob/M = rider
+ M.see_invisible = 0
+ var/jaunter = FALSE
+ for (var/obj/effect/cult_ritual/seer/seer_ritual in seer_rituals)
+ if (seer_ritual.caster == M)
+ jaunter = TRUE
+ break
+ if (!jaunter)
+ M.see_invisible = 0
+ if (iscarbon(rider))
+ var/mob/living/carbon/C = rider
+ if (istype(C.handcuffed, /obj/item/restraints/handcuffs/cult))
+ to_chat(C, span_danger("Traveling through the veil seems to have a recharging effect on the ghastly bindings as they begin to hurt you anew.") )
+ rider = null
+ if (packed.len > 0)
+ for(var/atom/movable/AM in packed)
+ AM.forceMove(target)
+ if (ismob(AM))
+ var/mob/M = AM
+ M.see_invisible = SEE_INVISIBLE_LIVING
+ for (var/obj/effect/cult_ritual/seer/seer_ritual in seer_rituals)
+ if (seer_ritual.caster == M)
+ break
+ if (iscarbon(AM))
+ var/mob/living/carbon/C = AM
+ if (istype(C.handcuffed, /obj/item/restraints/handcuffs/cult))
+ to_chat(C, span_danger("Traveling through the veil seems to have a recharging effect on the ghastly bindings as they begin to hurt you anew.") )
+ packed = list()
+
+ if (landing_animation)
+ flick("cult_jaunt_land", landing_animation)
+ qdel(src)
+
+/obj/effect/bloodcult_jaunt/traitor
+ invisibility = 0
+ alpha = 200
+ force_jaunt = TRUE
+
+/obj/effect/bloodcult_jaunt/traitor/init_jaunt()
+ animate(src, alpha = 0, time = 3)
+ ..()
+
+/obj/effect/bloodcult_jaunt/visible
+ invisibility = 0
+ alpha = 255
+
+///////////////////////////////////////BLOODSTONE DEFENSES////////////////////////////////////////////////
+
+var/bloodstone_backup = 0
+
+/obj/effect/cult_ritual/backup_spawn
+ name = "gateway"
+ desc = "Something is coming through!"
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "runetrigger-build"
+ anchored = 1
+ mouse_opacity = 1
+
+/obj/effect/cult_ritual/backup_spawn/New()
+ ..()
+ spawn (30)
+ bloodstone_backup++
+ var/mobtype
+ switch (bloodstone_backup)
+ if (0, 1, 2)
+ mobtype = pick(
+ 1;/mob/living/basic/bat,
+ 2;/mob/living/basic/bat,
+ )
+ if (3, 4)
+ mobtype = pick(
+ 1;/mob/living/basic/bat,
+ 3;/mob/living/basic/bat,
+ 2;/mob/living/basic/bat,
+ )
+ if (5, 6)
+ mobtype = pick(
+ 2;/mob/living/basic/bat,
+ 2;/mob/living/basic/bat,
+ 1;/mob/living/basic/bat,
+ )
+ if (7 to INFINITY)
+ mobtype = pick(
+ 2;/mob/living/basic/bat,
+ 1;/mob/living/basic/bat,
+ )
+ new mobtype(get_turf(src))
+ qdel(src)
+
+///////////////////////////////////////STUN INDICATOR////////////////////////////////////////////////
+/obj/effect/stun_indicator
+ icon = null
+ anchored = 1
+ mouse_opacity = 0
+ var/list/viewers = list()
+ var/image/indicator = null
+ var/current_dots = 6
+ var/mob/living/victim = null
+
+/obj/effect/stun_indicator/New()
+ ..()
+ if (!ismob(loc))
+ qdel(src)
+ return
+
+ victim = loc
+ if (isalien(victim))
+ current_dots = clamp(round(victim.AmountParalyzed() / 10), 0, 5)
+ else
+ current_dots = clamp(round(victim.AmountKnockdown() / 10), 0, 5)
+
+ if (!current_dots)
+ qdel(src)
+ return
+
+ current_dots++//so we get integers from 1 to 6
+
+ for (var/mob/M in GLOB.player_list)
+ if (IS_CULTIST(M) && M.client)
+ viewers += M.client
+
+ indicator = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', loc = victim, icon_state = "")
+ update_indicator()
+
+/obj/effect/stun_indicator/proc/update_indicator()
+ set waitfor = FALSE
+ while (victim && (victim.stat < DEAD) && (victim.AmountKnockdown() || (isalien(victim) && victim.AmountParalyzed())))
+ for (var/client/C in viewers)
+ C.images -= indicator
+ var/dots = clamp(1+round(victim.AmountKnockdown() / 10), 1, 6)
+ if (isalien(victim))
+ dots = clamp(1+round(victim.AmountParalyzed() / 10), 1, 6)
+ var/anim = 0
+ if (dots != current_dots)
+ anim = 1
+ current_dots = dots
+ indicator.overlays.len = 0
+ indicator = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', loc = victim, icon_state = "")
+ SET_PLANE_EXPLICIT(indicator, GAME_PLANE_UPPER, victim)
+ indicator.pixel_y = 8
+ for (var/i = 1 to dots)
+ var/state = "stun_dot1"
+ if (current_dots == i)
+ if (anim)
+ state = "stun_dot2-flick"
+ var/image/I = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', icon_state = "stun_dot-gone")
+ SET_PLANE_EXPLICIT(I, GAME_PLANE_UPPER, victim)
+ I = place_indicator(I, i+1)
+ indicator.overlays += I
+ else
+ state = "stun_dot2"
+ var/image/I = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', icon_state = state)
+ SET_PLANE_EXPLICIT(I, GAME_PLANE_UPPER, victim)
+ I = place_indicator(I, i)
+ indicator.overlays += I
+ for (var/client/C in viewers)
+ C.images += indicator
+ sleep(10)
+ qdel(src)
+
+/obj/effect/stun_indicator/proc/place_indicator(var/image/I, var/dot)
+ switch (dot)
+ if (2, 3)
+ I.pixel_x = -8
+ if (5, 6)
+ I.pixel_x = 8
+ switch (dot)
+ if (2, 6)
+ I.pixel_y = 4
+ if (3, 5)
+ I.pixel_y = -4
+ if (1)
+ I.pixel_y = 8
+ if (4)
+ I.pixel_y = -8
+ return I
+
+
+
+/obj/effect/stun_indicator/Destroy()
+ for (var/client/C in viewers)
+ C.images -= indicator
+ indicator = null
+ victim = null
+ ..()
+
+/obj/effect/stun_indicator/narsie_act()
+ return
+
+/obj/effect/stun_indicator/ex_act()
+ return
+
+/obj/effect/stun_indicator/emp_act()
+ return
+
+/obj/effect/stun_indicator/blob_act()
+ return
+
+/obj/effect/stun_indicator/singularity_act()
+ return
+
+/*
+///////////////////////////////////OFFERINGS EFFECT////////////////////////////
+/obj/effect/cult_offerings
+ anchored = 1
+ mouse_opacity = 0
+ icon_state = "offerings"
+*/
+
+///////////////////////////////////THROWN DAGGER TRAP////////////////////////////
+
+/obj/effect/rooting_trap/bloodnail
+ name = "blood nail"
+ desc = "A pointy red nail, appearing to pierce not through what it rests upon, but through the fabric of reality itself."
+ icon_state = "bloodnail"
+ icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi'
+
+/obj/effect/rooting_trap/bloodnail/New()
+ ..()
+ pixel_x = rand(-4, 4)
+ pixel_y = rand(-4, 4)
+
+/obj/effect/rooting_trap/bloodnail/stick_to(var/atom/A, var/side = null)
+ pixel_x = rand(-4, 4)
+ pixel_y = rand(-4, 4)
+ var/turf/T = get_turf(A)
+ . = ..()
+ if (.)
+ visible_message(span_warning("\The [src] nails \the [A] to \the [T].") )
+
+///////////////////////////////////CULT DANCE////////////////////////////////////
+//used by the cultdance emote. other cult dances have their own procs
+/obj/effect/cult_ritual/dance
+ var/list/dancers = list()
+ var/list/extras = list()
+ var/datum/rune_spell/tearreality/tear
+
+/obj/effect/cult_ritual/dance/New(var/turf/loc, var/mob/first_dancer)
+ ..()
+
+ if (first_dancer)
+ dancers += first_dancer
+ we_can_dance()
+
+
+/obj/effect/cult_ritual/dance/Destroy()
+ dancers = list()
+ tear = null
+ for (var/obj/effect/cult_ritual/dance_platform/P in extras)
+ P.dance_manager = null
+ extras = list()
+ ..()
+
+/obj/effect/cult_ritual/dance/proc/i_can_dance(var/mob/living/carbon/M)
+ if (!M.incapacitated())
+ return TRUE
+ else if (istype(M) && istype(M.handcuffed, /obj/item/restraints/handcuffs/cult)) //prisoners will be forced to dance even if incapacitated
+ return TRUE
+ return FALSE
+
+/obj/effect/cult_ritual/dance/proc/we_can_dance()
+ set waitfor = 0
+
+ if (dancers.len <= 0)
+ qdel(src)
+ return
+
+ if (tear)
+ add_particles(PS_TEAR_REALITY)
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "tear"
+ while(src && loc)
+ for (var/mob/M in dancers)
+ if (get_dist(src, M) > 1 || !i_can_dance(M) || M.occult_muted())
+ dancers -= M
+ continue
+ if ((dancers.len <= 0) && (tear.dance_count < tear.dance_target))
+ qdel(src)
+ return
+ dance_move()
+ sleep(3)
+ dance_move()
+ sleep(3)
+ dance_move()
+ if (tear.dance_count < tear.dance_target+10)
+ adjust_particles(PVAR_SPAWNING, clamp(0.1 + 0.00375 * tear.dance_count, 0.1, 1), PS_TEAR_REALITY)
+ var/scale = clamp(1 + 0.00416 * tear.dance_count, 1, 1.9)
+ adjust_particles(PVAR_SCALE, list(scale, scale), PS_TEAR_REALITY)
+ else
+ remove_particles(PS_TEAR_REALITY)
+ tear.update_crystals()
+ sleep(6)
+ else
+ while(TRUE)
+ for (var/mob/M in dancers)
+ if (get_dist(src, M) > 1 || M.incapacitated() || M.occult_muted())
+ dancers -= M
+ continue
+ if (dancers.len <= 0)
+ qdel(src)
+ return
+ dance_step()
+ sleep(3)
+ dance_step()
+ sleep(3)
+ dance_step()
+ sleep(6)
+
+/obj/effect/cult_ritual/dance/proc/add_dancer(var/mob/dancer)
+ if(dancer in dancers)
+ return
+ dancers += dancer
+
+/obj/effect/cult_ritual/dance/proc/dance_step()
+ var/dance_move = pick("clock", "counter", "spin")
+ switch(dance_move)
+ if ("clock")
+ for (var/mob/M in dancers)
+ switch (get_dir(src, M))
+ if (NORTHWEST, NORTH)
+ step_to(M, get_step(M, EAST))
+ if (NORTHEAST, EAST)
+ step_to(M, get_step(M, SOUTH))
+ if (SOUTHEAST, SOUTH)
+ step_to(M, get_step(M, WEST))
+ if (SOUTHWEST, WEST)
+ step_to(M, get_step(M, NORTH))
+ if ("counter")
+ for (var/mob/M in dancers)
+ switch (get_dir(src, M))
+ if (NORTHEAST, NORTH)
+ step_to(M, get_step(M, WEST))
+ if (SOUTHEAST, EAST)
+ step_to(M, get_step(M, NORTH))
+ if (SOUTHWEST, SOUTH)
+ step_to(M, get_step(M, EAST))
+ if (NORTHWEST, WEST)
+ step_to(M, get_step(M, SOUTH))
+ if ("spin")
+ for (var/mob/M in dancers)
+ spawn()
+ M.dir = SOUTH
+ sleep(0.75)
+ M.dir = EAST
+ sleep(0.75)
+ M.dir = NORTH
+ sleep(0.75)
+ M.dir = WEST
+ sleep(0.75)
+ M.dir = SOUTH
+
+
+
+/obj/effect/cult_ritual/dance/proc/dance_move()
+ var/dance_move = pick("clock", "counter", "spin")
+ switch(dance_move)
+ if ("clock")
+ for (var/obj/effect/cult_ritual/dance_platform/P in extras)
+ P.moving = TRUE
+ for (var/mob/M in dancers)
+ switch (get_dir(src, M))
+ if (NORTHWEST, NORTH)
+ M.forceMove(get_step(M, EAST))
+ M.dir = EAST
+ if (NORTHEAST, EAST)
+ M.forceMove(get_step(M, SOUTH))
+ M.dir = SOUTH
+ if (SOUTHEAST, SOUTH)
+ M.forceMove(get_step(M, WEST))
+ M.dir = WEST
+ if (SOUTHWEST, WEST)
+ M.forceMove(get_step(M, NORTH))
+ M.dir = NORTH
+ for (var/obj/effect/cult_ritual/dance_platform/P in extras)
+ switch (get_dir(src, P))
+ if (NORTHWEST, NORTH)
+ step_to(P, get_step(P, EAST))
+ if (NORTHEAST, EAST)
+ step_to(P, get_step(P, SOUTH))
+ if (SOUTHEAST, SOUTH)
+ step_to(P, get_step(P, WEST))
+ if (SOUTHWEST, WEST)
+ step_to(P, get_step(P, NORTH))
+ P.moving = FALSE
+ if ("counter")
+ for (var/obj/effect/cult_ritual/dance_platform/P in extras)
+ P.moving = TRUE
+ for (var/mob/M in dancers)
+ switch (get_dir(src, M))
+ if (NORTHEAST, NORTH)
+ M.forceMove(get_step(M, WEST))
+ M.dir = WEST
+ if (SOUTHEAST, EAST)
+ M.forceMove(get_step(M, NORTH))
+ M.dir = NORTH
+ if (SOUTHWEST, SOUTH)
+ M.forceMove(get_step(M, EAST))
+ M.dir = EAST
+ if (NORTHWEST, WEST)
+ M.forceMove(get_step(M, SOUTH))
+ M.dir = SOUTH
+ for (var/obj/effect/cult_ritual/dance_platform/P in extras)
+ switch (get_dir(src, P))
+ if (NORTHEAST, NORTH)
+ step_to(P, get_step(P, WEST))
+ if (SOUTHEAST, EAST)
+ step_to(P, get_step(P, NORTH))
+ if (SOUTHWEST, SOUTH)
+ step_to(P, get_step(P, EAST))
+ if (NORTHWEST, WEST)
+ step_to(P, get_step(P, SOUTH))
+ P.moving = FALSE
+ if ("spin")
+ for (var/mob/M in dancers)
+ spawn()
+ M.dir = SOUTH
+ sleep(0.75)
+ M.dir = EAST
+ sleep(0.75)
+ M.dir = NORTH
+ sleep(0.75)
+ M.dir = WEST
+ sleep(0.75)
+ M.dir = SOUTH
+
+///////////////////////////////////DANCE PLATEFORMS////////////////////////////////////
+//Tear Reality rune uses those
+
+var/list/dance_platform_prisoners = list()
+
+/obj/effect/cult_ritual/dance_platform
+ anchored = 1
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "blank"
+ layer = ABOVE_OBJ_LAYER
+ plane = ABOVE_GAME_PLANE
+ alpha = 0
+ var/moving = FALSE
+ var/mob/living/dancer = null
+ var/datum/rune_spell/tearreality/source = null
+ var/prisoner = FALSE
+ var/obj/effect/cult_ritual/dance/dance_manager
+
+/obj/effect/cult_ritual/dance_platform/New(var/turf/loc, var/datum/rune_spell/tearreality/runespell)
+ ..()
+ if (!runespell)
+ qdel(src)
+ return
+
+ var/image/I_circle = image(icon, src, "dance_platform_empty")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, dancer)
+ I_circle.appearance_flags |= RESET_COLOR
+ overlays += I_circle
+ transform *= 0.3
+ source = runespell
+ START_PROCESSING(SSobj, src)
+ idle_pulse()
+
+ spawn(5)
+ animate(src, alpha = 255, transform = matrix(), time = 4)
+
+/obj/effect/cult_ritual/dance_platform/Destroy()
+ if (dancer && prisoner)
+ dancer.SetStun(-4)
+ dancer = null
+ source = null
+ dance_manager = null
+ STOP_PROCESSING(SSobj, src)
+ ..()
+
+/obj/effect/cult_ritual/dance_platform/process()
+ if (dancer && prisoner)
+ dancer.SetStun(4)
+
+ if (dancer)
+ if (dancer.loc != loc)
+ dancer = null
+ source.lost_dancer()
+ prisoner = FALSE
+ overlays.len = 0
+ var/image/I_circle = image(icon, src, "dance_platform_empty")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, dancer)
+ I_circle.appearance_flags |= RESET_COLOR
+ overlays += I_circle
+ else
+ source.dance_increment(dancer)
+ else
+ for (var/mob/living/carbon/C in loc)
+ if (valid_dancer(C))
+ break
+
+/obj/effect/cult_ritual/dance_platform/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ . = ..()
+ valid_dancer(arrived)
+
+/obj/effect/cult_ritual/dance_platform/proc/valid_dancer(var/atom/movable/mover)
+ if (!dancer && !moving)
+ if (iscarbon(mover))
+ var/mob/living/carbon/C = mover
+ if (C.mind && C.stat != DEAD)
+ if (IS_CULTIST(C))
+ dancer = C
+ overlays.len = 0
+ var/image/I_circle = image(icon, src, "dance_platform_full")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, dancer)
+ I_circle.appearance_flags |= RESET_COLOR
+ var/image/I_markings = image(icon, src, "dance_platform_markings")
+ SET_PLANE_EXPLICIT(I_markings, GAME_PLANE, dancer)
+ overlays += I_circle
+ overlays += I_markings
+ source.dancer_check(C)
+ return TRUE
+ else
+ if (istype(C.handcuffed, /obj/item/restraints/handcuffs/cult))
+ dancer = C
+ prisoner = TRUE
+ dancer.SetStun(4)
+ overlays.len = 0
+ var/image/I_circle = image(icon, src, "dance_platform_full")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, dancer)
+ I_circle.appearance_flags |= RESET_COLOR
+ var/image/I_markings = image(icon, src, "dance_platform_markings")
+ SET_PLANE_EXPLICIT(I_markings, GAME_PLANE, dancer)
+ overlays += I_circle
+ overlays += I_markings
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/cult.dmi', src, "dance_prisoner")
+ SET_PLANE_EXPLICIT(I, GAME_PLANE, dancer)
+ overlays += I
+ var/mob_ref = "\ref[C]"
+ if (!(mob_ref in dance_platform_prisoners))//prevents chat spamming by dragging the prisoner across all the dance platforms
+ dance_platform_prisoners += mob_ref
+ to_chat(C, span_danger("Dark tentacles emerge from the rune and trap your legs in place. The occult bindings on your arms seem to react to them. You will need to resist out of those or get some outside help if you are to escape this circle.") )
+ spawn(20 SECONDS)
+ dance_platform_prisoners -= mob_ref
+ source.dancer_check(C)
+ return TRUE
+ else if (isshade(mover) || isconstruct(mover))
+ var/mob/living/simple_animal/SA = mover
+ if (SA.mind && SA.stat != DEAD)
+ if (IS_CULTIST(SA))
+ dancer = SA
+ overlays.len = 0
+ var/image/I_circle = image(icon, src, "dance_platform_full")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, dancer)
+ I_circle.appearance_flags |= RESET_COLOR
+ var/image/I_markings = image(icon, src, "dance_platform_markings")
+ SET_PLANE_EXPLICIT(I_markings, GAME_PLANE, dancer)
+ overlays += I_circle
+ overlays += I_markings
+ source.dancer_check(SA)
+ return TRUE
+ return FALSE
+
+/obj/effect/cult_ritual/dance_platform/Exit(atom/movable/mover, direction)
+ . = ..()
+ if (!moving && dancer && mover == dancer)
+ overlays.len = 0
+ var/image/I_circle = image(icon, src, "dance_platform_empty")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, dancer)
+ I_circle.appearance_flags |= RESET_COLOR
+ overlays += I_circle
+ if (prisoner)
+ dancer.SetStun(0)
+ prisoner = FALSE
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', flick_anim = "dancer_prisoner-stop", plane = ABOVE_LIGHTING_PLANE)
+ if (dance_manager)
+ dance_manager.dancers -= dancer
+ dancer = null
+ source.lost_dancer()
+
+/obj/structure/dance_check
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "blank"
+ density = 0
+ mouse_opacity = 0
+ invisibility = 101
+ var/datum/rune_spell/tearreality/source
+
+/obj/structure/dance_check/New(turf/loc, var/_source)
+ ..()
+ if (_source)
+ source = _source
+ else
+ qdel(src)
+
+/obj/structure/dance_check/Bump(atom/bumped_atom)
+ . = ..()
+ source.blocker = bumped_atom//So we can tell the rune's activator exactly what is blocking the dance path
+
+
+///////////////////////////////////TEAR SPAWNERS////////////////////////////////////
+//Used by the tear reality rune to spawn obsidian pillars and gateways across the station
+/obj/effect/cult_ritual/tear_spawners
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "blank"
+ density = 0
+ mouse_opacity = 0
+ invisibility = 101
+ var/datum/rune_spell/tearreality/source = null
+ var/finished = FALSE
+ var/direction = 0
+ var/steps = 0
+
+ var/near_turfs = list()
+ var/far_turfs = list()
+
+/obj/effect/cult_ritual/tear_spawners/New(turf/loc, var/datum/rune_spell/tearreality/_source)
+ if (!_source)
+ qdel(src)
+ return
+ source = _source
+ if (source.destroying_self)
+ qdel(src)
+ return
+ ..()
+ if (!finished)
+ start_loop()
+
+/obj/effect/cult_ritual/tear_spawners/Destroy()
+ source = null
+ ..()
+
+/obj/effect/cult_ritual/tear_spawners/proc/perform_step()
+ move_step()
+ after_step()
+
+/obj/effect/cult_ritual/tear_spawners/proc/start_loop()
+ set waitfor = 0
+ spawn()
+ while(!finished && direction)
+ perform_step()
+ sleep(1)
+
+/obj/effect/cult_ritual/tear_spawners/proc/execute(var/level)
+ return
+/obj/effect/cult_ritual/tear_spawners/proc/after_step()
+ return
+/obj/effect/cult_ritual/tear_spawners/proc/move_step()
+ switch(direction)
+ if (SOUTH)
+ y--
+ if (y <= TRANSITIONEDGE)
+ finished = TRUE
+ qdel(src)
+ return
+ if (NORTH)
+ y++
+ if (y >= (world.maxy - TRANSITIONEDGE))
+ finished = TRUE
+ qdel(src)
+ if (EAST)
+ x++
+ if (x >= (world.maxx - TRANSITIONEDGE))
+ finished = TRUE
+ qdel(src)
+ if (WEST)
+ x--
+ if (x <= TRANSITIONEDGE)
+ finished = TRUE
+ qdel(src)
+ return
+
+ steps++
+
+/obj/effect/cult_ritual/tear_spawners/vertical_spawner
+ direction = SOUTH
+ var/steps_to_pillars_and_gateways = 16
+ var/steps_to_pillars = 8
+
+ var/ready_to_spawn_pillars_and_gateways = FALSE
+
+/obj/effect/cult_ritual/tear_spawners/vertical_spawner/after_step()
+ if (steps == steps_to_pillars)
+ new /obj/effect/cult_ritual/tear_spawners/horizontal_spawner/alt/left(loc, source)
+ new /obj/effect/cult_ritual/tear_spawners/horizontal_spawner/alt/right(loc, source)
+ if (steps == steps_to_pillars_and_gateways)
+ new /obj/effect/cult_ritual/tear_spawners/horizontal_spawner/left(loc, source)
+ new /obj/effect/cult_ritual/tear_spawners/horizontal_spawner/right(loc, source)
+ ready_to_spawn_pillars_and_gateways = TRUE
+ steps = 0
+
+ if (ready_to_spawn_pillars_and_gateways)
+ if (isopenturf(loc))
+ new /obj/effect/cult_ritual/tear_spawners/gateway_spawner/special(loc, source)
+ ready_to_spawn_pillars_and_gateways = FALSE
+
+/obj/effect/cult_ritual/tear_spawners/vertical_spawner/up
+ direction = NORTH
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner
+ var/steps_to_spire = 8
+ var/steps_to_gateway = 15
+ var/also_spawn_gateways = TRUE
+
+ var/ready_to_spawn_pillar = FALSE
+ var/ready_to_spawn_gateway = FALSE
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner/after_step()
+ if (steps == steps_to_spire)
+ ready_to_spawn_pillar = TRUE
+ if (also_spawn_gateways && steps == steps_to_gateway)
+ ready_to_spawn_gateway = TRUE
+ if (steps == (2*steps_to_spire))
+ ready_to_spawn_pillar = TRUE
+ steps = 0
+
+ if (ready_to_spawn_pillar)
+ if (isopenturf(loc))
+ new /obj/effect/cult_ritual/tear_spawners/pillar_spawner(loc, source, direction)
+ ready_to_spawn_pillar = FALSE
+
+ if (ready_to_spawn_gateway)
+ if (isopenturf(loc))
+ new /obj/effect/cult_ritual/tear_spawners/gateway_spawner(loc, source)
+ ready_to_spawn_gateway = FALSE
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner/alt
+ steps = 4
+ also_spawn_gateways = FALSE
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner/left
+ direction = WEST
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner/right
+ direction = EAST
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner/alt/left
+ direction = WEST
+
+/obj/effect/cult_ritual/tear_spawners/horizontal_spawner/alt/right
+ direction = EAST
+
+/obj/effect/cult_ritual/tear_spawners/pillar_spawner
+ finished = TRUE
+ var/obj/structure/cult/pillar
+
+/obj/effect/cult_ritual/tear_spawners/pillar_spawner/New(turf/loc, var/datum/rune_spell/tearreality/_source, var/_direction)
+ if (!_direction)
+ qdel(src)
+ return
+ ..()
+ if (source)
+ source.pillar_spawners += src
+ direction = _direction
+
+ for(var/direc in GLOB.cardinals)
+ var/turf/T = get_step(src, direc)
+ near_turfs += T
+ var/turf/U = get_step(T, direc)
+ far_turfs += U
+
+ for(var/direc in GLOB.diagonals)
+ var/turf/T = get_step(src, direc)
+ far_turfs += T
+
+/obj/effect/cult_ritual/tear_spawners/pillar_spawner/move_step()
+ return
+
+/obj/effect/cult_ritual/tear_spawners/pillar_spawner/execute(var/level)
+ switch(level)
+ if (1)
+ var/turf/T = loc
+ if (T)
+ T.narsie_act()
+ if (2)
+ var/turf/T = loc
+ if (T)
+ switch(direction)
+ if (EAST)
+ pillar = new /obj/structure/cult/pillar/alt(loc)
+ if (WEST)
+ pillar = new /obj/structure/cult/pillar(loc)
+ for (var/turf/U in near_turfs)
+ U.narsie_act()
+ if (3)
+ if (pillar)
+ pillar.update_icon()
+ for (var/turf/T in far_turfs)
+ T.narsie_act()
+
+/obj/effect/cult_ritual/tear_spawners/pillar_spawner/proc/cancel()
+ if (!pillar)
+ qdel(src)
+ return
+ spawn(rand(1 SECONDS, 5 SECONDS))
+ if (pillar)
+ pillar.takeDamage(100)
+ for (var/turf/T in far_turfs)
+ T.decultify()
+ sleep(rand(10 SECONDS, 20 SECONDS))
+ if (pillar)
+ pillar.takeDamage(100)
+ for (var/turf/T in near_turfs)
+ T.decultify()
+ sleep(rand(20 SECONDS, 30 SECONDS))
+ var/turf/T = loc
+ T.decultify()
+
+/obj/effect/cult_ritual/tear_spawners/gateway_spawner
+ finished = TRUE
+
+/obj/effect/cult_ritual/tear_spawners/gateway_spawner/New(turf/loc, var/datum/rune_spell/tearreality/_source)
+ ..()
+ source?.gateway_spawners += src
+
+ for(var/direc in GLOB.cardinals)
+ var/turf/T = get_step(src, direc)
+ near_turfs += T
+ var/turf/U = get_step(T, direc)
+ far_turfs += U
+
+ for(var/direc in GLOB.diagonals)
+ var/turf/T = get_step(src, direc)
+ far_turfs += T
+
+/obj/effect/cult_ritual/tear_spawners/gateway_spawner/move_step()
+ return
+
+/obj/effect/cult_ritual/tear_spawners/gateway_spawner/special
+ finished = FALSE
+
+/obj/effect/cult_ritual/tear_spawners/gateway_spawner/special/start_loop()
+ spawn()
+ x++
+ if (isopenturf(loc))
+ new /obj/effect/cult_ritual/tear_spawners/pillar_spawner(loc, source, EAST)
+ sleep(1)
+ x -= 2
+ if (isopenturf(loc))
+ new /obj/effect/cult_ritual/tear_spawners/pillar_spawner(loc, source, WEST)
+ sleep(1)
+ x++
+ finished = TRUE
+
+
+/obj/effect/rooting_trap
+ name = "trap"
+ desc = "How did you get trapped in that? Try resisting."
+ mouse_opacity = 1
+ icon_state = "energynet"
+ anchored = 1
+ density = 0
+ plane = ABOVE_GAME_PLANE
+ var/atom/stuck_to
+ var/duration = 10 SECONDS
+
+/obj/effect/rooting_trap/singularity_act()
+ return
+
+/obj/effect/rooting_trap/singularity_pull()
+ return
+
+/obj/effect/rooting_trap/blob_act()
+ return
+
+
+/obj/effect/rooting_trap/Destroy()
+ if(stuck_to)
+ unbuckle_mob(stuck_to, TRUE)
+ REMOVE_TRAIT(stuck_to, TRAIT_IMMOBILIZED, REF(src))
+ stuck_to = null
+ ..()
+
+/obj/effect/rooting_trap/proc/stick_to(var/atom/A, var/side = null)
+ var/turf/T = get_turf(A)
+ if(isspaceturf(T)) //can't nail people down unless there's a turf to nail them to.
+ return FALSE
+ if(!isliving(A))
+ return FALSE
+ var/mob/living/M = A
+ if(M.stat < 2)
+ stuck_to = A
+ buckle_mob(A, TRUE)
+ ADD_TRAIT(A, TRAIT_IMMOBILIZED, REF(src))
+
+ QDEL_IN(src, duration)
+
+ return TRUE
+ return FALSE
+
+/obj/effect/rooting_trap/attack_hand(var/mob/user)
+ unstick_attempt(user)
+
+/obj/effect/rooting_trap/proc/unstick_attempt(var/mob/user)
+ if (do_after(user, 1.5 SECONDS, src))
+ unstick()
+
+/obj/effect/rooting_trap/proc/unstick()
+ if(stuck_to)
+ REMOVE_TRAIT(stuck_to, TRAIT_IMMOBILIZED, REF(src))
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/flavour.dm b/monkestation/code/modules/bloody_cult/cult/flavour.dm
new file mode 100644
index 000000000000..8e384df12026
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/flavour.dm
@@ -0,0 +1,288 @@
+
+
+/*
+DEITYLINK ~ 2021:
+
+Disabling those for now.
+On one hand I'd rather keep Nar-Sie talking as an admin-only thing
+On the other hand those don't quite portray the love of blood and chaos that Nar-Sie is associated with, aside from one or two lines those are very generic.
+
+As for Conversion Failure lines, they're pretty much deprecated since conversion failures no longer kill their victims
+Also since we might want to try converting them again later no need for Nar-Sie to be this petty, that's not a good image
+
+April 2021:
+Success Lines re-enabled.
+*/
+
+/* -- Flavour text for refusing/accepting conversion.
+ -- Possible context (static) :
+ => Dept (weighted 3)
+ => Specific job (weighted 5)
+ => Race (weighted 3)
+ => Specific special role (weighted 5)
+ -- Possible context (dynamic) :
+ => The guy that converted you is from the same dept (weighted 3)
+ => Your boss is in the cult (CMO for medbay, ...)
+ => Your underlings are in the cult
+ => Your colleagues are in the cult
+ => Cult has a few/a lot of alive members
+*/
+
+
+var/list/acceptance_lines_by_dept = list(
+ ACCOUNT_CMD = list(
+ "I knew you had it in you." = 3,
+ "The chains of commanding are broken." = 3,
+ "Be ready to lead the stronger side." = 3,
+ "Arise, new champion." = 3,
+ ),
+ ACCOUNT_ENG = list(
+ "The forges of the Geometer welcome you." = 3,
+ "Your true potential has been unraveled. " = 3,
+ "Forge the sword that will slay my enemies." = 3,
+ "Arise, new craftsman." = 3,
+ ),
+ ACCOUNT_MED = list(
+ "The blood was always your companion." = 3,
+ "You healed so many... but only now are you truly alive." = 3,
+ "Arise, new healer." = 3,
+ ),
+ ACCOUNT_SCI = list(
+ "This always was your calling." = 3,
+ "The secrets of the veil are now yours to research." = 3,
+ "The logical conclusion to your career choice." = 3,
+ "Was it not what you always wanted?" = 3,
+ "Arise, new adept." = 3,
+ ),
+ ACCOUNT_CIV = list(
+ "Only here will you be fulfilled." = 3,
+ "A task has finally been given to you." = 3,
+ "Rise up." = 3,
+ "And there goes a life of servitude." = 3,
+ "Arise, new peon." = 3,
+ ),
+ ACCOUNT_CAR = list(
+ "When this is over, expect much more than your dreamed 'Cargonia'." = 3,
+ "Be the hand that arms my soldiers." = 3,
+ "Arive, new armourer." = 3,
+ ),
+ ACCOUNT_SEC = list(
+ "Congratulations on joining the stronger side." = 3,
+ "The corporate slave died. Let a new, free man take their place." = 3,
+ "You have finally seen the light." = 3,
+ "Your freedom begins at this hour, in this place." = 3,
+ "Arise, new warrior." = 3,
+ ),
+)
+
+
+var/list/acceptance_lines_by_specific_race = list(
+ /datum/species/plasmaman = list(
+ "The pain ends now." = 3,
+ "Even the dead may serve." = 3,
+ ),
+)
+
+// Context lines
+
+var/list/acceptance_lines_few_cultists = list(
+ "Be the hand I need in these times." = 3,
+ "You have been chosen." = 3,
+)
+
+var/list/acceptance_lines_numerous_cultists = list(
+ "Our numbers are limitless." = 3,
+ "We get stronger with each soul." = 3,
+ "Nothing will resist our might." = 3,
+)
+
+
+#define acceptance_lines_same_dept list( \
+ "[converter.gender == MALE ? "He" : "She"] judged you well." = 5, \
+ "And now you both serve the same purpose." = 5, \
+ "Isn't teamwork a wonderful thing." = 5, \
+)
+
+
+/datum/team/cult/proc/send_flavour_text_accept(var/mob/victim, var/mob/converter)
+ // -- Static context
+ // Default lines
+ var/list/valid_lines = list(
+ "Another one joins the fold." = 1,
+ "With each new one, the veil gets thinner." = 1,
+ "All are welcome." = 1,
+ )
+ // The departement
+ var/datum/job/victim_job = victim?.mind.assigned_role
+ var/datum/job/converter_job = converter?.mind.assigned_role
+ for (var/list/L in acceptance_lines_by_dept)
+ if (victim_job.paycheck_department in L)
+ valid_lines += acceptance_lines_by_dept[victim_job.paycheck_department]
+
+ // The race
+ if (ishuman(victim))
+ var/mob/living/carbon/human/dude = victim
+ if(dude.dna.species.type in acceptance_lines_by_specific_race)
+ valid_lines += acceptance_lines_by_specific_race[dude.dna.species.type]
+
+ // -- Dynamic context
+ // Cultist count
+ var/cultists = 0
+ for (var/datum/mind/mind in members)
+ if (mind&& mind.current && !mind.current.stat) // If he's alive
+ cultists++
+
+ // Not a lot of cultists...
+ if (cultists < 3)
+ valid_lines += acceptance_lines_few_cultists
+
+ // Or a lot of them !
+ else if (cultists > 10)
+ valid_lines += acceptance_lines_numerous_cultists
+
+ // Converter and victim are of the same dept
+ if ((victim_job.paycheck_department) == (converter_job.paycheck_department))
+ valid_lines += acceptance_lines_same_dept
+
+ var/chosen_line = pick_weight(valid_lines)
+ to_chat(victim, "Nar-Sie murmurs, [chosen_line]")
+
+
+
+
+/*
+
+var/list/failure_lines_by_dept = list(
+ COMMAND_POSITIONS = list(
+ "You have failed to lead them. You would have failed to follow." = 3,
+ "And so ends your 'authority.'" = 3,
+ "This is what passes as command these days." = 3,
+ ),
+ ENGINEERING_POSITIONS = list(
+ "Our craft is more complex than your pathetic tinkering." = 3,
+ "These machines were beyond your skill anyway." = 3,
+ ),
+ MEDICAL_POSITIONS = list(
+ "Your refusal helps no one. The blood will still flow." = 3,
+ "There is no cure for this." = 3,
+ "Nothing can heal the veil." = 3,
+ ),
+ SCIENCE_POSITIONS = list(
+ "Your closed mind dishonours you." = 3,
+ "Our secrets were beyond your understanding." = 3,
+ "My science was not for weaklings such as you." = 3,
+ ),
+ CIVILIAN_POSITIONS = list(
+ "A little job in life, and a forgotten death." = 3,
+ "This refusal is but a footnote on my story." = 3,
+ "You refused the only opportunity you had to make a difference." = 3,
+ ),
+ CARGO_POSITION = list(
+ "I care little for the hoarders of your kind." = 3,
+ "As expected for a glorified crate handler." = 3,
+ ),
+ SECURITY_POSITION = list(
+ "You already failed." = 3,
+ "This refusal does not erase your failure." = 3,
+ "Your stubbornness amuses me. I already won." = 3,
+ "I could have freed you." = 3,
+ "You will always remain on the weaker side." = 3,
+ ),
+)
+
+var/list/failure_lines_by_specific_job = list(
+ "Paramedic" = list(
+ "You will not save anyone from where I sent you." = 5,
+ ),
+ "Trader" = list(
+ "I offered you a home, and you refused." = 5,
+ ),
+ "Captain" = list(
+ "Do you feel in charge?" = 5,
+ ),
+)
+
+var/list/failure_lines_by_specific_race = list(
+ /datum/species/plasmaman = list(
+ "Your loyalty to the company that twisted you into the living dead is amusing." = 3,
+ )
+)
+var/list/failure_lines_few_cultists = list(
+ "With or without you, my faithful shall triumph." = 3,
+ "Do you truly think you won?" = 3,
+ "I have no need of a coward in times like this." = 3,
+)
+var/list/failure_lines_numerous_cultists = list(
+ "Your refusal changes nothing." = 3,
+ "You will get to see this station fail from the first row." = 3,
+)
+
+#define failure_lines_same_dept list( \
+ "[converter.gender == MALE ? "He" : "She"] tried to save you." = 5, \
+ "You betrayed your friend." = 5, \
+ "Your arrogance must have disappointed your friend." = 5, \
+)
+
+
+
+/datum/faction/bloodcult/proc/send_flavour_text_refuse(var/mob/victim, var/mob/converter)
+ // -- Static context
+ // Default lines
+ var/list/valid_lines = list(
+ "Not worthy of the gift." = 1,
+ "A shame. Maybe you will see our light one day." = 1,
+ "Unconquered in spirit... and destroyed in the flesh." = 1,
+ "So much courage... for nothing." = 1,
+ )
+ // The departement
+ var/victim_job = victim?.mind.assigned_role
+ var/converter_job = converter?.mind.assigned_role
+ for (var/list/L in failure_lines_by_dept)
+ if (victim_job in L)
+ valid_lines += failure_lines_by_dept[L]
+ // The specific job
+ valid_lines += failure_lines_by_specific_job[victim_job]
+ // The roles he may add
+ if (victim.mind)
+ for (var/role in victim.mind.antag_roles)
+ valid_lines += failure_lines_by_specific_role[role]
+ // The race
+ if (ishuman(victim))
+ var/mob/living/carbon/human/dude = victim
+ valid_lines += failure_lines_by_specific_race[dude.species.type]
+
+ // -- Dynamic context
+ // Cultist count
+ var/cultists = 0
+ for (var/datum/role/R in members)
+ if (R.antag && R.antag.current && !R.antag.current.stat) // If he's alive
+ cultists++
+
+ // Not a lot of cultists...
+ if (cultists < 3)
+ valid_lines += failure_lines_few_cultists
+
+ // Or a lot of them !
+ else if (cultists > 10)
+ valid_lines += failure_lines_numerous_cultists
+
+ // Converter and victim are of the same dept
+ for (var/list/dept in all_depts_list)
+ if ((victim_job in dept) && (converter_job in dept))
+ valid_lines += failure_lines_same_dept
+ if(victim.mind && victim.mind.assigned_role == "Chaplain")
+ var/list/cult_blood_chaplain = list("cult", "narsie", "nar'sie", "narnar", "nar-sie")
+ var/list/cult_clock_chaplain = list("ratvar", "clockwork", "ratvarism")
+ if (religion_name in cult_blood_chaplain)
+ to_chat(victim, "Nar-Sie murmurs, Rejoice, I will give you the ending you desired.")
+ else if (religion_name in cult_clock_chaplain)
+ to_chat(victim, "Nar-Sie murmurs, I will take your body, but when your soul returns to Ratvar, tell him that[pick(\
+ "... he SUCKS!", \
+ " there isn't room enough for the two of us on this plane!", \
+ " he'll never be anything but a lame copycat.")]")
+
+ var/chosen_line = pickweight(valid_lines)
+ to_chat(victim, "Nar-Sie murmurs, [chosen_line]")
+ //to_chat(converter, "Nar-Sie murmurs to [victim]... [chosen_line]")
+
+*/
diff --git a/monkestation/code/modules/bloody_cult/cult/fullscreens.dm b/monkestation/code/modules/bloody_cult/cult/fullscreens.dm
new file mode 100644
index 000000000000..e223f57469a0
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/fullscreens.dm
@@ -0,0 +1,43 @@
+/atom/movable/screen/fullscreen/conversion_border
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1_full.dmi'
+ icon_state = "conversionoverlay"
+ alpha = 0
+
+/atom/movable/screen/fullscreen/confusion_border
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1_full.dmi'
+ icon_state = "conversionoverlay"
+ alpha = 0
+
+/atom/movable/screen/fullscreen/deafmute_border
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1_full.dmi'
+ icon_state = "conversionoverlay"
+ alpha = 0
+
+/atom/movable/screen/fullscreen/astral_border
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1_full.dmi'
+ icon_state = "astraloverlay"
+ alpha = 0
+
+/atom/movable/screen/fullscreen/conversion_red
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1.dmi'
+ screen_loc = "WEST, SOUTH to EAST, NORTH"
+ icon_state = "redoverlay"
+
+/atom/movable/screen/fullscreen/black
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1.dmi'
+ screen_loc = "WEST, SOUTH to EAST, NORTH"
+ icon_state = "black"
+ layer = BLIND_LAYER
+ alpha = 0
+
+/atom/movable/screen/fullscreen/white
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1.dmi'
+ screen_loc = "WEST, SOUTH to EAST, NORTH"
+ icon_state = "white"
+ layer = BLIND_LAYER
+ alpha = 0
+
+/atom/movable/screen/fullscreen/narsie_rising
+ icon = 'monkestation/code/modules/bloody_cult/icons/screen1.dmi'
+ screen_loc = "WEST, SOUTH to EAST, NORTH"
+ icon_state = "blank"
diff --git a/monkestation/code/modules/bloody_cult/cult/hell_universe_subsystem.dm b/monkestation/code/modules/bloody_cult/cult/hell_universe_subsystem.dm
new file mode 100644
index 000000000000..ad5d84aa6e11
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/hell_universe_subsystem.dm
@@ -0,0 +1,84 @@
+/atom/movable/screen/parallax_layer/rifts
+ icon = 'monkestation/code/modules/bloody_cult/icons/riftbox.dmi'
+ icon_state = "rift"
+ layer = 2
+ speed = 0.5
+
+/atom/movable/screen/parallax_layer/rifts/Initialize(mapload, mob/owner)
+ . = ..()
+ src.add_atom_colour(COLOR_CARP_RIFT_RED, ADMIN_COLOUR_PRIORITY)
+
+
+SUBSYSTEM_DEF(hell_universe)
+ name = "Hell Universe Processing"
+ flags = SS_NO_INIT | SS_TICKER
+ wait = LIGHTING_INTERVAL
+
+ var/hell_time = FALSE
+ var/old_starlight_color
+ var/list/turfs_to_process = list()
+ var/list/lights_to_break = list()
+
+/datum/controller/subsystem/hell_universe/proc/start_hell()
+ hell_time = TRUE
+ old_starlight_color = GLOB.starlight_color
+
+ for(var/mob/dead/observer/observer in GLOB.player_list)
+ observer.narsie_act()
+
+ for(var/client/client in GLOB.clients)
+ var/view = client.view || world.view
+ client.parallax_layers_cached += new /atom/movable/screen/parallax_layer/rifts(null, client.mob)
+
+ for(var/atom/movable/screen/parallax_layer/layer as anything in client.parallax_layers_cached)
+ if(!istype(layer, /atom/movable/screen/parallax_layer/layer_1))
+ continue
+ layer.remove_atom_colour(ADMIN_COLOUR_PRIORITY, GLOB.starlight_color)
+ layer.icon_state = "narsie"
+ layer.update_o(view)
+ client.mob.hud_used?.update_parallax_pref()
+
+ GLOB.starlight_color = COLOR_BLOOD
+
+ for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ var/list/turfs = get_area_turfs(/area/space, z)
+ for(var/turf/open/space/space in turfs)
+ space.update_starlight()
+ turfs_to_process |= space
+
+ for(var/datum/time_of_day/time in SSoutdoor_effects.time_cycle_steps)
+ time.color = COLOR_BLOOD
+ GLOB.GLOBAL_LIGHT_RANGE = 20
+
+ for (var/atom/movable/screen/fullscreen/lighting_backdrop/sunlight/SP in SSoutdoor_effects.sunlighting_planes)
+ SSoutdoor_effects.transition_sunlight_color(SP)
+
+ for(var/obj/machinery/light/light_to_break in GLOB.machines)
+ lights_to_break |= light_to_break
+
+ SSoutdoor_effects.InitializeTurfs()
+
+/datum/controller/subsystem/hell_universe/fire(resumed)
+ if(!hell_time)
+ return
+
+ for(var/turf/open/space/space as anything in turfs_to_process)
+ CHECK_TICK
+
+ space.add_particles(PS_SPACE_RUNES)//visible for everyone
+ space.adjust_particles(PVAR_SPAWNING, rand(5, 20)/1000 ,PS_SPACE_RUNES)
+ turfs_to_process -= space
+
+ for(var/obj/machinery/light/light_to_break in lights_to_break)
+ if(QDELETED(light_to_break))
+ lights_to_break -= light_to_break
+ continue
+
+ CHECK_TICK
+
+ light_to_break.break_light_tube()
+ lights_to_break -= light_to_break
+
+ if(!length(turfs_to_process) && !length(lights_to_break))
+ hell_time = FALSE
+
diff --git a/monkestation/code/modules/bloody_cult/cult/items/blood_candle.dm b/monkestation/code/modules/bloody_cult/cult/items/blood_candle.dm
new file mode 100644
index 000000000000..b84c773c9f35
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/items/blood_candle.dm
@@ -0,0 +1,41 @@
+/obj/item/candle/blood
+ name = "blood candle"
+ desc = "A candle made out of blood moth wax, burns much longer than regular candles. Used for moody lighting and occult rituals."
+ icon = 'monkestation/code/modules/bloody_cult/icons/candle.dmi'
+ icon_state = "bloodcandle"
+ food_candle = "foodbloodcandle"
+ color = null
+
+ wax = 3600 // 60 minutes
+ trashtype = /obj/item/trash/blood_candle
+
+/obj/item/candle/blood/get_cult_power()
+ return 1
+
+/obj/item/candle/blood/update_icon()
+ . = ..()
+ overlays.len = 0
+ if (wax == initial(wax))
+ icon_state = "bloodcandle"
+ else
+ var/i
+ if(wax > 2400)
+ i = 1
+ else if(wax > 1200)
+ i = 2
+ else i = 3
+ icon_state = "bloodcandle[i]"
+ if (lit)
+ var/image/I = image(icon, src, "[icon_state]_lit")
+ I.blend_mode = BLEND_ADD
+ if (isturf(loc))
+ I.plane = ABOVE_LIGHTING_PLANE
+ else
+ I.plane = ABOVE_HUD_PLANE // inventory
+ overlays += I
+
+/obj/item/trash/blood_candle
+ name = "blood candle"
+ desc = "A candle made out of blood moth wax, burns much longer than regular candles. Used for moody lighting and occult rituals."
+ icon = 'monkestation/code/modules/bloody_cult/icons/candle.dmi'
+ icon_state = "bloodcandle4"
diff --git a/monkestation/code/modules/bloody_cult/cult/items/blood_tesseract.dm b/monkestation/code/modules/bloody_cult/cult/items/blood_tesseract.dm
new file mode 100644
index 000000000000..50ef94f8b33e
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/items/blood_tesseract.dm
@@ -0,0 +1,97 @@
+/obj/item/weapon/blood_tesseract
+ name = "blood tesseract"
+ desc = "A small totem. Cultists use them as anchors from the other side of the veil to quickly swap gear."
+ gender = NEUTER
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "tesseract"
+ throwforce = 2
+ w_class = WEIGHT_CLASS_TINY
+
+ var/discarded_types = list(
+ /obj/item/clothing/shoes/cult,
+ /obj/item/clothing/suit/hooded/cultrobes,
+ /obj/item/clothing/gloves/color/black/cult,
+ )
+
+ var/list/stored_gear = list()
+
+ var/obj/item/weapon/talisman/remaining = null
+
+/obj/item/weapon/blood_tesseract/Destroy()
+ if (loc)
+ var/turf/T = get_turf(src)
+ for(var/slot in stored_gear)
+ var/obj/item/I = stored_gear["[slot]"]
+ stored_gear -= slot
+ I.forceMove(T)
+ for(var/obj/A in contents)
+ A.forceMove(T)
+ if (remaining)
+ QDEL_NULL(remaining)
+ ..()
+
+/obj/item/weapon/blood_tesseract/throw_impact(atom/hit_atom)
+ var/turf/T = get_turf(src)
+ if(T)
+ playsound(T, 'sound/effects/hit_on_shattered_glass.ogg', 70, 1)
+ anim(target = T, a_icon = 'icons/effects/effects.dmi', flick_anim = "tesseract_break", plane = ABOVE_LIGHTING_PLANE)
+ qdel(src)
+
+/obj/item/weapon/blood_tesseract/examine(var/mob/user)
+ ..()
+ if (IS_CULTIST(user))
+ to_chat(user, span_info("Press it in your hands to discard currently equiped cult clothing and re-equip your stored items.") )
+
+/obj/item/weapon/blood_tesseract/attack_self(var/mob/living/user)
+ if (IS_CULTIST(user))
+ //Alright so we'll discard cult gear and equip the stuff stored inside.
+ anim(target = user, a_icon = 'icons/effects/64x64.dmi', flick_anim = "rune_tesseract", offX = -32/2, offY = -32/2, plane = ABOVE_LIGHTING_PLANE)
+ user.dropItemToGround(src)
+ if (remaining)
+ remaining.forceMove(get_turf(user))
+ user.put_in_hands(remaining)
+ remaining = null
+
+ var/obj/item/plasma_tank = null
+ if(isplasmaman(user))
+ plasma_tank = user.get_item_by_slot(ITEM_SLOT_SUITSTORE)
+
+ for(var/obj/item/I in user)
+ if (is_type_in_list(I, discarded_types))
+ user.dropItemToGround(I)
+ qdel(I)
+
+ for(var/slot in stored_gear)
+ var/obj/item/stored_slot = stored_gear["[slot]"]
+ var/obj/item/user_slot = user.get_item_by_slot(text2num(slot))
+ if (!user_slot)
+ user.equip_to_slot_if_possible(stored_slot, text2num(slot))
+ else
+ if (istype(user_slot, /obj/item/storage/backpack/cultpack))
+ if (istype(stored_slot, /obj/item/storage/backpack))
+ //swapping backpacks
+ for(var/obj/item/I in user_slot.contents)
+ I.forceMove(stored_slot)
+ user.dropItemToGround(user_slot)
+ qdel(user_slot)
+ user.equip_to_slot_if_possible(stored_slot, ITEM_SLOT_NECK)
+ else
+ //free backpack
+ var/obj/item/storage/backpack/B = new(user)
+ for(var/obj/item/I in user_slot)
+ I.forceMove(B)
+ user.dropItemToGround(user_slot)
+ qdel(user_slot)
+ user.equip_to_slot_if_possible(B, text2num(slot))
+ user.put_in_hands(stored_slot)
+ else
+ user.dropItemToGround(user_slot)
+ qdel(user_slot)
+ user.equip_to_slot_if_possible(stored_slot, ITEM_SLOT_NECK)
+ stored_gear.Remove(slot)
+ if (plasma_tank)
+ user.equip_to_slot_if_possible(plasma_tank, ITEM_SLOT_SUITSTORE)
+ qdel(src)
+
+/obj/item/weapon/blood_tesseract/narsie_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/items/cult_storage.dm b/monkestation/code/modules/bloody_cult/cult/items/cult_storage.dm
new file mode 100644
index 000000000000..2102f31f3422
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/items/cult_storage.dm
@@ -0,0 +1,40 @@
+/obj/item/storage/cult
+ name = "coffer"
+ desc = "A gloomy-looking storage chest."
+ icon = 'monkestation/code/modules/bloody_cult/icons/storage.dmi'
+ icon_state = "cult"
+
+/obj/item/reagent_containers/cup/cult
+ name = "tempting goblet"
+ desc = "An obsidian cup in the shape of a skull. Used by the followers of Nar-Sie to collect the blood of their sacrifices."
+ icon_state = "cult"
+ icon = 'monkestation/code/modules/bloody_cult/icons/reagent_containers.dmi'
+
+ fill_icon = 'monkestation/code/modules/bloody_cult/icons/reagentfillings.dmi'
+ fill_icon_state = "cult"
+ amount_per_transfer_from_this = 10
+ volume = 60
+ force = 5
+ throwforce = 7
+
+/obj/item/reagent_containers/cup/cult/examine(var/mob/user)
+ ..()
+ if (IS_CULTIST(user))
+ if(issilicon(user))
+ to_chat(user, span_info("Drinking blood from this cup will always safely replenish the vessels of cultists, regardless of blood type. It's a shame you're a robot.") )
+ else
+ to_chat(user, span_info("Drinking blood from this cup will always safely replenish your own vessels, regardless of blood types. The opposite is true to non-cultists. Throwing this cup at them may force them to swallow some of its content if their face isn't covered.") )
+
+
+/obj/item/reagent_containers/cup/cult/gamer
+ name = "gamer goblet"
+ desc = "A plastic cup in the shape of a skull. Typically full of Geometer-Fuel."
+
+/obj/item/reagent_containers/cup/cult/narsie_act()
+ return
+
+/obj/item/reagent_containers/cup/cult/bloodfilled
+
+/obj/item/reagent_containers/cup/cult/bloodfilled/New()
+ ..()
+ reagents.add_reagent(/datum/reagent/blood, 50)
diff --git a/monkestation/code/modules/bloody_cult/cult/misc_procs.dm b/monkestation/code/modules/bloody_cult/cult/misc_procs.dm
new file mode 100644
index 000000000000..b41a841ac23e
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/misc_procs.dm
@@ -0,0 +1,217 @@
+/mob/proc/occult_muted()
+ if (reagents && reagents.has_reagent(/datum/reagent/water/holywater))
+ return TRUE
+ return FALSE
+
+//Requires either a target/location or both
+//Requires a_icon holding the animation
+//Requires either a_icon_state of the animation or the flick_anim
+//Does not require sleeptime, specifies for how long the animation should be allowed to exist before returning to pool
+//Does not require animation direction, but you can specify
+//Does not require a name
+/proc/anim(turf/location as turf, target as mob|obj, a_icon, a_icon_state as text, flick_anim as text, sleeptime = 15, direction as num, name as text, lay as num, offX as num, offY as num, col as text, alph as num, plane as num, var/trans, var/invis, var/animate_movement, var/blend)
+//This proc throws up either an icon or an animation for a specified amount of time.
+//The variables should be apparent enough.
+ if(!location && target)
+ location = get_turf(target)
+ if (!location)//target in nullspace
+ return
+ if(location && !target)
+ target = location
+ if(!location && !target)
+ return
+ var/obj/effect/abstract/animation = new /obj/effect/abstract(location)
+ if(name)
+ animation.name = name
+ if(direction)
+ animation.dir = direction
+ if(alph)
+ animation.alpha = alph
+ if(invis)
+ animation.invisibility = invis
+ if(blend)
+ animation.blend_mode = blend
+ animation.icon = a_icon
+ animation.animate_movement = animate_movement
+ animation.mouse_opacity = 0
+ if(!lay)
+ animation.layer = target:layer+1
+ else
+ animation.layer = lay
+ if(target && istype(target, /atom))
+ if(!plane)
+ animation.plane = target:plane
+ else
+ animation.plane = plane
+ if(offX)
+ animation.pixel_x = offX
+ if(offY)
+ animation.pixel_y = offY
+ if(col)
+ animation.color = col
+ if(trans)
+ animation.transform = trans
+ if(a_icon_state)
+ animation.icon_state = a_icon_state
+ else
+ animation.icon_state = "blank"
+ flick(flick_anim, animation)
+
+ spawn(max(sleeptime, 5))
+ qdel(animation)
+
+ return animation
+
+/atom/proc/get_cult_power()
+ return 0
+
+/mob/get_cult_power()
+ var/static/list/valid_cultpower_slots = list(
+ ITEM_SLOT_OCLOTHING,
+ ITEM_SLOT_HEAD,
+ ITEM_SLOT_GLOVES,
+ ITEM_SLOT_BACK,
+ ITEM_SLOT_FEET,
+ )
+ var/power = 0
+ for (var/slot in valid_cultpower_slots)
+ var/obj/item/I = get_item_by_slot(slot)
+ if (istype(I))
+ power += I.get_cult_power()
+
+ return power
+
+
+/mob/proc/get_convertibility()
+ if (!mind || stat == DEAD)
+ return CONVERTIBLE_NOMIND
+
+ if (IS_CULTIST(src))
+ return CONVERTIBLE_ALREADY
+
+ return 0
+
+/mob/living/carbon/get_convertibility()
+ var/convertibility = ..()
+
+ if (!convertibility)
+ //TODO: chaplain stuff
+ //this'll do in the meantime
+ if (mind.assigned_role == "Chaplain")
+ return CONVERTIBLE_NEVER
+
+ if (is_banned_from(src.key, ROLE_CULTIST))
+ return CONVERTIBLE_NEVER
+
+ return CONVERTIBLE_CHOICE
+
+ return convertibility//no mind, dead, or already a cultist
+
+/mob/living/carbon/proc/update_convertibility()
+ var/convertibility = get_convertibility()
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/hud.dmi', src, "hudblank")
+ switch(convertibility)
+ if (CONVERTIBLE_ALWAYS)
+ I.icon_state = "convertible"
+ if (CONVERTIBLE_CHOICE)
+ I.icon_state = "maybeconvertible"
+ if (CONVERTIBLE_IMPLANT)
+ I.icon_state = "unconvertible"
+ if (CONVERTIBLE_NEVER)
+ I.icon_state = "unconvertible2"
+
+ I.pixel_y = 16
+ I.plane = HUD_PLANE
+ I.appearance_flags |= RESET_COLOR|RESET_ALPHA
+
+ //inspired from the rune color matrix because boy am I proud of it
+ animate(I, color = list(2, 0.67, 0.27, 0, 0.27, 2, 0.67, 0, 0.67, 0.27, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 2)//9
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.7)//8
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.4)//7
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.1)//6
+ animate(color = list(1.5, 0.27, 0, 0, 0, 1.5, 0.27, 0, 0.27, 0, 1.5, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.8)//5
+ animate(color = list(1.375, 0.19, 0, 0, 0, 1.375, 0.19, 0, 0.19, 0, 1.375, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.5)//4
+ animate(color = list(1.25, 0.12, 0, 0, 0, 1.25, 0.12, 0, 0.12, 0, 1.25, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.2)//3
+ animate(color = list(1.125, 0.06, 0, 0, 0, 1.125, 0.06, 0, 0.06, 0, 1.125, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.1)//2
+ animate(color = list(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 5)//1
+
+ return I
+
+/mob/living/carbon/proc/boxify(var/delete_body = TRUE, var/new_anim = TRUE, var/box_state = "cult")//now its own proc so admins may atomProcCall it if they so desire.
+ var/turf/T = get_turf(src)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_failure.ogg', 75, 0, -4)
+ if (new_anim)
+ var/obj/effect/cult_ritual/conversion/anim = new(T)
+ anim.icon_state = ""
+ flick("rune_convert_failure", anim)
+ anim.Die()
+ var/obj/item/storage/cult/coffer = new(T)
+ coffer.icon_state = box_state
+ var/obj/item/reagent_containers/cup/cult/cup = new(coffer)
+ var/datum/blood_type/type = get_blood_type()
+ cup.reagents.add_reagent(type.reagent_type, 50)
+
+ for(var/obj/item/I in src)
+ dropItemToGround(I)
+ if(I)
+ I.forceMove(T)
+ I.dropped(src)
+ I.forceMove(coffer)
+ if (delete_body)
+ qdel(src)
+
+/proc/cheap_pythag(const/Ax, const/Ay)
+ var/dx = abs(Ax)
+ var/dy = abs(Ay)
+
+ if (dx >= dy)
+ return dx + (0.5 * dy) // The longest side add half the shortest side approximates the hypotenuse.
+ else
+ return dy + (0.5 * dx)
+
+/proc/get_distant_turf(turf/T, direction, distance)
+ if(!T || !direction || !distance)
+ return
+
+ var/dest_x = T.x
+ var/dest_y = T.y
+ var/dest_z = T.z
+
+ if(direction & NORTH)
+ dest_y = min(world.maxy, dest_y+distance)
+ if(direction & SOUTH)
+ dest_y = max(0, dest_y-distance)
+ if(direction & EAST)
+ dest_x = min(world.maxy, dest_x+distance)
+ if(direction & WEST)
+ dest_x = max(0, dest_x-distance)
+
+ return locate(dest_x, dest_y, dest_z)
+
+/// Returns whether the given mob is convertable to the blood cult, monkestation edit: or clock cult
+/proc/is_convertable_to_cult(mob/living/target, datum/team/cult/specific_cult, for_clock_cult) //monkestation edit: adds for_clock_cult
+ if(!istype(target))
+ return FALSE
+ if(isnull(target.mind) || !GET_CLIENT(target))
+ return FALSE
+ if(HAS_MIND_TRAIT(target, TRAIT_UNCONVERTABLE)) // monkestation edit: mind.unconvertable -> TRAIT_UNCONVERTABLE
+ return FALSE
+ if(ishuman(target) && target.mind.holy_role)
+ return FALSE
+ var/mob/living/master = target.mind.enslaved_to?.resolve()
+ if(master && (for_clock_cult ? !IS_CLOCK(master) : !IS_CULTIST(master))) //monkestation edit: master is now checked based off of for_clock_cult
+ return FALSE
+ if(IS_HERETIC_OR_MONSTER(target))
+ return FALSE
+ if(HAS_TRAIT(target, TRAIT_MINDSHIELD) || isbot(target)) //monkestation edit: moved isdrone() as well as issilicon() to the next check down
+ return FALSE //can't convert machines, shielded, or braindead
+ if((isdrone(target) || issilicon(target)) && !for_clock_cult) //monkestation edit: clock cult converts them into cogscarabs and clock borgs
+ return FALSE //monkestation edit
+ if(for_clock_cult ? IS_CULTIST(target) : IS_CLOCK(target)) //monkestation edit
+ return FALSE //monkestation edit
+ return TRUE
+
+/proc/locate_team(type)
+ return locate(type) in GLOB.antagonist_teams
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/_construct.dm b/monkestation/code/modules/bloody_cult/cult/mobs/_construct.dm
new file mode 100644
index 000000000000..9da191d36bbf
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/_construct.dm
@@ -0,0 +1,34 @@
+/mob/living/basic/construct
+ var/list/healers = list()
+ var/construct_color = COLOR_BLOOD
+ var/new_glow = FALSE
+
+/mob/living/basic/construct/Move(atom/newloc, direct, glide_size_override)
+ . = ..()
+ if (healers.len > 0)
+ for (var/mob/living/basic/construct/artificer/perfect/P in healers)
+ P.move_ray()
+
+/mob/living/basic/construct/update_overlays()
+ . = ..()
+ if(!new_glow)
+ return
+ var/icon/glowicon = icon(icon, "glow-[icon_state]", src)
+ glowicon.Blend(construct_color, ICON_ADD)
+ . += emissive_appearance(glowicon, offset_spokesman = src)
+ . += mutable_appearance(glowicon, offset_spokesman = src)
+
+ var/damage = maxHealth - health
+ var/icon/damageicon
+ if (damage > (2*maxHealth/3))
+ damageicon = icon(icon, "[icon_state]_damage_high", src)
+ else if (damage > (maxHealth/3))
+ damageicon = icon(icon, "[icon_state]_damage_low", src)
+ if (damageicon)
+ damageicon.Blend(construct_color, ICON_ADD)
+ . += emissive_appearance(damageicon, offset_spokesman = src)
+ . += mutable_appearance(damageicon, offset_spokesman = src)
+
+/mob/living/basic/construct/adjust_health(amount, updating_health, forced)
+ . = ..()
+ update_appearance()
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/artificer.dm b/monkestation/code/modules/bloody_cult/cult/mobs/artificer.dm
new file mode 100644
index 000000000000..4be1bc8df516
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/artificer.dm
@@ -0,0 +1,141 @@
+/mob/living/basic/construct/artificer/perfect
+ icon_state = "artificer2"
+ icon_living = "artificer2"
+ icon_dead = "artificer2"
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ see_in_dark = 7
+ new_glow = TRUE
+ construct_spells = list(
+ /datum/action/cooldown/spell/conjure/cult_floor,
+ /datum/action/cooldown/spell/conjure/cult_wall,
+ /datum/action/cooldown/spell/conjure/soulstone,
+ /datum/action/cooldown/spell/conjure/construct/lesser,
+ /datum/action/cooldown/spell/pointed/conjure/struct,
+ /datum/action/cooldown/spell/pointed/conjure/door,
+ /datum/action/cooldown/spell/pointed/conjure/pylon,
+ /datum/action/cooldown/spell/pointed/conjure/hex,
+ )
+ var/mob/living/basic/construct/heal_target = null
+ var/obj/effect/overlay/artificerray/ray = null
+ var/heal_range = 2
+ var/list/minions = list()
+ var/list/satellites = list()
+
+/obj/abstract/satellite
+ mouse_opacity = 0
+ invisibility = 101
+ icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi'
+ icon_state = "blank"
+
+/mob/living/basic/construct/artificer/perfect/proc/update_satellites()
+ var/turf/T = get_turf(src)
+ while(satellites.len < 3)
+ var/obj/abstract/satellite/S = new(T)
+ satellites.Add(S)
+ var/obj/abstract/satellite/satellite_A = satellites[1]
+ var/obj/abstract/satellite/satellite_B = satellites[2]
+ var/obj/abstract/satellite/satellite_C = satellites[3]
+ satellite_A.forceMove(get_step(T, turn(dir, 180)))//behind
+ satellite_B.forceMove(get_step(T, turn(dir, 135)))//behind on one side
+ satellite_C.forceMove(get_step(T, turn(dir, 225)))//behind on the other side
+
+/mob/living/basic/construct/artificer/perfect/Life()
+ . = ..()
+ if(. && heal_target)
+ heal_target.health = min(heal_target.maxHealth, heal_target.health + round(heal_target.maxHealth/10))
+ heal_target.update_icons()
+ anim(target = heal_target, a_icon = 'icons/effects/effects.dmi', flick_anim = "const_heal", plane = ABOVE_LIGHTING_PLANE)
+ move_ray()
+ update_satellites()
+
+/mob/living/basic/construct/artificer/perfect/Move(NewLoc, Dir = 0, step_x = 0, step_y = 0, var/glide_size_override = 0)
+ . = ..()
+ if (ray)
+ move_ray()
+ update_satellites()
+
+/mob/living/basic/construct/artificer/perfect/forceMove(atom/destination, step_x = 0, step_y = 0, no_tp = FALSE, harderforce = FALSE, glide_size_override = 0)
+ . = ..()
+ if (ray)
+ move_ray()
+ update_satellites()
+
+/mob/living/basic/construct/artificer/perfect/proc/start_ray(var/mob/living/basic/construct/target)
+ if (!istype(target))
+ return
+ if (locate(src) in target.healers)
+ to_chat(src, span_warning("You are already healing \the [target].") )
+ return
+ if (ray)
+ end_ray()
+ target.healers.Add(src)
+ heal_target = target
+ ray = new (loc)
+ to_chat(src, span_notice("You are now healing \the [target].") )
+ move_ray()
+
+/mob/living/basic/construct/artificer/perfect/UnarmedAttack(mob/living/basic/construct/attack_target, proximity_flag, list/modifiers)
+ . = ..()
+ if(isconstruct(attack_target) && (attack_target.health < attack_target.maxHealth))
+ start_ray(attack_target)
+
+/mob/living/basic/construct/artificer/perfect/proc/move_ray()
+ if(heal_target && ray && heal_target.health < heal_target.maxHealth && get_dist(heal_target, src) <= heal_range && isturf(loc) && isturf(heal_target.loc))
+ ray.forceMove(loc)
+ var/disty = heal_target.y - src.y
+ var/distx = heal_target.x - src.x
+ var/newangle
+ if(!disty)
+ if(distx >= 0)
+ newangle = 90
+ else
+ newangle = 270
+ else
+ newangle = arctan(distx/disty)
+ if(disty < 0)
+ newangle += 180
+ else if(distx < 0)
+ newangle += 360
+ var/matrix/M = matrix()
+ if (ray.oldloc_source && ray.oldloc_target && get_dist(src, ray.oldloc_source) <= 1 && get_dist(heal_target, ray.oldloc_target) <= 1)
+ animate(ray, transform = turn(M.Scale(1, sqrt(distx*distx+disty*disty)), newangle), time = 1)
+ else
+ ray.transform = turn(M.Scale(1, sqrt(distx*distx+disty*disty)), newangle)
+ ray.oldloc_source = src.loc
+ ray.oldloc_target = heal_target.loc
+ else
+ end_ray()
+
+/mob/living/basic/construct/artificer/perfect/proc/end_ray()
+ if (heal_target)
+ heal_target.healers.Remove(src)
+ heal_target = null
+ if (ray)
+ QDEL_NULL(ray)
+
+/obj/effect/overlay/artificerray
+ name = "ray"
+ icon = 'monkestation/code/modules/bloody_cult/icons/96x96.dmi'
+ icon_state = "artificer_ray"
+ layer = FLY_LAYER
+ anchored = 1
+ mouse_opacity = 0
+ pixel_x = -32
+ pixel_y = -29
+ var/turf/oldloc_source = null
+ var/turf/oldloc_target = null
+
+/obj/effect/overlay/artificerray/narsie_act()
+ return
+
+/obj/effect/overlay/artificerray/ex_act()
+ return
+
+/obj/effect/overlay/artificerray/emp_act()
+ return
+
+/obj/effect/overlay/artificerray/blob_act()
+ return
+
+/obj/effect/overlay/artificerray/singularity_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/astral_projection.dm b/monkestation/code/modules/bloody_cult/cult/mobs/astral_projection.dm
new file mode 100644
index 000000000000..17d6c7dfb266
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/astral_projection.dm
@@ -0,0 +1,257 @@
+/mob
+ var/obj/effect/new_rune/ajourn
+
+////////////////////////////////////////////////////////////////////////////////////////
+GLOBAL_LIST_INIT(astral_projections, list())
+
+/mob/living/basic/astral_projection
+ name = "astral projection"
+ real_name = "astral projection"
+ desc = "A fragment of a cultist's soul, freed from the laws of physics."
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ icon_state = "ghost-narsie"
+ icon_living = "ghost-narsie"
+ icon_dead = "ghost-narsie"
+ movement_type = FLYING
+ maxHealth = 1
+ health = 1
+ melee_damage_lower = 0
+ melee_damage_upper = 0
+ speed = 1
+ faction = list("cult")
+ speed = 0.5
+ density = 0
+ status_flags = GODMODE
+ plane = GHOST_PLANE
+ invisibility = INVISIBILITY_REVENANT
+ see_invisible = INVISIBILITY_REVENANT
+ incorporeal_move = INCORPOREAL_MOVE_BASIC
+ alpha = 127
+ now_pushing = 1 //prevents pushing atoms
+
+ //keeps track of whether we're in "ghost" form or "slightly less ghost" form
+ var/tangibility = FALSE
+
+ //the cultist's original body
+ var/mob/living/anchor
+
+ var/image/incorporeal_appearance
+ var/image/tangible_appearance
+
+ var/time_last_speech = 0//speech bubble cooldown
+
+ //sechud stuff
+ var/cardjob = "hudunknown"
+
+ //convertibility HUD
+ var/list/propension = list()
+
+ var/projection_destroyed = FALSE
+ var/direct_delete = FALSE
+
+ var/image/hudicon
+
+ var/last_devotion_gain = 0
+ var/devotion_gain_delay = 60 SECONDS
+
+ var/datum/action/cooldown/spell/astral_return/astral_return
+ var/datum/action/cooldown/spell/astral_toggle/astral_toggle
+
+/mob/living/basic/astral_projection/New()
+ ..()
+ GLOB.astral_projections += src
+ last_devotion_gain = world.time
+ incorporeal_appearance = image('monkestation/code/modules/bloody_cult/icons/mob.dmi', "blank")
+ tangible_appearance = image('monkestation/code/modules/bloody_cult/icons/mob.dmi', "blank")
+ //change_sight(adding = SEE_TURFS | SEE_MOBS | SEE_OBJS | SEE_SELF)
+ see_in_dark = 100
+
+ astral_return = new
+ astral_toggle = new
+
+ astral_return.Grant(src)
+ astral_toggle.Grant(src)
+
+/mob/living/basic/astral_projection/Login()
+ ..()
+
+ if (!tangibility)
+ overlay_fullscreen("astralborder", /atom/movable/screen/fullscreen/astral_border)
+ update_fullscreen_alpha("astralborder", 255, 5)
+
+/mob/living/basic/astral_projection/proc/destroy_projection()
+ if (projection_destroyed)
+ return
+ projection_destroyed = TRUE
+ GLOB.astral_projections -= src
+ //the projection has ended, let's return to our body
+ if (anchor && anchor.stat != DEAD && client)
+ if (key)
+ if (tangibility)
+ var/obj/effect/afterimage/A = new (loc, anchor, 10)
+ A.dir = dir
+ for(var/mob/M in dview(world.view, loc, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(loc, get_sfx("disappear_sound"), 75, 0, -2)
+ anchor.key = key
+ to_chat(anchor, span_notice("You reconnect with your body.") )
+ anchor.ajourn = null
+ //if our body was somehow already destroyed however, we'll become a shade right here
+ else if(client)
+ var/turf/T = get_turf(src)
+ if (T)
+ var/mob/living/basic/shade/shade = new (T)
+ playsound(T, 'sound/hallucinations/growl1.ogg', 50, 1)
+ shade.name = "[real_name] the Shade"
+ shade.real_name = "[real_name]"
+ mind.transfer_to(shade)
+ shade.key = key
+ to_chat(shade, span_cult("It appears your body was unfortunately destroyed. The remains of your soul made their way to your astral projection where they merge together, forming a shade.") )
+ invisibility = 101
+ set_density(FALSE)
+ sleep(20)
+ if (!direct_delete)
+ qdel(src)
+
+/mob/living/basic/astral_projection/Destroy()
+ if (!projection_destroyed)
+ direct_delete = TRUE
+ INVOKE_ASYNC(src, PROC_REF(destroy_projection))
+ ..()
+
+/mob/living/basic/astral_projection/Life()
+ . = ..()
+
+ if (anchor)
+ var/turf/T = get_turf(anchor)
+ var/turf/U = get_turf(src)
+ if (T.z != U.z)
+ to_chat(src, span_warning("You cannot sustain the astral projection at such a distance.") )
+ death()
+ return
+ else
+ death()
+ return
+
+ if (world.time >= (last_devotion_gain + devotion_gain_delay))
+ last_devotion_gain += devotion_gain_delay
+ var/datum/antagonist/cult/cult_datum = mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_2, "astral_journey")
+
+
+/mob/living/basic/astral_projection/death(var/gibbed = FALSE)
+ spawn()
+ destroy_projection(src)
+
+/mob/living/basic/astral_projection/examine(mob/user)
+ if (!tangibility)
+ if ((user == src) && anchor)
+ to_chat(user, span_notice("You check yourself to see how others would see you were you tangible:") )
+ anchor.examine(user)
+ else if (IS_CULTIST(user))
+ to_chat(user, span_notice("It's an astral projection.") )
+ else
+ to_chat(user, span_cult("Wait something's not right here.") )//it's a g-g-g-g-ghost!
+ else if (anchor)
+ anchor.examine(user)//examining the astral projection alone won't be enough to see through it, although the user might want to make sure they cannot be identified first.
+
+//no pulling stuff around
+/mob/living/basic/astral_projection/start_pulling(atom/movable/AM, state, force = pull_force, supress_message = FALSE)
+ return
+
+
+//this should prevent most other edge cases
+/mob/living/basic/astral_projection/incapacitated()
+ return TRUE
+
+//bullets instantly end us
+/mob/living/basic/astral_projection/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ . = ..()
+ if (tangibility)
+ death()
+
+
+/mob/living/basic/astral_projection/ex_act(var/severity)
+ if(tangibility)
+ death()
+
+
+//called once when we are created, shapes our appearance in the image of our anchor
+/mob/living/basic/astral_projection/proc/ascend(var/mob/living/body)
+ if (!body)
+ return
+ anchor = body
+ //memorizing our anchor's appearance so we can toggle to it
+ tangible_appearance = body.appearance
+
+ //getting our ghostly looks
+ overlays.len = 0
+ if (ishuman(body))
+ var/mob/living/carbon/human/H = body
+ //instead of just adding an overlay of the body's uniform and suit, we'll first process them a bit so the leg part is mostly erased, for a ghostly look.
+ //overlays += crop_human_suit_and_uniform(body)
+ overlays += H.overlays_standing[ID_LAYER]
+ overlays += H.overlays_standing[EARS_LAYER]
+ overlays += H.overlays_standing[GLASSES_LAYER]
+ overlays += H.overlays_standing[BELT_LAYER]
+ overlays += H.overlays_standing[BACK_LAYER]
+ overlays += H.overlays_standing[HEAD_LAYER]
+ overlays += H.overlays_standing[HANDCUFF_LAYER]
+
+ //giving control to the player
+ key = body.key
+
+ //name & examine stuff
+ desc = body.desc
+ gender = body.gender
+ if(body.mind && body.mind.name)
+ name = body.mind.name
+ else
+ if(body.real_name)
+ name = body.real_name
+ else
+ if(gender == MALE)
+ name = capitalize(pick(GLOB.first_names_male)) + " " + capitalize(pick(GLOB.last_names))
+ else
+ name = capitalize(pick(GLOB.first_names_female)) + " " + capitalize(pick(GLOB.last_names))
+ real_name = name
+
+ //important to trick sechuds
+ var/list/target_id_cards = body.get_all_contents_type(/obj/item/card/id)
+ var/obj/item/card/id/card = target_id_cards[1]
+ if(card)
+ cardjob = card.assignment
+
+ //memorizing our current appearance so we can toggle back to it later. Has to be done AFTER setting our new name.
+ incorporeal_appearance = appearance
+
+ //we don't transfer the mind but we keep a reference to it.
+ mind = body.mind
+
+/mob/living/basic/astral_projection/proc/toggle_tangibility()
+ if (tangibility)
+ set_density(FALSE)
+ appearance = incorporeal_appearance
+ movement_type = FLYING
+ incorporeal_move = 1
+ speed = 0.5
+ overlay_fullscreen("astralborder", /atom/movable/screen/fullscreen/astral_border)
+ update_fullscreen_alpha("astralborder", 255, 5)
+ var/obj/effect/afterimage/A = new (loc, anchor, 10)
+ A.dir = dir
+ else
+ set_density(TRUE)
+ appearance = tangible_appearance
+ incorporeal_move = 0
+ movement_type = GROUND
+ see_invisible = SEE_INVISIBLE_OBSERVER
+ speed = 1
+ clear_fullscreen("astralborder", animated = 5)
+ alpha = 0
+ animate(src, alpha = 255, time = 10)
+
+ tangibility = !tangibility
+
+//saycode
+/mob/living/basic/astral_projection/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)
+ . = ..(tangibility ? "[message]" : "..[message]", tangibility ? "" : "C")
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/hex.dm b/monkestation/code/modules/bloody_cult/cult/mobs/hex.dm
new file mode 100644
index 000000000000..50d909aa985c
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/hex.dm
@@ -0,0 +1,212 @@
+/mob/living/simple_animal/hostile/hex
+ name = "\improper Hex"
+ desc = "A lesser construct, crafted by an Artificer."
+ stop_automated_movement_when_pulled = 1
+ movement_type = FLYING
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ icon_state = "hex"
+ icon_living = "hex"
+ icon_dead = "hex"
+ speak_chance = 0
+ turns_per_move = 8
+ speed = 0.2
+ maxHealth = 50
+ health = 50
+ ranged = 1
+ retreat_distance = 4
+ minimum_distance = 4
+ projectilesound = 'monkestation/code/modules/bloody_cult/sound/forge.ogg'
+ projectiletype = /obj/projectile/bloodslash
+ move_to_delay = 1
+ harm_intent_damage = 10
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ //attack_sound = 'sound/weapons/rapidslice.ogg'
+ speed = 5
+ faction = "cult"
+ var/mob/living/basic/construct/artificer/perfect/master = null
+ var/no_master = TRUE
+ var/glow_color = "#FFFFFF"
+
+ var/image/master_glow = null
+ var/image/harm_glow = null
+
+ var/mode = HEX_MODE_ROAMING
+ var/passive = FALSE
+ var/turf/guard_spot = null
+
+ //Used to determine behavior
+ var/stance = HOSTILE_STANCE_IDLE
+ /*
+ HEX_MODE_ROAMING : Usual mob roaming behaviour.
+ HEX_MODE_GUARD : Stands in place. If it spots an enemy, will chase it, then attempt to return to the spot where they were placed.
+ HEX_MODE_ESCORT : Follows the Artificer that summoned them around if they're close enough, otherwise stays idle. Shoots at enemies but doesn't chase them.
+ */
+
+/mob/living/simple_animal/hostile/hex/New()
+ ..()
+ setupglow(glow_color)
+ update_harmglow()
+ animate(src, pixel_y = 4 * 1 , time = 10, loop = -1, easing = SINE_EASING)
+ animate(pixel_y = 2 * 1, time = 10, loop = -1, easing = SINE_EASING)
+
+/mob/living/simple_animal/hostile/hex/FindTarget(list/possible_targets)
+ if(stance != HOSTILE_STANCE_ATTACK || stance != HOSTILE_STANCE_ATTACKING)
+ return
+ . = ..()
+
+/mob/living/simple_animal/hostile/hex/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype = ALL)
+ ..()
+ update_icons()
+
+/mob/living/simple_animal/hostile/hex/update_icons()
+ . = ..()
+ overlays = 0
+
+ var/damage = maxHealth - health
+ var/icon/damageicon
+ if (damage > (2*maxHealth/3))
+ damageicon = icon(icon, "wraith2_damage_high")//fits well enough
+ else if (damage > (maxHealth/3))
+ damageicon = icon(icon, "wraith2_damage_low")
+ if (damageicon)
+ damageicon.Blend(glow_color, ICON_ADD)
+ var/image/damage_overlay = image(icon = damageicon)
+ damage_overlay.plane = ABOVE_LIGHTING_PLANE
+ overlays += damage_overlay
+
+ setupglow(glow_color)
+ update_harmglow()
+
+/mob/living/simple_animal/hostile/hex/proc/setupglow(var/_glowcolor = "#FFFFFF")
+ glow_color = _glowcolor
+ overlays -= master_glow
+ var/icon/glowicon = icon(icon, "glow-[icon_state]")
+ glowicon.Blend(_glowcolor, ICON_ADD)
+ master_glow = image(icon = glowicon)
+ master_glow.plane = ABOVE_LIGHTING_PLANE
+ overlays += master_glow
+
+/mob/living/simple_animal/hostile/hex/proc/update_harmglow()
+ overlays -= harm_glow
+ harm_glow = image(icon, src, "[passive ? "glow-hex-passive" : "glow-hex-harm"]")
+ harm_glow.plane = ABOVE_LIGHTING_PLANE+1
+ overlays += harm_glow
+
+/mob/living/simple_animal/hostile/hex/Destroy()
+ if (master)
+ master.minions.Remove(src)
+ if (IS_CULTIST(master))
+ master.DisplayUI("Cultist Right Panel")
+ master = null
+ ..()
+
+/mob/living/simple_animal/hostile/hex/Life()
+ if (mode != HEX_MODE_ROAMING)
+ stop_automated_movement = 1
+ . = ..()
+ if (!no_master)
+ if (!master || QDELETED(master) || master.stat == DEAD)
+ adjustBruteLoss(20)//we shortly die out after our master's demise
+ mode = HEX_MODE_ROAMING
+ switch(mode)
+ if (HEX_MODE_GUARD)
+ if (stance == HOSTILE_STANCE_IDLE)
+ if (!guard_spot)
+ guard_spot = get_turf(src)
+ else
+ var/turf/T = get_turf(src)
+ if (T != guard_spot)
+ if ((T.z == guard_spot.z) && (get_dist(T, guard_spot) < 50))
+ Goto(guard_spot, move_to_delay, 0)
+ else
+ guard_spot = get_turf(src)
+ else
+ dir = turn(dir, -90)
+ if (HEX_MODE_ESCORT)
+ escort_routine()
+ else
+ guard_spot = null
+
+/mob/living/simple_animal/hostile/hex/proc/escort_routine()
+ guard_spot = null
+ var/escorts = 0
+ var/spot = 0
+ for (var/mob/living/simple_animal/hostile/hex/H in master.minions)
+ if (H.mode == HEX_MODE_ESCORT)
+ escorts++
+ if (H == src)
+ spot = escorts
+ if (escorts == 1)
+ Goto(master.satellites[1], move_to_delay, 0)//trailing behind
+ if (!target && (loc == get_turf(master.satellites[1])))
+ dir = master.dir
+ else
+ Goto(master.satellites[spot+1], move_to_delay, 0)//trailing on each sides
+ if (!target && (loc == get_turf(master.satellites[spot+1])))
+ dir = master.dir
+
+/mob/living/simple_animal/hostile/hex/Cross(var/atom/movable/mover, var/turf/target, var/height = 1.5, var/air_group = 0)
+ if(istype(mover, /obj/projectile/bloodslash))//stop hitting yourself ffs!
+ return 1
+ if ((mode == HEX_MODE_ESCORT) && (istype(mover, /mob/living/simple_animal/hostile/hex) || (mover == master)))//Escort mode is janky otherwise
+ return 1
+
+ return ..()
+
+/mob/living/simple_animal/hostile/hex/Move(NewLoc, Dir = 0, step_x = 0, step_y = 0, var/glide_size_override = 0)
+ . = ..()
+ if (target)
+ dir = get_dir(src, target)
+
+/mob/living/simple_animal/hostile/hex/forceMove(atom/destination, step_x = 0, step_y = 0, no_tp = FALSE, harderforce = FALSE, glide_size_override = 0)
+ . = ..()
+ if (target)
+ dir = get_dir(src, target)
+
+/mob/living/simple_animal/hostile/hex/death(var/gibbed = FALSE)
+ ..(TRUE) //If they qdel, they gib regardless
+ visible_message(span_warning("\The [src] collapses in a shattered heap. ") )
+ qdel (src)
+
+/mob/living/simple_animal/hostile/hex/PickTarget(list/Targets)
+ if (passive)
+ Targets = list()
+
+ for(var/mob/the_target as anything in Targets)
+ if(ismob(the_target))
+ var/mob/M = the_target
+ if(IS_CULTIST(M))
+ Targets -= M
+ if (iscarbon(M))
+ var/mob/living/carbon/C = M
+ if (istype(C.handcuffed, /obj/item/restraints/handcuffs/cult)) //hex don't attack prisoners
+ Targets -= M
+ if (locate(/obj/effect/stun_indicator) in M)//or people that got stun paper'd
+ Targets -= M
+ if (locate(/obj/effect/cult_ritual/conversion) in M.loc)//or people that stand on top of an active conversion rune
+ Targets -= M
+ . = ..()
+
+/mob/living/simple_animal/hostile/hex/MoveToTarget()
+ if (mode == HEX_MODE_ESCORT)
+ stop_automated_movement = 1
+ if(!target || !CanAttack(target))
+ LoseTarget()
+ return
+ if(isturf(loc))
+ if(target in ListTargets())
+ dir = get_dir(src, target)
+ if(get_dist(src, target) >= 2 && ranged_cooldown <= 0)
+ OpenFire(target)
+ if(target.Adjacent(src))
+ AttackingTarget()
+ return
+ stance = HOSTILE_STANCE_IDLE
+ walk(src, 0)
+ LoseAggro()
+ else
+ ..()
+
+/mob/living/simple_animal/hostile/hex/narsie_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/juggernaut.dm b/monkestation/code/modules/bloody_cult/cult/mobs/juggernaut.dm
new file mode 100644
index 000000000000..01dde3509766
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/juggernaut.dm
@@ -0,0 +1,114 @@
+/mob/living/basic/construct
+ var/construct_type = "Unknown"
+
+/mob/living/basic/construct/juggernaut/perfect
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ icon_state = "juggernaut2"
+ icon_living = "juggernaut2"
+ icon_dead = "juggernaut2"
+ new_glow = TRUE
+ construct_spells = list(
+ /datum/action/cooldown/spell/forcewall/cult,
+ /datum/action/cooldown/spell/basic_projectile/juggernaut,
+ /datum/action/innate/cult/create_rune/wall,
+ /datum/action/cooldown/spell/juggerdash,
+ )
+ see_in_dark = 7
+ var/dash_dir = null
+ var/turf/crashing = null
+
+
+/mob/living/basic/construct/juggernaut/perfect/Bump(atom/obstacle)
+ . = ..()
+ if(src.throwing)
+ var/breakthrough = 0
+ if(istype(obstacle, /obj/structure/window/))
+ var/obj/structure/window/W = obstacle
+ W.take_damage(1000)
+ breakthrough = 1
+
+ else if(istype(obstacle, /obj/structure/grille/))
+ var/obj/structure/grille/G = obstacle
+ G.take_damage(G.max_integrity * 0.25)
+ breakthrough = 1
+
+ else if(istype(obstacle, /obj/structure/table))
+ var/obj/structure/table/T = obstacle
+ qdel(T)
+ breakthrough = 1
+
+ else if(istype(obstacle, /obj/structure/rack))
+ new /obj/item/rack_parts(obstacle.loc)
+ qdel(obstacle)
+ breakthrough = 1
+
+ else if(istype(obstacle, /turf/closed/wall))
+ var/turf/closed/wall/W = obstacle
+ if (W.hardness <= 60)
+ //playsound(W, 'sound/weapons/heavysmash.ogg', 75, 1)
+ W.dismantle_wall(1)
+ breakthrough = 1
+ else
+ src.throwing = 0
+ src.crashing = null
+
+ else if(istype(obstacle, /obj/structure/reagent_dispensers))
+ var/obj/structure/reagent_dispensers/R = obstacle
+ qdel(R)
+
+ else if(istype(obstacle, /mob/living))
+ var/mob/living/L = obstacle
+ if (!(L.status_flags & CANKNOCKDOWN) || istype(L, /mob/living/silicon))
+ //can't be knocked down? you'll still take the damage.
+ src.throwing = 0
+ src.crashing = null
+ L.take_overall_damage(5, 0)
+ if(L.buckled)
+ L.buckled.unbuckle_mob(L)
+ else
+ L.take_overall_damage(5, 0)
+ if(L.buckled)
+ L.buckled.unbuckle_mob(L)
+ L.Stun(2)
+ L.Knockdown(2)
+ //playsound(src, 'sound/weapons/heavysmash.ogg', 50, 0, 0)
+ breakthrough = 1
+ else
+ src.throwing = 0
+ src.crashing = null
+
+ if(breakthrough)
+ if(crashing && !istype(crashing, /turf/open/space))
+ spawn(1)
+ src.throw_at(crashing, 50, src.throw_speed)
+ else
+ spawn(1)
+ crashing = get_distant_turf(get_turf(src), dash_dir, 2)
+ src.throw_at(crashing, 50, src.throw_speed)
+
+ if(istype(obstacle, /obj))
+ var/obj/O = obstacle
+ if(!O.anchored)
+ step(obstacle, src.dir)
+ else
+ obstacle.Bumped(src)
+ else if(istype(obstacle, /mob))
+ step(obstacle, src.dir)
+ else
+ obstacle.Bumped(src)
+
+/datum/action/cooldown/spell/juggerdash
+ name = "Jugger-Dash"
+ desc = "Charge in a line and knock down anything in your way, even some walls."
+ var/range = 4
+
+ cooldown_time = 40 SECONDS
+ invocation_type = INVOCATION_NONE
+
+/datum/action/cooldown/spell/juggerdash/cast(atom/cast_on)
+ . = ..()
+ //playsound(owner, 'sound/effects/juggerdash.ogg', 100, 1)
+ var/mob/living/basic/construct/juggernaut/perfect/jugg = owner
+ jugg.crashing = null
+ var/landing = get_distant_turf(get_turf(owner), jugg.dir, range)
+ jugg.throw_at(landing, range , 2)
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/observers.dm b/monkestation/code/modules/bloody_cult/cult/mobs/observers.dm
new file mode 100644
index 000000000000..9871cbaf8e7c
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/observers.dm
@@ -0,0 +1,20 @@
+/mob/dead/observer
+ var/appearance_backup
+
+/mob/dead/observer/Initialize(mapload)
+ . = ..()
+ if(GLOB.eclipse.eclipse_start_time && !GLOB.eclipse.eclipse_finished)
+ narsie_act()
+
+/mob/dead/observer/narsie_act()
+ if(invisibility != 0)
+ var/datum/action/cooldown/blood_doodle/doodle = new
+ doodle.Grant(src)
+ appearance_backup = appearance
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ icon_state = "ghost-narsie"
+ invisibility = 0
+ alpha = 0
+ animate(src, alpha = 127, time = 0.5 SECONDS)
+ //to_chat(src, span_cult("Even as a non-corporal being, you can feel Nar-Sie's presence altering you. You are now visible to everyone.") )
+ flick("rune_seer", src)
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/shade.dm b/monkestation/code/modules/bloody_cult/cult/mobs/shade.dm
new file mode 100644
index 000000000000..3f3f0af431c4
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/shade.dm
@@ -0,0 +1,145 @@
+/mob/living/basic/shade
+ name = "Shade"
+ real_name = "Shade"
+ desc = "A bound spirit."
+ gender = PLURAL
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ icon_state = "shade"
+ icon_living = "shade"
+ mob_biotypes = MOB_SPIRIT
+ maxHealth = 40
+ health = 40
+ speak_emote = list("hisses")
+ response_help_continuous = "puts their hand through"
+ response_help_simple = "put your hand through"
+ response_disarm_continuous = "flails at"
+ response_disarm_simple = "flail at"
+ response_harm_continuous = "punches"
+ response_harm_simple = "punch"
+ melee_damage_lower = 5
+ melee_damage_upper = 12
+ attack_verb_continuous = "metaphysically strikes"
+ attack_verb_simple = "metaphysically strike"
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
+ unsuitable_atmos_damage = 0
+ speed = -1
+ faction = list(FACTION_CULT)
+ basic_mob_flags = DEL_ON_DEATH
+ initial_language_holder = /datum/language_holder/construct
+ /// Theme controls color. THEME_CULT is red THEME_WIZARD is purple and THEME_HOLY is blue
+ var/theme = THEME_CULT
+ /// The different flavors of goop shades can drop, depending on theme.
+ var/static/list/remains_by_theme = list(
+ THEME_CULT = list(/obj/item/ectoplasm/construct),
+ THEME_HOLY = list(/obj/item/ectoplasm/angelic),
+ THEME_WIZARD = list(/obj/item/ectoplasm/mystic),
+ )
+ var/soulblade_ritual = FALSE
+ var/blade_harm = TRUE
+ var/mob/master = null
+ var/mob/living/carbon/human/body
+
+/mob/living/basic/shade/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ add_traits(list(TRAIT_HEALS_FROM_CULT_PYLONS, TRAIT_SPACEWALK, TRAIT_VENTCRAWLER_ALWAYS), INNATE_TRAIT)
+ if(isnull(theme))
+ return
+ if(theme != THEME_CULT)
+ icon = 'icons/mob/nonhuman-player/cult.dmi'
+ icon_state = "shade_[theme]"
+ var/list/remains = string_list(remains_by_theme[theme])
+ if(length(remains))
+ AddElement(/datum/element/death_drops, remains)
+
+/mob/living/basic/shade/update_icon_state()
+ . = ..()
+ if(theme == THEME_CULT)
+ return
+
+ if(!isnull(theme))
+ icon = 'icons/mob/nonhuman-player/cult.dmi'
+ icon_state = "shade_[theme]"
+ icon_living = icon_state
+
+/mob/living/basic/shade/death()
+ if(death_message == initial(death_message))
+ death_message = "lets out a contented sigh as [p_their()] form unwinds."
+ if(body)
+ body.forceMove(get_turf(src))
+ ..()
+
+/mob/living/basic/shade/can_suicide()
+ if(istype(loc, /obj/item/soulstone)) //do not suicide inside the soulstone
+ return FALSE
+ return ..()
+
+/mob/living/basic/shade/attackby(obj/item/item, mob/user, params)
+ if(istype(item, /obj/item/soulstone))
+ var/obj/item/soulstone/stone = item
+ stone.capture_shade(src, user)
+ else if (istype(item, /obj/item/weapon/melee/soulblade))
+ var/obj/item/weapon/melee/soulblade/blade = item
+ blade.capture_shade(src, user)
+ else
+ . = ..()
+
+/mob/living/basic/shade/Life()
+ if (istype(loc, /obj/item/weapon/melee/soulblade))
+ var/obj/item/weapon/melee/soulblade/SB = loc
+ if (istype(SB.loc, /obj/structure/cult/altar))
+ if (SB.blood < SB.maxblood)
+ SB.blood = min(SB.maxblood, SB.blood+10)//fastest blood regen when planted on an altar
+ if (SB.get_integrity() < SB.max_integrity)
+ SB.update_integrity(min(SB.max_integrity, SB.get_integrity()+10))//and health regen on top
+ else if (istype(SB.loc, /mob/living))
+ var/mob/living/L = SB.loc
+ if (IS_CULTIST(L) && SB.blood < SB.maxblood)
+ SB.blood = min(SB.maxblood, SB.blood+3)//fast blood regen when held by a cultist (stacks with the one below for an effective +5)
+ if (SB.linked_cultist && (get_dist(get_turf(SB.linked_cultist), get_turf(src)) <= 5))
+ SB.blood = min(SB.maxblood, SB.blood+2)//slow blood regen when near your linked cultist
+ if (SB.passivebloodregen < (SB.blood/3))
+ SB.passivebloodregen++
+ if ((SB.passivebloodregen >= (SB.blood/3)) && (SB.blood < SB.maxblood))
+ SB.passivebloodregen = 0
+ SB.blood++//very slow passive blood regen that goes slower and slower the more blood you currently have.
+ SB.update_icon()
+
+/mob/living/basic/shade/ClickOn(atom/A, params)
+ . = ..()
+ if (istype(loc, /obj/item/weapon/melee/soulblade))
+ var/obj/item/weapon/melee/soulblade/SB = loc
+ SB.dir = get_dir(get_turf(SB), A)
+ var/datum/action/cooldown/spell/pointed/soulblade/blade_spin/BS = locate() in actions
+ if (BS)
+ BS.Activate(src)
+ return
+
+//Giving the spells
+/mob/living/basic/shade/proc/give_blade_powers()
+ if (!istype(loc, /obj/item/weapon/melee/soulblade))
+ return
+ DisplayUI("Soulblade")
+
+ var/obj/item/weapon/melee/soulblade/SB = loc
+ var/datum/control/new_control = new /datum/control/soulblade(src, SB)
+ control_object = new_control
+ new_control.take_control()
+
+ grant_actions_by_list(list(
+ /datum/action/cooldown/spell/pointed/soulblade/blade_kinesis,
+ /datum/action/cooldown/spell/pointed/soulblade/blade_spin,
+ /datum/action/cooldown/spell/pointed/soulblade/blade_perforate,
+ /datum/action/cooldown/spell/pointed/soulblade/blade_mend,
+ /datum/action/cooldown/spell/pointed/soulblade/blade_harm,
+ ))
+
+//Removing the spells, this should always fire when the shade gets removed from the blade, such as when it gets destroyed
+/mob/living/basic/shade/proc/remove_blade_powers()
+ HideUI("Soulblade")
+ for(var/datum/action/cooldown/spell/pointed/soulblade/spell_to_remove in actions)
+ qdel(spell_to_remove)
+
+/mob/living/basic/shade/proc/add_HUD(var/mob/user)
+ DisplayUI("Soulblade")
diff --git a/monkestation/code/modules/bloody_cult/cult/mobs/wraith.dm b/monkestation/code/modules/bloody_cult/cult/mobs/wraith.dm
new file mode 100644
index 000000000000..dd91eb2446df
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/mobs/wraith.dm
@@ -0,0 +1,12 @@
+/mob/living/basic/construct/wraith/perfect
+ icon = 'monkestation/code/modules/bloody_cult/icons/mob.dmi'
+ icon_state = "wraith2"
+ icon_living = "wraith2"
+ icon_dead = "wraith2"
+ new_glow = TRUE
+ see_in_dark = 7
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift,
+ /datum/action/cooldown/spell/pointed/conjure/path_entrance,
+ /datum/action/cooldown/spell/pointed/conjure/path_exit,
+ )
diff --git a/monkestation/code/modules/bloody_cult/cult/new_rune.dm b/monkestation/code/modules/bloody_cult/cult/new_rune.dm
new file mode 100644
index 000000000000..e650835a76c9
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/new_rune.dm
@@ -0,0 +1,628 @@
+var/list/runes = list()
+var/list/rune_appearances_cache = list()
+
+/datum/hover_data/rune_data
+ var/obj/effect/overlay/hover/text_holder
+
+/datum/hover_data/rune_data/New(datum/component/hovering_information, atom/parent)
+ . = ..()
+ text_holder = new(null)
+ text_holder.maptext_width = 128
+ text_holder.maptext_y = 32
+ text_holder.maptext_x = -48
+ text_holder.color = COLOR_BLOOD
+ text_holder.alpha = 180
+
+/datum/hover_data/rune_data/setup_data(obj/effect/new_rune/rune, mob/enterer)
+ . = ..()
+ if(!IS_CULTIST(enterer))
+ return
+
+ var/datum/antagonist/cult/cultist = enterer.mind.has_antag_datum(/datum/antagonist/cult)
+ if(cultist.cultist_role != CULTIST_ROLE_ACOLYTE)
+ return
+
+ var/datum/rune_spell/rune_name = get_rune_spell(null, null, "examine", rune.word1, rune.word2, rune.word3)
+ text_holder.maptext = MAPTEXT_YOU_MURDERER(" [rune_name ? rune_name.name : "Unknown Rune"] ")
+ var/image/new_image = new(rune)
+ new_image.appearance = text_holder.appearance
+ SET_PLANE_EXPLICIT(new_image, new_image.plane, rune)
+ if(!isturf(rune.loc))
+ new_image.loc = rune.loc
+ else
+ new_image.loc = rune
+ add_client_image(new_image, enterer.client)
+
+/obj/effect/new_rune //Abstract, currently only supports blood as a reagent without some serious overriding.
+ name = "rune"
+ desc = "A strange collection of symbols drawn in blood."
+ anchored = 1
+ icon = 'monkestation/code/modules/bloody_cult/icons/deityrunes.dmi'
+ icon_state = ""
+ layer = ABOVE_OPEN_TURF_LAYER
+ plane = GAME_PLANE
+
+ mouse_opacity = 1 //So we can actually click these
+
+ //Whether the rune is pulsating
+ var/animated = 0
+ var/activated = 0 // how many times the rune was activated. goes back to 0 if a word is erased.
+
+ //A rune is made of up to 3 words.
+ var/datum/rune_word/word1
+ var/datum/rune_word/word2
+ var/datum/rune_word/word3
+
+ //An image we'll show to the AI instead of the rune
+ var/image/blood_image
+
+ //When a rune is created, we see if there's any data to copy from the blood used (colour, DNA, viruses) for all 3 words
+ var/datum/reagent/blood/blood1
+ var/datum/reagent/blood/blood2
+ var/datum/reagent/blood/blood3
+ var/list/datum/disease2/disease/virus2 = list()
+
+ var/map_id = HOLOMAP_MARKER_CULT_RUNE
+ var/marker_icon_state = "rune"
+ var/marker_icon = 'monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi'
+
+ //Used when a nullrod is preventing a rune's activation TODO: REWORK NULL ROD INTERACTIONS
+ var/nullblock = 0
+
+ //The spell currently triggered by the rune. Prevents a rune from being used by different cultists at the same time.
+ var/datum/rune_spell/active_spell = null
+
+ //Prevents the same rune from being concealed/revealed several times on a row.
+ var/conceal_cooldown = 0
+
+/obj/effect/new_rune/Initialize(mapload)
+ . = ..()
+ blood_image = image(src)
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
+ COMSIG_ATOM_EXITED = PROC_REF(on_exited)
+ )
+
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+ //AI cannot see runes, instead they see blood splatters.
+ for(var/mob/living/silicon/ai/AI in GLOB.player_list)
+ if(AI.client)
+ AI.client.images += blood_image
+
+ runes += src
+
+ AddComponent(/datum/component/hovering_information, /datum/hover_data/rune_data)
+
+ if(map_id)
+ var/datum/holomap_marker/holomarker = new(src)
+ holomarker.id = map_id
+ holomarker.filter = HOLOMAP_FILTER_CULT
+ holomarker.x = src.x
+ holomarker.y = src.y
+ holomarker.z = src.z
+ holomarker.icon = marker_icon
+ holomarker.icon_state = marker_icon_state
+
+
+/obj/effect/new_rune/Destroy()
+ for(var/mob/living/silicon/ai/AI in GLOB.player_list)
+ if (AI.client)
+ AI.client.images -= blood_image
+ QDEL_NULL(blood_image)
+
+ if (word1)
+ erase_word(word1.english, blood1)
+ word1 = null
+ if (word2)
+ erase_word(word2.english, blood2)
+ word2 = null
+ if (word3)
+ erase_word(word3.english, blood3)
+ word3 = null
+
+ blood1 = null
+ blood2 = null
+ blood3 = null
+
+ if (active_spell)
+ active_spell.abort()
+ active_spell = null
+
+ runes -= src
+
+ ..()
+
+/obj/effect/new_rune/examine(mob/user)
+ . = ..()
+ if(can_read_rune(user) || isobserver(user))
+ var/datum/rune_spell/rune_name = get_rune_spell(null, null, "examine", word1, word2, word3)
+ . += span_info("It reads: [word1 ? "[word1.rune]" : ""][word2 ? " [word2.rune]" : ""][word3 ? " [word3.rune]" : ""]. [rune_name ? " That's a [initial(rune_name.name)] rune." : "It doesn't match any rune spells."]")
+ if(rune_name)
+ . += initial(rune_name.desc)
+ if (istype(active_spell, /datum/rune_spell/portalentrance))
+ var/datum/rune_spell/portalentrance/PE = active_spell
+ if (PE.network)
+ . += span_info("This entrance was attuned to the [PE.network] path.")
+ if (istype(active_spell, /datum/rune_spell/portalexit))
+ var/datum/rune_spell/portalexit/PE = active_spell
+ if (PE.network)
+ . += span_info("This exit was attuned to the [PE.network] path.")
+
+
+ //"Cult" chaplains can read the words, but they have to figure out the spell themselves. Also has a chance to trigger a taunt from Nar-Sie.
+ else if(istype(user, /mob/living/carbon/human) && (user.mind?.assigned_role.title == JOB_CHAPLAIN))
+ to_chat(user, span_info("It reads: [word1.rune] [word2.rune] [word3.rune]. What spell was that already?...") )
+ if (prob(25))
+ spawn(50)
+ to_chat(user, "???-??? murmurs, [pick(\
+ "Your toys won't get you much further", \
+ "Bitter that you weren't chosen?", \
+ "I dig your style, but I crave for your blood.", \
+ "Shall we gamble then? Obviously blood is the only acceptable bargaining chip")].")
+
+
+/obj/effect/new_rune/proc/can_read_rune(var/mob/user) //Overload for specific criteria.
+ return IS_CULTIST(user)
+
+
+/obj/effect/new_rune/get_cult_power()
+ return 1
+
+/obj/effect/new_rune/narsie_act()
+ return
+
+
+
+/obj/effect/new_rune/salt_act()
+ var/turf/T = get_turf(src)
+ anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_break", plane = ABOVE_LIGHTING_PLANE)
+ if (active_spell)
+ active_spell.salt_act(T)
+ qdel(src)
+
+
+/obj/effect/new_rune/proc/write_word(var/word, var/datum/reagent/blood/blood)
+ if (!word)
+ return
+ var/turf/T = get_turf(src)
+ var/write_color = COLOR_BLOOD
+ if (blood)
+ write_color = GLOB.blood_types[blood.data["blood_type"]]?.color
+ anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', flick_anim = "[word]-write", lay = layer+0.1, col = write_color, plane = plane)
+
+/obj/effect/new_rune/proc/erase_word(var/word, var/datum/reagent/blood/blood)
+ if (!word)
+ return
+ var/turf/T = get_turf(src)
+ var/erase_color = COLOR_BLOOD
+ if (blood)
+ erase_color = GLOB.blood_types[blood.data["blood_type"]]?.color
+ anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', flick_anim = "[word]-erase", lay = layer+0.1, col = erase_color, plane = plane)
+
+/obj/effect/new_rune/proc/cast_word(var/word)
+ if (!word)
+ return
+ var/obj/effect/abstract/A = anim(target = get_turf(src), a_icon = 'monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', a_icon_state = "[word]-tear", lay = layer+0.2, plane = plane)
+ animate(A, alpha = 0, time = 5)
+
+/obj/effect/new_rune/ex_act(var/severity)
+ switch (severity)
+ if (1)
+ qdel(src)
+ if (2)
+ if (prob(15))
+ qdel(src)
+
+/obj/effect/new_rune/emp_act()
+ return
+
+/obj/effect/new_rune/blob_act()
+ return
+
+/obj/effect/new_rune/update_icon(var/draw_up_to = 3)
+ . = ..()
+ var/datum/rune_spell/spell = get_rune_spell(null, null, "examine", word1, word2, word3)
+
+ if (active_spell)
+ return
+
+ overlays.len = 0
+
+ if(spell && activated)
+ animated = 1
+ draw_up_to = 3
+ else
+ animated = 0
+
+ var/lookup = ""
+ if (word1)
+ lookup += "[word1.english]-[animated]-[GLOB.blood_types[blood1.data["blood_type"]]?.color]]"
+ if (word2 && draw_up_to >= 2)
+ lookup += "-[word2.english]-[animated]-[GLOB.blood_types[blood2.data["blood_type"]]?.color]]"
+ if (word3 && draw_up_to >= 3)
+ lookup += "-[word3.english]-[animated]-[GLOB.blood_types[blood3.data["blood_type"]]?.color]]"
+
+ var/image/rune_render
+ if (lookup in rune_appearances_cache)
+ rune_render = image(rune_appearances_cache[lookup])
+ else
+ var/image/I1 = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "")
+ if (word1)
+ I1.icon_state = word1.english
+ if (blood1)
+ I1.color = GLOB.blood_types[blood1.data["blood_type"]]?.color || COLOR_BLOOD
+
+ var/image/I2 = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "")
+ if (word2 && draw_up_to >= 2)
+ I2.icon_state = word2.english
+ if (blood2)
+ I2.color = GLOB.blood_types[blood2.data["blood_type"]]?.color || COLOR_BLOOD
+ var/image/I3 = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "")
+ if (word3 && draw_up_to >= 3)
+ I3.icon_state = word3.english
+ if (blood3)
+ I3.color = GLOB.blood_types[blood3.data["blood_type"]]?.color || COLOR_BLOOD
+
+ rune_render = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "")
+ rune_render.overlays += I1
+ rune_render.overlays += I2
+ rune_render.overlays += I3
+
+ if(animated)
+ if (word1)
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "[word1.english]-tear")
+ I.color = "black"
+ I.appearance_flags = RESET_COLOR
+ rune_render.overlays += I
+ if (word2)
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "[word2.english]-tear")
+ I.color = "black"
+ I.appearance_flags = RESET_COLOR
+ rune_render.overlays += I
+ if (word3)
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, "[word3.english]-tear")
+ I.color = "black"
+ I.appearance_flags = RESET_COLOR
+ rune_render.overlays += I
+
+ if(GLOB.blood_types[blood1?.data["blood_type"]]?.glows)
+ rune_render.overlays += emissive_appearance('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', word1.english, src)
+ if(GLOB.blood_types[blood2?.data["blood_type"]]?.glows)
+ rune_render.overlays += emissive_appearance('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', word2.english, src)
+ if(GLOB.blood_types[blood3?.data["blood_type"]]?.glows)
+ rune_render.overlays += emissive_appearance('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', word3.english, src)
+
+ rune_appearances_cache[lookup] = rune_render
+ overlays += rune_render
+
+ if(animated)
+ idle_pulse()
+ else
+ animate(src)
+
+/obj/effect/proc/idle_pulse()
+ //This masterpiece of a color matrix stack produces a nice animation no matter which color the rune is.
+ animate(src, color = list(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 10, loop = -1)//1
+ animate(color = list(1.125, 0.06, 0, 0, 0, 1.125, 0.06, 0, 0.06, 0, 1.125, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 2)//2
+ animate(color = list(1.25, 0.12, 0, 0, 0, 1.25, 0.12, 0, 0.12, 0, 1.25, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 2)//3
+ animate(color = list(1.375, 0.19, 0, 0, 0, 1.375, 0.19, 0, 0.19, 0, 1.375, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.5)//4
+ animate(color = list(1.5, 0.27, 0, 0, 0, 1.5, 0.27, 0, 0.27, 0, 1.5, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1.5)//5
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//6
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//7
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//8
+ animate(color = list(2, 0.67, 0.27, 0, 0.27, 2, 0.67, 0, 0.67, 0.27, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 5)//9
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//8
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//7
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//6
+ animate(color = list(1.5, 0.27, 0, 0, 0, 1.5, 0.27, 0, 0.27, 0, 1.5, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//5
+ animate(color = list(1.375, 0.19, 0, 0, 0, 1.375, 0.19, 0, 0.19, 0, 1.375, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//4
+ animate(color = list(1.25, 0.12, 0, 0, 0, 1.25, 0.12, 0, 0.12, 0, 1.25, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//3
+ animate(color = list(1.125, 0.06, 0, 0, 0, 1.125, 0.06, 0, 0.06, 0, 1.125, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)//2
+
+
+/obj/effect/new_rune/proc/one_pulse()
+ animate(src, color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(2, 0.67, 0.27, 0, 0.27, 2, 0.67, 0, 0.67, 0.27, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 2)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.75)
+ animate(color = list(1.5, 0.27, 0, 0, 0, 1.5, 0.27, 0, 0.27, 0, 1.5, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.75)
+ animate(color = list(1.375, 0.19, 0, 0, 0, 1.375, 0.19, 0, 0.19, 0, 1.375, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.5)
+ animate(color = list(1.25, 0.12, 0, 0, 0, 1.25, 0.12, 0, 0.12, 0, 1.25, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.5)
+ animate(color = list(1.125, 0.06, 0, 0, 0, 1.125, 0.06, 0, 0.06, 0, 1.125, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 0.25)
+ animate(color = list(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+
+ spawn (10)
+ if(animated)
+ idle_pulse()
+ else
+ animate(src)
+
+
+/obj/effect/new_rune/proc/on_entered(turf/entered_turf, atom/movable/mover)
+ if (ismob(mover))
+ var/mob/user = mover
+ var/datum/rune_spell/rune_effect = get_rune_spell(user, src, "walk" , word1, word2, word3)
+ if (rune_effect)
+ rune_effect.Added(mover)
+
+/obj/effect/new_rune/proc/on_exited(turf/entered_turf, atom/movable/mover)
+ if (active_spell && ismob(mover))
+ active_spell.Removed(mover)
+
+/obj/effect/new_rune/attack_animal(var/mob/living/simple_animal/user)
+ if(istype(user, /mob/living/basic/construct))
+ trigger(user)
+ if(istype(user, /mob/living/basic/shade))
+ trigger(user)
+
+/obj/effect/new_rune/attack_paw(var/mob/living/user)
+ if(ismonkey(user))
+ //assume_contact_diseases(user)
+ trigger(user)
+
+/obj/effect/new_rune/attack_alien(var/mob/living/user)
+ if(isalien(user))
+ trigger(user)
+
+/obj/effect/new_rune/attack_hand(var/mob/living/user)
+ //assume_contact_diseases(user)
+ trigger(user)
+
+/obj/effect/new_rune/attack_robot(var/mob/living/user) //Allows for robots to remotely trigger runes, since attack_robot has infinite range.
+ trigger(user)
+
+/*
+/obj/effect/new_rune/proc/assume_contact_diseases(var/mob/living/user)
+ var/block = 0
+ var/bleeding = 0
+ block = user.check_contact_sterility(HANDS)
+ bleeding = user.check_bodypart_bleeding(HANDS)
+ user.assume_contact_diseases(virus2, src, block, bleeding)
+*/
+
+/obj/effect/new_rune/attackby(obj/I, mob/user)
+ ..()
+ if(isholyweapon(I))
+ to_chat(user, span_notice("You disrupt the vile magic with the deadening field of \the [I]!") )
+ qdel(src)
+ return
+ if(istype(I, /obj/item/weapon/tome) || istype(I, /obj/item/weapon/melee/cultblade) || istype(I, /obj/item/weapon/melee/soulblade) || istype(I, /obj/item/weapon/melee/blood_dagger))
+ trigger(user)
+ if(istype(I, /obj/item/weapon/talisman))
+ var/obj/item/weapon/talisman/T = I
+ T.imbue(user, src)
+ return
+
+/obj/effect/new_rune/proc/trigger(var/mob/living/user, var/talisman_trigger = 0)
+
+ if(!IS_CULTIST(user))
+ to_chat(user, span_danger("You can't mouth the arcane scratchings without fumbling over them.") )
+ return
+
+ if(iscarbon(user))
+ var/mob/living/carbon/C = user
+ if (C.occult_muted())
+ to_chat(user, span_danger("You find yourself unable to focus your mind on the arcane words of the rune.") )
+ return
+
+ if(!user.checkTattoo(TATTOO_SILENT))
+ if(user.is_muzzled())
+ to_chat(user, span_danger("You are unable to speak the words of the rune because of the muzzle.") )
+ return
+
+ if(HAS_TRAIT(user, TRAIT_MUTE))
+ to_chat(user, span_danger("You don't have the ability to perform rituals without voicing the incantations, there has to be some way...") )
+ return
+
+ if(!word1 || !word2 || !word3 || prob(user.get_organ_loss(ORGAN_SLOT_BRAIN)))
+ return fizzle(user)
+
+ add_hiddenprint(user)
+
+ if(active_spell)//rune is already channeling a spell? let's see if we can interact with it somehow.
+ if(talisman_trigger)
+ var/datum/rune_spell/active_spell_typecast = active_spell
+ if(!istype(active_spell_typecast))
+ return
+ active_spell_typecast.midcast_talisman(user)
+ else
+ active_spell.midcast(user)
+ return
+
+ reveal()//concealed rune get automatically revealed upon use (either through using Seer or an attuned talisman). Placed after midcast: exception for Path talismans.
+
+ active_spell = get_rune_spell(user, src, "ritual", word1, word2, word3)
+
+ if (!active_spell)
+ return fizzle(user)
+ else
+ if (active_spell.destroying_self)
+ active_spell = null
+
+/obj/effect/new_rune/proc/fizzle(var/mob/living/user)
+ var/silent = user.checkTattoo(TATTOO_SILENT)
+ if(!silent)
+ user.say(pick("B'ADMINES SP'WNIN SH'T", "IC'IN O'OC", "RO'SHA'M I'SA GRI'FF'N ME'AI", "TOX'IN'S O'NM FI'RAH", "IA BL'AME TOX'IN'S", "FIR'A NON'AN RE'SONA", "A'OI I'RS ROUA'GE", "LE'OAN JU'STA SP'A'C Z'EE SH'EF", "IA PT'WOBEA'RD, IA A'DMI'NEH'LP", "I'F ON'Y I 'AD 'TAB' E"))
+ one_pulse()
+ visible_message(span_warning("The markings pulse with a small burst of light, then fall dark.") , \
+ span_warning("The markings pulse with a small burst of light, then fall dark.") , \
+ span_warning("You hear a faint fizzle.") )
+
+/obj/effect/new_rune/proc/conceal()
+ if(active_spell && !active_spell.can_conceal)
+ active_spell.abort(RITUALABORT_CONCEAL)
+ alpha = 0
+ if (word1)
+ erase_word(word1.english, blood1)
+ if (word2)
+ erase_word(word2.english, blood2)
+ if (word3)
+ erase_word(word3.english, blood3)
+ spawn(6)
+ invisibility = INVISIBILITY_OBSERVER
+ alpha = 127
+
+/obj/effect/new_rune/proc/reveal() //Returns 1 if rune was revealed from a invisible state.
+ if(invisibility != 0)
+ invisibility = 0
+ if (!(active_spell?.custom_rune))
+ overlays.len = 0
+ if (word1)
+ write_word(word1.english, blood1)
+ if (word2)
+ write_word(word2.english, blood2)
+ if (word3)
+ write_word(word3.english, blood3)
+ spawn(8)
+ alpha = 255
+ update_icon()
+ else
+ alpha = 255
+ conceal_cooldown = 1
+ spawn(100)
+ if (src && loc)
+ conceal_cooldown = 0
+ return 1
+ return 0
+
+/*
+/obj/effect/new_rune/proc/manage_diseases(var/datum/reagent/blood/source)
+ virus2 = list()
+
+ if (blood1)
+ blood1.data["virus2"] = virus_copylist(source.data["virus2"])
+ var/list/datum/disease2/disease/blood1_diseases = blood1.data["virus2"]
+ for (var/ID in blood1_diseases)
+ var/datum/disease2/disease/V = blood1_diseases[ID]
+ if(istype(V))
+ virus2["[V.uniqueID]-[V.subID]"] = V.getcopy()
+ if (blood2)
+ blood2.data["virus2"] = virus_copylist(source.data["virus2"])
+ var/list/datum/disease2/disease/blood2_diseases = blood2.data["virus2"]
+ for (var/ID in blood2_diseases)
+ if (ID in virus2)
+ continue
+ var/datum/disease2/disease/V = blood2_diseases[ID]
+ if(istype(V))
+ virus2["[V.uniqueID]-[V.subID]"] = V.getcopy()
+ if (blood3)
+ blood3.data["virus2"] = virus_copylist(source.data["virus2"])
+ var/list/datum/disease2/disease/blood3_diseases = blood3.data["virus2"]
+ for (var/ID in blood3_diseases)
+ if (ID in virus2)
+ continue
+ var/datum/disease2/disease/V = blood3_diseases[ID]
+ if(istype(V))
+ virus2["[V.uniqueID]-[V.subID]"] = V.getcopy()
+*/
+
+/*
+/obj/effect/new_rune/clean_act(var/cleanliness)
+ qdel(src)
+*/
+
+/proc/write_rune_word(var/turf/T, var/datum/rune_word/word = null, var/datum/reagent/blood/source, var/mob/caster = null)
+ if (!word)
+ return RUNE_WRITE_CANNOT
+
+ if (!source)
+ source = new
+
+ source.color = GLOB.blood_types[source.data["blood_type"]]?.color || COLOR_BLOOD
+ //Add word to a rune if there is one, otherwise create one. However, there can be no more than 3 words.
+ //Returns 0 if failure, 1 if finished a rune, 2 if success but rune still has room for words.
+
+ var/newrune = FALSE
+ var/obj/effect/new_rune/rune = locate() in T
+ if(!rune)
+ rune = new /obj/effect/new_rune(T)
+ newrune = TRUE
+
+ if (rune.word1 && rune.word2 && rune.word3)
+ return RUNE_WRITE_CANNOT
+
+ if (caster)
+ if (newrune)
+ log_admin("BLOODCULT: [key_name(caster)] has created a new rune at [T.loc] (@[T.x], [T.y], [T.z]).")
+ message_admins("BLOODCULT: [key_name(caster)] has created a new rune at [ADMIN_JMP(T)].")
+ rune.add_hiddenprint(caster)
+
+ rune.write_word(word.english, source)
+
+ if (!rune.word1)
+ rune.word1 = word
+ rune.blood1 = new()
+ rune.blood1.data = source.data
+ spawn (8)
+ rune.update_icon(1)
+
+ else if (!rune.word2)
+ rune.word2 = word
+ rune.blood2 = new()
+ rune.blood2.data = source.data
+ spawn (8)
+ rune.update_icon(2)
+
+ else if (!rune.word3)
+ rune.word3 = word
+ rune.blood3 = new()
+ rune.blood3.data = source.data
+ spawn (8)
+ rune.update_icon(3)
+
+ //rune.manage_diseases(source)
+
+ if (rune.blood3)
+ return RUNE_WRITE_COMPLETE
+ return RUNE_WRITE_CONTINUE
+
+/proc/erase_rune_word(var/turf/T)
+ var/obj/effect/new_rune/rune = locate() in T
+ if(!rune)
+ return null
+
+ var/word_erased
+
+ if(rune.word3)
+ rune.erase_word(rune.word3.english, rune.blood3)
+ word_erased = rune.word3.rune
+ rune.word3 = null
+ rune.blood3 = null
+ if (rune.active_spell)
+ rune.active_spell.abort(RITUALABORT_ERASED)
+ rune.active_spell = null
+ rune.overlays.len = 0
+ rune.update_icon()
+ else if(rune.word2)
+ rune.erase_word(rune.word2.english, rune.blood2)
+ word_erased = rune.word2.rune
+ rune.word2 = null
+ rune.blood2 = null
+ rune.update_icon()
+ else if(rune.word1)
+ rune.erase_word(rune.word1.english, rune.blood1)
+ word_erased = rune.word1.rune
+ rune.word1 = null
+ rune.blood1 = null
+ qdel(rune)
+ else
+ message_admins("Error! Trying to erase a word from a rune with no words!")
+ qdel(rune)
+ return null
+ rune.activated = 0
+ return word_erased
+
+
+/proc/write_full_rune(var/turf/T, var/spell_type, var/datum/reagent/blood/source, var/mob/caster = null)
+ if (!spell_type)
+ return
+
+ var/datum/rune_spell/spell_instance = spell_type
+ var/datum/rune_word/word1_instance = initial(spell_instance.word1)
+ var/datum/rune_word/word2_instance = initial(spell_instance.word2)
+ var/datum/rune_word/word3_instance = initial(spell_instance.word3)
+ write_rune_word(T, GLOB.rune_words[initial(word1_instance.english)], source, caster)
+ write_rune_word(T, GLOB.rune_words[initial(word2_instance.english)], source, caster)
+ write_rune_word(T, GLOB.rune_words[initial(word3_instance.english)], source, caster)
diff --git a/monkestation/code/modules/bloody_cult/cult/object_control.dm b/monkestation/code/modules/bloody_cult/cult/object_control.dm
new file mode 100644
index 000000000000..60502ff8822e
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/object_control.dm
@@ -0,0 +1,120 @@
+/datum/control
+ var/name = "controlling something else"
+ var/mob/controller
+ var/atom/movable/controlled
+ var/control_flags = 0
+ var/is_controlled = FALSE //Whether we're in strict control
+
+/datum/control/New(var/mob/new_controller, var/atom/new_controlled)
+ ..()
+ controller = new_controller
+ RegisterSignal(new_controller, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(user_damaged))
+ controlled = new_controlled
+
+/datum/control/Destroy()
+ break_control()
+ if(controller)
+ controller.control_object = null
+ controller = null
+ controlled = null
+ ..()
+
+/datum/control/proc/user_damaged(datum/soruce, amount, kind)
+ if(amount > 0 && control_flags & REVERT_ON_CONTROLLER_DAMAGED)
+ break_control()
+
+/datum/control/proc/break_control()
+ if(controller && controller.client)
+ controller.client.eye = controller.client.mob
+ controller.client.perspective = MOB_PERSPECTIVE
+ is_controlled = FALSE
+ if(control_flags & LOCK_MOVEMENT_OF_CONTROLLER)
+ REMOVE_TRAIT(controller, TRAIT_IMMOBILIZED, REF(src))
+
+/datum/control/proc/take_control()
+ if(!is_valid(0))
+ return
+ if(control_flags & LOCK_EYE_TO_CONTROLLED)
+ controller.client.perspective = EYE_PERSPECTIVE
+ controller.client.eye = controlled
+ is_controlled = TRUE
+ if(control_flags & LOCK_MOVEMENT_OF_CONTROLLER)
+ ADD_TRAIT(controller, TRAIT_IMMOBILIZED, REF(src))
+
+/datum/control/proc/is_valid(var/check_control = FALSE)
+ if(!controller || !controller.client || !controlled || QDELETED(controller) || QDELETED(controlled))
+ qdel(src)
+ return 0
+ if(check_control && !(control_flags & REQUIRES_CONTROL && is_controlled))
+ return 0
+ return 1
+
+/datum/control/proc/Move_object(var/direction)
+ if(!is_valid())
+ return
+ if(controlled)
+ if(control_flags & LOCK_MOVEMENT_OF_CONTROLLER)
+ ADD_TRAIT(controller, TRAIT_IMMOBILIZED, REF(src))
+ if(controlled.density)
+ step(controlled, direction)
+ if(!controlled)
+ return
+ controlled.dir = direction
+ else
+ controlled.forceMove(get_step(controlled, direction))
+
+/datum/control/proc/Orient_object(var/direction)
+ if(!is_valid())
+ return
+ if(control_flags & LOCK_MOVEMENT_OF_CONTROLLER)
+ ADD_TRAIT(controller, TRAIT_IMMOBILIZED, REF(src))
+ controlled.dir = direction
+
+/////////////////////////////LOCK MOVE//////////////////////////////
+
+/datum/control/lock_move
+ control_flags = LOCK_MOVEMENT_OF_CONTROLLER | LOCK_EYE_TO_CONTROLLED
+
+///////////////////////////////SOULBLADE CONTROLLER///////////////////////////////
+
+/datum/control/soulblade
+ var/obj/item/weapon/melee/soulblade/blade = null
+ var/move_delay = 0
+
+/datum/control/soulblade/New(var/mob/new_controller, var/atom/new_controlled)
+ ..()
+ blade = new_controlled
+
+/datum/control/soulblade/is_valid(var/direction)
+ if (blade.blood <= 0 || move_delay || blade.throwing)
+ return 0
+ if (!isturf(blade.loc))
+ if (istype(blade.loc, /obj/structure/cult/altar))
+ var/obj/structure/cult/altar/A = blade.loc
+ blade.forceMove(A.loc)
+ A.blade = null
+ playsound(A.loc, 'sound/weapons/blade1.ogg', 50, 1)
+ if (A.buckled_mobs)
+ var/mob/M = A.buckled_mobs[1]
+ A.unbuckle_mob(M)
+ A.update_icon()
+ else
+ return 0
+ return ..()
+
+/datum/control/soulblade/Move_object(var/direction)
+ if(!controlled)
+ return
+ var/atom/start = blade.loc
+ if(!is_valid())
+ return
+ step(controlled, direction)
+ controlled.dir = direction
+ if (blade.loc != start)
+ if (!blade.linked_cultist || (get_dist(get_turf(blade.linked_cultist), get_turf(controller)) > 5))
+ blade.blood = max(blade.blood-1, 0)
+ move_delay = 1
+ spawn(blade.movespeed)
+ move_delay = 0
+
+ controller.DisplayUI("Soulblade")
diff --git a/monkestation/code/modules/bloody_cult/cult/projectiles/blood_nail.dm b/monkestation/code/modules/bloody_cult/cult/projectiles/blood_nail.dm
new file mode 100644
index 000000000000..b924bb230aa4
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/projectiles/blood_nail.dm
@@ -0,0 +1,69 @@
+//////////////////////////////
+// //
+// BLOOD NAIL ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Used when a cultist throws a blood dagger
+//////////////////////////////
+
+/obj/projectile/blooddagger
+ name = "blood dagger"
+ icon = 'monkestation/code/modules/bloody_cult/icons/projectiles_experimental.dmi'
+ icon_state = "blood_dagger"
+ damage = 5
+ speed = 0.66
+ extra_rotation = 45
+ var/absorbed = 0
+ var/stacks = 0
+
+/obj/projectile/blooddagger/Destroy()
+ var/turf/T = get_turf(src)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/forge_over.ogg', 100, 1)
+ if (!absorbed && !locate(/obj/effect/decal/cleanable/blood/splatter) in T)
+ var/obj/effect/decal/cleanable/blood/splatter/S = new (T)//splash
+ if (color)
+ S.color = color
+ S.update_icon()
+ ..()
+
+
+/obj/projectile/blooddagger/on_hit(atom/target, blocked = 0, pierce_hit)
+ . = ..()
+ if (isliving(target))
+ var/mob/living/M = target
+ if (IS_CULTIST(M))
+ var/mob/living/carbon/human/H = M
+ if (!HAS_TRAIT(H, TRAIT_NOBLOOD))
+ H.blood_volume += 5 + stacks * 5
+ to_chat(H, span_notice("[firer ? "\The [firer]'s" : "The"] [src] enters your body painlessly, irrigating your vessels with some fresh blood.") )
+ else
+ to_chat(H, span_notice("[firer ? "\The [firer]'s" : "The"] [src] enters your body, but you have no vessels to irrigate.") )
+ absorbed = 1
+ playsound(H, 'monkestation/code/modules/bloody_cult/sound/bloodyslice.ogg', 30, 1)
+ return BULLET_ACT_BLOCK
+ if (M.stat == DEAD)
+ return BULLET_ACT_BLOCK
+
+ if (!IS_CULTIST(M))
+ density = FALSE
+ invisibility = 101
+ var/obj/effect/rooting_trap/bloodnail/nail = new (target.loc)
+ nail.transform = transform
+ if (color)
+ nail.color = color
+ else
+ nail.color = COLOR_BLOOD
+ if(isliving(target))
+ nail.stick_to(target)
+ var/mob/living/L = target
+ L.take_overall_damage(damage, 0)
+ to_chat(L, span_warning("\The [src] stabs your body, sticking you in place.") )
+ to_chat(L, span_danger("Resist or click the nail to dislodge it.") )
+ else if(loc)
+ var/turf/T = get_turf(src)
+ nail.stick_to(T, get_dir(src, target))
+ qdel(src)
+ return
+ qdel(src)
+
+
+/obj/projectile/blooddagger/narsie_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/projectiles/bloodslash.dm b/monkestation/code/modules/bloody_cult/cult/projectiles/bloodslash.dm
new file mode 100644
index 000000000000..6c23481e67cb
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/projectiles/bloodslash.dm
@@ -0,0 +1,43 @@
+//////////////////////////////
+// //
+// BLOOD SLASH ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Used when a cultist swings a soul blade that has at least 5 blood in it.
+//////////////////////////////
+
+/obj/projectile/bloodslash
+ name = "blood slash"
+ icon = 'monkestation/code/modules/bloody_cult/icons/projectiles_experimental.dmi'
+ icon_state = "bloodslash"
+ damage = 15
+ speed = 0.4
+ extra_rotation = 45
+ damage_type = BURN
+
+/obj/projectile/bloodslash/Destroy()
+ var/turf/T = get_turf(src)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/forge_over.ogg', 100, 1)
+ if (!locate(/obj/effect/decal/cleanable/blood/splatter) in T)
+ var/obj/effect/decal/cleanable/blood/splatter/S = new (T)//splash
+ S.count = 1
+ ..()
+
+/obj/projectile/bloodslash/Bump(atom/A)
+ if (isliving(A))
+ forceMove(A.loc)
+ var/mob/living/M = A
+ if (!IS_CULTIST(M))
+ ..()
+ qdel(src)
+
+/obj/projectile/bloodslash/on_hit(atom/target, blocked = 0, pierce_hit)
+ . = ..()
+ if (isliving(target))
+ var/mob/living/M = target
+ if (IS_CULTIST(M))
+ return BULLET_ACT_BLOCK
+ if (M.stat == DEAD)
+ return BULLET_ACT_BLOCK
+ to_chat(M, span_warning("You feel a searing heat inside of you!") )
+
+/obj/projectile/bloodslash/narsie_act()
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/projectiles/soulbullet.dm b/monkestation/code/modules/bloody_cult/cult/projectiles/soulbullet.dm
new file mode 100644
index 000000000000..d66b6455bcb0
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/projectiles/soulbullet.dm
@@ -0,0 +1,145 @@
+
+//////////////////////////////
+// //
+// PERFORATING BLADE ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Used when a filled soul blade performs a perforation
+//////////////////////////////
+
+/obj/projectile/soulbullet
+ name = "soul blade"
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ icon_state = "soulbullet"
+ pixel_x = -16 * 1
+ pixel_y = -10 * 1
+ damage = 30//Only affects obj/turf. Mobs take a regular hit from the sword.
+ mouse_opacity = 1
+ extra_rotation = 45
+ var/turf/secondary_target = null
+ var/obj/item/weapon/melee/soulblade/blade = null
+ var/mob/living/basic/shade/shade = null
+ var/redirected = 0
+ var/leave_shadows = -1
+ var/matrix/shadow_matrix = null
+
+/obj/projectile/soulbullet/Destroy()
+ var/turf/T = get_turf(src)
+ if (T)
+ if (blade)
+ blade.forceMove(T)
+ blade = null
+ shade = null
+ ..()
+
+/obj/projectile/soulbullet/fire(angle, atom/direct_target)
+ var/atom/target = get_turf(direct_target)
+ var/target_angle = get_angle(starting, direct_target)
+
+ if (!secondary_target)
+ secondary_target = target
+ if (!shade)
+ icon_state = "soulbullet-empty"
+ if (target != secondary_target)
+ target_angle = round(get_angle(target, secondary_target))
+ blade.dir = get_dir(target, secondary_target)
+ else
+ target_angle = round(get_angle(starting, target))
+ blade.dir = get_dir(starting, target)
+ shadow_matrix = turn(matrix(), target_angle+45)
+ transform = shadow_matrix
+ if (shade)
+ icon_state = "soulbullet_spin"
+ plane = HUD_PLANE
+ else
+ icon_state = "soulbullet-empty_spin"
+ spawn(5)
+ leave_shadows = 0
+ if (shade)
+ icon_state = "soulbullet"
+ else
+ icon_state = "soulbullet-empty"
+ . = ..()
+
+/*
+/obj/projectile/soulbullet/bresenham_step(var/distA, var/distB, var/dA, var/dB)
+ if (shade && leave_shadows >= 0)
+ leave_shadows++
+ if ((leave_shadows%3) = = 0)
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi', flick_anim = "soulblade-shadow", lay = NARSIE_GLOW, offX = pixel_x, offY = pixel_y, plane = ABOVE_LIGHTING_PLANE, trans = shadow_matrix)
+ if(..())
+ return 2
+ else
+ return 0
+*/
+
+/obj/projectile/soulbullet/Bump(atom/A)
+ . = ..()
+ if (shade)
+ if (ismob(A))
+ var/mob/M = A
+ if (!IS_CULTIST(M))
+ A.attackby(blade, shade)
+ else if (!M.get_active_held_item())//cultists and the blade's master can catch the blade on the fly
+ blade.forceMove(loc)
+ blade.attack_hand(M)
+ to_chat(M, span_warning("Your hand moves by itself and catches \the [blade] out of the air.") )
+ blade = null
+ qdel(src)
+ else if (!M.get_inactive_held_item())
+ blade.forceMove(loc)
+ M.swap_hand() // guarrantees
+ blade.attack_hand(M)
+ to_chat(M, span_warning("Your hand moves by itself and catches \the [blade] out of the air.") )
+ M.swap_hand()
+ blade = null
+ qdel(src)
+ else
+ A.attackby(blade, shade)
+ else
+ if (ismob(A))
+ var/mob/M = A
+ if (!IS_CULTIST(M))
+ A.hitby(blade)
+ else if (!M.get_active_held_item())//cultists can catch the blade on the fly
+ blade.forceMove(loc)
+ blade.attack_hand(M)
+ to_chat(M, span_warning("Your hand moves by itself and catches \the [blade] out of the air.") )
+ blade = null
+ qdel(src)
+ else if (!M.get_inactive_held_item())
+ blade.forceMove(loc)
+ M.swap_hand()
+ blade.attack_hand(M)
+ to_chat(M, span_warning("Your hand moves by itself and catches \the [blade] out of the air.") )
+ M.swap_hand()
+ blade = null
+ qdel(src)
+ else
+ A.hitby(blade)
+ if(isliving(A))
+ forceMove(get_step(loc, dir))
+ if (!redirected)
+ redirect()
+ else
+ ..()
+
+
+/obj/projectile/soulbullet/proc/redirect()
+ redirected = 1
+ speed = 0.66
+ set_angle(rand(0, 360))
+
+/obj/projectile/soulbullet/narsie_act()
+ return
+
+/obj/projectile/soulbullet/attackby(var/obj/item/I, var/mob/user)
+ if (blade)
+ return blade.attackby(I, user)
+
+/obj/projectile/soulbullet/hitby(atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ if (blade)
+ return blade.hitby(hitting_atom)
+
+/obj/projectile/soulbullet/bullet_act(var/obj/projectile/P)
+ . = ..()
+ if (blade)
+ return blade.bullet_act(P)
diff --git a/monkestation/code/modules/bloody_cult/cult/rituals.dm b/monkestation/code/modules/bloody_cult/cult/rituals.dm
new file mode 100644
index 000000000000..193eb637e13c
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rituals.dm
@@ -0,0 +1,656 @@
+
+/*
+"Rituals" in this context are basically objectives that cultists can accomplish to get rewarded with devotion.
+Devotion both serves to unlock some cult powers, quicken the arrival of the Eclipse, and overall bragging rights on the scoreboard.
+In essence, these provide cultists with things to work toward to disrupt the crew without necessarily ending the round.
+*/
+
+GLOBAL_LIST_INIT(bloodcult_faction_rituals, list(
+ /datum/bloodcult_ritual/reach_cap,
+ /datum/bloodcult_ritual/convert_station,
+ /datum/bloodcult_ritual/produce_constructs,
+ /datum/bloodcult_ritual/blind_cameras_multi,
+ /datum/bloodcult_ritual/bloodspill,
+ /datum/bloodcult_ritual/sacrifice_captain,
+ //datum/bloodcult_ritual/cursed_infection,
+ ))
+
+GLOBAL_LIST_INIT(bloodcult_personal_rituals, list(
+ /datum/bloodcult_ritual/blind_cameras,
+ /datum/bloodcult_ritual/confuse_crew,
+ /datum/bloodcult_ritual/harm_crew,
+ /datum/bloodcult_ritual/sacrifice_mouse,
+ /datum/bloodcult_ritual/sacrifice_monkey,
+ /datum/bloodcult_ritual/altar/simple,
+ /datum/bloodcult_ritual/altar/elaborate,
+ /datum/bloodcult_ritual/altar/excentric,
+ /datum/bloodcult_ritual/altar/unholy,
+ /datum/bloodcult_ritual/suicide_tome,
+ /datum/bloodcult_ritual/suicide_soulblade,
+ ))
+
+/datum/bloodcult_ritual
+ var/name = "Ritual"
+ var/desc = "Lorem Ipsum (you shouldn't be reading this!)"
+
+ var/only_once = FALSE //If TRUE the ritual won't return to the pool of possible rituals after completion
+ var/ritual_type = "error"//ritual category. the game tries to assign rituals of diverse categories
+ var/difficulty = "easy"//"medium", "hard"
+ var/personal = FALSE//FALSE = Faction ritual. TRUE = Personal ritual
+ var/datum/antagonist/cult/owner = null//Only really matters if ritual is personal but you can also assign it on key_found on faction ritual to give them extra devotion
+ var/reward_achiever = 0//Reward to the cultist who completed the achievement
+ var/reward_faction = 0//Reward to every member of the faction
+
+ var/list/keys = list()
+
+//Needs to be TRUE for the Ritual to be assigned
+/datum/bloodcult_ritual/proc/pre_conditions(var/datum/antagonist/cult/potential)
+ if (potential)
+ owner = potential
+ return TRUE
+
+//Perform custom ritual setup here
+/datum/bloodcult_ritual/proc/init_ritual()
+
+//Called when a cultist is about to hover the corresponding ritual UI button
+/datum/bloodcult_ritual/proc/update_desc()
+ return
+
+//Perform custom ritual validation checks here
+/datum/bloodcult_ritual/proc/key_found(var/extra)
+ return TRUE
+
+/datum/bloodcult_ritual/proc/complete()
+ owner?.gain_devotion(reward_achiever, DEVOTION_TIER_4)//no key, duh
+ if (reward_faction)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ for(var/datum/antagonist/cult/cult_datum in cult.members)
+ cult_datum.gain_devotion(reward_faction, DEVOTION_TIER_4)//yes this means a larger cult gets more total devotion.
+
+ if (personal)
+ message_admins("BLOODCULT: [key_name(owner.owner.current)] has completed the [name] ritual.")
+ log_admin("BLOODCULT: [key_name(owner.owner.current)] has completed the [name] ritual.")
+ else
+ message_admins("BLOODCULT: The [name] ritual has been completed.")
+ log_admin("BLOODCULT: The [name] ritual has been completed.")
+
+////////////////////////////////////////////////////////////////////
+// //
+// FACTION RITUALS //
+// //
+////////////////////////////////////////////////////////////////////
+
+////////////////////////CONVERSION/////////////////////////////
+
+/datum/bloodcult_ritual/reach_cap
+ name = "Reach the cap"
+ desc = "the cult must grow... until it cannot..."
+
+ only_once = TRUE
+ ritual_type = "conversion"
+ difficulty = "medium"
+ reward_faction = 400
+
+ keys = list(
+ "conversion",
+ "converted_prisoner",
+ "soulstone",
+ "soulstone_prisoner",
+ )
+
+/datum/bloodcult_ritual/reach_cap/pre_conditions(var/datum/antagonist/cult/potential)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult.CanConvert())
+ return TRUE
+ return FALSE
+
+/datum/bloodcult_ritual/reach_cap/key_found(var/extra)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!cult.CanConvert())
+ return TRUE
+ return FALSE
+
+////////////////////////CONSTRUCT/////////////////////////////
+
+/datum/bloodcult_ritual/convert_station
+ name = "Cultify the Station"
+ desc = "convert the floors... convert the walls..."
+
+ ritual_type = "constructs"
+ difficulty = "easy"
+ reward_faction = 200
+
+ keys = list(
+ "convert_floor",
+ "convert_wall",
+ )
+
+ var/target = 30
+ var/list/turfs = list()
+
+/datum/bloodcult_ritual/convert_station/init_ritual()
+ turfs = list()
+
+/datum/bloodcult_ritual/convert_station/update_desc()
+ desc = "convert the floors... convert the walls... need [target - turfs.len] more..."
+
+/datum/bloodcult_ritual/convert_station/key_found(var/turf/T)
+ if (T in turfs)
+ return FALSE
+ turfs += T
+ if(turfs.len >= target)
+ return TRUE
+ return FALSE
+
+
+///////////////////////////////////////////////////////////////
+
+/datum/bloodcult_ritual/produce_constructs
+ name = "One of each"
+ desc = "artificer... wraith... juggernaut..."
+
+ ritual_type = "constructs"
+ difficulty = "medium"
+ reward_faction = 300
+
+ keys = list("build_construct")
+
+ var/list/types_to_build = list("Artificer", "Wraith", "Juggernaut")
+
+/datum/bloodcult_ritual/produce_constructs/init_ritual()
+ types_to_build = list("Artificer", "Wraith", "Juggernaut")
+
+/datum/bloodcult_ritual/produce_constructs/key_found(var/extra)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ for (var/datum/mind/mind in cult.members)
+ var/mob/M = mind.current
+ if (istype(M, /mob/living/basic/construct))
+ var/mob/living/basic/construct/C = M
+ types_to_build -= C.construct_type
+ if (types_to_build.len <= 0)
+ return TRUE
+ return FALSE
+
+////////////////////////CONFUSION/////////////////////////////
+
+/datum/bloodcult_ritual/blind_cameras_multi
+ name = "Blind Many Cameras"
+ desc = "confusion runes and talismans... darken their lenses..."
+
+ ritual_type = "confusion"
+ difficulty = "easy"
+ reward_faction = 200
+
+ keys = list("confusion_camera")
+
+ var/target_cameras = 20
+
+/datum/bloodcult_ritual/blind_cameras_multi/init_ritual()
+ target_cameras = 20
+
+/datum/bloodcult_ritual/blind_cameras_multi/update_desc()
+ desc = "confusion runes and talismans... darken their lenses... [target_cameras] to go..."
+
+/datum/bloodcult_ritual/blind_cameras_multi/key_found(var/extra)
+ target_cameras--
+ if(target_cameras <= 0)
+ return TRUE
+ return FALSE
+
+////////////////////////BLOODSPILL/////////////////////////////
+
+/datum/bloodcult_ritual/bloodspill
+ name = "Spill Blood"
+ desc = "more blood...need more... on the floors...on the walls..."
+
+ only_once = TRUE
+ ritual_type = "bloodspill"
+ difficulty = "hard"
+ reward_achiever = 0
+ reward_faction = 500
+
+ keys = list("bloodspill")
+
+ var/percent_bloodspill = 4//percent of all the station's simulated floors, you should keep it under 5.
+ var/target_bloodspill = 1000//actual amount of bloodied floors to reach
+ var/max_bloodspill = 0//max amount of bloodied floors simultanously reached
+
+/datum/bloodcult_ritual/bloodspill/init_ritual()
+ var/floor_count = 0
+ for(var/i = 1 to ((2 * world.view + 1)*32))
+ for(var/r = 1 to ((2 * world.view + 1)*32))
+ var/turf/tile = locate(i, r, SSmapping.levels_by_trait(ZTRAIT_STATION)[1])
+ if(tile && isopenturf(tile) && !isspaceturf(tile.loc) && !istype(tile.loc, /area/station/security/prison))
+ floor_count++
+ target_bloodspill = round(floor_count * percent_bloodspill / 100)
+ target_bloodspill += rand(-20, 20)
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ cult.bloodspill_ritual = src
+
+/datum/bloodcult_ritual/bloodspill/update_desc()
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ desc = "more blood...need more... on the floors...on the walls... at least [target_bloodspill - cult.bloody_floors.len] more..."
+
+/datum/bloodcult_ritual/bloodspill/key_found(var/extra)
+ if(extra > max_bloodspill)
+ max_bloodspill = extra
+ if(max_bloodspill >= target_bloodspill)
+ return TRUE
+ return FALSE
+
+/datum/bloodcult_ritual/bloodspill/complete()
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ cult.bloodspill_ritual = null
+ ..()
+
+
+////////////////////////SACRIFICE/////////////////////////////
+
+/datum/bloodcult_ritual/sacrifice_captain
+ name = "Sacrifice Captain"
+ desc = "a captain... an altar... and a proper blade..."
+
+ only_once = TRUE
+ ritual_type = "sacrifice"
+ difficulty = "hard"
+ reward_faction = 500
+
+ keys = list("altar_sacrifice_human")
+
+/datum/bloodcult_ritual/sacrifice_captain/pre_conditions(var/datum/antagonist/cult/potential)
+ if (potential)
+ owner = potential
+ for(var/mob/M in GLOB.player_list)
+ if(M.mind && M.mind.assigned_role == "Captain")
+ return TRUE
+ return FALSE
+
+/datum/bloodcult_ritual/sacrifice_captain/key_found(var/mob/living/O)
+ if (istype(O) && O.mind && O.mind.assigned_role == "Captain")
+ return TRUE
+ return FALSE
+
+/*
+////////////////////////INFECTION/////////////////////////////
+
+/datum/bloodcult_ritual/cursed_infection
+ name = "Cursed Blood"
+ desc = "from a tempting goblet... pours a wicked drink..."
+
+ ritual_type = "infection"
+ difficulty = "medium"
+ reward_faction = 300
+
+ keys = list("cursed_infection")
+
+ var/targets = 5
+ var/list/infected_targets = list()
+
+/datum/bloodcult_ritual/cursed_infection/init_ritual()
+ infected_targets = list()
+
+/datum/bloodcult_ritual/cursed_infection/update_desc()
+ desc = "from a tempting goblet... pours a wicked drink... have at least [targets - infected_targets.len] more individuals consume it..."
+
+/datum/bloodcult_ritual/cursed_infection/key_found(mob/living/L)
+ if (!L.mind)
+ return FALSE
+ if (IS_CULTIST(L))
+ return FALSE
+ if (L.mind in infected_targets)
+ return FALSE
+ infected_targets += L.mind
+ if(infected_targets.len >= targets)
+ return TRUE
+ return FALSE
+*/
+
+////////////////////////////////////////////////////////////////////
+// //
+// PERSONAL RITUALS //
+// //
+////////////////////////////////////////////////////////////////////
+
+////////////////////////CONFUSION/////////////////////////////
+
+/datum/bloodcult_ritual/blind_cameras
+ name = "Blind Cameras"
+ desc = "confusion runes and talismans... darken their lenses..."
+
+ ritual_type = "confusion"
+ difficulty = "easy"
+ personal = TRUE
+ reward_achiever = 200
+ reward_faction = 2
+
+ keys = list("confusion_camera")
+
+ var/target_cameras = 3
+
+/datum/bloodcult_ritual/blind_cameras/init_ritual()
+ target_cameras = 3
+
+//Called when a cultist is about to hover the corresponding ritual UI button
+/datum/bloodcult_ritual/blind_cameras/update_desc()
+ desc = "confusion runes and talismans... darken their lenses... [target_cameras] to go..."
+
+/datum/bloodcult_ritual/blind_cameras/key_found(var/extra)
+ target_cameras--
+ if(target_cameras <= 0)
+ return TRUE
+ return FALSE
+
+////////////////////////////////////////////////////////////////
+
+/datum/bloodcult_ritual/confuse_crew
+ name = "Confuse Crew"
+ desc = "confusion runes and talismans... bring their nightmares to life..."
+
+ ritual_type = "confusion"
+ difficulty = "medium"
+ personal = TRUE
+ reward_achiever = 200
+ reward_faction = 2
+
+ keys = list(
+ "confusion_carbon",
+ "confusion_papered",
+ )
+
+/datum/bloodcult_ritual/confuse_crew/key_found(var/mob/living/extra)
+ if (!extra.client)
+ return FALSE
+ return TRUE
+
+////////////////////////HARM CREW MEMBERS/////////////////////////////
+
+/datum/bloodcult_ritual/harm_crew
+ name = "Harm Crew"
+ desc = "wield cult weaponry... spill their blood... sear their skin..."
+
+ ritual_type = "harm"
+ difficulty = "medium"
+ personal = TRUE
+ reward_achiever = 200
+ reward_faction = 2
+
+ keys = list(
+ "attack_tome",
+ "attack_cultblade",
+ "attack_blooddagger",
+ "attack_construct",
+ "attack_shade",
+ "attack_ritualknife",
+ )
+
+ var/targets = 3
+ var/list/hit_targets = list()
+
+/datum/bloodcult_ritual/harm_crew/init_ritual()
+ hit_targets = list()
+
+/datum/bloodcult_ritual/harm_crew/update_desc()
+ desc = "wield cult weaponry... spill their blood... sear their skin... at least [targets - hit_targets.len] different individuals..."
+
+/datum/bloodcult_ritual/harm_crew/key_found(var/mob/living/L)
+ if (IS_CULTIST(L))
+ return FALSE
+ if (L.mind in hit_targets)
+ return FALSE
+ hit_targets += L.mind
+ if(hit_targets.len >= targets)
+ return TRUE
+ return FALSE
+
+////////////////////////SACRIFICE/////////////////////////////
+
+/datum/bloodcult_ritual/sacrifice_mouse
+ name = "Sacrifice Mouse"
+ desc = "a rodent... an altar... and a proper blade..."
+
+ ritual_type = "sacrifice"
+ difficulty = "easy"
+ personal = TRUE
+ reward_achiever = 200
+ reward_faction = 2
+
+ keys = list("altar_sacrifice_animal")
+
+/datum/bloodcult_ritual/sacrifice_mouse/key_found(mob/living/basic/mouse/extra)
+ if(istype(extra))
+ return TRUE
+ return FALSE
+
+
+//////////////////////////////////////////////////////////////
+
+/datum/bloodcult_ritual/sacrifice_monkey
+ name = "Sacrifice Monkey"
+ desc = "a simian... an altar... and a proper blade..."
+
+ ritual_type = "sacrifice"
+ difficulty = "easy"
+ personal = TRUE
+ reward_achiever = 200
+ reward_faction = 2
+
+ keys = list("altar_sacrifice_monkey")
+
+/datum/bloodcult_ritual/sacrifice_monkey/key_found(var/extra)
+ return TRUE
+
+
+////////////////////////ALTAR/////////////////////////////////
+
+/datum/bloodcult_ritual/altar
+ name = "Prepare Altar"
+ desc = "raise an altar... add proper paraphernalia around... then plant a ritual knife on top..."
+
+ ritual_type = "altar"
+ difficulty = "easy"
+ personal = TRUE
+ reward_achiever = 200
+ reward_faction = 2
+
+ var/required_candles = 0
+ var/required_tomes = 0
+ var/required_runes = 0
+ var/required_pylons = 0
+ var/required_animal = 0
+ var/required_humanoid = 0
+ var/required_cultblade = 0
+
+ keys = list("altar_plant")
+
+/datum/bloodcult_ritual/altar/key_found(var/obj/structure/cult/altar/altar)
+ var/mob/user = owner.owner.current
+
+ var/valid = TRUE
+ var/found_candles = 0
+ for (var/obj/item/candle/blood/CB in range(1, altar))
+ if (CB.lit)
+ found_candles++
+ if (found_candles < required_candles)
+ to_chat(user, span_cult("Need more lit blood candles...") )
+ valid = FALSE
+
+ var/found_tomes = 0
+ for (var/obj/item/weapon/tome/T in range(1, altar))
+ found_tomes++
+ if (found_tomes < required_tomes)
+ to_chat(user, span_cult("Need more arcane tomes...") )
+ valid = FALSE
+
+ var/found_runes = 0
+ for (var/obj/effect/new_rune/R in range(1, altar))
+ found_runes++
+ if (found_runes < required_runes)
+ to_chat(user, span_cult("Need more runes...") )
+ valid = FALSE
+
+ var/found_pylons = 0
+ for (var/obj/structure/cult/pylon/P in range(1, altar))
+ found_pylons++
+ if (found_pylons < required_pylons)
+ to_chat(user, span_cult("You must construct additional pylons...") )
+ valid = FALSE
+
+ var/found_animal = FALSE
+ var/found_humanoid = FALSE
+ if(altar.has_buckled_mobs())
+ var/mob/M = altar.buckled_mobs[1]
+ if (ishuman(M))
+ found_humanoid = TRUE
+ if (ismonkey(M) || isanimal(M))
+ found_animal = TRUE
+ if (required_animal && !found_animal)
+ to_chat(user, span_cult("You must impale an animal on top...") )
+ valid = FALSE
+ if (required_humanoid && !found_humanoid)
+ to_chat(user, span_cult("You must impale an humanoid on top...") )
+ valid = FALSE
+
+ var/obj/item/weapon/melee/B = altar.blade
+ if (required_cultblade && !istype(B))
+ to_chat(user, span_cult("Lastly, a mere ritual knife won't do here. Forge a better implement...") )
+
+ return valid
+
+/datum/bloodcult_ritual/altar/simple
+ name = "Prepare Simple Altar"
+ desc = "raise an altar... add some lit blood candles around... then plant a ritual knife on top..."
+
+ difficulty = "easy"
+ reward_achiever = 200
+ reward_faction = 2
+
+ required_candles = 4
+ required_tomes = 0
+ required_runes = 0
+ required_pylons = 0
+ required_animal = 0
+ required_humanoid = 0
+ required_cultblade = 0
+
+/datum/bloodcult_ritual/altar/elaborate
+ name = "Prepare Elaborate Altar"
+ desc = "raise an altar... add proper paraphernalia around... then plant a ritual knife on top..."
+
+ difficulty = "easy"
+ reward_achiever = 200
+ reward_faction = 2
+
+ required_candles = 4
+ required_tomes = 1
+ required_runes = 4
+ required_pylons = 0
+ required_animal = 0
+ required_humanoid = 0
+ required_cultblade = 0
+
+/datum/bloodcult_ritual/altar/excentric
+ name = "Prepare Excentric Altar"
+ desc = "raise an altar... add proper paraphernalia around... lay an animal on top... then plant a ritual knife into it..."
+
+ difficulty = "medium"
+ reward_achiever = 400
+ reward_faction = 4
+
+ required_candles = 4
+ required_tomes = 0
+ required_runes = 4
+ required_pylons = 0
+ required_animal = 1
+ required_humanoid = 0
+ required_cultblade = 0
+
+/datum/bloodcult_ritual/altar/unholy
+ name = "Prepare Unholy Altar"
+ desc = "raise an altar... add proper paraphernalia around... lay a humanoid on top... then plant a cult blade into them..."
+
+ difficulty = "hard"
+ reward_achiever = 600
+ reward_faction = 6
+
+ required_candles = 2
+ required_tomes = 1
+ required_runes = 3
+ required_pylons = 2
+ required_animal = 0
+ required_humanoid = 1
+ required_cultblade = 1
+
+////////////////////////SUICIDE/////////////////////////////
+
+/datum/bloodcult_ritual/suicide_tome
+ name = "An Ending"
+ desc = "grab a tome... then think of an ending... preferably one with many witnesses..."
+
+ only_once = TRUE
+ ritual_type = "suicide"
+ difficulty = "hard"
+ personal = TRUE
+ reward_achiever = 500
+ reward_faction = 100
+
+ keys = list("suicide_tome")
+
+/datum/bloodcult_ritual/suicide_tome/pre_conditions(var/datum/antagonist/cult/potential)
+ if (potential)
+ owner = potential
+ if (potential.devotion > DEVOTION_TIER_4)
+ return TRUE
+ return FALSE
+
+/datum/bloodcult_ritual/suicide_tome/key_found(var/mob/living/extra)
+ for(var/mob/M in dview(world.view, get_turf(extra), INVISIBILITY_MAXIMUM))
+ if (!M.client)
+ continue
+ if (isobserver(M))
+ reward_achiever += 50
+ reward_faction += 10
+ else if (IS_CULTIST(M))
+ reward_achiever += 100
+ reward_faction += 20
+ else
+ reward_achiever += 200
+ reward_faction += 40
+ return TRUE
+
+///////////////////////////////////////////////////////////////////////////
+
+/datum/bloodcult_ritual/suicide_soulblade
+ name = "Soul Blade"
+ desc = "Become the bone of your own sword..."
+
+ only_once = TRUE
+ ritual_type = "suicide"
+ difficulty = "hard"
+ personal = TRUE
+ reward_achiever = 500
+ reward_faction = 100
+
+ keys = list("suicide_tome")
+
+/datum/bloodcult_ritual/suicide_soulblade/pre_conditions(var/datum/antagonist/cult/potential)
+ if (potential)
+ owner = potential
+ if (potential.devotion > DEVOTION_TIER_3)
+ return TRUE
+ return FALSE
+
+/datum/bloodcult_ritual/suicide_soulblade/key_found(var/mob/living/extra)
+ for(var/mob/M in dview(world.view, get_turf(extra), INVISIBILITY_MAXIMUM))
+ if (!M.client)
+ continue
+ if (isobserver(M))
+ reward_achiever += 25
+ reward_faction += 5
+ else if (IS_CULTIST(M))
+ reward_achiever += 50
+ reward_faction += 10
+ else
+ reward_achiever += 100
+ reward_faction += 20
+ return TRUE
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/_base_spell.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/_base_spell.dm
new file mode 100644
index 000000000000..060d3ece400c
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/_base_spell.dm
@@ -0,0 +1,240 @@
+//Returns a rune spell based on the given 3 words.
+/proc/get_rune_spell(var/mob/user, var/obj/spell_holder, var/use = "ritual", var/datum/rune_word/word1, var/datum/rune_word/word2, var/datum/rune_word/word3)
+ if(!word1 || !word2 || !word3)
+ return
+ for(var/subtype in subtypesof(/datum/rune_spell))
+ var/datum/rune_spell/instance = subtype
+ if(word1.type == initial(instance.word1) && word2.type == initial(instance.word2) && word3.type == initial(instance.word3))
+ switch (use)
+ if ("ritual")
+ return new subtype(user, spell_holder, use)
+ if ("examine")
+ return instance
+ if ("walk")
+ if (initial(instance.walk_effect))
+ return new subtype(user, spell_holder, use) //idk man
+ else
+ return null
+ if ("imbue")
+ return subtype
+ return new subtype(user, spell_holder, use)
+ return null
+
+
+/datum/rune_spell
+ var/secret = FALSE // When set to true, this spell will not appear in the list of runes, when using the "Draw Rune with a Guide" button.
+ var/name = "rune spell" // The spell's name.
+ var/desc = "you shouldn't be reading this." // Appears to cultists when examining a rune that triggers this spell
+ var/desc_talisman = "you shouldn't be reading this." // Appears to cultists when examining a taslisman that triggers this spell
+ var/obj/spell_holder = null //The rune or talisman calling the spell. If using a talisman calling an attuned rune, the holder is the rune.
+ var/mob/activator = null //The original mob that cast the spell
+ var/datum/rune_word/word1 = null
+ var/datum/rune_word/word2 = null
+ var/datum/rune_word/word3 = null
+ var/invocation = "Lo'Rem Ip'Sum" //Spoken whenever cast.
+ var/touch_cast = 0 //If set to 1, will proc cast_touch() when touching someone with an imbued talisman (example: Stun)
+ var/can_conceal = 0 //If set to 1, concealing the rune will not abort the spell. (example: Path Exit)
+ var/rune_flags = null //If set to RUNE_STAND (or 1), the user will need to stand right above the rune to use cast the spell
+ var/walk_effect = 0 //If set to 1, procs Added() when step over
+ var/custom_rune = FALSE // Prevents the rune's normal UpdateIcon() from firing.
+
+ //Optional (These vars aren't used by default rune code, but many runes make use of them, so set them up as you need, the comments below are suggestions)
+ var/cost_invoke = 0 //Blood cost upon cast
+ var/cost_upkeep = 0 //Blood cost upon upkeep proc
+ var/list/contributors = list() //List of people currently participating in the ritual
+ var/remaining_cost = 0 //How much blood to gather for the ritual to succeed
+ var/accumulated_blood = 0 //How much blood has been gathered so far
+ var/cancelling = 3 //Check this variable to abort the ritual due to blood flow being interrupted
+ var/list/ingredients = list() //Items that should be on the rune for it to work
+ var/list/ingredients_found = list() //Items that are found on the rune
+
+ var/destroying_self = FALSE //Sanity var to prevent abort loops, ignore
+ var/image/progbar = null //Bar for channeling spells
+
+ var/talisman_absorb = RUNE_CAN_IMBUE //Whether the rune is absorbed into the talisman (and thus deleted), or linked to the talisman (RUNE_CAN_ATTUNE)
+ var/talisman_uses = 1 //How many times can a spell be cast from a single talisman. The talisman disappears upon the last use.
+
+ var/page = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut\
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint\
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." //Arcane tome page description.
+
+/datum/rune_spell/New(var/mob/user, var/obj/holder, var/use = "ritual", var/mob/target)
+ spell_holder = holder
+ activator = user
+
+ if(use == "ritual")
+ pre_cast()
+ else if(use == "touch" && target)
+ cast_touch(target) //Skips pre_cast() for talismans)
+
+
+/datum/rune_spell/Destroy()
+ destroying_self = TRUE
+ if(spell_holder)
+ if(istype(spell_holder, /obj/effect/new_rune))
+ var/obj/effect/new_rune/rune_holder = spell_holder
+ rune_holder.active_spell = null
+ spell_holder = null
+ word1 = null
+ word2 = null
+ word3 = null
+ activator = null
+ ..()
+
+/datum/rune_spell/proc/invoke(var/mob/user, var/text = "", var/whisper = 0)
+ if(user.checkTattoo(TATTOO_SILENT) || (spell_holder.icon_state == "temp"))
+ return
+ if(!whisper)
+ user.say(text, "C")
+ else
+ user.whisper(text)
+
+/datum/rune_spell/proc/pre_cast()
+ if(istype(spell_holder, /obj/effect/new_rune))
+ var/obj/effect/new_rune/R = spell_holder
+ R.activated++
+ R.update_icon()
+ if (R.word1)// "invisible" temporary runes spawned by some talismans shouldn't display those
+ R.cast_word(R.word1.english)
+ R.cast_word(R.word2.english)
+ R.cast_word(R.word3.english)
+ if((rune_flags & RUNE_STAND) && (get_turf(activator) != get_turf(spell_holder)))
+ abort(RITUALABORT_STAND)
+ else
+ invoke(activator, invocation)
+ cast()
+ else if(istype (spell_holder, /obj/item/weapon/talisman))
+ invoke(activator, invocation, 1)//talisman incantations are whispered
+ cast_talisman()
+
+/datum/rune_spell/proc/pay_blood()
+ var/data = use_available_blood(activator, cost_invoke)
+ if(data[BLOODCOST_RESULT] == BLOODCOST_FAILURE)
+ to_chat(activator, span_warning("This ritual requires more blood than you can offer.") )
+ return FALSE
+ else
+ return TRUE
+
+/datum/rune_spell/proc/Added(var/mob/M)
+
+/datum/rune_spell/proc/Removed(var/mob/M)
+
+/datum/rune_spell/proc/midcast(mob/add_cultist)
+ return
+
+/datum/rune_spell/proc/cast() //Override for your spell functionality.
+ spell_holder.visible_message(span_warning("This rune wasn't properly set up, tell a coder.") )
+ qdel(src)
+
+/datum/rune_spell/proc/abort(var/cause) //The error message for aborting, usable by any runeset.
+ if(destroying_self)
+ return
+ destroying_self = TRUE
+ switch(cause)
+ if (RITUALABORT_ERASED)
+ if (istype (spell_holder, /obj/effect/new_rune))
+ spell_holder.visible_message(span_warning("The rune's destruction ended the ritual.") )
+ if (RITUALABORT_STAND)
+ if (activator)
+ to_chat(activator, span_warning("The [name] ritual requires you to stand on top of the rune.") )
+ if (RITUALABORT_GONE)
+ if (activator)
+ to_chat(activator, span_warning("The ritual ends as you move away from the rune.") )
+ if (RITUALABORT_BLOCKED)
+ if (activator)
+ to_chat(activator, span_warning("There is a building blocking the ritual..") )
+ if (RITUALABORT_BLOOD)
+ spell_holder.visible_message(span_warning("Deprived of blood, the channeling is disrupted.") )
+ if (RITUALABORT_TOOLS)
+ if (activator)
+ to_chat(activator, span_warning("The necessary tools have been misplaced.") )
+ if (RITUALABORT_TOOLS)
+ spell_holder.visible_message(span_warning("The ritual ends as the victim gets pulled away from the rune.") )
+ if (RITUALABORT_CONVERT)
+ if (activator)
+ to_chat(activator, span_notice("The conversion ritual successfully brought a new member to the cult. Inform them of the current situation so they can take action."))
+ if (RITUALABORT_REFUSED)
+ if (activator)
+ to_chat(activator, span_notice("The conversion ritual ended with the target being restrained by some eldritch contraption. Deal with them how you see fit so their life may serve our plans."))
+ if (RITUALABORT_NOCHOICE)
+ if (activator)
+ to_chat(activator, span_notice("The target never manifested any clear reaction to the ritual. As such they were automatically restrained."))
+ if (RITUALABORT_SACRIFICE)
+ if (activator)
+ to_chat(activator, span_warning("The ritual ends leaving behind nothing but a creepy chest, filled with your lost soul's belongings.") )
+ if (RITUALABORT_CONCEAL)
+ if (activator)
+ to_chat(activator, span_warning("The ritual is disrupted by the rune's sudden phasing out.") )
+ if (RITUALABORT_NEAR)
+ if (activator)
+ to_chat(activator, span_warning("You cannot perform this ritual that close from another similar structure.") )
+ if (RITUALABORT_OVERCROWDED)
+ if (activator)
+ to_chat(activator, span_warning("There are too many human cultists and constructs already.") )
+
+ for(var/mob/living/L in contributors)
+ if (L.client)
+ L.client.images -= progbar
+ contributors.Remove(L)
+
+ if (activator && activator.client)
+ activator.client.images -= progbar
+
+ if (progbar)
+ progbar.loc = null
+
+ if (spell_holder.icon_state == "temp")
+ qdel(spell_holder)
+ else
+ qdel(src)
+
+/datum/rune_spell/proc/salt_act(var/turf/T)
+ return
+
+/datum/rune_spell/proc/missing_ingredients_count()
+ var/list/missing_ingredients = ingredients.Copy()
+ var/turf/T = get_turf(spell_holder)
+ for (var/path in missing_ingredients)
+ var/atom/A = locate(path) in T
+ if (A)
+ missing_ingredients -= path
+ ingredients_found += A
+
+ if (missing_ingredients.len > 0)
+ var/missing = "You need "
+ var/i = 1
+ for (var/I in missing_ingredients)
+ i++
+ var/atom/A = I
+ missing += "\a [initial(A.name)]"
+ if (i <= missing_ingredients.len)
+ missing += ", "
+ if (i == missing_ingredients.len)
+ missing += "and "
+ else
+ missing += "."
+ to_chat(activator, span_warning("The necessary ingredients for this ritual are missing. [missing]") )
+ abort(RITUALABORT_MISSING)
+ return TRUE
+ return FALSE
+
+/datum/rune_spell/proc/update_progbar()
+ if(!progbar)
+ progbar = image("icon" = 'monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi', "loc" = spell_holder, "icon_state" = "prog_bar_0")
+ progbar.pixel_z = 32
+ progbar.plane = HUD_PLANE
+ progbar.appearance_flags = RESET_COLOR
+ progbar.icon_state = "prog_bar_[round((min(1, accumulated_blood / remaining_cost) * 100), 10)]"
+ return
+
+/datum/rune_spell/proc/cast_talisman() //Override for unique talisman behavior.
+ cast()
+
+/datum/rune_spell/proc/cast_touch(var/mob/M) //Behavior on using the talisman on somebody. See - stun talisman.
+ return
+
+/datum/rune_spell/proc/midcast_talisman(var/mob/add_cultist)
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/astral_journey.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/astral_journey.dm
new file mode 100644
index 000000000000..fc8f8ca78052
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/astral_journey.dm
@@ -0,0 +1,54 @@
+
+/datum/rune_spell/astraljourney
+ name = "Astral Journey"
+ desc = "Channel a fragment of your soul into an astral projection so you can spy on the crew and communicate your findings with the rest of the cult."
+ desc_talisman = "Leave your body so you can go spy on your enemies."
+ invocation = "Fwe'sh mah erl nyag r'ya!"
+ word1 = /datum/rune_word/hell
+ word2 = /datum/rune_word/travel
+ word3 = /datum/rune_word/self
+ page = "Upon use, your soul will float above your body, allowing you to freely move invisibly around the Z-Level. Words you speak while in this state will be heard by everyone in the cult. You can also become tangible which lets you converse with people, but taking any damage while in this state will end the ritual. Your body being moved away from the rune will also end the ritual.\
+
Should your body die while you were still using the rune, a shade will form wherever your astral projection stands.\
+
This rune persists upon use, allowing repeated usage."
+ rune_flags = RUNE_STAND
+ var/mob/living/basic/astral_projection/astral = null
+ var/cultist_key = ""
+ var/list/restricted_verbs = list()
+
+/datum/rune_spell/astraljourney/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ cultist_key = activator.key
+
+ to_chat(activator, span_notice("As you recite the invocation, you feel your consciousness rise up in the air above your body.") )
+ //astral = activator.ghostize(1, 1)
+ astral = new(activator.loc)
+ astral.ascend(activator)
+ activator.ajourn = src
+
+ step(astral, NORTH)
+ astral.dir = SOUTH
+
+ spawn()
+ handle_astral()
+
+/datum/rune_spell/astraljourney/cast_talisman()//we spawn an invisible rune under our feet that works like the regular one
+ var/obj/effect/new_rune/R = new(get_turf(activator))
+ R.icon_state = "temp"
+ R.active_spell = new type(activator, R)
+ qdel(src)
+
+
+/datum/rune_spell/astraljourney/abort(var/cause)
+ qdel(astral)
+ ..()
+
+/datum/rune_spell/astraljourney/proc/handle_astral()
+ while(!destroying_self && activator && activator.stat != DEAD && astral && astral.loc && activator.loc == spell_holder.loc)
+ sleep(10)
+ abort()
+
+/datum/rune_spell/astraljourney/Removed(var/mob/M)
+ if (M == activator)
+ abort(RITUALABORT_GONE)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/bloodmagnetism.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/bloodmagnetism.dm
new file mode 100644
index 000000000000..d54ef73dde91
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/bloodmagnetism.dm
@@ -0,0 +1,260 @@
+
+/datum/rune_spell/bloodmagnetism
+ name = "Blood Magnetism"
+ desc = "Bring forth one of your fellow believers, no matter how far they are, as long as their heart beats."
+ desc_talisman = "Use to begin the Blood Magnetism ritual where you stand."
+ invocation = "N'ath reth sh'yro eth d'rekkathnor!"
+ word1 = /datum/rune_word/join
+ word2 = /datum/rune_word/other
+ word3 = /datum/rune_word/self
+ page = "This rune actually has two different rituals built into it:\
+
The first one, Summon Cultist, lets you summon a cultist from anywhere in the world whether they're alive or dead, for a cost of 50u of blood, which can be split by having other cultists participate in the ritual. \
+ The ritual will fail however should the target cultist be anchored to their location, or have a holy implant.\
+
The second ritual, Rejoin Cultist, lets you summon yourself next to the target cultist instead for a cost of 15u of blood. \
+ Other cultists can participate in the second ritual to accompany you, but the cost will remain 15u for every participating cultist. \
+ Again, the ritual will fail if the target has a holy implant (or has been made to drink\
+
This rune persists upon use, allowing repeated usage."
+ remaining_cost = 10
+ cost_upkeep = 1
+ var/rejoin = 0
+ var/mob/target = null
+ var/list/feet_portals = list()
+ var/cost_summon = 50//you probably don't want to pay that up alone
+ var/cost_rejoin = 15//static cost for every contributor
+
+/datum/rune_spell/bloodmagnetism/Destroy()
+ target = null
+ for (var/guy in feet_portals)
+ var/obj/object = feet_portals[guy]
+ qdel(object)
+ feet_portals -= guy
+ feet_portals = list()
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/effects.dmi', "rune_summon")
+ ..()
+
+
+/datum/rune_spell/bloodmagnetism/abort()
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/effects.dmi', "rune_summon")
+ for (var/guy in feet_portals)
+ var/obj/object = feet_portals[guy]
+ qdel(object)
+ ..()
+
+/datum/rune_spell/bloodmagnetism/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ rejoin = alert(activator, "Will you pull them toward you, or pull yourself toward them?", "Blood Magnetism", "Summon Cultist", "Rejoin Cultist") == "Rejoin Cultist"
+
+ var/list/possible_targets = list()
+ var/list/prisoners = list()
+ var/datum/antagonist/cult/cultist = activator.mind?.has_antag_datum(/datum/antagonist/cult)
+ var/datum/team/cult/bloodcult = cultist.cult_team
+ for (var/datum/mind/mind in bloodcult.members)
+ if (mind.current)
+ if (mind.current.occult_muted())
+ continue
+ possible_targets.Add(mind.current)
+
+ //Prisoners are valid Blood Magnetism targets!
+ for(var/obj/item/restraints/handcuffs/cult/cuffs in bloodcult.bindings)
+ if (iscarbon(cuffs.loc))
+ var/mob/living/carbon/carbon = cuffs.loc
+ if (carbon.handcuffed == cuffs)
+ prisoners.Add(carbon)
+
+ var/list/annotated_targets = list()
+ var/list/visible_mobs = viewers(activator)
+ var/i = 1
+ for(var/mob/mob in possible_targets)
+ var/status = ""
+ if(mob == activator)
+ status = " (You)"
+ else if(mob in visible_mobs)
+ status = " (Visible)"
+ else if(mob.stat == DEAD)
+ status = " (Dead)"
+ annotated_targets["\Roman[i]-[mob.real_name][status]"] = mob
+ i++
+
+ for(var/mob/prisoner in prisoners)
+ annotated_targets["\Roman[i]-[prisoner.real_name] (Prisoner)"] = prisoner
+ i++
+
+ var/choice = input(activator, "Choose who you wish to [rejoin ? "rejoin" : "summon"]", "Blood Magnetism") as null|anything in annotated_targets
+ if (!choice)
+ qdel(src)
+ return
+ target = annotated_targets[choice]
+ if (!target)
+ qdel(src)
+ return
+
+ contributors.Add(activator)
+ update_progbar()
+ if (activator.client)
+ activator.client.images |= progbar
+ spell_holder.overlays += image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ if (!rejoin)
+ spell_holder.overlays += image('monkestation/code/modules/bloody_cult/icons/effects.dmi', "rune_summon")
+ else
+ feet_portals.Add(activator)
+ var/obj/effect/cult_ritual/feet_portal/P = new (activator.loc, activator, src)
+ feet_portals[activator] = P
+ to_chat(activator, span_rose("This ritual's blood toll can be substantially reduced by having multiple cultists partake in it.") )
+ spawn()
+ payment()
+
+/datum/rune_spell/bloodmagnetism/cast_talisman()//we spawn an invisible rune under our feet that works like the regular one
+ var/obj/effect/new_rune/R = new(get_turf(activator))
+ R.icon_state = "temp"
+ R.active_spell = new type(activator, R)
+ qdel(src)
+
+/datum/rune_spell/bloodmagnetism/midcast(mob/add_cultist)
+ if (add_cultist in contributors)
+ return
+ invoke(add_cultist, invocation)
+ contributors.Add(add_cultist)
+ if (add_cultist.client)
+ add_cultist.client.images |= progbar
+ if (rejoin)
+ feet_portals.Add(add_cultist)
+ var/obj/effect/cult_ritual/feet_portal/P = new (add_cultist.loc, add_cultist, src)
+ feet_portals[add_cultist] = P
+
+/datum/rune_spell/bloodmagnetism/proc/payment()//an extra payment is spent at the end of the channeling, and shared between contributors
+ var/failsafe = 0
+ while(failsafe < 1000)
+ failsafe++
+ //are our payers still here and about?
+ for(var/mob/living/contributor in contributors)
+ if (!IS_CULTIST(contributor) || !(contributor in range(spell_holder, 1)) || (contributor.stat != CONSCIOUS))
+ if (contributor.client)
+ contributor.client.images -= progbar
+ var/obj/effect/cult_ritual/feet_portal/P = feet_portals[contributor]
+ qdel(P)
+ feet_portals.Remove(contributor)
+ contributors.Remove(contributor)
+ //alright then, time to pay in blood
+ var/amount_paid = 0
+ for(var/mob/living/contributor in contributors)
+ var/data = use_available_blood(contributor, cost_upkeep/contributors.len, contributors[contributor])//always 1u total per payment
+ if (data[BLOODCOST_RESULT] == BLOODCOST_FAILURE)//out of blood are we?
+ contributors.Remove(contributor)
+ var/obj/effect/cult_ritual/feet_portal/P = feet_portals[contributor]
+ qdel(P)
+ feet_portals.Remove(contributor)
+ else
+ amount_paid += data[BLOODCOST_TOTAL]
+ contributors[contributor] = data[BLOODCOST_RESULT]
+ make_tracker_effects(contributor.loc, spell_holder, 1, "soul", 3, /obj/effect/tracker/drain, 1)//visual feedback
+
+ accumulated_blood += amount_paid
+
+ //if there's no blood for over 3 seconds, the channeling fails
+ if (amount_paid)
+ cancelling = 3
+ else
+ cancelling--
+ if (cancelling <= 0)
+ if(accumulated_blood && !(locate(/obj/effect/decal/cleanable/blood/splatter) in spell_holder.loc))
+ var/obj/effect/decal/cleanable/blood/splatter/splatter = new(spell_holder.loc)//splash
+ splatter.count = 2
+ abort(RITUALABORT_BLOOD)
+ return
+
+ if (accumulated_blood >= remaining_cost)
+ success()
+ return
+
+ update_progbar()
+
+ sleep(10)
+ message_admins("A rune ritual has iterated for over 1000 blood payment procs. Something's wrong there.")
+
+/datum/rune_spell/bloodmagnetism/proc/success()
+ if (target.occult_muted())
+ for(var/mob/living/contributor in contributors)
+ to_chat(activator, span_warning("The ritual failed, the target seems to be under a curse that prevents us from reaching them through the veil.") )
+ else
+ if (rejoin)
+ var/list/valid_turfs = list()
+ for(var/turf/T in orange(target, 1))
+ if(!T.is_blocked_turf(TRUE))
+ valid_turfs.Add(T)
+ if (valid_turfs.len)
+ for(var/mob/living/contributor in contributors)
+ use_available_blood(contributor, cost_rejoin, contributors[contributor])
+ var/datum/antagonist/cult/cult_datum = contributor.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(100, DEVOTION_TIER_2, "bloodmagnetism_rejoin", contributor)
+ make_tracker_effects(contributor.loc, spell_holder, 1, "soul", 3, /obj/effect/tracker/drain, 3)
+ var/obj/effect/abstract/landing_animation = anim(target = contributor, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "cult_jaunt_prepare", plane = GAME_PLANE_UPPER)
+ playsound(contributor, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_prepare.ogg', 75, 0, -3)
+ spawn(10)
+ playsound(contributor, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_land.ogg', 30, 0, -3)
+ new /obj/effect/bloodcult_jaunt(get_turf(contributor), contributor, pick(valid_turfs))
+ flick("cult_jaunt_land", landing_animation)
+ else
+ if(target.buckled || !isturf(target.loc))
+ to_chat(target, span_warning("You feel that some force wants to pull you through the veil, but cannot proceed while you are buckled or inside something.") )
+ for(var/mob/living/contributor in contributors)
+ to_chat(activator, span_warning("The ritual failed, the target seems to be anchored to where they are.") )
+ else
+ for(var/mob/living/contributor in contributors)
+ use_available_blood(contributor, cost_summon/contributors.len, contributors[contributor])
+ make_tracker_effects(contributor.loc, spell_holder, 1, "soul", 3, /obj/effect/tracker/drain, 3)
+ var/datum/antagonist/cult/cult_datum = contributor.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(100, DEVOTION_TIER_2, "bloodmagnetism_summon", contributor)
+ var/obj/effect/abstract/landing_animation = anim(target = src.target, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "cult_jaunt_prepare", lay = CULT_OVERLAY_LAYER, plane = GAME_PLANE_UPPER)
+ var/mob/mob_target = target//so we keep track of them after the datum is ded until we jaunt
+ var/turf/T = get_turf(spell_holder)
+ playsound(mob_target, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_prepare.ogg', 75, 0, -3)
+ spawn(10)
+ playsound(mob_target, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_land.ogg', 30, 0, -3)
+ new /obj/effect/bloodcult_jaunt(get_turf(mob_target), mob_target, T)
+ flick("cult_jaunt_land", landing_animation)
+
+ for(var/mob/living/contributor in contributors)
+ if (contributor.client)
+ contributor.client.images -= progbar
+ contributors.Remove(contributor)
+
+ if (activator && activator.client)
+ activator.client.images -= progbar
+
+ if (progbar)
+ progbar.loc = null
+
+ if (spell_holder.icon_state == "temp")
+ qdel(spell_holder)
+ else
+ qdel(src)
+
+/obj/effect/cult_ritual/feet_portal
+ anchored = 1
+ icon_state = "rune_rejoin"
+ pixel_y = -10
+ layer = ABOVE_OBJ_LAYER
+ plane = GAME_PLANE
+ var/mob/living/caster = null
+ var/turf/source = null
+
+/obj/effect/cult_ritual/feet_portal/New(var/turf/loc, var/mob/living/user, var/datum/rune_spell/seer/runespell)
+ ..()
+ caster = user
+ source = get_turf(runespell?.spell_holder)
+ if (!caster)
+ qdel(src)
+ return
+
+/obj/effect/cult_ritual/feet_portal/Destroy()
+ caster = null
+ source = null
+ ..()
+
+/obj/effect/cult_ritual/feet_portal/HasProximity(var/atom/movable/AM)
+ if (caster && caster.loc != loc)
+ forceMove(get_turf(caster))
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/communication.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/communication.dm
new file mode 100644
index 000000000000..4c317a39a924
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/communication.dm
@@ -0,0 +1,136 @@
+
+
+
+
+/datum/rune_spell/communication
+ name = "Communication"
+ desc = "Speak so that every cultists may hear your voice. Can be used even when there is no spire nearby."
+ desc_talisman = "Use it to write and send a message to all followers of Nar-Sie. When in the middle of a ritual, use it again to transmit a message that will be remembered by all."
+ invocation = "O bidai nabora se'sma!"
+ rune_flags = RUNE_STAND
+ talisman_uses = 10
+ var/obj/effect/cult_ritual/cult_communication/comms = null
+ word1 = /datum/rune_word/self
+ word2 = /datum/rune_word/other
+ word3 = /datum/rune_word/technology
+ page = "By standing on top of the rune and touching it, everyone in the cult will then be able to hear what you say or whisper. \
+ You will also systematically speak in the language of the cult when using it.\
+
Talismans imbued with this rune can be used 10 times to send messages to the rest of the cult.\
+
Lastly touching the rune a second time while you are already using it lets you set cult reminders that will be heard by newly converts and added to their notes.\
+
This rune persists upon use, allowing repeated usage."
+
+/datum/rune_spell/communication/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+ var/mob/living/user = activator
+ comms = new /obj/effect/cult_ritual/cult_communication(spell_holder.loc, user, src)
+
+/datum/rune_spell/communication/midcast(var/mob/living/user)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!istype(cult))
+ return
+ if (!istype(user)) // Ghosts
+ return
+ var/reminder = input("Write the reminder.", text("Cult reminder")) as null | message
+ if (!reminder)
+ return
+ reminder = strip_html(reminder) // No weird HTML
+ var/number = cult.cult_reminders.len
+ var/text = "[number + 1]) [reminder], by [user.real_name]."
+ cult.cult_reminders += text
+ for(var/datum/mind/mind in cult.members)
+ if (IS_CULTIST(mind.current))//failsafe for cultist brains put in MMIs
+ to_chat(mind.current, span_cult("[user.real_name]'s voice echoes in your head, [span_cultbold(reminder)]"))
+
+ for(var/mob/living/basic/astral_projection/astral in GLOB.astral_projections)
+ to_chat(astral, span_cult("[user.real_name]'s voice echoes in your head, [span_cultbold(reminder)]"))
+
+ for(var/mob/dead/observer/observer in GLOB.player_list)
+ to_chat(observer, span_cult("[user.real_name]'s voice echoes in your head, [span_cultbold(reminder)]"))
+
+ //log_cultspeak("[key_name(user)] Cult reminder: [reminder]")
+
+/datum/rune_spell/communication/cast_talisman()//we write our message on the talisman, like in previous versions.
+ var/message = sanitize(input("Write a message to send to your acolytes.", "Blood Letter", "") as null|message, MAX_MESSAGE_LEN)
+ if(!message)
+ return
+
+ var/datum/antagonist/cult/team = activator.mind?.has_antag_datum(/datum/antagonist/cult)
+ var/datum/team/cult = team.cult_team
+ for (var/datum/mind/mind in cult.members)
+ if (IS_CULTIST(mind.current))//failsafe for cultist brains put in MMIs
+ to_chat(mind.current, span_cult("[activator.real_name]'s voice echoes in your head, [span_cultbold(message)]"))
+
+ for(var/mob/living/basic/astral_projection/astral in GLOB.astral_projections)
+ to_chat(astral, span_cult("[activator.real_name]'s voice echoes in your head, [span_cultbold(message)]"))
+
+ for(var/mob/dead/observer/observer in GLOB.player_list)
+ to_chat(observer, span_cult("[activator.real_name]'s voice echoes in your head, [span_cultbold(message)]"))
+
+ //log_cultspeak("[key_name(activator)] Cult Communicate Talisman: [message]")
+
+ qdel(src)
+
+/datum/rune_spell/communication/Destroy()
+ destroying_self = 1
+ if (comms)
+ qdel(comms)
+ comms = null
+ ..()
+
+/obj/effect/cult_ritual/cult_communication
+ anchored = 1
+ icon_state = "rune_communication"
+ pixel_y = 8
+ alpha = 200
+ layer = ABOVE_OBJ_LAYER
+ plane = GAME_PLANE
+ mouse_opacity = 0
+ var/mob/living/caster = null
+ var/datum/rune_spell/communication/source = null
+
+
+/obj/effect/cult_ritual/cult_communication/New(var/turf/loc, var/mob/living/user, var/datum/rune_spell/communication/runespell)
+ ..()
+ caster = user
+ source = runespell
+
+/obj/effect/cult_ritual/cult_communication/Destroy()
+ caster = null
+ source = null
+ ..()
+
+/obj/effect/cult_ritual/cult_communication/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), message_range)
+ if(speaker && speaker.loc == loc)
+ var/speaker_name = speaker.name
+ var/mob/living/L
+ if (isliving(speaker))
+ L = speaker
+ if (!IS_CULTIST(L))//geez we don't want that now do we
+ return
+ if (ishuman(speaker))
+ var/mob/living/carbon/human/human = speaker
+ speaker_name = human.real_name
+ L = speaker
+
+ //var/rendered_message = compose_message(speaker, message_language, raw_message, radio_freq, spans, message_mods)
+ var/datum/antagonist/cult/user = L?.mind?.has_antag_datum(/datum/antagonist/cult)
+ var/datum/team/cult/cult = user.cult_team
+ for (var/datum/mind/mind in cult.members)
+ if (mind.current == speaker)//echoes are annoying
+ continue
+ if (IS_CULTIST(mind.current))//failsafe for cultist brains put in MMIs
+ to_chat(mind.current, "[speaker_name]'s voice echoes in your head, [raw_message]")
+
+ for(var/mob/living/basic/astral_projection/astral in GLOB.astral_projections)
+ to_chat(astral, "[speaker_name] communicates, [raw_message]")
+
+ for(var/mob/dead/observer/observer in GLOB.player_list)
+ to_chat(observer, "[speaker_name] communicates, [raw_message]")
+ //log_cultspeak("[key_name(speech.speaker)] Cult Communicate Rune: [rendered_message]")
+
+/obj/effect/cult_ritual/cult_communication/HasProximity(var/atom/movable/AM)
+ if (!caster || caster.loc != loc)
+ if (source)
+ source.abort(RITUALABORT_GONE)
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/conceal.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/conceal.dm
new file mode 100644
index 000000000000..7f8969af4546
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/conceal.dm
@@ -0,0 +1,49 @@
+
+/datum/rune_spell/conceal
+ name = "Conceal"
+ desc = "Hide runes and cult structures. Some runes can still be used when concealed, but using them might reveal them."
+ desc_talisman = "Hide runes and cult structures. Covers a smaller range than when used from a rune."
+ invocation = "Kla'atu barada nikt'o!"
+ word1 = /datum/rune_word/hide
+ word2 = /datum/rune_word/see
+ word3 = /datum/rune_word/blood
+ page = "This rune allows you to hide every rune and structures in a circular 7 tile range around it. You cannot hide a rune or structure that got revealed less than 10 seconds ago. Affects through walls.\
+
The talisman version has a 5 tile radius."
+ var/rune_effect_range = 7
+ var/talisman_effect_range = 5
+
+/datum/rune_spell/conceal/cast(effect_range = rune_effect_range, size = 'monkestation/code/modules/bloody_cult/icons/480x480.dmi')
+ var/turf/T = get_turf(spell_holder)
+ var/obj/effect/abstract/animation = anim(target = T, a_icon = size, a_icon_state = "rune_conceal", offX = -32*effect_range, offY = -32*effect_range, plane = ABOVE_LIGHTING_PLANE)
+ animation.alpha = 0
+ animate(animation, alpha = 255, time = 2)
+ animate(alpha = 0, time = 3)
+ to_chat(activator, "All runes and cult structures in range hide themselves behind a thin layer of reality.")
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/conceal.ogg', 50, 0, -5)
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+
+ for(var/obj/structure/cult/S in range(effect_range, T))
+ var/dist = cheap_pythag(S.x - T.x, S.y - T.y)
+ if (S.conceal_cooldown)
+ continue
+ if (dist <= effect_range+0.5)
+ S.conceal()
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "conceal_structure", S)
+
+ for(var/obj/effect/new_rune/R in range(effect_range, T))
+ if (R == spell_holder)
+ continue
+ if (R.conceal_cooldown)
+ continue
+ var/dist = cheap_pythag(R.x - T.x, R.y - T.y)
+ if (dist <= effect_range+0.5)
+ R.conceal()
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "conceal_rune", R)
+ var/obj/effect/abstract/trail = shadow(R, T, "rune_conceal")
+ trail.alpha = 0
+ animate(trail, alpha = 200, time = 2)
+ animate(alpha = 0, time = 3)
+ qdel(spell_holder)
+
+/datum/rune_spell/conceal/cast_talisman()
+ cast(talisman_effect_range, 'monkestation/code/modules/bloody_cult/icons/352x352.dmi')
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/confusion.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/confusion.dm
new file mode 100644
index 000000000000..7ff7a2405ca9
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/confusion.dm
@@ -0,0 +1,220 @@
+GLOBAL_LIST_INIT(confusion_victims, list())
+
+/datum/rune_spell/confusion
+ name = "Confusion"
+ desc = "Sow panic in the mind of your enemies, and obscure cameras."
+ desc_talisman = "Sow panic in the mind of your enemies, and obscure cameras. The effect is shorter than when used from a rune."
+ invocation = "Sti' kaliesin!"
+ word1 = /datum/rune_word/destroy
+ word2 = /datum/rune_word/see
+ word3 = /datum/rune_word/other
+ page = "This rune instills paranoia in the heart and mind of your enemies. \
+ Every non-cultist human in range will see their surroundings appear covered with occult markings, and everyone will look like monsters to them. \
+ HUDs won't help officer differentiate their owns for the duration of the illusion.\
+
Robots in view will be simply blinded for a short while, cameras however will remain dark until someone resets their wiring.\
+
Because it also causes a few seconds of blindness to those affected, this rune is useful as both a way to initiate a fight, escape, or kidnap someone amidst the chaos.\
+
The duration is a bit shorter when used from a talisman, but you can slap it directly on someone to only afflict them with the same duration as a rune's."
+ var/rune_duration = 30 SECONDS
+ var/talisman_duration = 20 SECONDS
+ var/hallucination_radius = 25
+ touch_cast = 1
+
+/datum/rune_spell/confusion/cast_touch(mob/mob)
+ var/turf/T = get_turf(mob)
+ invoke(activator, invocation, 1)
+
+ new /obj/effect/cult_ritual/confusion(T, rune_duration, hallucination_radius, mob, activator)
+
+ qdel(src)
+
+/datum/rune_spell/confusion/cast(duration = rune_duration)
+ new /obj/effect/cult_ritual/confusion(spell_holder, duration, hallucination_radius, null, activator)
+ qdel(spell_holder)
+
+/datum/rune_spell/confusion/cast_talisman()//talismans have the same range, but the effect lasts shorter.
+ cast(talisman_duration)
+
+/obj/effect/cult_ritual/confusion
+ anchored = 1
+ icon = 'monkestation/code/modules/bloody_cult/icons/64x64.dmi'
+ icon_state = ""
+ pixel_x = -32/2
+ pixel_y = -32/2
+ plane = ABOVE_LIGHTING_PLANE
+ mouse_opacity = 0
+ var/duration = 5
+ var/hallucination_radius = 25
+
+/obj/effect/cult_ritual/confusion/New(turf/loc, duration = 30 SECONDS, radius = 25, mob/specific_victim = null, mob/culprit)
+ ..()
+ //Alright, this is a pretty interesting rune, first of all we prepare the fake cult floors & walls that the victims will see.
+ var/turf/T = get_turf(src)
+ var/list/hallucinated_turfs = list()
+ if (!specific_victim)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/confusion_start.ogg', 75, 0, 0)
+ for(var/turf/U in range(radius, T))
+ if (istype(U, /area/station/service/chapel))//the chapel is protected against such illusions, the mobs in it will still be affected however.
+ continue
+ var/dist = cheap_pythag(U.x - T.x, U.y - T.y)
+ if (dist < 15 || prob((radius-dist)*4))
+ var/image/I_turf
+ if (!U.density)
+ I_turf = image(icon = 'icons/turf/floors.dmi', loc = U, icon_state = "cult")
+ //if it's a floor, give it a chance to have some runes written on top
+ if (rune_appearances_cache.len > 0 && prob(7))
+ var/lookup = pick(rune_appearances_cache)//finally a good use for that cache
+ var/image/I = rune_appearances_cache[lookup]
+ I_turf.overlays += I
+ hallucinated_turfs.Add(I_turf)
+
+ //now let's round up our victims: any non-cultist with an unobstructed line of sight to the rune/talisman will be affected
+ var/list/potential_victims = list()
+
+ if (specific_victim)
+ potential_victims.Add(specific_victim)
+ specific_victim.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/confusion_start.ogg', 75, 0, 0)
+ else
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ potential_victims.Add(M)
+
+ var/datum/antagonist/cult/our_cultist
+ if (culprit && culprit.mind)
+ our_cultist = culprit.mind.has_antag_datum(/datum/antagonist/cult)
+
+ for(var/mob/living/M in potential_victims)
+
+ if (iscarbon(M))
+ var/mob/living/carbon/C = M
+ if (IS_CULTIST(C))
+ continue
+
+ var/datum/confusion_manager/CM
+ if (M in GLOB.confusion_victims)
+ CM = GLOB.confusion_victims[M]
+ else
+ CM = new(M, duration)
+ GLOB.confusion_victims[M] = CM
+
+ if (M.stat != DEAD && our_cultist)
+ if (specific_victim == M)
+ our_cultist.gain_devotion(50, DEVOTION_TIER_2, "confusion_papered", M)
+ else
+ our_cultist.gain_devotion(50, DEVOTION_TIER_2, "confusion_carbon", M)
+
+ spawn()
+ CM.apply_confusion(T, hallucinated_turfs)
+
+ if (issilicon(M) && !isAI(M))//Silicons get a fade to black, then just a flash, until I can think of something else
+ shadow(M, T)
+ if (M.stat != DEAD && our_cultist)
+ our_cultist.gain_devotion(50, DEVOTION_TIER_2, "confusion_silicon", M)
+ M.overlay_fullscreen("blindblack", /atom/movable/screen/fullscreen/black)
+ M.update_fullscreen_alpha("blindblack", 255, 5)
+ spawn(5)
+ M.clear_fullscreen("blindblack", animated = FALSE)
+
+ //now to blind cameras, the effects on cameras do not time out, but they can be fixed
+ if (!specific_victim)
+ for(var/obj/machinery/camera/C in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ shadow(C, T)
+ var/col = C.color
+ animate(C, color = col, time = 4)
+ animate(color = "black", time = 5)
+ animate(color = col, time = 5)
+ if (our_cultist)
+ our_cultist.gain_devotion(50, DEVOTION_TIER_2, "confusion_camera", C)
+ C.setViewRange(-1)//The camera won't reveal the area for the AI anymore
+
+ qdel(src)
+
+//each affected mob gets their own
+/datum/confusion_manager
+ var/time_of_last_confusion = 0
+ var/list/my_hallucinated_stuff = list()
+ var/mob/victim = null
+ var/duration = 30 SECONDS
+
+/datum/confusion_manager/New(mob/M, D)
+ ..()
+ victim = M
+ duration = D
+
+/datum/confusion_manager/Destroy()
+ my_hallucinated_stuff = list()
+ victim = null
+ ..()
+
+/datum/confusion_manager/proc/apply_confusion(turf/T, list/hallucinated_turfs)
+ shadow(victim, T)//shadow trail moving from the spell_holder to the victim
+ anim(target = victim, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_blind", plane = ABOVE_LIGHTING_PLANE)
+
+ if (!time_of_last_confusion)
+ start_confusion(T, hallucinated_turfs)
+ return
+ if (victim.mind)
+ message_admins("BLOODCULT: [key_name(victim)] had the effects of Confusion refreshed back to [duration/10] seconds.")
+ log_admin("BLOODCULT: [key_name(victim)] had the effects of Confusion refreshed by back to [duration/10] seconds.")
+ var/time_key = world.time
+ time_of_last_confusion = time_key
+ victim.update_fullscreen_alpha("blindblack", 255, 5)
+ ADD_TRAIT(victim, TRAIT_POOR_AIM, "rune")
+ sleep(10)
+ refresh_confusion(T, hallucinated_turfs, time_key)
+
+/datum/confusion_manager/proc/start_confusion(var/turf/T, var/list/hallucinated_turfs)
+ var/time_key = world.time
+ time_of_last_confusion = time_key
+ if (victim.mind)
+ message_admins("BLOODCULT: [key_name(victim)] is now under the effects of Confusion for [duration/10] seconds.")
+ log_admin("BLOODCULT: [key_name(victim)] is now under the effects of Confusion for [duration/10] seconds.")
+ to_chat(victim, span_danger("Your vision goes dark, panic and paranoia take their toll on your mind.") )
+ victim.overlay_fullscreen("blindborder", /atom/movable/screen/fullscreen/confusion_border)//victims DO still get blinded for a second
+ victim.overlay_fullscreen("blindblack", /atom/movable/screen/fullscreen/black)//which will allow us to subtly reveal the surprise
+ victim.update_fullscreen_alpha("blindblack", 255, 5)
+ victim.playsound_local(victim, 'monkestation/code/modules/bloody_cult/sound/confusion.ogg', 50, 0, 0, 0, 0)
+ sleep(10)
+ victim.overlay_fullscreen("blindblind", /atom/movable/screen/fullscreen/blind)
+ refresh_confusion(T, hallucinated_turfs, time_key)
+
+/datum/confusion_manager/proc/refresh_confusion(turf/T, list/hallucinated_turfs, time_key)
+ victim.update_fullscreen_alpha("blindblind", 255, 0)
+ victim.update_fullscreen_alpha("blindblack", 0, 10)
+ victim.update_fullscreen_alpha("blindblind", 0, 80)
+ victim.update_fullscreen_alpha("blindborder", 150, 5)
+
+ if (victim.client)
+ var/static/list/hallucination_mobs = list("faithless", "forgotten", "otherthing")
+ victim.client.images.Remove(my_hallucinated_stuff)//removing images caused by every blind rune used consecutively on that mob
+ my_hallucinated_stuff = hallucinated_turfs.Copy()
+ for(var/mob/living/L in range(T, 25))//All mobs in a large radius will look like monsters to the victims.
+ if (L == victim)
+ continue//the victims still see themselves as humans (or whatever they are)
+ var/image/override_overlay = image(icon = 'monkestation/code/modules/bloody_cult/icons/animal.dmi', loc = L, icon_state = pick(hallucination_mobs))
+ override_overlay.override = TRUE
+ my_hallucinated_stuff.Add(override_overlay)
+ victim.client.images.Add(my_hallucinated_stuff)
+
+ sleep(duration - 5)
+
+ if (time_of_last_confusion != time_key)//only the last applied confusion gets to end it
+ return
+
+ victim.update_fullscreen_alpha("blindborder", 0, 5)
+ victim.overlay_fullscreen("blindwhite", /atom/movable/screen/fullscreen/white)
+ victim.update_fullscreen_alpha("blindwhite", 255, 3)
+ sleep(5)
+ REMOVE_TRAIT(victim, TRAIT_POOR_AIM, "rune")
+ GLOB.confusion_victims.Remove(victim)
+ victim.update_fullscreen_alpha("blindwhite", 0, 12)
+ victim.clear_fullscreen("blindblack", animated = FALSE)
+ victim.clear_fullscreen("blindborder", animated = FALSE)
+ victim.clear_fullscreen("blindblind", animated = FALSE)
+ anim(target = victim, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_blind_remove", plane = ABOVE_LIGHTING_PLANE)
+ if (victim.client)
+ victim.client.images.Remove(my_hallucinated_stuff)//removing images caused by every blind rune used consecutively on that mob
+ if (victim.mind)
+ message_admins("BLOODCULT: [key_name(victim)] is no longer under the effects of Confusion.")
+ log_admin("BLOODCULT: [key_name(victim)] is no longer under the effects of Confusion.")
+ sleep(15)
+ victim.clear_fullscreen("blindwhite", animated = FALSE)
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/conversion.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/conversion.dm
new file mode 100644
index 000000000000..b7878de6b0fe
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/conversion.dm
@@ -0,0 +1,440 @@
+GLOBAL_LIST_INIT(converted_minds, list())
+
+/datum/rune_spell/conversion
+ name = "Conversion"
+ desc = "The unenlightened will bask before Nar-Sie's glory and given the chance to join the cult, or they will be made your prisoner."
+ desc_talisman = "Use to remotely trigger the rune and incapacitate someone on top."
+ invocation = "Mah'weyh pleggh at e'ntrath!"
+ word1 = /datum/rune_word/join
+ word2 = /datum/rune_word/blood
+ word3 = /datum/rune_word/self
+ talisman_absorb = RUNE_CAN_ATTUNE
+ page = "By touching this rune while a non-cultist stands above it, you will knock them down and keep them unable to move or speak as Nar-Sie's words reach out to them. \
+ The ritual will take longer on trained security personnel and some Nanotrasen official, but can also be sped up by wearing cult robes or armor.\
+
If the target is willing and there are few enough cult members, they will be converted and become an honorary cultist.\
+
However if the target has a loyalty implants or the cult already has 9 human members, they will instead be restrained by ghastly bindings. \
+ More than one construct of each time will also reduce the maximum amount of permitted human cultists.\
+
Do not seek to convert everyone, instead use the Seer or Astral Journey runes first to locate the most interesting candidates.\
+
Touching the rune again during the early part of the ritual lets you toggle it between \"conversion\" and \"entrapment\", should you just want to restrain someone.\
+
By attuning a talisman to this rune, you can trigger it remotely, but you will have to move closer afterwards or the ritual will stop.\
+
This rune persists upon use, allowing repeated usage."
+ var/remaining = 100
+ var/mob/living/carbon/victim = null
+ var/flavor_text = 0
+ var/success = CONVERSION_NOCHOICE
+ var/list/impede_medium = list(
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Internal Affairs Agent",
+ "Head of Personnel",
+ )
+ var/list/impede_hard = list(
+ "Chaplain",
+ "Captain",
+ )
+ var/obj/effect/cult_ritual/conversion/conversion = null
+
+ var/phase = 1
+ var/entrapment = FALSE
+
+
+/datum/rune_spell/conversion/Destroy()
+ if(conversion)
+ conversion.Die()
+ ..()
+
+/datum/rune_spell/conversion/update_progbar()//progbar tracks conversion progress instead of paid blood
+ if (!progbar)
+ progbar = image("icon" = 'monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi', "loc" = spell_holder, "icon_state" = "prog_bar_0")
+ progbar.pixel_z = 32
+ progbar.plane = HUD_PLANE
+ progbar.appearance_flags = RESET_COLOR
+ progbar.icon_state = "prog_bar_[min(100, round((100-remaining), 10))]"
+ return
+
+/datum/rune_spell/conversion/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ var/mob/converter = activator//trying to fix logs showing the converter as *null*
+
+ R.one_pulse()
+ var/turf/T = R.loc
+ var/list/targets = list()
+
+
+ for (var/mob/living/carbon/carbon in T)//all carbons can be converted...but only carbons. no cult silicons. (unless it's April 1st)
+ if (!IS_CULTIST(carbon) && carbon.stat != DEAD)//no more corpse conversions!
+ targets.Add(carbon)
+ if (targets.len > 0)
+ victim = pick(targets)
+ else
+ to_chat(activator, span_warning("There needs to be a potential convert standing or lying on top of the rune.") )
+ qdel(src)
+ return
+
+ var/mob/convertee = victim//trying to fix logs showing the victim as *null*
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+
+ update_progbar()
+ if (activator.client)
+ activator.client.images |= progbar
+
+ //secondly, let's stun our victim and begin the ritual
+ to_chat(victim, span_danger("Occult energies surge from below your [issilicon(victim) ? "actuators" : "feet"] and seep into your [issilicon(victim) ? "chassis" : "body"].") )
+ victim.Knockdown(5 SECONDS)
+ victim.Stun(5 SECONDS)
+ if (isalien(victim))
+ victim.Paralyze(5 SECONDS)
+ victim.overlay_fullscreen("conversionborder", /atom/movable/screen/fullscreen/conversion_border)
+ victim.update_fullscreen_alpha("conversionborder", 255, 5)
+ conversion = new(T)
+ flick("rune_convert_start", conversion)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_start.ogg', 50, 0, -4)
+
+
+ if (!cult.CanConvert())
+ to_chat(activator, span_warning("There are already too many cultists. \The [victim] will be made a prisoner.") )
+
+ if (victim.mind)
+ if (victim.mind.assigned_role in impede_medium)
+ to_chat(victim, span_warning("Your devotion to Nanotrasen slows down the ritual.") )
+ to_chat(activator, span_warning("Their devotion to Nanotrasen is strong, the ritual will take longer.") )
+
+ if (victim.mind.assigned_role in impede_hard)
+ var/higher_cause = "Space Jesus"
+ switch(victim.mind.assigned_role)
+ if ("Captain")
+ higher_cause = "Nanotrasen"
+ if ("Chaplain")
+ higher_cause = "a higher God"
+ to_chat(victim, span_warning("Your devotion to [higher_cause] slows down the ritual.") )
+ to_chat(activator, span_warning("Their devotion to [higher_cause] is amazing, the ritual will be lengthy.") )
+
+ spawn()
+ while (remaining > 0)
+ if (destroying_self || !spell_holder || !activator || !victim)
+ return
+ //first let's make sure they're on the rune
+ if (victim.loc != T)//Removed() should take care of it, but just in case
+ victim.clear_fullscreen("conversionborder", 10)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_abort.ogg', 50, 0, -4)
+ conversion.icon_state = ""
+ flick("rune_convert_abort", conversion)
+ abort(RITUALABORT_REMOVED)
+ return
+
+ //and that we're next to them
+ if (!spell_holder.Adjacent(activator))
+ cancelling--
+ if (cancelling <= 0)
+ victim.clear_fullscreen("conversionborder", 10)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_abort.ogg', 50, 0, -4)
+ conversion.icon_state = ""
+ flick("rune_convert_abort", conversion)
+ abort(RITUALABORT_GONE)
+ return
+
+ else
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_process.ogg', 10, 0, -4)
+ //then progress through the ritual
+ victim.Knockdown(5 SECONDS)
+ victim.Stun(5 SECONDS)
+ if (isalien(victim))
+ victim.Paralyze(5 SECONDS)
+ var/progress = 10//10 seconds to reach second phase for a naked cultist
+ progress += activator.get_cult_power()//down to 1-2 seconds when wearing cult gear
+ var/delay = 0
+ if (victim.mind)
+ if (victim.mind.assigned_role in impede_medium)
+ delay = 1
+ progress = progress/2
+
+ if (victim.mind.assigned_role in impede_hard)
+ delay = 1
+ progress = progress/4
+
+ if (delay)
+ progress = clamp(progress, 1, 10)
+ remaining -= progress
+ update_progbar()
+
+ //spawning some messages
+ var/threshold = min(100, round((100-remaining), 10))
+ if (flavor_text < 3)
+ if (flavor_text == 0 && threshold > 10)//it's ugly but gotta account for the possibility of several messages appearing at once
+ to_chat(victim, span_cult("WE ARE THE BLOOD PUMPING THROUGH THE FABRIC OF SPACE") )
+ flavor_text++
+ if (flavor_text == 1 && threshold > 40)
+ to_chat(victim, span_cult("THE GEOMETER CALLS FOR YET ANOTHER FEAST") )
+ flavor_text++
+ if (flavor_text == 2 && threshold > 70)
+ to_chat(victim, span_cult("FRIEND OR FOE, YOU TOO SHALL JOIN THE FESTIVITIES") )
+ flavor_text++
+ sleep(10)
+
+ if (activator && activator.client)
+ activator.client.images -= progbar
+
+ //alright, now the second phase, which always lasts an additional 10 seconds, but no longer requires the proximity of the activator.
+ phase = 2
+ var/acceptance = "Yes"
+ victim.Knockdown(15 SECONDS)
+ victim.Stun(15 SECONDS)
+ if (isalien(victim))
+ victim.Paralyze(15 SECONDS)
+
+ if (victim.client)
+ if(victim.mind.assigned_role == "Chaplain")
+ acceptance = "Chaplain"
+
+ for(var/obj/item/implant/mindshield/I in victim)
+ if(I.imp_in)
+ acceptance = "Implanted"
+ else if (!victim.mind)
+ acceptance = "Mindless"
+
+ if (is_banned_from(victim.ckey, ROLE_CULTIST))
+ acceptance = "Banned"
+
+
+ if (!cult.CanConvert())
+ acceptance = "Overcrowded"
+
+ if (entrapment)
+ acceptance = "Overcrowded"
+
+ //Players with cult enabled in their preferences will always get converted.
+ //Others get a choice, unless they're cult-banned or have their preferences set to Never (or disconnected), in which case they always die.
+ var/conversion_delay = 100
+ switch (acceptance)
+ if ("Always", "Yes")
+ conversion.icon_state = "rune_convert_good"
+ to_chat(activator, span_cult("The ritual immediately stabilizes, \the [victim] appears eager help prepare the festivities.") )
+ cult.send_flavour_text_accept(victim, activator)
+ success = CONVERSION_ACCEPT
+ conversion_delay = 30
+ if ("No", "???", "Never")
+ if (victim.client)
+ to_chat(activator, span_cult("The ritual arrives in its final phase. How it ends depends now of \the [victim]. You do not have to remain adjacent for the remainder of the ritual.") )
+ spawn()
+ if (alert(victim, "The Cult of Nar-Sie has much in store for you, but what specifically?", "You have 10 seconds to decide", "Join the Cult", "Become Prisoner") == "Join the Cult")
+ conversion.icon_state = "rune_convert_good"
+ success = CONVERSION_ACCEPT
+ to_chat(victim, span_cult("THAT IS GOOD. COME CLOSER. THERE IS MUCH TO TEACH YOU") )
+ else
+ to_chat(victim, span_danger("THAT IS ALSO GOOD, FOR YOU WILL ENTERTAIN US") )
+ success = CONVERSION_REFUSE
+ else//converting a braindead carbon will always lead to them being captured
+ to_chat(activator, span_cult("\The [victim] doesn't really seem to have all their wits about them. Letting the ritual conclude will let you restrain them.") )
+ if ("Implanted")
+ if (victim.client)
+ to_chat(activator, span_cult("A loyalty implant interferes with the ritual. They will not be able to accept the conversion.") )
+ to_chat(victim, span_danger("Your loyalty implant prevents you from hearing any more of what they have to say.") )
+ success = CONVERSION_REFUSE
+ else//converting a braindead carbon will always lead to them being captured
+ to_chat(activator, span_cult("\The [victim] doesn't really seem to have all their wits about them. Letting the ritual conclude will let you restrain them.") )
+ if ("Chaplain")//Chaplains can never be converted
+ if (victim.client)
+ to_chat(activator, span_cult("Chaplains won't ever let themselves be converted. They will be restrained.") )
+ to_chat(victim, span_danger("Your devotion to Space Jesus shields you from Nar-Sie's temptations.") )
+ success = CONVERSION_REFUSE
+ else//converting a braindead carbon will always lead to them being captured
+ to_chat(activator, span_cult("\The [victim] doesn't really seem to have all their wits about them. Letting the ritual conclude will let you restrain them.") )
+ if ("Banned")
+ conversion.icon_state = "rune_convert_bad"
+ to_chat(activator, span_cult("Given how unstable the ritual is becoming, \The [victim] will surely be consumed entirely by it. They weren't meant to become one of us.") )
+ to_chat(victim, span_danger("Except your past actions have displeased us. You will be our snack before the feast begins. \[You are banned from this role\]") )
+ success = CONVERSION_BANNED
+ if ("Mindless")
+ conversion.icon_state = "rune_convert_bad"
+ to_chat(activator, span_cult("This mindless creature will be sacrificed.") )
+ success = CONVERSION_MINDLESS
+ if ("Overcrowded")
+ to_chat(victim, span_cult("EXCEPT...THERE ARE NO VACANT SEATS LEFT!") )
+ success = CONVERSION_OVERCROWDED
+ conversion_delay = 30
+
+ //since we're no longer checking for the cultist's adjacency, let's finish this ritual without a loop
+ sleep(conversion_delay)
+
+ if (destroying_self || !spell_holder || !activator || !victim)
+ return
+
+ if (victim.loc != T)//Removed() should take care of it, but just in case
+ victim.clear_fullscreen("conversionborder", 10)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_abort.ogg', 50, 0, -4)
+ conversion.icon_state = ""
+ flick("rune_convert_abort", conversion)
+ abort(RITUALABORT_REMOVED)
+ return
+
+ if (victim.mind && !(victim.mind in GLOB.converted_minds))
+ GLOB.converted_minds += victim.mind
+ if (!cult)
+ message_admins("Blood Cult: A conversion ritual occured...but we cannot find the cult faction...")//failsafe in case of admin varedit fuckery
+ var/datum/antagonist/streamer/streamer_role = activator?.mind?.has_antag_datum(/datum/antagonist/streamer)
+ if(streamer_role && streamer_role.team == "Cult")
+ streamer_role.conversions += 1
+ streamer_role.update_streamer_hud()
+
+ switch (success)
+ if (CONVERSION_ACCEPT)
+ conversion.layer = BELOW_OBJ_LAYER
+ conversion.plane = GAME_PLANE_UPPER
+ victim.clear_fullscreen("conversionborder", 10)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_success.ogg', 75, 0, -4)
+ //new cultists get purged of the debuffs
+ victim.SetKnockdown(0)
+ victim.SetStun(0)
+ if (isalien(victim))
+ victim.SetParalyzed(0)
+ //let's also remove cult cuffs if they have them
+ if (istype(victim.handcuffed, /obj/item/restraints/handcuffs/cult))
+ victim.dropItemToGround(victim.handcuffed)
+
+ convert(convertee, converter)
+ conversion.icon_state = ""
+
+ flick("rune_convert_success", conversion)
+ message_admins("BLOODCULT: [key_name(convertee)] has been converted by [key_name(converter)].")
+ log_admin("BLOODCULT: [key_name(convertee)] has been converted by [key_name(converter)].")
+ abort(RITUALABORT_CONVERT)
+ return
+ if (CONVERSION_NOCHOICE, CONVERSION_REFUSE, CONVERSION_OVERCROWDED)
+ conversion.icon_state = ""
+ flick("rune_convert_refused", conversion)
+ for(var/mob/living/M in dview(world.view, T, INVISIBILITY_MAXIMUM))
+ if (M.client)
+ M.playsound_local(T, 'monkestation/code/modules/bloody_cult/sound/convert_abort.ogg', 75, 0, -4)
+
+ victim.Knockdown(7)
+ victim.Stun(6)
+ if (isalien(victim))
+ victim.Paralyze(8)
+
+ if (cult && victim.mind)
+ if (!(victim.mind in cult.previously_made_prisoner))
+ cult.previously_made_prisoner |= victim.mind
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(250, DEVOTION_TIER_3, "made_prisoner", victim)
+
+ //let's start by removing any cuffs they might already have
+ if (victim.handcuffed)
+ var/obj/item/restraints/handcuffs/cuffs = victim.handcuffed
+ victim.dropItemToGround(cuffs)
+
+ var/obj/item/restraints/handcuffs/cult/restraints = new(victim)
+ victim.set_handcuffed(restraints)
+ restraints.gaoler = IS_CULTIST(converter)
+ victim.update_handcuffed()
+
+ if (success == CONVERSION_NOCHOICE)
+ if (convertee.mind)//no need to generate logs when capturing mindless monkeys
+ to_chat(victim, span_danger("Because you didn't give your answer in time, you were automatically made prisoner.") )
+ message_admins("BLOODCULT: [key_name(convertee)] has timed-out during conversion by [key_name(converter)].")
+ log_admin("BLOODCULT: [key_name(convertee)] has timed-out during conversion by [key_name(converter)].")
+
+ abort(RITUALABORT_NOCHOICE)
+ else if (success == CONVERSION_REFUSE)
+ message_admins("BLOODCULT: [key_name(convertee)] has refused conversion by [key_name(converter)].")
+ log_admin("BLOODCULT: [key_name(convertee)] has refused conversion by [key_name(converter)].")
+
+ abort(RITUALABORT_REFUSED)
+ else
+ message_admins("BLOODCULT: [key_name(convertee)] was made prisoner by [key_name(converter)] because the cult is overcrowded.")
+ log_admin("BLOODCULT: [key_name(convertee)] was made prisoner by [key_name(converter)] because the cult is overcrowded.")
+
+ abort(RITUALABORT_REFUSED)
+
+ if (CONVERSION_BANNED)
+
+ message_admins("BLOODCULT: [key_name(convertee)] died because they were converted by [key_name(converter)] while cult-banned.")
+ log_admin("BLOODCULT: [key_name(convertee)] died because they were converted by [key_name(converter)] while cult-banned.")
+ conversion.icon_state = ""
+ flick("rune_convert_failure", conversion)
+
+ //sacrificed victims have all their stuff stored in a coffer that also contains their skull and a cup of their blood, should they have either
+ victim.boxify(TRUE, FALSE, "cult")
+ abort(RITUALABORT_SACRIFICE)
+
+ if (CONVERSION_MINDLESS)
+
+ conversion.icon_state = ""
+ flick("rune_convert_failure", conversion)
+
+ victim.boxify(TRUE, FALSE, "cult")
+ abort(RITUALABORT_SACRIFICE)
+ victim.clear_fullscreen("conversionborder", 10)
+
+/datum/rune_spell/conversion/proc/convert(var/mob/M, var/mob/converter)
+ var/datum/antagonist/cult/newCultist = new(M.mind)
+ M.mind.add_antag_datum(newCultist)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ cult.HandleRecruitedRole(newCultist)
+ if (!(victim.mind in cult.previously_converted))
+ cult.previously_made_prisoner |= M.mind
+ var/datum/antagonist/cult/cult_datum = converter.mind.has_antag_datum(/datum/antagonist/cult)
+ if (victim.mind in cult.previously_made_prisoner)
+ cult_datum.gain_devotion(250, DEVOTION_TIER_4, "converted_prisoner", victim)//making someone prisoner already grants 250 devotion on top.
+ else
+ cult_datum.gain_devotion(500, DEVOTION_TIER_4, "conversion", victim)
+ //newCultist.OnPostSetup()
+ //newCultist.Greet(GREET_CONVERTED)
+ newCultist.conversion["converted"] = activator
+ newCultist.update_cult_hud()
+
+/datum/rune_spell/conversion/midcast(mob/add_cultist)
+ if (add_cultist != activator)
+ return
+ if (phase == 1)
+ if (entrapment)
+ to_chat(add_cultist, span_notice("You perform the conversion sign, allowing the victim to become a cultist if they qualify.") )
+ entrapment = FALSE
+ else
+ to_chat(add_cultist, span_warning("You perform the entrapment sign, ensuring that the victim will be restrained.") )
+ entrapment = TRUE
+
+/datum/rune_spell/conversion/Removed(var/mob/M)
+ if (victim == M)
+ for(var/mob/living/L in dview(world.view, spell_holder.loc, INVISIBILITY_MAXIMUM))
+ if (L.client)
+ L.playsound_local(spell_holder.loc, 'monkestation/code/modules/bloody_cult/sound/convert_abort.ogg', 50, 0, -4)
+ conversion.icon_state = ""
+ flick("rune_convert_abort", conversion)
+ abort(RITUALABORT_REMOVED)
+
+/datum/rune_spell/conversion/cast_talisman()//handled by /obj/item/weapon/talisman/proc/trigger instead
+ return
+
+/datum/rune_spell/conversion/abort(var/cause)
+ if (victim)
+ victim.clear_fullscreen("conversionborder", 10)
+ victim = null
+ ..()
+
+/obj/effect/cult_ritual/conversion
+ anchored = 1
+ icon = 'monkestation/code/modules/bloody_cult/icons/64x64.dmi'
+ icon_state = "rune_convert_process"
+ pixel_x = -32/2
+ pixel_y = -32/2
+ plane = ABOVE_LIGHTING_PLANE
+ mouse_opacity = 0
+
+/obj/effect/cult_ritual/conversion/proc/Die()
+ spawn(10)
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/dark_pulse.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/dark_pulse.dm
new file mode 100644
index 000000000000..7c4d34de36d8
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/dark_pulse.dm
@@ -0,0 +1,88 @@
+
+/datum/rune_spell/pulse
+ name = "Dark Pulse"
+ desc = "Scramble the circuits of nearby devices."
+ desc_talisman = "Use to scramble the circuits of nearby devices."
+ invocation = "Ta'gh fara'qha fel d'amar det!"
+ word1 = /datum/rune_word/destroy
+ word2 = /datum/rune_word/see
+ word3 = /datum/rune_word/technology
+ page = "This rune triggers a strong EMP that messes with electronic machinery, devices, and robots up to 3 tiles away.\
+
Cultists and the objects they carry will be unaffected.\
+
You may also slap someone directly with the talisman to have its effects only affect them, but with double intensity."
+ touch_cast = 1
+
+/datum/rune_spell/pulse/cast_touch(var/mob/M)
+ var/turf/T = get_turf(M)
+ invoke(activator, invocation, 1)
+ playsound(T, 'sound/items/Welder2.ogg', 25, 0, -5)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/bloodboil.ogg', 25, 0, -5)
+ var/obj/effect/abstract/animation = anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', flick_anim = "rune_pulse", sleeptime = 15, plane = GAME_PLANE_UPPER, lay = MOB_UPPER_LAYER)
+ animation.add_particles(PS_CULT_SMOKE_BOX)
+ spawn(6)
+ animation.adjust_particles(PVAR_SPAWNING, 0, PS_CULT_SMOKE_BOX)
+ M.emp_act(1)
+ M.emp_act(1)
+ qdel(src)
+
+/datum/rune_spell/pulse/cast()
+ var/turf/T = get_turf(spell_holder)
+ playsound(T, 'sound/items/Welder2.ogg', 25, 1)
+ //T.hotspot_expose(700, 125, surfaces = 1)
+ spawn(0)
+ darkpulse(T, 3, 3, cultist = activator)
+ qdel(spell_holder)
+
+/proc/darkpulse(turf/epicenter, heavy_range, light_range, log = 0, var/mob/living/cultist = null)
+ if(!epicenter)
+ return
+
+ if(!istype(epicenter, /turf))
+ epicenter = get_turf(epicenter.loc)
+
+ if(heavy_range > light_range)
+ light_range = heavy_range
+
+ var/max_range = max(heavy_range, light_range)
+
+ var/x0 = epicenter.x
+ var/y0 = epicenter.y
+ var/z0 = epicenter.z
+
+ if(log)
+ message_admins("EMP with size ([heavy_range], [light_range]) in area [epicenter.loc.name] ([x0], [y0], [z0]) (JMP).")
+ log_game("EMP with size ([heavy_range], [light_range]) in area [epicenter.loc.name].")
+
+ spawn()
+ for (var/mob/M in GLOB.player_list)
+ //Double check for client
+ if(M && M.client)
+ var/turf/M_turf = get_turf(M)
+ if(M_turf && (M_turf.z == epicenter.z))
+ var/dist = cheap_pythag(M_turf.x - x0, M_turf.y - y0)
+ if((dist <= round(heavy_range + world.view - 2, 1)) && (M_turf.z - epicenter.z <= max_range) && (epicenter.z - M_turf.z <= max_range))
+ M.playsound_local(epicenter, 'monkestation/code/modules/bloody_cult/sound/bloodboil.ogg', 25, 0)
+
+ for(var/turf/T in spiral_range(max_range, epicenter))
+ CHECK_TICK
+ spawn(get_dist(T, epicenter))
+ var/obj/effect/abstract/animation = anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi', flick_anim = "rune_pulse", sleeptime = 15, plane = GAME_PLANE_UPPER, lay = MOB_UPPER_LAYER)
+ animation.add_particles(PS_CULT_SMOKE_BOX)
+ sleep(6)
+ animation.adjust_particles(PVAR_SPAWNING, 0, PS_CULT_SMOKE_BOX)
+ var/dist = cheap_pythag(T.x - x0, T.y - y0)
+ if(dist > max_range)
+ continue
+ var/act = 2
+ if(dist <= heavy_range)
+ act = 1
+ for(var/atom/movable/A in T.contents)
+ if (cultist && isliving(A))
+ var/mob/living/L = A
+ if (IS_CULTIST(L))
+ continue
+ else if (L.client && L.stat != DEAD)
+ var/datum/antagonist/cult/cult_datum = cultist.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_2, "EMP", L)
+ A.emp_act(act)
+ return
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/deaf_mute.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/deaf_mute.dm
new file mode 100644
index 000000000000..6409e5288d69
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/deaf_mute.dm
@@ -0,0 +1,85 @@
+
+/datum/rune_spell/deafmute
+ name = "Deaf-Mute"
+ desc = "Deafen nearby enemies. Including robots."
+ desc_talisman = "Deafen nearby enemies. Including robots. The effect is shorter than when used from a rune."
+ invocation = "Sti' kaliedir!"
+ word1 = /datum/rune_word/hide
+ word2 = /datum/rune_word/other
+ word3 = /datum/rune_word/see
+ page = "This rune causes every non-cultist (both humans and robots) in a 7 tile radius to be unable to hear for 50 seconds. \
+ The durations are halved when cast from a talisman, unless you slap someone directly with one, which will also limits the effects to them.\
+
This rune is great to sow disorder and delay the arrival of security, and can potentially combo with a Stun talisman used on an area. The only downside is that you can't hear them scream while they are muted."
+ var/deaf_rune_duration= 50 SECONDS//times are in seconds
+ var/deaf_talisman_duration = 30 SECONDS
+ var/mute_rune_duration = 25 SECONDS
+ var/mute_talisman_duration = 15 SECONDS
+ var/effect_range = 7
+ touch_cast = 1
+
+/datum/rune_spell/deafmute/cast_touch(var/mob/living/M)
+ invoke(activator, invocation, 1)
+
+ var/deaf_duration = deaf_rune_duration
+ var/mute_duration = mute_rune_duration
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ if (!IS_CULTIST(M) && M.mind && M.stat != DEAD)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_2, "deafmute_papered", M)
+ M.overlay_fullscreen("deafborder", /atom/movable/screen/fullscreen/deafmute_border)//victims see a red overlay fade in-out for a second
+ M.update_fullscreen_alpha("deafborder", 100, 5)
+ if (!(HAS_TRAIT(M, TRAIT_DEAF)))
+ to_chat(M, span_notice("The world around you suddenly becomes quiet.") )
+ if (!(HAS_TRAIT(M, TRAIT_MUTE)))
+ if (iscarbon(M))
+ to_chat(M, span_warning("You feel a terrible chill! You find yourself unable to speak a word...") )
+ else if (issilicon(M))
+ to_chat(M, span_warning("A shortcut appears to have temporarily disabled your speaker!") )
+
+ ADD_TRAIT(M, TRAIT_MUTE, "rune")
+ ADD_TRAIT(M, TRAIT_DEAF, "rune")
+
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(remove_deaf), M), deaf_duration)
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(remove_mute), M), mute_duration)
+ spawn(8)
+ M.update_fullscreen_alpha("deafborder", 0, 5)
+ sleep(8)
+ M.clear_fullscreen("deafborder", animated = FALSE)
+
+ qdel(src)
+
+/datum/rune_spell/deafmute/cast(var/deaf_duration = deaf_rune_duration, var/mute_duration = mute_rune_duration)
+ for(var/mob/living/M in range(effect_range, get_turf(spell_holder)))
+ if (IS_CULTIST(M))
+ continue
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ if (M.stat != DEAD)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_2, "deafmute", M)
+ M.overlay_fullscreen("deafborder", /atom/movable/screen/fullscreen/deafmute_border)//victims see a red overlay fade in-out for a second
+ M.update_fullscreen_alpha("deafborder", 100, 5)
+ if (!(HAS_TRAIT(M, TRAIT_DEAF)))
+ to_chat(M, span_notice("The world around you suddenly becomes quiet.") )
+ if (!(HAS_TRAIT(M, TRAIT_MUTE)))
+ if (iscarbon(M))
+ to_chat(M, span_warning("You feel a terrible chill! You find yourself unable to speak a word...") )
+ else if (issilicon(M))
+ to_chat(M, span_warning("A shortcut appears to have temporarily disabled your speaker!") )
+ ADD_TRAIT(M, TRAIT_MUTE, "rune")
+ ADD_TRAIT(M, TRAIT_DEAF, "rune")
+
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(remove_deaf), M), deaf_duration)
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(remove_mute), M), mute_duration)
+
+ spawn(8)
+ M.update_fullscreen_alpha("deafborder", 0, 5)
+ sleep(8)
+ M.clear_fullscreen("deafborder", animated = FALSE)
+ qdel(spell_holder)
+
+/datum/rune_spell/deafmute/cast_talisman()
+ cast(deaf_talisman_duration, mute_talisman_duration)
+
+/proc/remove_deaf(mob/remover) //fuicking why does this not work on server if its not a global
+ REMOVE_TRAIT(remover, TRAIT_DEAF, "rune")
+
+/proc/remove_mute(mob/remover)
+ REMOVE_TRAIT(remover, TRAIT_MUTE, "rune")
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/door.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/door.dm
new file mode 100644
index 000000000000..c7e2a421cda2
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/door.dm
@@ -0,0 +1,30 @@
+
+/datum/rune_spell/door
+ name = "Door"
+ desc = "Raise a door to impede your enemies. It automatically opens and closes behind you, but the others may eventually break it down."
+ desc_talisman = "Use to remotely trigger the rune and have it spawn a door to block your enemies."
+ invocation = "Khari'd! Eske'te tannin!"
+ word1 = /datum/rune_word/destroy
+ word2 = /datum/rune_word/travel
+ word3 = /datum/rune_word/self
+ talisman_absorb = RUNE_CAN_ATTUNE
+ page = "This rune spawns a Cult Door immediately upon use, for a cost of 10u of blood.\
+
This rune cannot be activated if there's another cult door currently adjacent to it.\
+
Cult doors can be broken down relatively quickly with weapons, but let cultist move through them with barely any slowdown, making them great to retreat. Spawning them in maintenance will exasperate the crew.\
+
Lastly, the rune can be attuned to a talisman to be remotely activated. Allowing for interesting traps if the rune was concealed."
+ cost_invoke = 10
+
+/datum/rune_spell/door/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ if (istype(R))
+ R.one_pulse()
+
+ if (pay_blood())
+ if (locate(/obj/machinery/door/airlock/cult) in range(spell_holder, 1))
+ abort(RITUALABORT_NEAR)
+ else
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ var/obj/machinery/door/airlock/cult/new_door = new /obj/machinery/door/airlock/cult(get_turf(spell_holder))
+ cult_datum.gain_devotion(10, DEVOTION_TIER_1, "summon_door", new_door)
+ qdel(spell_holder)
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/fervor.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/fervor.dm
new file mode 100644
index 000000000000..2f71e9d4a8f6
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/fervor.dm
@@ -0,0 +1,43 @@
+
+/datum/rune_spell/fervor
+ name = "Fervor"
+ desc = "Inspire nearby cultists to purge their stuns and raise their movement speed."
+ desc_talisman = "Use to inspire nearby cultists to purge their stuns and raise their movement speed."
+ invocation = "Khari'd! Gual'te nikka!"
+ word1 = /datum/rune_word/travel
+ word2 = /datum/rune_word/technology
+ word3 = /datum/rune_word/other
+ page = "For a 20u blood cost, this rune immediately buffs all cultists in a 7 tile range by immediately removing any stuns, oxygen loss damage, holy water, and various other bad conditions.\
+
Additionally, it injects them with 3u of determination, negating slowdown from low health or clothing. This makes it a very potent rune in a fight, especially as a follow up to a flash bang, or prior to a fight. Best used as a talisman. "
+ cost_invoke = 20
+ var/effect_range = 7
+
+/datum/rune_spell/fervor/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ if (istype(R))
+ R.one_pulse()
+
+ if (pay_blood())
+ for(var/mob/living/L in range(effect_range, get_turf(spell_holder)))
+ if (iscarbon(L))
+ var/mob/living/carbon/carbon = L
+ if (carbon.occult_muted())
+ continue
+ if(L.stat != DEAD && IS_CULTIST(L))
+ if (L != activator)
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_1, "fervor", L)
+ playsound(L, 'monkestation/code/modules/bloody_cult/sound/fervor.ogg', 50, 0, -2)
+ anim(target = L, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_fervor", plane = ABOVE_LIGHTING_PLANE, direction = L.dir)
+ L.oxyloss = 0
+ L.SetParalyzed(0)
+ L.SetStun(0)
+ L.SetKnockdown(0)
+ L.bodytemperature = L.standard_body_temperature
+ L.reagents?.add_reagent(/datum/reagent/determination, 3)
+
+ L.stat = CONSCIOUS
+ if (L.reagents)
+ L.reagents.del_reagent(/datum/reagent/water/holywater)
+ qdel(spell_holder)
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/paraphernalia.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/paraphernalia.dm
new file mode 100644
index 000000000000..9c5580e589ec
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/paraphernalia.dm
@@ -0,0 +1,192 @@
+
+/datum/rune_spell/paraphernalia
+ name = "Paraphernalia"
+ desc = "Produce various apparatus such as talismans."
+ desc_talisman = "LIKE, HOW, NO SERIOUSLY CALL AN ADMIN."
+ invocation = "H'drak v'loso, mir'kanas verbot!"
+ word1 = /datum/rune_word/hell
+ word2 = /datum/rune_word/technology
+ word3 = /datum/rune_word/join
+ cost_invoke = 2
+ cost_upkeep = 1
+ remaining_cost = 5
+ talisman_absorb = RUNE_CANNOT
+ var/obj/item/weapon/tome/target = null
+ var/obj/item/weapon/talisman/tool = null
+ page = "This rune lets you conjure occult items carefully crafted in the realm of Nar-Sie, such as the tome you are currently holding, or talismans that let you carry a rune's power in your pocket.\
+
Each conjured item takes a small drop of your blood so be sure to manage yourself.\
+
Once you've imbued a rune into a talisman, you can then place the talisman back on top of this rune and activate it again to send it to one of your fellow cultist's arcane tome should they carry one.\
+
This rune persists upon use, allowing repeated usage."
+
+
+/datum/rune_spell/paraphernalia/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ var/obj/item/weapon/talisman/AT = locate() in get_turf(spell_holder)
+ if (AT)
+ if (AT.spell_type)
+ var/mob/living/user = activator
+ var/list/valid_tomes = list()
+ var/i = 0
+ for (var/obj/item/weapon/tome/T in arcane_tomes)
+ var/mob/M = locate(/mob/living) in T.contents
+ if (M && IS_CULTIST(M))
+ i++
+ valid_tomes["[i] - Tome carried by [M.real_name] ([T.talismans.len]/[MAX_TALISMAN_PER_TOME])"] = T
+
+ for (var/datum/action/cooldown/spell/cult/arcane_dimension/A in arcane_pockets)
+ if (A.owner && A.owner.loc && ismob(A.owner) && A.stored_tome)
+ i++
+ var/mob/M = A.owner
+ valid_tomes["[i] - Tome in [M.real_name]'s arcane dimension ([A.stored_tome.talismans.len]/[MAX_TALISMAN_PER_TOME])"] = A.stored_tome
+
+ if (valid_tomes.len <= 0)
+ to_chat(user, span_warning("No cultists are currently carrying a tome.") )
+ qdel(src)
+ return
+
+ var/datum/rune_spell/spell = AT.spell_type
+ var/chosen_tome = input(user, "Choose a tome where to transfer this [initial(spell.name)] talisman.", "Transfer talisman", null) as null|anything in valid_tomes
+ if (!chosen_tome)
+ qdel(src)
+ return
+
+ target = valid_tomes[chosen_tome]
+ tool = AT
+
+ if (target.talismans.len >= MAX_TALISMAN_PER_TOME)
+ to_chat(activator, span_warning("This tome cannot contain any more talismans.") )
+ abort(RITUALABORT_FULL)
+ return
+
+ R.one_pulse()
+ contributors.Add(user)
+ update_progbar()
+ if (user.client)
+ user.client.images |= progbar
+ spell_holder.overlays += image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ spawn()
+ payment()
+ else
+ to_chat(activator, span_warning("You may only transfer an imbued or attuned talisman.") )
+ qdel(src)
+ else
+ var/list/choices = list(
+ list("Talisman", "radial_paraphernalia_talisman", "Can absorb runes (or attune to them in some cases), allowing you to carry their power in your pocket. Has a few other miscellaneous uses."),
+ list("Blood Candle", "radial_paraphernalia_candle", "A candle that can burn up to a full hour. Offers moody lighting."),
+ list("Tempting Goblet", "radial_paraphernalia_goblet", "A classy holder for your beverage of choice. Prank your enemies by hitting them with a goblet full of blood."),
+ list("Ritual Knife", "radial_paraphernalia_knife", "A long time ago a wizard enchanted one of those to infiltrate the realm of Nar-Sie and steal some soul stone shards. Now it's just a cool knife. Don't rely on it in a fight though."),
+ list("Arcane Tome", "radial_paraphernalia_tome", "Bring forth an arcane tome filled with Nar-Sie's knowledge. Contains a wealth of information regarding each runes, along with many other aspects of the cult."),
+ )
+ var/list/made_choices = list()
+ for(var/list/choice in choices)
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', icon_state = choice[2])
+ option.info = span_boldnotice(choice[3])
+ made_choices[choice[1]] = option
+ var/task = show_radial_menu(activator, get_turf(spell_holder), made_choices, tooltips = TRUE, radial_icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi')
+ if (!spell_holder.Adjacent(activator) || !task || QDELETED(src))
+ qdel(src)
+ return
+ if (pay_blood())
+ R.one_pulse()
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "conjure_paraphernalia", task)
+ var/obj/spawned_object
+ var/turf/T = get_turf(spell_holder)
+ switch (task)
+ if ("Talisman")
+ spawned_object = new /obj/item/weapon/talisman(T)
+ if ("Blood Candle")
+ spawned_object = new /obj/item/candle/blood(T)
+ if ("Tempting Goblet")
+ spawned_object = new /obj/item/reagent_containers/cup/cult(T)
+ if ("Ritual Knife")
+ spawned_object = new /obj/item/knife/ritual(T)
+ if ("Arcane Tome")
+ spawned_object = new /obj/item/weapon/tome(T)
+ spell_holder.visible_message(span_rose("The blood drops merge into the rune, and \a [spawned_object] materializes on top.") )
+ anim(target = spawned_object, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_imbue")
+ new /obj/effect/afterimage/black(T, spawned_object)
+ qdel(src)
+
+
+/datum/rune_spell/paraphernalia/midcast(mob/add_cultist) // failsafe should someone be hogging the radial menu.
+ var/obj/effect/new_rune/R = spell_holder
+ R.active_spell = null
+ R.trigger(add_cultist)
+ qdel(src)
+
+/datum/rune_spell/paraphernalia/abort(var/cause)
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ ..()
+
+
+/datum/rune_spell/paraphernalia/cast_talisman()//there should be no ways for this to ever proc
+ return
+
+
+/datum/rune_spell/paraphernalia/proc/payment()
+ var/failsafe = 0
+ while(failsafe < 1000)
+ failsafe++
+
+ if (tool && tool.loc != spell_holder.loc)
+ abort(RITUALABORT_TOOLS)
+
+ //are our payers still here and about?
+ for(var/mob/living/L in contributors)
+ if (!IS_CULTIST(L) || !(L in range(spell_holder, 1)) || (L.stat != CONSCIOUS))
+ if (L.client)
+ L.client.images -= progbar
+ contributors.Remove(L)
+ //alright then, time to pay in blood
+ var/amount_paid = 0
+ for(var/mob/living/L in contributors)
+ var/data = use_available_blood(L, cost_upkeep, contributors[L])
+ if (data[BLOODCOST_RESULT] == BLOODCOST_FAILURE)//out of blood are we?
+ contributors.Remove(L)
+ else
+ amount_paid += data[BLOODCOST_TOTAL]
+ contributors[L] = data[BLOODCOST_RESULT]
+ make_tracker_effects(L.loc, spell_holder, 1, "soul", 3, /obj/effect/tracker/drain, 1)//visual feedback
+
+ accumulated_blood += amount_paid
+
+ //if there's no blood for over 3 seconds, the channeling fails
+ if (amount_paid)
+ cancelling = 3
+ else
+ cancelling--
+ if (cancelling <= 0)
+ abort(RITUALABORT_BLOOD)
+ return
+
+
+ if (accumulated_blood >= remaining_cost)
+ success()
+ return
+
+ update_progbar()
+
+ sleep(10)
+ message_admins("A rune ritual has iterated for over 1000 blood payment procs. Something's wrong there.")
+
+/datum/rune_spell/paraphernalia/proc/success()
+ for(var/mob/living/L in contributors)
+ if (L.client)
+ L.client.images -= progbar
+ contributors.Remove(L)
+ if (progbar)
+ progbar.loc = null
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+
+ if (target.talismans.len < MAX_TALISMAN_PER_TOME)
+ target.talismans.Add(tool)
+ tool.forceMove(target)
+ to_chat(activator, span_notice("You slip \the [tool] into \the [target].") )
+ if (target.state == TOME_OPEN && ismob(target.loc))
+ var/mob/M = target.loc
+ M << browse(target.tome_text(), "window = arcanetome;size = 537x375")
+ else
+ to_chat(activator, span_warning("This tome cannot contain any more talismans.") )
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/path_enter.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/path_enter.dm
new file mode 100644
index 000000000000..27aa1fc983ab
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/path_enter.dm
@@ -0,0 +1,112 @@
+
+/datum/rune_spell/portalentrance
+ name = "Path Entrance"
+ desc = "Take a shortcut through the veil between this world and the other one."
+ desc_talisman = "Use to remotely trigger the rune and force objects and creatures on top through the Path."
+ invocation = "Sas'so c'arta forbici!"
+ word1 = /datum/rune_word/travel
+ word2 = /datum/rune_word/self
+ word3 = /datum/rune_word/other
+ talisman_absorb = RUNE_CAN_ATTUNE
+ can_conceal = 1
+ page = "This rune lets you set teleportation networks between any two tiles in the worlds, when used in combination with the Path Exit rune. \
+ Upon its first use, the rune asks you to set a path for it to attune to. There are 10 possible paths, each corresponding to a cult word.\
+
Upon subsequent uses the rune will, after a 1 second delay, teleport everything not anchored above it to the Path Exit attuned to the same word (if there aren't any, no teleportation will occur).\
+
Talismans will remotely activate this rune.\
+
You can deactivate a Path Entrance by simply using the Erase Word spell on it once, and rewrite Other afterwards.\
+
Lastly if the crew destroys this rune using salt or holy salts, they will learn the direction toward the corresponding Exit if it's on the same level."
+ var/network = ""
+
+/datum/rune_spell/portalentrance/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ var/list/available_networks = GLOB.rune_words_english.Copy()
+
+ network = input(activator, "Choose an available Path, you may change paths later by erasing the rune.", "Path Entrance") as null|anything in available_networks
+ if (!network)
+ qdel(src)
+ return
+
+ var/datum/rune_word/W = GLOB.rune_words[network]
+
+ invoke(activator, "[W.rune]")
+ var/image/I_crystals = image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "path_pad")
+ SET_PLANE_EXPLICIT(I_crystals, GAME_PLANE, R)
+ var/image/I_stone = image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "path_entrance")
+ SET_PLANE_EXPLICIT(I_crystals, GAME_PLANE_UPPER, R)
+ I_stone.appearance_flags |= RESET_COLOR//we don't want the stone to pulse
+
+ var/image/I_network
+ var/lookup = "[W.english]-0-[COLOR_BLOOD]"//0 because the rune will pulse anyway, and make this overlay pulse along
+ if (lookup in rune_appearances_cache)
+ I_network = image(rune_appearances_cache[lookup])
+ else
+ I_network = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, W.english)
+ I_network.color = COLOR_BLOOD
+ SET_PLANE_EXPLICIT(I_network, GAME_PLANE, R)
+ I_network.transform /= 1.5
+ I_network.pixel_x = round(W.offset_x*0.75)
+ I_network.pixel_y = -3 + round(W.offset_y*0.75)
+
+ spell_holder.overlays.len = 0
+ spell_holder.overlays += I_crystals
+ spell_holder.overlays += I_stone
+ spell_holder.overlays += I_network
+ custom_rune = TRUE
+
+ var/datum/holomap_marker/marker = new(R)
+ marker.id = HOLOMAP_MARKER_CULT_ENTRANCE
+ marker.filter = HOLOMAP_FILTER_CULT
+ marker.x = R.x
+ marker.y = R.y
+ marker.z = R.z
+ marker.icon = 'monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi'
+ marker.icon_state = "path_entrance"
+
+ to_chat(activator, span_notice("This rune will now let you travel through the \"[network]\" Path.") )
+
+ var/datum/antagonist/cult/cult_datum = IS_CULTIST(activator)
+ cult_datum?.gain_devotion(30, DEVOTION_TIER_1, "new_path_entrance", R)
+
+ talisman_absorb = RUNE_CAN_ATTUNE//once the network has been set, talismans will attune instead of imbue
+
+/datum/rune_spell/portalentrance/midcast(var/mob/add_cultist, turf/cast_from)
+ if (istype(spell_holder, /obj/item/weapon/talisman))
+ invoke(add_cultist, invocation, 1)
+ else
+ invoke(add_cultist, invocation)
+
+ var/turf/destination = null
+ for (var/datum/rune_spell/portalexit/P in GLOB.bloodcult_exitportals)
+ if (P.network == network)
+ destination = get_turf(P.spell_holder)
+ break
+
+ if (!destination)
+ to_chat(activator, span_warning("The \"[network]\" Path is closed. Set up a Path Exit rune to establish a Path.") )
+ return
+
+ var/datum/antagonist/cult/cult_datum = add_cultist.mind.has_antag_datum(/datum/antagonist/cult)
+
+ var/turf/T = get_turf(spell_holder)
+ if(cast_from)
+ T = cast_from
+ var/obj/effect/abstract/landing_animation = anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "cult_jaunt_prepare", plane = GAME_PLANE_UPPER)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_prepare.ogg', 75, 0, -3)
+ spawn(10)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/cultjaunt_land.ogg', 30, 0, -3)
+ var/obj/effect/bloodcult_jaunt/new_jaunt = new /obj/effect/bloodcult_jaunt(T, null, destination, T, activator = activator)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "path_entrance", new_jaunt)
+ flick("cult_jaunt_land", landing_animation)
+
+/datum/rune_spell/portalentrance/midcast_talisman(var/mob/add_cultist)
+ midcast(add_cultist, get_turf(add_cultist))
+
+/datum/rune_spell/portalentrance/salt_act(var/turf/T)
+ var/turf/destination = null
+ for (var/datum/rune_spell/portalexit/P in GLOB.bloodcult_exitportals)
+ if (P.network == network)
+ destination = get_turf(P.spell_holder)
+ new /obj/effect/bloodcult_jaunt/traitor(T, null, destination, null)
+ break
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/path_exit.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/path_exit.dm
new file mode 100644
index 000000000000..2b694757b2fd
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/path_exit.dm
@@ -0,0 +1,135 @@
+GLOBAL_LIST_INIT(bloodcult_exitportals, list())
+
+/datum/rune_spell/portalexit
+ name = "Path Exit"
+ desc = "We hope you enjoyed your flight with Air Nar-Sie."//might change it later or not.
+ desc_talisman = "Use to immediately jaunt through the Path."
+ invocation = "Sas'so c'arta forbici!"
+ word1 = /datum/rune_word/travel
+ word2 = /datum/rune_word/other
+ word3 = /datum/rune_word/self
+ talisman_absorb = RUNE_CAN_IMBUE
+ can_conceal = 1
+ page = "This rune lets you set free teleports between any two tiles in the worlds, when used in combination with the Path Entrance rune. \
+ Upon its first use, the rune asks you to set a path for it to attune to. There are 10 possible paths, each corresponding to a cult word.\
+
Unlike for entrances, there may only exist 1 exit for each path.\
+
By using a talisman on an attuned rune, the talisman will teleport you to that rune immediately upon use.\
+
By using a talisman on a non-attuned rune, the rune will be absorbed instead, and you'll be able to set a destination path on the talisman, allowing you to check which path exits currently exist.\
+
You can deactivate a Path Exit by simply using the Erase Word spell on it once, and rewrite Self afterwards.\
+
Lastly if an empty jaunt bubble pops over the rune with an ominous noise, that means a corresponding path entrance has been destroyed and the location of this rune might end up compromised."
+ var/network = ""
+
+/datum/rune_spell/portalexit/New()
+ ..()
+ GLOB.bloodcult_exitportals.Add(src)
+
+/datum/rune_spell/portalexit/Destroy()
+ GLOB.bloodcult_exitportals.Remove(src)
+ ..()
+
+/datum/rune_spell/portalexit/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ var/list/available_networks = GLOB.rune_words_english.Copy()
+ for (var/datum/rune_spell/portalexit/P in GLOB.bloodcult_exitportals)
+ if (P.network)
+ available_networks -= P.network
+
+ if (available_networks.len <= 0)
+ to_chat(activator, span_warning("There is no room for any more Paths through the veil.") )
+ qdel(src)
+ return
+
+ network = input(activator, "Choose an available Path, you may free the path later by erasing the rune.", "Path Exit") as null|anything in available_networks
+ if (!network)
+ qdel(src)
+ return
+
+ var/datum/holomap_marker/marker = new(R)
+ marker.id = HOLOMAP_MARKER_CULT_EXIT
+ marker.filter = HOLOMAP_FILTER_CULT
+ marker.x = R.x
+ marker.y = R.y
+ marker.z = R.z
+ marker.icon = 'monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi'
+ marker.icon_state = "path_exit"
+
+
+ var/datum/rune_word/W = GLOB.rune_words[network]
+
+ invoke(activator, "[W.rune]")
+ var/image/I_crystals = image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "path_crystals")
+ SET_PLANE_EXPLICIT(I_crystals, GAME_PLANE, spell_holder)
+ var/image/I_stone = image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "path_stone")
+ SET_PLANE_EXPLICIT(I_stone, GAME_PLANE, spell_holder)
+ I_stone.appearance_flags |= RESET_COLOR//we don't want the stone to pulse
+
+ var/image/I_network
+ var/lookup = "[W.english]-0-[COLOR_BLOOD]"//0 because the rune will pulse anyway, and make this overlay pulse along
+ if (lookup in rune_appearances_cache)
+ I_network = image(rune_appearances_cache[lookup])
+ else
+ I_network = image('monkestation/code/modules/bloody_cult/icons/deityrunes.dmi', src, W.english)
+ I_network.color = COLOR_BLOOD
+ SET_PLANE_EXPLICIT(I_network, GAME_PLANE, spell_holder)
+ I_network.transform /= 1.5
+ I_network.pixel_x = round(W.offset_x*0.75)
+ I_network.pixel_y = -3 + round(W.offset_y*0.75)
+
+ spell_holder.overlays.len = 0
+ spell_holder.overlays += I_crystals
+ spell_holder.overlays += I_stone
+ spell_holder.overlays += I_network
+ custom_rune = TRUE
+
+ to_chat(activator, span_notice("This rune will now serve as a destination for the \"[network]\" Path.") )
+
+ var/datum/antagonist/cult/cult_datum = IS_CULTIST(activator)
+ cult_datum?.gain_devotion(30, DEVOTION_TIER_1, "new_path_exit", R)
+
+ talisman_absorb = RUNE_CAN_ATTUNE//once the network has been set, talismans will attune instead of imbue
+
+/datum/rune_spell/portalexit/midcast(mob/add_cultist)
+ to_chat(add_cultist, span_notice("You may teleport to this rune by using a Path Entrance, or a talisman attuned to it.") )
+
+/datum/rune_spell/portalexit/midcast_talisman(var/mob/add_cultist)
+ var/turf/T = get_turf(add_cultist)
+ invoke(add_cultist, invocation, 1)
+ anim(target = T, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_teleport")
+ new /obj/effect/bloodcult_jaunt (T, add_cultist, get_turf(spell_holder))
+
+/datum/rune_spell/portalexit/cast_talisman()
+ var/obj/item/weapon/talisman/T = spell_holder
+ T.uses++//so the talisman isn't deleted when setting the network
+ var/list/valid_choices = list()
+ for (var/datum/rune_spell/portalexit/P in GLOB.bloodcult_exitportals)
+ if (P.network)
+ valid_choices.Add(P.network)
+ valid_choices[P.network] = P
+ if (valid_choices.len <= 0)
+ to_chat(activator, span_warning("There are currently no Paths through the veil.") )
+ qdel(src)
+ return
+ var/network = input(activator, "Choose an available Path.", "Path Talisman") as null|anything in valid_choices
+ if (!network)
+ qdel(src)
+ return
+
+ invoke(activator, "[GLOB.rune_words_english[network]]!", 1)
+
+ to_chat(activator, span_notice("This talisman will now serve as a key to the \"[network]\" Path.") )
+
+ var/datum/rune_spell/portalexit/PE = valid_choices[network]
+
+ T.attuned_rune = PE.spell_holder
+ T.word_pulse(GLOB.rune_words[network])
+
+/datum/rune_spell/portalexit/salt_act(var/turf/T)
+ if (T != spell_holder.loc)
+ var/turf/destination = null
+ for (var/datum/rune_spell/portalexit/P in GLOB.bloodcult_exitportals)
+ if (P.network == network)
+ destination = get_turf(P.spell_holder)
+ new /obj/effect/bloodcult_jaunt/traitor(T, null, destination, null)
+ break
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/raise_structure.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/raise_structure.dm
new file mode 100644
index 000000000000..2ac6b772e5cd
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/raise_structure.dm
@@ -0,0 +1,167 @@
+/datum/rune_spell/raisestructure
+ name = "Raise Structure"
+ desc = "Drag-in eldritch structures from the realm of Nar-Sie."
+ desc_talisman = "Use to begin raising a structure where you stand."
+ word1 = /datum/rune_word/blood
+ word2 = /datum/rune_word/technology
+ word3 = /datum/rune_word/join
+ cost_upkeep = 1
+ remaining_cost = 300
+ accumulated_blood = 0
+ page = "Channel this rune to create either an Altar, a Forge, or a Spire. You can speed up the ritual by having other cultist touch the rune, or by wearing cult garments. \
+
Altars let you commune with Nar-Sie, conjure soul gems, and keep tabs on the cult's members and activities over the station.\
+
Forges let you craft armors, powerful blades, as well as construct shells. Blades and shells can be combined with soul gems to great effect, \
+ but note that Forges tend to sear those who stay near them too long. You can mitigate the effect with cult apparel, or use the Fervor rune to reset your temperature.\
+
Spires provide easy communication for the cult in the entire region. Use :x (or .x, or #x) to use cult chat after one is built."
+ var/turf/loc_memory = null
+ var/spawntype = /obj/structure/cult/altar
+ var/structure
+
+/datum/rune_spell/raisestructure/proc/proximity_check()
+ var/obj/effect/new_rune/R = spell_holder
+ if (locate(/obj/structure/cult) in range(R.loc, 0))
+ abort(RITUALABORT_BLOCKED)
+ return FALSE
+
+ if (locate(/obj/machinery/door/airlock/cult) in range(R.loc, 1))
+ abort(RITUALABORT_NEAR)
+ return FALSE
+
+ else return TRUE
+
+/datum/rune_spell/raisestructure/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ var/mob/living/user = activator
+
+ proximity_check() //See above
+
+ var/list/choices = list(
+ list("Altar", "radial_altar", "The nexus of a cult base. Lets you commune with Nar-Sie, conjure soul gems, and keep tabs on the cult's members and activities over the station."),
+ list("Spire", "radial_spire", "Allows all cultists in the level to communicate with each others using :x"),
+ list("Forge", "radial_forge", "Can be used to forge of cult blades and armor, as well as construct shells. Standing close for too long without proper cult attire can be a searing experience."),
+ list("Pylon", "radial_pylon", "Provides some light in the surrounding area.")
+ )
+
+ var/list/made_choices = list()
+ for(var/list/choice in choices)
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', icon_state = choice[2])
+ option.info = span_boldnotice(choice[3])
+ made_choices[choice[1]] = option
+
+ structure = show_radial_menu(user, R.loc, made_choices ,tooltips = TRUE, radial_icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi')
+
+ if(!R.Adjacent(user) || !structure )
+ abort()
+ return
+
+ if(R.active_spell)
+ to_chat(user, span_rose("A structure is already being raised from this rune, so you contribute to that instead.") )
+ R.active_spell.midcast(user)
+ return
+
+ switch(structure)
+ if("Altar")
+ spawntype = /obj/structure/cult/altar
+ if("Spire")
+ spawntype = /obj/structure/cult/spire
+ if("Forge")
+ spawntype = /obj/structure/cult/forge
+ if("Pylon")
+ spawntype = /obj/structure/cult/pylon
+
+ if(!spell_holder)
+ return
+ loc_memory = spell_holder.loc
+ contributors.Add(user)
+ update_progbar()
+ if(user.client)
+ user.client.images |= progbar
+ spell_holder.overlays += image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ to_chat(activator, span_rose("This ritual can be sped up by having multiple cultists partake in it or by wearing cult attire.") )
+ spawn()
+ payment()
+
+/datum/rune_spell/raisestructure/cast_talisman() //Raise structure talismans create an invisible summoning rune beneath the caster's feet.
+ var/obj/effect/new_rune/R = new(get_turf(activator))
+ R.icon_state = "temp"
+ R.active_spell = new type(activator, R)
+ qdel(src)
+
+/datum/rune_spell/raisestructure/midcast(mob/add_cultist)
+ if (add_cultist in contributors)
+ return
+ invoke(add_cultist, invocation)
+ contributors.Add(add_cultist)
+ if (add_cultist.client)
+ add_cultist.client.images |= progbar
+
+/datum/rune_spell/raisestructure/abort(var/cause)
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "runetrigger-build")
+ ..()
+
+/datum/rune_spell/raisestructure/proc/payment()
+ var/failsafe = 0
+ while(failsafe < 1000)
+ failsafe++
+ //are our payers still here and about?
+ var/summoners = 2//the higher, the easier it is to perform the ritual without many cultists. default = 2
+ for(var/mob/living/L in contributors)
+ if (IS_CULTIST(L) && (L in range(spell_holder, 1)) && (L.stat == CONSCIOUS))
+ summoners++
+ summoners += round(L.get_cult_power()/30) //For every 30 cult power, you count as one additional cultist. So with Robes and Shoes, you already count as 3 cultists.
+ else //This makes using the rune alone hard at roundstart, but fairly easy later on.
+ if (L.client)
+ L.client.images -= progbar
+ contributors.Remove(L)
+ var/amount_paid = 0
+ for(var/mob/living/L in contributors)
+ var/data = use_available_blood(L, cost_upkeep, contributors[L])
+ if (data[BLOODCOST_RESULT] == BLOODCOST_FAILURE)//out of blood are we?
+ contributors.Remove(L)
+ else
+ amount_paid += data[BLOODCOST_TOTAL]
+ contributors[L] = data[BLOODCOST_RESULT]
+ make_tracker_effects(L.loc, spell_holder, 1, "soul", 3, /obj/effect/tracker/drain, 1)//visual feedback
+
+ accumulated_blood += amount_paid
+
+ if(amount_paid) //3 seconds without blood and the ritual fails.
+ cancelling = 3
+ else
+ cancelling--
+ if (cancelling <= 0)
+ if(accumulated_blood && !(locate(/obj/effect/decal/cleanable/blood/splatter) in loc_memory))
+ var/obj/effect/decal/cleanable/blood/splatter/S = new (loc_memory)//splash
+ S.count = 2
+ abort(RITUALABORT_BLOOD)
+ return
+
+ switch(summoners)
+ if (1)
+ remaining_cost = 300
+ if (2)
+ remaining_cost = 120
+ if (3)
+ remaining_cost = 18
+ if (4 to INFINITY)
+ remaining_cost = 0
+
+ if(accumulated_blood >= remaining_cost )
+ proximity_check()
+ success()
+ return
+
+ update_progbar()
+
+ sleep(10)
+ message_admins("A rune ritual has iterated for over 1000 blood payment procs. Something's wrong there.")
+
+/datum/rune_spell/raisestructure/proc/success()
+ for(var/mob/living/L in contributors)
+ var/datum/antagonist/cult/cult_datum = L.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_1, "raise_structure", structure)
+ new spawntype(spell_holder.loc)
+ qdel(spell_holder) //Deletes the datum as well.
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/reincarnation.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/reincarnation.dm
new file mode 100644
index 000000000000..b06c6786d47c
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/reincarnation.dm
@@ -0,0 +1,180 @@
+/mob/living/carbon/human/death(gibbed, cause_of_death)
+ . = ..()
+ if(!client || !IS_CULTIST(src))
+ return
+ var/mob/living/basic/shade/shade = new (get_turf(src))
+ shade.name = "[real_name] the Shade"
+ shade.real_name = "[real_name]"
+ mind.transfer_to(shade)
+
+ to_chat(shade, span_cult("Dark energies rip your dying body appart, anchoring your soul inside the form of a Shade. You retain your memories, and devotion to the cult."))
+ shade.body = src
+ src.forceMove(shade)
+
+/datum/rune_spell/reincarnation
+ name = "Reincarnation"
+ desc = "Provide shades with a replica of their original body."
+ desc_talisman = "Provide shades with a replica of their original body."
+ invocation = "Pasnar val'keriam usinar. Savrae ines amutan. Yam'toth remium il'tarat!"
+ word1 = /datum/rune_word/blood
+ word2 = /datum/rune_word/join
+ word3 = /datum/rune_word/hell
+ page = "This rune lets you provide a shade with a body replicated from the one they originally had (or at least the one their soul remembers them having)\
+
The shade must stand above the rune for the ritual to begin. However mind that this rune has a very steep cost in blood of 300u that have to be paid over 60 seconds of channeling. \
+ Other cultists can join in the ritual to help you share the burden you might prefer having a construct use their connection to the other side to bypass the blood cost entirely.\
+
Note that the resulting body might look much paler than the original, this is an unfortunate side-effect that you may have to resolve on your own.\
+
This rune persists upon use, allowing repeated usage."
+ cost_upkeep = 5
+ remaining_cost = 300
+ var/obj/effect/cult_ritual/resurrect/husk = null
+ var/mob/living/basic/shade/shade = null
+
+/datum/rune_spell/reincarnation/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ shade = locate(/mob/living/basic/shade) in R.loc
+ if (!shade)
+ to_chat(activator, span_warning("There needs to be a shade standing above the rune.") )
+ qdel(src)
+ return
+
+ husk = new (R.loc)
+ flick("rune_resurrect_start", husk)
+ shade.forceMove(husk)
+
+ contributors.Add(activator)
+ update_progbar()
+ if (activator.client)
+ activator.client.images |= progbar
+ spell_holder.overlays += image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "build")
+ to_chat(activator, span_rose("This ritual has a very high blood cost per second, but it can be completed faster by having multiple cultists partake in it.") )
+ spawn()
+ payment()
+
+/datum/rune_spell/reincarnation/cast_talisman()//we spawn an invisible rune under our feet that works like the regular one
+ var/obj/effect/new_rune/R = new(get_turf(activator))
+ R.icon_state = "temp"
+ R.active_spell = new type(activator, R)
+ qdel(src)
+
+/datum/rune_spell/reincarnation/midcast(mob/add_cultist)
+ if (add_cultist in contributors)
+ return
+ invoke(add_cultist, invocation)
+ contributors.Add(add_cultist)
+ if (add_cultist.client)
+ add_cultist.client.images |= progbar
+
+/datum/rune_spell/reincarnation/abort(var/cause)
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "build")
+ if (shade)
+ shade.forceMove(get_turf(husk))
+ if (husk)
+ qdel(husk)
+ if (spell_holder.loc && (!cause || cause != RITUALABORT_MISSING))
+ new /obj/effect/gibspawner/human(spell_holder.loc)
+ ..()
+
+/datum/rune_spell/reincarnation/proc/payment()
+ var/failsafe = 0
+ while(failsafe < 1000)
+ failsafe++
+ //are our payers still here and about?
+ for(var/mob/living/L in contributors)
+ if (!IS_CULTIST(L) || !(L in range(spell_holder, 1)) || (L.stat != CONSCIOUS))
+ if (L.client)
+ L.client.images -= progbar
+ contributors.Remove(L)
+ //alright then, time to pay in blood
+ var/amount_paid = 0
+ for(var/mob/living/L in contributors)
+ var/data = use_available_blood(L, cost_upkeep, contributors[L])
+ if (data[BLOODCOST_RESULT] == BLOODCOST_FAILURE)//out of blood are we?
+ contributors.Remove(L)
+ else
+ amount_paid += data[BLOODCOST_TOTAL]
+ contributors[L] = data[BLOODCOST_RESULT]
+ make_tracker_effects(L.loc, spell_holder, 1, "soul2", 3, /obj/effect/tracker/drain, 1)//visual feedback
+
+ accumulated_blood += amount_paid
+
+ //if there's no blood for over 3 seconds, the channeling fails
+ if (amount_paid)
+ cancelling = 3
+ else
+ cancelling--
+ if (cancelling <= 0)
+ abort(RITUALABORT_BLOOD)
+ return
+
+ if (accumulated_blood >= remaining_cost)
+ success()
+ return
+
+ update_progbar()
+
+ sleep(10)
+ message_admins("A rune ritual has iterated for over 1000 blood payment procs. Something's wrong there.")
+
+/datum/rune_spell/reincarnation/proc/success()
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "build")
+ var/resurrector = activator.real_name
+ if (shade && husk)
+ shade.forceMove(get_turf(husk))
+ var/mob/living/carbon/human/M = new /mob/living/carbon/human(shade.loc)
+ shade.client?.prefs.apply_prefs_to(M, TRUE)
+ M.mind = shade.mind
+ M.key = shade.key
+ qdel(husk)
+ qdel(shade)
+ playsound(M, 'monkestation/code/modules/bloody_cult/sound/spawn.ogg', 50, 0, 0)
+ var/datum/antagonist/cult/newCultist = M.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (!newCultist)
+ newCultist = new(M?.mind)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ cult.HandleRecruitedRole(newCultist)
+ newCultist.conversion["resurrected"] = resurrector
+
+ if (ishuman(M))
+ // purely cosmetic tattoos. giving cultists some way to have tattoos until those get reworked
+ newCultist.tattoos[TATTOO_POOL] = new /datum/cult_tattoo/bloodpool()
+ newCultist.tattoos[TATTOO_HOLY] = new /datum/cult_tattoo/holy()
+ newCultist.tattoos[TATTOO_MANIFEST] = new /datum/cult_tattoo/manifest()
+
+ M.regenerate_icons()
+
+ for(var/mob/living/L in contributors)
+ var/datum/antagonist/cult/cult_datum = L.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(200, DEVOTION_TIER_3, "reincarnation", M)
+
+ else
+ for(var/mob/living/L in contributors)
+ to_chat(L, span_warning("Something went wrong with the ritual, the shade appears to have vanished.") )
+
+
+ for(var/mob/living/L in contributors)
+ if (L.client)
+ L.client.images -= progbar
+ contributors.Remove(L)
+
+ if (activator && activator.client)
+ activator.client.images -= progbar
+
+ if (progbar)
+ progbar.loc = null
+
+ if (spell_holder.icon_state == "temp")
+ qdel(spell_holder)
+ else
+ qdel(src)
+
+/obj/effect/cult_ritual/resurrect
+ anchored = 1
+ icon_state = "rune_resurrect"
+ plane = ABOVE_GAME_PLANE
+ mouse_opacity = 0
+
+/obj/effect/cult_ritual/resurrect/New(turf/loc)
+ ..()
+ overlays += "summoning"
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/reveal.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/reveal.dm
new file mode 100644
index 000000000000..3b067b53791f
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/reveal.dm
@@ -0,0 +1,207 @@
+
+///this is hell and should be replaced.
+/image/reveal
+ var/client/owner_client
+
+/image/reveal/proc/set_client(mob/user)
+ owner_client = user.client
+ RegisterSignal(owner_client.mob, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(offset_image))
+
+/image/reveal/Destroy(force)
+ . = ..()
+ UnregisterSignal(owner_client.mob, COMSIG_MOVABLE_PRE_MOVE)
+ owner_client.images -= src
+
+/image/reveal/proc/offset_image(atom/mover, turf/new_loc)
+ if(new_loc.density)
+ return // this is sanity checking incase running into wall
+ var/direction = get_dir(mover, new_loc)
+
+ switch(direction)
+ if(NORTH)
+ pixel_y -= 32
+ if(SOUTH)
+ pixel_y += 32
+ if(EAST)
+ pixel_x -= 32
+ if(WEST)
+ pixel_x += 32
+
+/datum/rune_spell/reveal
+ name = "Reveal"
+ desc = "Reveal what you have previously hidden, terrifying enemies in the process."
+ desc_talisman = "Reveal what you have previously hidden, terrifying enemies in the process. The effect is shorter than when used from a rune."
+ invocation = "Nikt'o barada kla'atu!"
+ word1 = /datum/rune_word/blood
+ word2 = /datum/rune_word/see
+ word3 = /datum/rune_word/hide
+ page = "This rune (whose words are the same as the Conceal rune in reverse) lets you reveal every rune and structures in a circular 7 tile range around it.\
+
Each revealed rune will stun non-cultists in a 3 tile range around them, stunning and muting them for 2 seconds, up to a total of 10 seconds. Affects through walls. The stun ends if the victims are moved away from where they stand, unless they get knockdown first, so you might want to follow up with a Stun talisman."
+
+ walk_effect = TRUE
+
+ var/effect_range = 7
+ var/shock_range = 3
+ var/shock_per_obj = 2
+ var/max_shock = 10
+ var/last_threshold = -1
+ var/total_uses = 5
+
+/datum/rune_spell/reveal/cast()
+ var/turf/T = get_turf(spell_holder)
+ var/list/shocked = list()
+ to_chat(activator, span_notice("All concealed runes and cult structures in range phase back into reality, stunning nearby foes.") )
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/reveal.ogg', 50, 0, -3)
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+
+ for(var/obj/structure/cult/concealed/S in range(effect_range, T))//only concealed structures trigger the effect
+ var/dist = cheap_pythag(S.x - T.x, S.y - T.y)
+ if (dist <= effect_range+0.5)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "reveal_structure", S)
+ anim(target = S, a_icon = 'monkestation/code/modules/bloody_cult/icons/224x224.dmi', flick_anim = "rune_reveal", offX = -32*shock_range, offY = -32*shock_range, plane = ABOVE_LIGHTING_PLANE)
+ for(var/mob/living/L in viewers(S))
+ if (IS_CULTIST(L))
+ continue
+ var/dist2 = cheap_pythag(L.x - S.x, L.y - S.y)
+ if (dist2 > shock_range+0.5)
+ continue
+ shadow(L, S.loc, "rune_reveal")
+ if (L in shocked)
+ shocked[L] = min(shocked[L]+shock_per_obj, max_shock)
+ else
+ shocked[L] = 2
+ S.reveal()
+
+ for(var/obj/effect/new_rune/R in range(effect_range, T))
+ var/dist = cheap_pythag(R.x - T.x, R.y - T.y)
+ if (dist <= effect_range+0.5)
+ if (R.reveal())//only hidden runes trigger the effect
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "reveal_rune", R)
+ anim(target = R, a_icon = 'monkestation/code/modules/bloody_cult/icons/224x224.dmi', flick_anim = "rune_reveal", offX = -32*shock_range, offY = -32*shock_range, plane = ABOVE_LIGHTING_PLANE)
+ for(var/mob/living/L in viewers(R))
+ if (IS_CULTIST(L))
+ continue
+ var/dist2 = cheap_pythag(L.x - R.x, L.y - R.y)
+ if (dist2 > shock_range+0.5)
+ continue
+ shadow(L, R.loc, "rune_reveal")
+ if (L in shocked)
+ shocked[L] = min(shocked[L]+shock_per_obj, max_shock)
+ else
+ shocked[L] = 2
+
+ for(var/mob/living/L in shocked)
+ if (L.stat != DEAD)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_2, "reveal_stun", L)
+ new /obj/effect/cult_ritual/reveal(L.loc, L, shocked[L])
+ to_chat(L, span_danger("You feel a terrifying shock resonate within your body as the hidden runes are revealed!") )
+ L.update_fullscreen_alpha("shockborder", 100, 5)
+ spawn(8)
+ L.update_fullscreen_alpha("shockborder", 0, 5)
+ sleep(8)
+ L.clear_fullscreen("shockborder", animated = FALSE)
+
+ qdel(spell_holder)
+
+/datum/rune_spell/reveal/Added(var/mob/mover)
+ if (total_uses <= 0)
+ return
+ if (!isliving(mover))
+ return
+ var/mob/living/L = mover
+ if (last_threshold + 20 SECONDS > world.time)
+ return
+ if (!IS_CULTIST(L))
+ total_uses--
+ last_threshold = world.time
+ var/list/seers = list()
+ for (var/mob/living/seer in range(7, get_turf(spell_holder)))
+ if (IS_CULTIST(seer) && seer.client)
+ seers += seer
+
+ cast_image(mover, seers, 10)
+
+/datum/rune_spell/reveal/proc/cast_image(mob/mover, list/seers, count)
+ if(count == 0)
+ return
+ var/mob/living/L = mover
+ for (var/mob/living/seer in seers)
+ if (QDELETED(seer))
+ seers -= seer
+ continue
+ var/image/reveal/image_intruder = new
+ image_intruder.appearance = L
+ image_intruder.loc = seer
+ image_intruder.dir = L.dir
+ var/delta_x = (L.x - seer.x)
+ var/delta_y = (L.y - seer.y)
+
+ image_intruder.set_client(seer)
+
+ image_intruder.pixel_x = delta_x*32
+ image_intruder.plane = ABOVE_LIGHTING_PLANE
+ image_intruder.pixel_y = delta_y*32
+ image_intruder.alpha = 200
+ image_intruder.color = COLOR_BLOOD
+
+ animate(image_intruder, alpha = 0, time = 3)
+ seer.client.images += image_intruder // see the mover for a set period of time
+ QDEL_IN(image_intruder, 4)
+
+ count--
+ addtimer(CALLBACK(src, PROC_REF(cast_image), mover, seers, count), 1 SECONDS)
+
+/datum/rune_spell/reveal/cast_talisman()
+ shock_per_obj = 1.5
+ max_shock = 8
+ cast()
+
+
+/obj/effect/cult_ritual/reveal
+ anchored = 1
+ icon_state = "rune_reveal"
+ plane = ABOVE_LIGHTING_PLANE
+ var/mob/living/victim = null
+ var/duration = 2
+
+/obj/effect/cult_ritual/reveal/Destroy()
+ victim = null
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "rune_reveal-stop", plane = ABOVE_LIGHTING_PLANE)
+ ..()
+
+/obj/effect/cult_ritual/reveal/New(var/turf/loc, var/mob/living/vic = null, var/dur = 2)
+ ..()
+ if (!vic)
+ vic = locate() in loc
+ if (!vic)
+ qdel(src)
+ return
+ playsound(loc, 'monkestation/code/modules/bloody_cult/sound/shock.ogg', 20, 0, 0)
+ victim = vic
+ duration = dur
+ victim.Stun(duration)
+ if (isalien(victim))
+ victim.Paralyze(duration)
+ spawn (duration*10)
+ if (src && loc && victim && victim.loc == loc && !victim.IsKnockdown())
+ to_chat(victim, span_warning("You come back to your senses.") )
+ victim.AdjustStun(-duration)
+ if (isalien(victim))
+ victim.AdjustParalyzed(-duration)
+ victim = null
+ qdel(src)
+
+/obj/effect/cult_ritual/reveal/HasProximity(var/atom/movable/AM)//Pulling victims will immediately dispel the effects
+ if (!victim)
+ qdel(src)
+ return
+
+ if (victim.loc != loc)
+ if (!victim.IsKnockdown())//if knockdown (by any cause), moving away doesn't purge you from the remaining stun.
+ if (victim.pulledby)
+ to_chat(victim, span_warning("You come back to your senses as \the [victim.pulledby] drags you away.") )
+ victim.AdjustStun(-duration)
+ if (isalien(victim))
+ victim.AdjustParalyzed(-duration)
+ victim = null
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/seer.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/seer.dm
new file mode 100644
index 000000000000..814a515371cb
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/seer.dm
@@ -0,0 +1,131 @@
+
+/datum/hover_data/convertability_data/setup_data(mob/living/carbon/source, mob/enterer)
+ . = ..()
+ if(IS_CULTIST(source))
+ return
+
+ var/image/new_image = new(source)
+ new_image.appearance = source.update_convertibility()
+ SET_PLANE_EXPLICIT(new_image, new_image.plane, source)
+ if(!isturf(source.loc))
+ new_image.loc = source.loc
+ else
+ new_image.loc = source
+ add_client_image(new_image, enterer.client)
+
+/mob/living/carbon/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/hovering_information, /datum/hover_data/convertability_data, TRAIT_SEER)
+
+/datum/rune_spell/seer
+ name = "Seer"
+ desc = "See the invisible, the dead, the concealed, and the propensity of the living to serve our agenda."
+ desc_talisman = "For a whole minute, you may see the invisible, the dead, the concealed, and the propensity of the living to serve our agenda."
+ invocation = "Rash'tla sektath mal'zua. Zasan therium viortia."
+ rune_flags = RUNE_STAND
+ talisman_uses = 10
+ word1 = /datum/rune_word/see
+ word2 = /datum/rune_word/hell
+ word3 = /datum/rune_word/join
+ page = "This rune grants the ability to see invisible ghosts, runes, and structures, but most of all, it also reveals the willingness of crew members to accept conversion, indicated by marks over their heads:\
+
Green marks indicate people who will always accept conversion.\
+
Yellow marks indicate people who might either accept or refuse.\
+
Red marks with two spikes indicate loyalty implanted crew members, who will thus automatically refuse conversion regardless of their will.\
+
Red marks with three spikes indicate crew members who have pledged themselves to fight the cult, and while they might not automatically refuse conversion, are very unlikely to be develop into useful cultists.\
+
Also note that you can activate runes while they are concealed. In talisman form, it has 10 uses that last for a minute each. Activate the talisman before moving into a public area so nobody hears you whisper the invocation.\
+
This rune persists upon use, allowing repeated usage."
+ cost_invoke = 5
+ var/obj/effect/cult_ritual/seer/seer_ritual = null
+ var/talisman_duration = 60 SECONDS
+
+/datum/rune_spell/seer/Destroy()
+ destroying_self = 1
+ if (seer_ritual && !seer_ritual.talisman)
+ qdel(seer_ritual)
+ seer_ritual = null
+ ..()
+
+/datum/rune_spell/seer/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ if (pay_blood())
+ seer_ritual = new /obj/effect/cult_ritual/seer(R.loc, activator, src)
+ else
+ qdel(src)
+
+/datum/rune_spell/seer/cast_talisman()
+ var/mob/living/M = activator
+
+ if (locate(/obj/effect/cult_ritual/seer) in M)
+ var/obj/item/weapon/talisman/T = spell_holder
+ T.uses++
+ to_chat(M, span_warning("You are still under the effects of a Seer talisman.") )
+ qdel(src)
+ return
+
+ M.see_invisible = SEE_INVISIBLE_OBSERVER
+ anim(target = M, a_icon = 'monkestation/code/modules/bloody_cult/icons/160x160.dmi', a_icon_state = "rune_seer", lay = ABOVE_OBJ_LAYER, offX = -32*2, offY = -32*2, plane = GAME_PLANE, invis = INVISIBILITY_OBSERVER, alph = 200, sleeptime = talisman_duration, animate_movement = TRUE)
+ new /obj/effect/cult_ritual/seer(activator, activator, null, TRUE, talisman_duration)
+ qdel(src)
+
+var/list/seer_rituals = list()
+
+/obj/effect/cult_ritual/seer
+ anchored = 1
+ icon = 'monkestation/code/modules/bloody_cult/icons/160x160.dmi'
+ icon_state = "rune_seer"
+ pixel_x = -32*2
+ pixel_y = -32*2
+ alpha = 200
+ invisibility = INVISIBILITY_OBSERVER
+ layer = ABOVE_OBJ_LAYER
+ plane = GAME_PLANE
+ mouse_opacity = 0
+ var/mob/living/caster = null
+ var/datum/rune_spell/seer/source = null
+ var/list/propension = list()
+ var/talisman = FALSE
+
+/obj/effect/cult_ritual/seer/New(var/turf/loc, var/mob/living/user, var/datum/rune_spell/seer/runespell, var/talisman_ritual = FALSE, var/talisman_duration = 60 SECONDS)
+ ..()
+ if(user)
+ ADD_TRAIT(user, TRAIT_SEER, REF(src))
+ seer_rituals.Add(src)
+ START_PROCESSING(SSobj, src)
+ talisman = talisman_ritual
+ caster = user
+ source = runespell
+ if (!caster)
+ if (source)
+ source.abort(RITUALABORT_GONE)
+ qdel(src)
+ return
+ to_chat(caster, span_notice("You find yourself able to see through the gaps in the veil. You can see and interact with the other side, and also find out the crew's propensity to be successfully converted, whether they are Willing, Uncertain, or Unconvertible.") )
+ if (talisman)
+ spawn(talisman_duration)
+ qdel(src)
+
+
+/obj/effect/cult_ritual/seer/Destroy()
+ seer_rituals.Remove(src)
+ STOP_PROCESSING(SSobj, src)
+ if(caster)
+ REMOVE_TRAIT(caster, TRAIT_SEER, REF(src))
+ to_chat(caster, span_notice("You can no longer discern through the veil.") )
+ caster = null
+ if (source)
+ source.abort()
+ source = null
+ ..()
+
+/obj/effect/cult_ritual/seer/HasProximity(var/atom/movable/AM)
+ if (!talisman)
+ if (!caster || caster.loc != loc)
+ qdel(src)
+
+/obj/effect/cult_ritual/seer/process(seconds_per_tick)
+ if(!HasProximity(caster))
+ caster.see_invisible = SEE_INVISIBLE_LIVING
+ return
+ caster.see_invisible = SEE_INVISIBLE_OBSERVER
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/stun.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/stun.dm
new file mode 100644
index 000000000000..48047837bcf4
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/stun.dm
@@ -0,0 +1,127 @@
+
+/datum/rune_spell/stun
+ name = "Stun"
+ desc = "Overwhelm everyone's senses with a blast of pure chaotic energy. Cultists will recover their senses a bit faster."
+ desc_talisman = "Use to produce a smaller radius blast, or touch someone with it to focus the entire power of the spell on their person."
+ invocation = "Fuu ma'jin!"
+ touch_cast = TRUE
+ word1 = /datum/rune_word/join
+ word2 = /datum/rune_word/hide
+ word3 = /datum/rune_word/technology
+ page = "Concentrated chaotic energies violently released that will temporarily enfeeble anyone in a large radius, even cultists, although those recover a second faster than non-cultists.\
+
When cast from a talisman, the energy affects creatures in a smaller radius and for a smaller duration, which might still be useful in an enclosed space.\
+
However the real purpose of this rune when imbued into a talisman is revealed when you directly touch someone with it, as all of the energies will be concentrated onto their single body, \
+ paralyzing and muting them for a longer duration. This application was created to allow cultists to easily kidnap crew members to convert or torture."
+
+
+/datum/rune_spell/stun/pre_cast()
+ var/mob/living/user = activator
+
+ if (istype (spell_holder, /obj/effect/new_rune))
+ invoke(user, invocation)
+ cast()
+ else if (istype (spell_holder, /obj/item/weapon/talisman))
+ invoke(user, invocation, 1)
+ cast_talisman()
+
+/datum/rune_spell/stun/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ new/obj/effect/cult_ritual/stun(R.loc, 1, activator)
+
+ qdel(R)
+
+/datum/rune_spell/stun/cast_talisman()
+ var/turf/T = get_turf(spell_holder)
+ new/obj/effect/cult_ritual/stun(T, 2, activator)
+ qdel(src)
+
+/datum/rune_spell/stun/cast_touch(mob/living/M)
+ anim(target = M, a_icon = 'monkestation/code/modules/bloody_cult/icons/64x64.dmi', flick_anim = "touch_stun", offX = -32/2, offY = -32/2, plane = ABOVE_LIGHTING_PLANE)
+
+ playsound(spell_holder, 'monkestation/code/modules/bloody_cult/sound/stun_talisman.ogg', 25, 0, -5)
+ if (prob(15))//for old times' sake
+ invoke(activator, "Dream sign ''Evil sealing talisman''!", 1)
+ else
+ invoke(activator, invocation, 1)
+
+ if (M.stat != DEAD)
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(100, DEVOTION_TIER_2, "stun_papered", M)
+
+ if(issilicon(M))
+ to_chat(M, span_danger("WARNING: Short-circuits detected, Rebooting...") )
+ M.Knockdown(9 SECONDS)
+
+ else if(iscarbon(M))
+ to_chat(M, span_danger("A surge of dark energies takes hold of your limbs. You stiffen and fall down.") )
+ var/mob/living/carbon/C = M
+ C.Knockdown(5 SECONDS)//used to be 25
+ C.Stun(5 SECONDS)//used to be 25
+ if (isalien(C))
+ C.Paralyze(5 SECONDS)
+
+ if (!(locate(/obj/effect/stun_indicator) in M))
+ new /obj/effect/stun_indicator(M)
+
+ qdel(src)
+
+/obj/effect/cult_ritual/stun
+ icon_state = "stun_warning"
+ color = "black"
+ anchored = 1
+ alpha = 0
+ //plane = HIDING_MOB_PLANE
+ mouse_opacity = 0
+ var/stun_duration = 10 SECONDS
+
+/obj/effect/cult_ritual/stun/New(turf/loc, var/type = 1, var/mob/living/carbon/caster)
+ ..()
+
+ switch (type)
+ if (1)
+ stun_duration = 10 SECONDS
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/64x64.dmi', flick_anim = "rune_stun", sleeptime = 20, offX = -32/2, offY = -32/2, plane = ABOVE_LIGHTING_PLANE)
+ icon = 'monkestation/code/modules/bloody_cult/icons/480x480.dmi'
+ pixel_x = -224
+ pixel_y = -224
+ animate(src, alpha = 255, time = 10)
+ if (2)
+ stun_duration = 5 SECONDS
+ anim(target = loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/64x64.dmi', flick_anim = "talisman_stun", sleeptime = 20, offX = -32/2, offY = -32/2, plane = ABOVE_LIGHTING_PLANE)
+ icon = 'monkestation/code/modules/bloody_cult/icons/224x224.dmi'
+ pixel_x = -96
+ pixel_y = -96
+ animate(src, alpha = 255, time = 10)
+
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/stun_rune_charge.ogg', 75, 0, 0)
+ spawn(20)
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/stun_rune.ogg', 75, 0, 0)
+ visible_message(span_warning("The rune explodes in a bright flash of chaotic energies.") )
+
+ for(var/mob/living/L in dview(7, get_turf(src)))
+ var/duration = stun_duration
+ var/dist = cheap_pythag(L.x - src.x, L.y - src.y)
+ if (type == 1 && dist >= 8)
+ continue
+ if (type == 2 && dist >= 4)//talismans have a reduced range
+ continue
+ shadow(L, loc, "rune_stun")
+ if (IS_CULTIST(L))
+ duration = 1 SECONDS
+ else if (caster)
+ if (L.stat != DEAD)
+ var/datum/antagonist/cult/cult_datum = caster.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_2, "stun_rune", L)
+ if(iscarbon(L))
+ var/mob/living/carbon/C = L
+ C.Knockdown(duration)
+ C.Stun(duration)
+ if (isalien(C))
+ C.Paralyze(duration)
+
+ else if(issilicon(L))
+ var/mob/living/silicon/S = L
+ S.Knockdown(duration)//TODO: TEST THAT
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/summon_plushie.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/summon_plushie.dm
new file mode 100644
index 000000000000..7cf6784e9e30
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/summon_plushie.dm
@@ -0,0 +1,31 @@
+/datum/rune_spell/summon_plushie
+ secret = TRUE
+ name = "Manifest Nar'sie Plushie"
+ desc = "Manifest's Nar'sie in plushie form, helps prepare yourself for the ritual."
+ desc_talisman = "Summons Nar'sie in plushie form, helps prepare yourself for the ritual."
+ invocation = "Sa Tatha Plu'Shi"
+ word1 = /datum/rune_word/see
+ word2 = /datum/rune_word/self
+ word3 = /datum/rune_word/hide
+ talisman_uses = 1
+ page = "This rune, summons a plushie of your god."
+ var/casting = FALSE
+
+/datum/rune_spell/summon_plushie/cast()
+ if(casting)
+ return
+ var/obj/effect/new_rune/R = spell_holder
+ if (istype(R))
+ R.one_pulse()
+
+ spell_holder.overlays += image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "build")
+ casting = TRUE
+ sleep(3 SECONDS)
+ var/obj/item/toy/plush/narplush/plush = new /obj/item/toy/plush/narplush(get_turf(R))
+ plush.alpha = 0
+ plush.anchored = TRUE
+ animate(plush, alpha = 255, 1.4 SECONDS)
+ sleep(1.5 SECONDS)
+ plush.anchored = FALSE
+ spell_holder.overlays -= image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "build")
+ qdel(R)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_spells/summon_robes.dm b/monkestation/code/modules/bloody_cult/cult/rune_spells/summon_robes.dm
new file mode 100644
index 000000000000..13ee4dcd5230
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_spells/summon_robes.dm
@@ -0,0 +1,98 @@
+/datum/rune_spell/summonrobes
+ name = "Summon Robes"
+ desc = "Swap your clothes for the robes of Nar-Sie's followers. Significantly improves the efficiency of some rituals. Provides a tesseract to instantly swap back to your old clothes."
+ desc_talisman = "Swap your clothes for the robes of Nar-Sie's followers. Significantly improves the efficiency of some rituals. Provides a tesseract to instantly swap back to your old clothes. Using the tesseract will also give you the talisman back, granted it has some uses left."
+ invocation = "Sa tatha najin"
+ word1 = /datum/rune_word/hell
+ word2 = /datum/rune_word/destroy
+ word3 = /datum/rune_word/other
+ talisman_uses = 5
+ page = "This rune, which you have to stand above to use, equips your character in cult apparel. Namely, a hood, robes, shoes, gloves, and a backpack.\
+
Wearing cult gear speeds up channeling of Conversion and Raise Structures runes, but the hood can also be toggled to hide your face and voice, granting you sweet anonymity (so long as you don't forget to pocket your ID card).\
+
After using the rune, a Blood Tesseract appears in your hand, containing clothes that had to be swapped out because you were already wearing them in your head/suit slots. \
+ You can use it to get your clothing back instantly, or throw the tesseract to break it and get its content back this way.\
+
Lastly, the talisman version has 5 uses, and gets back in your hand after you use the Blood Tesseract. The inventory of your backpack gets always gets transferred upon use.\
+
This rune persists upon use, allowing repeated usage."
+ var/list/slots_to_store = list(
+ ITEM_SLOT_FEET,
+ ITEM_SLOT_HEAD,
+ ITEM_SLOT_GLOVES,
+ ITEM_SLOT_BACK,
+ ITEM_SLOT_OCLOTHING,
+ ITEM_SLOT_ICLOTHING,
+ )
+
+/datum/rune_spell/summonrobes/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ if (istype(R))
+ R.one_pulse()
+
+
+ var/list/potential_targets = list()
+ var/turf/TU = get_turf(spell_holder)
+
+ for(var/mob/living/carbon/C in TU)
+ potential_targets += C
+ if(potential_targets.len == 0)
+ to_chat(activator, span_warning("There needs to be someone standing or lying on top of the rune.") )
+ qdel(src)
+ return
+ var/mob/living/carbon/target
+ if(activator in potential_targets)
+ target = activator
+ else
+ target = pick(potential_targets)
+
+ if (!ishuman(target) && !ismonkey(target))
+ qdel(src)
+ return
+
+ anim(target = target, a_icon = 'monkestation/code/modules/bloody_cult/icons/64x64.dmi', flick_anim = "rune_robes", offX = -32/2, offY = -32/2, plane = ABOVE_LIGHTING_PLANE)
+
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(50, DEVOTION_TIER_0, "summon_robes", target)
+
+ var/obj/item/weapon/blood_tesseract/BT = new(get_turf(activator))
+ if (istype (spell_holder, /obj/item/weapon/talisman))
+ var/obj/item/weapon/talisman/T = spell_holder
+ if (!T.linked_ui)
+ activator.dropItemToGround(spell_holder)
+ if (T.uses > 1)
+ BT.remaining = spell_holder
+ spell_holder.forceMove(BT)
+
+ for(var/slot in slots_to_store)
+ var/obj/item/user_slot = target.get_item_by_slot(slot)
+ if (user_slot)
+ BT.stored_gear |= "[slot]"
+ BT.stored_gear["[slot]"] = user_slot
+
+ //looping again in case the suit had a stored item
+ for(var/slot in BT.stored_gear)
+ var/obj/item/user_slot = BT.stored_gear[slot]
+ BT.stored_gear["[slot]"] = user_slot
+ target.dropItemToGround(user_slot)
+ user_slot.forceMove(BT)
+
+ target.equip_to_slot_or_del(new /obj/item/clothing/under/color/black, ITEM_SLOT_ICLOTHING)
+ target.equip_to_slot_or_del(new /obj/item/clothing/suit/hooded/cultrobes/alt(target), ITEM_SLOT_OCLOTHING)
+ target.equip_to_slot_or_del(new /obj/item/clothing/shoes/cult/alt(target), ITEM_SLOT_FEET)
+ target.equip_to_slot_or_del(new /obj/item/clothing/gloves/color/black/cult(target), ITEM_SLOT_GLOVES)
+
+ //transferring backpack items
+ var/obj/item/storage/backpack/cultpack/new_pack = new (target)
+ if ((ITEM_SLOT_BACK in BT.stored_gear))
+ var/obj/item/stored_slot = BT.stored_gear[ITEM_SLOT_BACK]
+ if (istype (stored_slot, /obj/item/storage/backpack))
+ for(var/obj/item/I in stored_slot.contents)
+ I.forceMove(new_pack)
+ target.equip_to_slot_if_possible(new_pack, ITEM_SLOT_BACK)
+
+ activator.put_in_hands(BT)
+ target.put_in_hands(new /obj/item/restraints/legcuffs/bola/cult(target))
+ if(IS_CULTIST(target))
+ to_chat(target, span_notice("Robes and gear of the followers of Nar-Sie manifests around your body. You feel empowered.") )
+ else
+ to_chat(target, span_warning("Robes and gear of the followers of Nar-Sie manifests around your body. You feel sickened.") )
+ to_chat(activator, span_notice("A [BT] materializes in your hand, you may use it to instantly swap back into your stored clothing.") )
+ qdel(src)
diff --git a/monkestation/code/modules/bloody_cult/cult/rune_word.dm b/monkestation/code/modules/bloody_cult/cult/rune_word.dm
new file mode 100644
index 000000000000..51d6d1558bb2
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/rune_word.dm
@@ -0,0 +1,102 @@
+GLOBAL_VAR_INIT(rune_words_initialized, FALSE)
+GLOBAL_LIST_INIT(rune_words, list())
+GLOBAL_LIST_INIT(rune_words_english, list("travel", "blood", "join", "hell", "destroy", "technology", "self", "see", "other", "hide"))
+GLOBAL_LIST_INIT(rune_words_rune, list("ire", "ego", "nahlizet", "certum", "veri", "jatkaa", "mgar", "balaq", "karazet", "geeri"))
+GLOBAL_LIST_INIT(rune_words_icons, list("rune-1", "rune-2", "rune-4", "rune-8", "rune-16", "rune-32", "rune-64", "rune-128", "rune-256", "rune-512"))
+
+/datum/rune_word
+ var/english = "word"//don't change those
+ var/rune = "zek'kon"
+ var/icon = 'monkestation/code/modules/bloody_cult/icons/uristrunes.dmi'
+ var/icon_state = ""
+ var/color//used by path rune markers
+ var/offset_x = 0
+ var/offset_y = 0
+
+/proc/initialize_rune_words()
+ if (GLOB.rune_words_initialized)
+ return
+ for(var/subtype in subtypesof(/datum/rune_word))
+ var/datum/rune_word/new_word = new subtype()
+ GLOB.rune_words[new_word.english] = new_word
+ GLOB.rune_words_initialized = 1
+
+/datum/rune_word/travel
+ english = "travel"
+ rune = "ire"
+ icon_state = "rune-1"
+ color = "yellow"
+ offset_x = 6
+ offset_y = -5
+
+/datum/rune_word/blood
+ english = "blood"
+ rune = "ego"
+ icon_state = "rune-2"
+ color = "maroon"
+ offset_x = 0
+ offset_y = 5
+
+/datum/rune_word/join
+ english = "join"
+ rune = "nahlizet"
+ icon_state = "rune-4"
+ color = "green"
+ offset_x = 2
+ offset_y = 1
+
+/datum/rune_word/hell
+ english = "hell"
+ rune = "certum"
+ icon_state = "rune-8"
+ color = "red"
+ offset_x = 0
+ offset_y = 10
+
+/datum/rune_word/destroy
+ english = "destroy"
+ rune = "veri"
+ icon_state = "rune-16"
+ color = "purple"
+ offset_x = 10
+ offset_y = 3
+
+/datum/rune_word/technology
+ english = "technology"
+ rune = "jatkaa"
+ icon_state = "rune-32"
+ color = "blue"
+ offset_x = -10
+ offset_y = 1
+
+/datum/rune_word/self
+ english = "self"
+ rune = "mgar"
+ icon_state = "rune-64"
+ color = null
+ offset_x = -6
+ offset_y = -9
+
+/datum/rune_word/see
+ english = "see"
+ rune = "balaq"
+ icon_state = "rune-128"
+ color = "fuchsia"
+ offset_x = 10
+ offset_y = -11
+
+/datum/rune_word/other
+ english = "other"
+ rune = "karazet"
+ icon_state = "rune-256"
+ color = "teal"
+ offset_x = -8
+ offset_y = 8
+
+/datum/rune_word/hide
+ english = "hide"
+ rune = "geeri"
+ icon_state = "rune-512"
+ color = "silver"
+ offset_x = -2
+ offset_y = -1
diff --git a/monkestation/code/modules/bloody_cult/cult/salt_act.dm b/monkestation/code/modules/bloody_cult/cult/salt_act.dm
new file mode 100644
index 000000000000..0f3f47cc0d8a
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/salt_act.dm
@@ -0,0 +1,34 @@
+/atom/proc/salt_act()
+ return
+
+/obj/effect/decal/cleanable/food/salt/Initialize(mapload, list/datum/disease/diseases)
+ . = ..()
+ for(var/atom/movable/AM in loc)
+ AM.salt_act()
+
+/obj/item/clothing/suit/hooded/cultrobes/salt_act()
+ acid_melt()
+
+/obj/item/clothing/shoes/cult/salt_act()
+ acid_melt()
+
+/obj/item/clothing/gloves/color/black/cult/salt_act()
+ acid_melt()
+
+/obj/item/storage/backpack/cultpack/salt_act()
+ acid_melt()
+
+/obj/item/weapon/bloodcult_pamphlet/salt_act()
+ fire_act(1000, 200)
+
+/obj/item/storage/cult/salt_act()
+ acid_melt()
+
+/obj/item/reagent_containers/cup/cult/salt_act()
+ acid_melt()
+
+/obj/item/restraints/cult/salt_act()
+ acid_melt()
+
+/obj/item/weapon/blood_tesseract/salt_act()
+ throw_impact()
diff --git a/monkestation/code/modules/bloody_cult/cult/soulstone_gems.dm b/monkestation/code/modules/bloody_cult/cult/soulstone_gems.dm
new file mode 100644
index 000000000000..ee1167705577
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/soulstone_gems.dm
@@ -0,0 +1,14 @@
+/obj/item/soulstone/gem
+ name = "soul gem"
+ desc = "A freshly cut stone which appears to hold the same soul catching properties as shards of the Soul Stone. This one however is cut to perfection."
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "soulstone"
+ perfect = TRUE
+
+/obj/item/soulstone/gem/update_icon_state()
+ . = ..()
+ var/mob/living/basic/shade/shade = locate(/mob/living/basic/shade) in contents
+ if(shade)
+ icon_state = "soulstone2"
+ else
+ icon_state = "soulstone"
diff --git a/monkestation/code/modules/bloody_cult/cult/special_rune_spells.dm b/monkestation/code/modules/bloody_cult/cult/special_rune_spells.dm
new file mode 100644
index 000000000000..90b4c909a6e8
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/special_rune_spells.dm
@@ -0,0 +1,411 @@
+
+// Rune Spells that aren't listed when the player tries to draw a guided rune.
+
+
+
+
+////////////////////////////////////////////////////////////////////
+// //
+// SUMMON TOME //
+// //
+////////////////////////////////////////////////////////////////////
+//Reason: Redundant with paraphernalia. No harm in keeping the rune somewhat usable until another use is found for that word combination.
+
+/datum/rune_spell/summontome
+ secret = TRUE
+ name = "Summon Tome"
+ desc = "Bring forth an arcane tome filled with Nar-Sie's knowledge."
+ desc_talisman = "Turns into an arcane tome upon use."
+ invocation = "N'ath reth sh'yro eth d'raggathnor!"
+ word1 = /datum/rune_word/see
+ word2 = /datum/rune_word/blood
+ word3 = /datum/rune_word/hell
+ cost_invoke = 4
+ page = ""
+
+/datum/rune_spell/summontome/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+
+ if (pay_blood())
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "conjure_paraphernalia", "Arcane Tome")
+ spell_holder.visible_message(span_rose("The rune's symbols merge into each others, and an Arcane Tome takes form in their place") )
+ var/turf/T = get_turf(spell_holder)
+ var/obj/item/weapon/tome/AT = new (T)
+ anim(target = AT, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "tome_spawn")
+ qdel(spell_holder)
+ else
+ qdel(src)
+
+/datum/rune_spell/summontome/cast_talisman()//The talisman simply turns into a tome.
+ var/turf/T = get_turf(spell_holder)
+ var/obj/item/weapon/tome/AT = new (T)
+ if (spell_holder == activator.get_active_held_item())
+ activator.dropItemToGround(spell_holder, T)
+ activator.put_in_active_hand(AT)
+ var/datum/antagonist/cult/cult_datum = activator.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum.gain_devotion(10, DEVOTION_TIER_0, "conjure_paraphernalia", "Arcane Tome")
+ else//are we using the talisman from a tome?
+ activator.put_in_hands(AT)
+ flick("tome_spawn", AT)
+ qdel(src)
+
+////////////////////////////////////////////////////////////////////
+// //
+// TEAR REALITY //
+// //
+////////////////////////////////////////////////////////////////////
+//Reason: the words for that one are revealed to cultists on their UI once the Eclipse timer has reached zero
+
+/datum/rune_spell/tearreality
+ secret = TRUE
+ name = "Tear Reality"
+ desc = "Bring 8 cultists or prisoners to kickstart the ritual to bring forth Nar-Sie."
+ desc_talisman = "Use to kickstart the ritual to bring forth Nar-Sie where you stand."
+ invocation = "Tok-lyr rqa'nap g'lt-ulotf!"
+ word1 = /datum/rune_word/hell
+ word2 = /datum/rune_word/join
+ word3 = /datum/rune_word/self
+ page = ""
+ var/atom/blocker
+ var/list/dance_platforms = list()
+ var/dance_count = 0
+ var/dance_target = 240
+ var/obj/effect/cult_ritual/dance/dance_manager
+ var/image/crystals
+ var/image/top_crystal
+ var/image/narsie_glint
+
+ var/spawners_sent = FALSE
+ var/list/pillar_spawners = list()
+ var/list/gateway_spawners = list()
+
+/datum/rune_spell/tearreality/cast()
+ var/obj/effect/new_rune/R = spell_holder
+ R.one_pulse()
+ var/turf/T = get_turf(R)
+
+ //The most fickle rune there ever was
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (!istype(cult))
+ to_chat(activator, span_warning("Couldn't find the cult faction. Something's broken, please report the issue to an admin or using the BugReport button at the top.") )
+ return
+
+ switch(cult.stage)
+ if (BLOODCULT_STAGE_NORMAL)
+ to_chat(activator, span_cult("The rune pulses but no energies respond to its signal.") )
+ to_chat(activator, span_cult("The Eclipse is coming, but until then this rune serves no purpose.") )
+ if (!is_station_level(R.z))
+ to_chat(activator, span_cult("When it does, you should try again aboard the station.") )
+ var/obj/structure/dance_check/checker = new(T, src)
+ var/list/moves_to_do = list(SOUTH, WEST, NORTH, NORTH, EAST, EAST, SOUTH, SOUTH, WEST)
+ for (var/direction in moves_to_do)
+ if (!checker.Move(get_step(checker, direction)))//The checker passes through mobs and non-dense objects, but bumps against dense objects and turfs
+ to_chat(activator, span_cult("and in a more open area.") )
+ abort()
+ return
+
+ if (BLOODCULT_STAGE_MISSED)
+ to_chat(activator, span_cult("The rune pulses but no energies respond to its signal.") )
+ to_chat(activator, span_cult("The window of opportunity has passed along with the Eclipse. Make your way off this space station so you may attempt another day.") )
+ abort()
+ return
+
+ if (BLOODCULT_STAGE_ECLIPSE)
+ to_chat(activator, span_cult("The Bloodstone has been raised! Now is not the time to use that rune.") )
+ abort()
+ return
+
+ if (BLOODCULT_STAGE_DEFEATED)
+ to_chat(activator, span_cult("The rune pulses but no energies respond to its signal.") )
+ to_chat(activator, span_cult("With the Bloodstone's collapse, the veil in this region of space has fully mended itself. Another cult will make an attempt in another space station someday.") )
+ abort()
+ return
+
+ if (BLOODCULT_STAGE_NARSIE)
+ to_chat(activator, span_cult("The tear has already be opened. Praise the Geometer in this most unholy day!") )
+ abort()
+ return
+
+ if (cult.stage != BLOODCULT_STAGE_READY)
+ to_chat(activator, span_warning("Cult faction appears to be in an unset stage. Something's broken, please report the issue to an admin or using the BugReport button at the top.") )
+ abort()
+ return
+
+ if (!is_station_level(R.z))
+ to_chat(activator, span_cult("The rune pulses but no energies respond to its signal.") )
+ to_chat(activator, span_cult("You should try again aboard the station.") )
+ abort()
+ return
+
+ if (cult.tear_ritual)
+ var/obj/effect/new_rune/U = cult.tear_ritual.spell_holder
+ to_chat(activator, span_cult("The rune pulses but no energies respond to its signal.") )
+ to_chat(activator, span_cult("It appears that another tear is currently being opened. Somewhere...to the [dir2text(get_dir(R, U))].") )
+ abort()
+ return
+
+ var/obj/structure/dance_check/checker = new(T, src)
+ var/list/moves_to_do = list(SOUTH, WEST, NORTH, NORTH, EAST, EAST, SOUTH, SOUTH, WEST)
+ for (var/direction in moves_to_do)
+ if (!checker.Move(get_step(checker, direction)))//The checker passes through mobs and non-dense objects, but bumps against dense objects and turfs
+ if (blocker)
+ to_chat(activator, span_cult("The nearby [blocker] will impede the ritual.") )
+ to_chat(activator, span_cult("You should try again in a more open area.") )
+ abort()
+ return
+
+ //Alright now we can get down to business
+ cult.tear_ritual = src
+ R.overlays.len = 0
+ R.icon = 'monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi'
+ R.pixel_x = -32
+ R.pixel_y = -32
+ R.layer = SIGIL_LAYER
+ R.plane = GAME_PLANE
+ R.set_light(1, 2, COLOR_RED)
+
+ var/datum/holomap_marker/marker = new(R)
+ marker.id = HOLOMAP_MARKER_TEARREALITY
+ marker.filter = HOLOMAP_FILTER_CULT
+ marker.x = R.x
+ marker.y = R.y
+ marker.z = R.z
+ marker.icon = 'monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi'
+ marker.icon_state = "tearreality"
+
+ anim(target = R.loc, a_icon = 'monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi', flick_anim = "rune_tearreality_activate", lay = SIGIL_LAYER, offX = -32, offY = -32, plane = GAME_PLANE)
+
+ var/list/platforms_to_spawn = list(NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, NORTHWEST)
+ for (var/direction in platforms_to_spawn)
+ if (!destroying_self)
+ var/turf/U = get_step(R, direction)
+ shadow(U, R.loc)
+ var/obj/effect/cult_ritual/dance_platform/platform = new(U, src)
+ dance_platforms += platform
+ sleep(1)
+
+ if (!destroying_self)
+ message_admins("[key_name(activator)] is preparing the Tear Reality ritual at [T.loc] ([T.x], [T.y], [T.z]).")
+ for (var/datum/mind/mind in cult.members)
+ var/mob/living/M = mind.current
+ to_chat(M, span_cult("The ritual to tear reality apart and pull the station into the realm of Nar-Sie is now taking place in [T.loc].") )
+ to_chat(M, span_cult("A total of 8 persons, either cultists or prisoners, is required for the ritual to start. Go there to help start and then protect the ritual.") )
+
+ var/image/I_circle = image('monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi', "rune_tearreality")
+ SET_PLANE_EXPLICIT(I_circle, GAME_PLANE, spell_holder)
+ I_circle.layer = SIGIL_LAYER
+ I_circle.appearance_flags |= RESET_COLOR
+ var/image/I_crystals = image('monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi', "tear_stones")
+ SET_PLANE_EXPLICIT(I_crystals, GAME_PLANE, spell_holder)
+ I_crystals.layer = SIGIL_LAYER
+ I_crystals.appearance_flags |= RESET_COLOR
+ R.overlays += I_circle
+ R.overlays += I_crystals
+ custom_rune = TRUE
+
+ crystals = image('monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi', "tear_stones_[min(8, 1+(dance_count/30))]")
+ SET_PLANE_EXPLICIT(crystals, GAME_PLANE, spell_holder)
+
+ top_crystal = image('monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi', "tear_stones_top")
+ SET_PLANE_EXPLICIT(top_crystal, GAME_PLANE, spell_holder)
+ top_crystal.layer = SIGIL_LAYER + 0.1
+ top_crystal.appearance_flags |= RESET_COLOR
+ R.overlays += top_crystal
+
+ narsie_glint = image('monkestation/code/modules/bloody_cult/icons/cult.dmi', "narsie_glint")
+ SET_PLANE_EXPLICIT(narsie_glint, ABOVE_LIGHTING_PLANE, spell_holder)
+ narsie_glint.alpha = 0
+ narsie_glint.pixel_x = 32
+ narsie_glint.pixel_y = 32
+ R.overlays += narsie_glint
+
+
+/datum/rune_spell/tearreality/cast_talisman() //Tear Reality talismans create an invisible summoning rune beneath the caster's feet.
+ var/obj/effect/new_rune/R = new(get_turf(activator))
+ R.icon_state = "temp"
+ R.active_spell = new type(activator, R)
+ qdel(src)
+
+/datum/rune_spell/tearreality/midcast(mob/add_cultist)
+ to_chat(add_cultist, span_cult("Stand in the surrounding circles with fellow cultists and captured prisoners until every spot is filled.") )
+
+/datum/rune_spell/tearreality/abort(var/cause)
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult && (cult.tear_ritual == src))
+ cult.tear_ritual = null
+ if (dance_manager)
+ QDEL_NULL(dance_manager)
+
+ var/obj/effect/new_rune/R = spell_holder
+ R.set_light(0)
+ R.icon = 'monkestation/code/modules/bloody_cult/icons/deityrunes.dmi'
+ R.pixel_x = 0
+ R.pixel_y = 0
+ R.layer = CULT_OVERLAY_LAYER
+ R.plane = GAME_PLANE
+
+ for(var/obj/effect/cult_ritual/dance_platform/platform in dance_platforms)
+ qdel(platform)
+
+ spawn()
+ for(var/obj/effect/cult_ritual/tear_spawners/pillar_spawner/CR in pillar_spawners)
+ CR.cancel()
+ sleep(1)
+
+ for(var/obj/effect/cult_ritual/CR in gateway_spawners)
+ qdel(CR)
+
+ ..()
+
+/datum/rune_spell/tearreality/proc/dancer_check(var/mob/living/C)
+ var/obj/effect/new_rune/R = spell_holder
+ if (dance_platforms.len <= 0)
+ return
+ if (!isturf(R.loc))//moved inside the blood stone
+ return
+ if (dance_manager && C)
+ dance_manager.dancers |= C
+ if(IS_CULTIST(C))
+ C.say("Tok-lyr rqa'nap g'lt-ulotf!", "C")
+ else
+ to_chat(C, span_cult("The tentacles shift and force your body to move alongside them, performing some kind of dance.") )
+ return
+ for(var/obj/effect/cult_ritual/dance_platform/platform in dance_platforms)
+ if (!platform.dancer)
+ return
+
+ //full dancers!
+ var/turf/T = get_turf(R)
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ cult.twister = TRUE
+
+ if (!spawners_sent)
+ spawners_sent = TRUE
+ new /obj/effect/cult_ritual/tear_spawners/vertical_spawner(T, src)
+ new /obj/effect/cult_ritual/tear_spawners/vertical_spawner/up(T, src)
+ new /obj/effect/cult_ritual/tear_spawners/horizontal_spawner/left(T, src)
+ new /obj/effect/cult_ritual/tear_spawners/horizontal_spawner/right(T, src)
+
+ dance_manager = new(T)
+
+ for(var/obj/effect/cult_ritual/dance_platform/platform in dance_platforms)
+ dance_manager.extras += platform
+ platform.dance_manager = dance_manager
+ if (platform.dancer)
+ dance_manager.dancers += platform.dancer
+ if(IS_CULTIST(platform.dancer))
+ C.say("Tok-lyr rqa'nap g'lt-ulotf!", "C")
+ else
+ to_chat(C, span_cult("The tentacles shift and force your body to move alongside them, performing some kind of dance.") )
+
+ dance_manager.tear = src
+ dance_manager.we_can_dance()
+
+/datum/rune_spell/tearreality/proc/update_crystals()
+ var/obj/effect/new_rune/R = spell_holder
+ R.overlays -= crystals
+ R.overlays -= top_crystal
+ R.overlays -= narsie_glint
+ crystals.icon_state = "tear_stones_[min(8, 1+round(dance_count/30))]"
+ top_crystal.icon_state = "tear_stones_1"
+ narsie_glint.alpha = max(0, (dance_count-105)*2)//Nar-Sie's eyes become about visible half-way through the dance
+ top_crystal.appearance_flags &= ~RESET_COLOR
+ R.overlays += crystals
+ R.overlays += top_crystal
+ if (isturf(R.loc))
+ if (dance_count >= dance_target)// DANCE IS OVER!!
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult && !cult.bloodstone)
+ var/obj/structure/cult/bloodstone/blood_stone = new(R.loc)
+ cult.bloodstone = blood_stone
+ cult.stage(BLOODCULT_STAGE_ECLIPSE)
+ R.mouse_opacity = 0
+ R.forceMove(blood_stone)//keeping the rune safe inside the bloodstone
+ QDEL_NULL(dance_manager)
+ blood_stone.flashy_entrance(src)
+ else
+ R.overlays += narsie_glint
+
+/datum/rune_spell/tearreality/proc/pillar_update(var/update_level)
+ for (var/obj/effect/cult_ritual/tear_spawners/pillar_spawner/PS in pillar_spawners)
+ PS.execute(update_level)
+
+ for (var/obj/effect/cult_ritual/tear_spawners/gateway_spawner/GS in gateway_spawners)
+ GS.execute(update_level)
+
+/datum/rune_spell/tearreality/proc/lost_dancer()
+ for(var/obj/effect/cult_ritual/dance_platform/platform in dance_platforms)
+ if (platform.dancer)
+ return
+ dance_count = 0
+ QDEL_NULL(dance_manager)
+ var/obj/effect/new_rune/R = spell_holder
+ R.overlays -= crystals
+ R.overlays -= top_crystal
+ top_crystal.icon_state = "tear_stones_top"
+ top_crystal.appearance_flags |= RESET_COLOR
+ R.overlays += top_crystal
+
+/datum/rune_spell/tearreality/proc/dance_increment(var/mob/living/L)
+ if (dance_manager)
+ var/increment = 0.5
+ if (iscarbon(L))
+ var/mob/living/carbon/C = L
+ if (istype(C.handcuffed, /obj/item/restraints/handcuffs/cult))
+ increment += 0.5
+ increment += (C.get_cult_power()) / 100
+
+ var/obj/item/candle/blood/candle
+ if (istype(C.get_active_held_item(), /obj/item/candle/blood))
+ candle = C.get_active_held_item()
+ else if (istype(C.get_inactive_held_item(), /obj/item/candle/blood))
+ candle = C.get_inactive_held_item()
+ if (candle && candle.lit)
+ increment += 0.5
+ dance_count += increment
+
+//---------------------------------------------------------------------------------------------------------------------
+
+////////////////////////////////////////////////////////////////////
+// //
+// STREAM //
+// //
+////////////////////////////////////////////////////////////////////
+//Reason: we don't want a new cultist player to use this rune by accident, better leave it to savvy ones
+
+/datum/rune_spell/stream
+ secret = TRUE
+ name = "Stream"
+ desc = "Start or stop streaming on Spess.TV."
+ desc_talisman = "Start or stop streaming on Spess.TV."
+ invocation = "L'k' c'mm'nt 'n' s'bscr'b! P'g ch'mp! Kappah!"
+ word1 = /datum/rune_word/other
+ word2 = /datum/rune_word/see
+ word3 = /datum/rune_word/self
+ page = "This rune lets you start (or stop) streaming on Spess.TV so that you can let your audience watch and cheer for you while you slay infidels in the name of Nar-sie. #Sponsored"
+
+/datum/rune_spell/stream/cast()
+ var/datum/antagonist/streamer/streamer = activator.mind?.has_antag_datum(/datum/antagonist/streamer)
+ if(!streamer)
+ streamer = new /datum/antagonist/streamer
+ streamer.team = "Cult"
+ activator.mind.add_antag_datum(streamer)
+ streamer.team = "Cult"
+ if(!streamer.camera)
+ streamer.set_camera(new /obj/machinery/camera/spesstv(activator))
+ streamer.toggle_streaming()
+ qdel(src)
+
+
+/*
+Hall of fame of previous deprecated runes, might redesign later, noting their old word combinations there so I can easily retrieve them later.
+
+MANIFEST GHOST: Blood See Travel
+SACRIFICE: Hell Blood Join
+DRAIN BLOOD: Travel Blood Self
+BLOOD BOIL: Destroy See Blood
+
+*/
diff --git a/monkestation/code/modules/bloody_cult/cult/spells/artificer_spells.dm b/monkestation/code/modules/bloody_cult/cult/spells/artificer_spells.dm
new file mode 100644
index 000000000000..ca8b5286be19
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/spells/artificer_spells.dm
@@ -0,0 +1,119 @@
+/datum/action/cooldown/spell/pointed/conjure/hex
+ name = "Conjure Hex"
+ desc = "Build a lesser construct to defend an area."
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ cast_range = 3
+ cast_delay = 60
+ summon_type = list(/mob/living/simple_animal/hostile/hex)
+
+
+/datum/action/cooldown/spell/pointed/conjure/hex/post_summon(var/mob/living/simple_animal/hostile/hex/AM, var/mob/user)
+ var/mob/living/basic/construct/artificer/perfect/builder = owner
+ AM.master = builder
+ AM.no_master = FALSE
+ builder.minions.Add(AM)
+ AM.setupglow(builder.construct_color)
+ if (builder.minions.len >= 3)
+ var/mob/living/simple_animal/hostile/hex/SA = builder.minions[1]
+ builder.minions.Remove(SA)
+ SA.master = null//The old hex will crumble on its own within the next 10 seconds.
+
+ if (IS_CULTIST(builder))
+ builder.DisplayUI("Cultist Right Panel")
+
+ var/datum/antagonist/cult/cult_datum = user?.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum?.gain_devotion(40, DEVOTION_TIER_2, "summon_hex", AM)
+
+/datum/action/cooldown/spell/pointed/conjure/struct
+ name = "Conjure Structure"
+ desc = "Raise a cult structure that you may then operate, such as an altar, a forge, or a spire."
+
+
+ cast_range = 4
+ cast_delay = 60
+ summon_type = list(/obj/structure/cult/altar)
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ var/structure
+
+
+/datum/action/cooldown/spell/pointed/conjure/struct/PreActivate(atom/target)
+ if (locate(/obj/structure/cult) in range(owner, 1))
+ to_chat(owner, span_warning("You cannot perform this ritual that close from another similar structure.") )
+ return 1
+ var/turf/T = owner.loc
+ if (!istype(T))
+ return 1
+ var/list/choices = list(
+ list("Altar", "radial_altar", "Allows for crafting soul gems, and performing various other cult rituals."),
+ list("Spire", "radial_spire", "Allows all cultists in the level to communicate with each others using :x"),
+ list("Forge", "radial_forge", "Enables the forging of cult blades and armor, as well as new construct shells. Raise the temperature of nearby creatures."),
+ )
+ structure = show_radial_menu(owner, T, choices, 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', "radial-cult")
+ if (!T.Adjacent(owner) || !structure )
+ return 1
+ switch(structure)
+ if("Altar")
+ summon_type = list(/obj/structure/cult/altar)
+ if("Spire")
+ summon_type = list(/obj/structure/cult/spire)
+ if("Forge")
+ summon_type = list(/obj/structure/cult/forge)
+ return Activate(target)
+
+/datum/action/cooldown/spell/pointed/conjure/struct/post_summon(atom/movable/AM, mob/user)
+ var/datum/antagonist/cult/cult_datum = user?.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum?.gain_devotion(10, DEVOTION_TIER_1, "raise_structure", structure)
+
+/datum/action/cooldown/spell/pointed/conjure/pylon
+ name = "Conjure Pylon"
+ desc = "This spell conjures a fragile crystal from Nar-Sie's realm. Makes for a convenient light source, or a weak obstacle."
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ cast_range = 4
+ cast_delay = 20
+
+ summon_type = list(/obj/structure/cult/pylon)
+
+
+/datum/action/cooldown/spell/pointed/conjure/pylon/post_summon(atom/movable/AM, mob/user)
+ var/datum/antagonist/cult/cult_datum = user?.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum?.gain_devotion(10, DEVOTION_TIER_1, "raise_structure", "Pylon")
+
+
+/datum/action/cooldown/spell/pointed/conjure/door
+ name = "Conjure Door"
+ desc = "This spell conjures a cult door. Those automatically open and close upon the passage of a cultist, construct or shade."
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ cast_range = 4
+ cast_delay = 4
+
+ summon_type = list(/obj/machinery/door/airlock/cult)
+
+
+
+/datum/action/cooldown/spell/pointed/conjure/door/conjure_animation(var/obj/effect/abstract/animation, var/turf/target)
+ animation.icon_state = ""
+ flick("", animation)
+ shadow(target, owner.loc, "artificer_convert")
+ spawn(10)
+ QDEL_NULL(animation)
+
+/datum/action/cooldown/spell/pointed/conjure/door/post_summon(atom/movable/AM, mob/user)
+ var/datum/antagonist/cult/cult_datum = user?.mind.has_antag_datum(/datum/antagonist/cult)
+ cult_datum?.gain_devotion(10, DEVOTION_TIER_1, "summon_door", AM)
diff --git a/monkestation/code/modules/bloody_cult/cult/spells/blood_doodle.dm b/monkestation/code/modules/bloody_cult/cult/spells/blood_doodle.dm
new file mode 100644
index 000000000000..682f0ec7b126
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/spells/blood_doodle.dm
@@ -0,0 +1,83 @@
+/obj/effect/decal/cleanable/blood/writing
+ icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi'
+ icon_state = "nothing"
+ can_dry = FALSE
+
+/datum/action/cooldown/blood_doodle
+ name = "Blood Doodle"
+ desc = "Draw a blood rune message on the ground for others to see."
+ button_icon_state = "cult_word"
+ button_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon_state = "const_spell_base"
+
+ var/override = FALSE
+
+/datum/action/cooldown/blood_doodle/PreActivate(atom/target)
+ . = ..()
+ if(!override)
+ var/datum/team/cult/cult_team = locate_team(/datum/team/cult)
+ if(!cult_team)
+ return
+
+ if(!GLOB.eclipse.eclipse_start_time || GLOB.eclipse.eclipse_finished)
+ return
+
+
+/datum/action/cooldown/blood_doodle/Activate(atom/target)
+ . = ..()
+ var/turf/parent_turf = get_turf(owner)
+
+ if(locate(/obj/effect/decal/cleanable/blood/writing) in parent_turf)
+ to_chat(span_cultbold("There is already a blood drawing here"))
+ return
+
+ var/obj/effect/decal/cleanable/blood/blood = locate(/obj/effect/decal/cleanable/blood) in parent_turf
+ if(!blood)
+ to_chat(span_cultbold("There is no blood to draw with here!"))
+ return
+
+ var/blood_color = blood.color
+
+ var/maximum_length = 30
+ var/message = stripped_input(owner, "Write a message. You will be able to preview it.", "Bloody writings", "")
+ if(!message)
+ return
+ message = copytext(message, 1, maximum_length)
+
+ var/letter_amount = length(replacetext(message, " ", ""))
+ if(!letter_amount) //If there is no text
+ return
+
+ var/angle = rand(-25, 25)
+ var/image/preview = image(icon = null)
+ preview.maptext = MAPTEXT_YOU_MURDERER(" [message] ")
+ preview.maptext_height = 64
+ preview.maptext_width = 128
+ preview.maptext_x = -48
+ preview.maptext_y = 8
+ preview.alpha = 180
+ preview.loc = parent_turf
+ preview.transform = matrix(angle, MATRIX_ROTATE)
+
+ owner.client?.images.Add(preview)
+ var/continue_drawing = alert(owner, "This is how your message will look. Continue?", "Bloody writings", "Yes", "Cancel")
+ owner.client?.images.Remove(preview)
+ animate(preview)
+ preview.loc = null
+ qdel(preview)
+
+ if(continue_drawing != "Yes")
+ return
+
+ message_admins("[owner] created a blood doodle containing the phrase:[message][ADMIN_JMP(parent_turf)]")
+ var/obj/effect/decal/cleanable/blood/writing/spawned_writing = new /obj/effect/decal/cleanable/blood/writing(parent_turf)
+ spawned_writing.color = blood_color
+
+ spawned_writing.maptext = MAPTEXT_YOU_MURDERER(" [message] ")
+ spawned_writing.maptext_height = 64
+ spawned_writing.maptext_width = 128
+ spawned_writing.maptext_x = -48
+ spawned_writing.maptext_y = 8
+ spawned_writing.transform = matrix(angle, MATRIX_ROTATE)
+ qdel(blood)
diff --git a/monkestation/code/modules/bloody_cult/cult/spells/path_spells.dm b/monkestation/code/modules/bloody_cult/cult/spells/path_spells.dm
new file mode 100644
index 000000000000..e716f2aaf946
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/spells/path_spells.dm
@@ -0,0 +1,72 @@
+
+/datum/action/cooldown/spell/pointed/conjure/path_entrance
+ name = "Path Entrance"
+ desc = "Place an entrance to a shortcut through the veil between this world and the other one."
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 60 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ cast_range = 1
+ summon_type = list(/obj/effect/new_rune)
+
+
+ var/chosen_path = ""
+
+/datum/action/cooldown/spell/pointed/conjure/path_entrance/PreActivate(atom/target)
+ var/turf/T = get_turf(target)
+ var/obj/effect/new_rune/rune = locate() in T
+ if (rune)
+ to_chat(owner, span_warning("You cannot draw on top of an already existing rune.") )
+ return FALSE
+ if(istype(T, /turf/open/space))
+ to_chat(owner, span_warning("Get over a solid surface first!") )
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/conjure/path_entrance/post_summon(obj/effect/new_rune/R, atom/cast_on)
+ var/turf/T = R.loc
+ log_admin("BLOODCULT: [key_name(owner)] has created a new rune at [T.loc] (@[T.x], [T.y], [T.z]).")
+ message_admins("BLOODCULT: [key_name(owner)] has created a new rune at [T.loc] (JMP).")
+ write_full_rune(R.loc, /datum/rune_spell/portalentrance)
+ R.one_pulse()
+ R.trigger(owner)
+
+ var/datum/antagonist/cult/C = owner.mind?.has_antag_datum(/datum/antagonist/cult)
+ C?.gain_devotion(30, DEVOTION_TIER_1, "new_path_entrance", R)
+
+/datum/action/cooldown/spell/pointed/conjure/path_exit
+ name = "Path Exit"
+ desc = "Place an exit to a shotcut through the veil between this world and the other one."
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 60 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ cast_range = 3
+ summon_type = list(/obj/effect/new_rune)
+
+ var/chosen_path = ""
+
+/datum/action/cooldown/spell/pointed/conjure/path_exit/PreActivate(atom/target)
+ var/turf/T = get_turf(target)
+ var/obj/effect/new_rune/rune = locate() in T
+ if (rune)
+ to_chat(owner, span_warning("You cannot draw on top of an already existing rune.") )
+ return FALSE
+ if(istype(T, /turf/open/space))
+ to_chat(owner, span_warning("Get over a solid surface first!") )
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/conjure/path_exit/post_summon(obj/effect/new_rune/R, atom/cast_on)
+ . = ..()
+ var/turf/T = R.loc
+ log_admin("BLOODCULT: [key_name(owner)] has created a new rune at [T.loc] (@[T.x], [T.y], [T.z]).")
+ message_admins("BLOODCULT: [key_name(owner)] has created a new rune at [T.loc] (JMP).")
+ write_full_rune(R.loc, /datum/rune_spell/portalexit)
+ R.one_pulse()
+ R.trigger(owner)
+
+ var/datum/antagonist/cult/C = owner.mind?.has_antag_datum(/datum/antagonist/cult)
+ C?.gain_devotion(30, DEVOTION_TIER_1, "new_path_exit", R)
diff --git a/monkestation/code/modules/bloody_cult/cult/spells/pointed_conjure.dm b/monkestation/code/modules/bloody_cult/cult/spells/pointed_conjure.dm
new file mode 100644
index 000000000000..ebcc11c98705
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/spells/pointed_conjure.dm
@@ -0,0 +1,68 @@
+/datum/action/cooldown/spell/pointed/conjure
+ sound = 'sound/items/welder.ogg'
+ school = SCHOOL_CONJURATION
+
+ /// A list of types that will be created on summon.
+ /// The type is picked from this list, not all provided are guaranteed.
+ var/list/summon_type = list()
+ /// How long before the summons will be despawned. Set to 0 for permanent.
+ var/summon_lifespan = 0
+ /// Amount of summons to create.
+ var/summon_amount = 1
+ /// If TRUE, summoned objects will not be spawned in dense turfs.
+ var/summon_respects_density = FALSE
+ /// If TRUE, no two summons can be spawned in the same turf.
+ var/summon_respects_prev_spawn_points = TRUE
+
+ var/cast_duration = 0
+ var/cast_delay = 0
+
+/datum/action/cooldown/spell/pointed/conjure/is_valid_target(atom/cast_on)
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/conjure/cast(atom/cast_on)
+ . = ..()
+ if(!do_after(owner, cast_delay, owner))
+ return
+
+ for(var/i in 1 to summon_amount)
+ var/atom/summoned_object_type = pick(summon_type)
+ var/turf/spawn_place = get_turf(cast_on)
+
+ if(ispath(summoned_object_type, /turf))
+ if(isclosedturf(spawn_place))
+ spawn_place.ChangeTurf(summoned_object_type, flags = CHANGETURF_INHERIT_AIR)
+ return
+ if(ispath(summoned_object_type, /turf/closed))
+ if (spawn_place.overfloor_placed)
+ spawn_place.ChangeTurf(summoned_object_type, flags = CHANGETURF_INHERIT_AIR)
+ else
+ spawn_place.PlaceOnTop(summoned_object_type, flags = CHANGETURF_INHERIT_AIR)
+ return
+ var/turf/open/open_turf = spawn_place
+ open_turf.replace_floor(summoned_object_type, flags = CHANGETURF_INHERIT_AIR)
+ return
+
+ spawn(cast_duration)
+ var/atom/summoned_object = new summoned_object_type(spawn_place)
+
+ summoned_object.flags_1 |= ADMIN_SPAWNED_1
+ if(summon_lifespan > 0)
+ QDEL_IN(summoned_object, summon_lifespan)
+
+ post_summon(summoned_object, cast_on)
+
+ var/obj/effect/abstract/animation = new /obj/effect/abstract(spawn_place)
+ animation.name = "conjure"
+ animation.set_density(FALSE)
+ animation.anchored = 1
+ animation.icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi'
+
+ conjure_animation(animation, spawn_place)
+
+/// Called on atoms summoned after they are created, allows extra variable editing and such of created objects
+/datum/action/cooldown/spell/pointed/conjure/proc/post_summon(atom/summoned_object, atom/cast_on)
+ return
+
+/datum/action/cooldown/spell/pointed/conjure/proc/conjure_animation(var/obj/effect/abstract/animation, var/turf/target)
+ QDEL_NULL(animation)
diff --git a/monkestation/code/modules/bloody_cult/cult/spells/shade_spells.dm b/monkestation/code/modules/bloody_cult/cult/spells/shade_spells.dm
new file mode 100644
index 000000000000..fdc7106258c9
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/spells/shade_spells.dm
@@ -0,0 +1,191 @@
+/////////////////////////////
+// //
+// SELF TELEKINESIS ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //
+//////////////////////////////Not a real spell, but informs the player that moving consums blood.
+
+/datum/action/cooldown/spell/pointed/soulblade
+ panel = "Cult"
+ button_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon = 'monkestation/code/modules/bloody_cult/icons/spells.dmi'
+ background_icon_state = "const_spell_base"
+ var/blood_cost = 0
+
+/datum/action/cooldown/spell/pointed/soulblade/soulblade/PreActivate(atom/target)
+ var/obj/item/weapon/melee/soulblade/SB = owner.loc
+ if (SB.blood < blood_cost)
+ to_chat(owner, span_danger("You don't have enough blood left for this move.") )
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/spell/pointed/soulblade/after_cast(atom/cast_on)
+ ..()
+ var/obj/item/weapon/melee/soulblade/SB = owner.loc
+ SB.blood = max(0, SB.blood-blood_cost)
+ var/mob/shade = owner
+ shade.DisplayUI("Soulblade")
+
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_kinesis
+ name = "Self Telekinesis"
+ desc = "(1 BLOOD) Move yourself without the need of being held."
+ button_icon_state = "souldblade_move"
+
+
+
+//////////////////////////////Basic attack
+// //Can be used by clicking anywhere on the screen for convenience
+// SPIN SLASH ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //Attackes EVERY (almost) atoms on your turf, and the one in the direction you're facing.
+//////////////////////////////That means unexpected behaviours are likely, for instance you can open doors, harvest meat off dead animals, or break important stuff
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_spin
+ name = "Spin Slash"
+ desc = "(5 BLOOD) Stop your momentum and cut in front of you."
+ button_icon_state = "soulblade_spin"
+
+ blood_cost = 5
+ COOLDOWN_DECLARE(spin_cooldown) //gotta use that to get a more strict cooldown at such a small value
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_spin/PreActivate(atom/target)
+ . = ..()
+ if(!COOLDOWN_FINISHED(src, spin_cooldown))
+ return
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_spin/cast(atom/cast_on)
+ ..()
+ if(!COOLDOWN_FINISHED(src, spin_cooldown))
+ return
+ COOLDOWN_START(src, spin_cooldown, 1 SECONDS)
+ var/obj/item/weapon/melee/soulblade/SB = owner.loc
+ var/turf/source_turf = SB.loc
+ SB.reflector = TRUE
+
+ addtimer(VARSET_CALLBACK(SB, reflector, FALSE), 0.4 SECONDS)
+ SB.throwing = FALSE
+
+ if (istype(SB.loc, /obj/projectile))
+ var/obj/projectile/P = SB.loc
+ qdel(P)
+ var/obj/structure/cult/altar/altar = cast_on
+ var/turf/step_turf = get_step(source_turf, SB.dir)
+ if (istype(altar))
+ altar.attackby(SB, owner)
+ return//gotta make sure we're not gonna bug ourselves out of the altar if there's one by hitting a table or something.
+ flick("soulblade-spin", SB)
+ for(var/atom/listed in source_turf.contents)
+ if(listed == SB)
+ continue
+
+ listed.attackby(SB, owner)
+ for(var/atom/listed in step_turf.contents)
+ if(listed == SB)
+ continue
+ listed.attackby(SB, owner)
+
+//////////////////////////////Puts the blade inside a bullet that shoots forward.
+// //Can be used by drag n dropping from turf A to turf B. Will cause the bullet to fire first toward A then change direction toward B
+// PERFORATE ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //You need to hit at least two living mobs to make up for the cost of using this spell
+//////////////////////////////The blade moves much faster from A to B than from starting to A
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_perforate
+ name = "Perforate"
+ desc = "(20 BLOOD) Hurl yourself through the air. You can cast this spell by doing a Drag n Drop with your mouse for more interesting trajectories. If you hit a cultist, they'll automatically grab you."
+ button_icon_state = "soulblade_perforate"
+
+ blood_cost = 20
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_perforate/cast(atom/cast_on, atom/second_cast)
+ ..()
+ var/obj/item/weapon/melee/soulblade/blade = owner.loc
+ if (istype(blade.loc, /obj/projectile))
+ var/obj/projectile/P = blade.loc
+ qdel(P)
+ var/turf/starting = get_turf(blade)
+ var/turf/target = cast_on
+ var/obj/projectile/soulbullet/soul_bullet = new (starting)
+ soul_bullet.preparePixelProjectile(target, starting)
+ soul_bullet.secondary_target = second_cast
+ soul_bullet.shade = owner
+ soul_bullet.blade = blade
+ blade.forceMove(soul_bullet)
+ soul_bullet.fire()
+ soul_bullet.process()
+
+
+/client/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params)
+ if(!mob || !isshade(mob) || !istype(mob.loc, /obj/item/weapon/melee/soulblade))
+ return ..()
+ var/obj/item/weapon/melee/soulblade/SB = mob.loc
+ if(!isturf(src_location) || !isturf(over_location))
+ return ..()
+ if(src_location == over_location)
+ return ..()
+ var/datum/action/cooldown/spell/pointed/soulblade/blade_perforate/BP = locate() in mob.actions
+ if (BP && isturf(SB.loc))
+ BP.cast(src_location, over_location)
+
+
+//////////////////////////////
+// //Spend 10 blood -> Heal 10 brute damage on your wielder and clamp their bleeding wounds. Good trade, yes?
+// MEND ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //
+//////////////////////////////
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_mend
+ name = "Mend"
+ desc = "(10 BLOOD) Heal some of your wielder's brute damage using your blood."
+ button_icon_state = "soulblade_mend"
+
+ blood_cost = 10
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_mend/cast(atom/cast_on)
+ ..()
+ var/obj/item/weapon/melee/soulblade/SB = owner.loc
+ var/mob/living/wielder = SB.loc
+ if(istype(wielder, /mob/living/carbon/human))
+ var/mob/living/carbon/human/H = wielder
+ for(var/datum/wound/wound as anything in H.all_wounds)
+ wound.adjust_blood_flow(-20)
+
+ //playsound(wielder.loc, 'sound/effects/mend.ogg', 50, 0, -2)
+ wielder.adjustBruteLoss(-10)
+ to_chat(owner, "You heal some of your wielder's wounds.")
+ to_chat(wielder, "\The [owner] heals some of your wounds.")
+
+
+//////////////////////////////
+// //
+// TOGGLE BLADE HARM ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// //
+//////////////////////////////
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_harm
+ name = "Toggle Harm to Non-Masters"
+ desc = "(FREE) Change whether you allow people who aren't either cultists or the person that soulstone'd you to wield you."
+ button_icon_state = "soulblade_harm"
+
+/datum/action/cooldown/spell/pointed/soulblade/blade_harm/cast(atom/cast_on)
+ . = ..()
+ var/mob/living/basic/shade/user = owner
+ if (istype(user))
+ if (user.blade_harm)
+ user.blade_harm = FALSE
+ button_icon_state = "soulblade_calm"
+ to_chat(user, span_notice("You now allow anyone to wield you.") )
+ else
+ user.blade_harm = TRUE
+ button_icon_state = "soulblade_harm"
+ to_chat(user, span_notice("You now harm and make dizzy miscreants trying to wield you.") )
+
+ var/obj/item/weapon/melee/soulblade/SB = user.loc
+ if (istype(SB))
+ var/mob/living/M = SB.loc//bloke holding the blade
+ if (istype(M) && !IS_CULTIST(M) && (user.master != M))
+ if (user.blade_harm)
+ M.adjust_dizzy(120)
+ to_chat(M, span_warning("You feel a chill as \the [SB]'s murderous intents suddenly turn against you.") )
+ else
+ M.adjust_dizzy(-120)
+ to_chat(M, span_notice("\The energies emanated by the [SB] subside a little, allowing you to wield it.") )
diff --git a/monkestation/code/modules/bloody_cult/cult/structures/candle.dm b/monkestation/code/modules/bloody_cult/cult/structures/candle.dm
new file mode 100644
index 000000000000..4009f8a8ae8e
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/structures/candle.dm
@@ -0,0 +1,143 @@
+/obj/item/candle
+ name = "candle"
+ desc = "A candle made out of wax, used for moody lighting and solar flares."
+ icon = 'monkestation/code/modules/bloody_cult/icons/candle.dmi'
+ icon_state = "candle"
+ heat = 1000
+ var/food_candle = "foodcandle"
+ w_class = WEIGHT_CLASS_TINY
+ light_color = LIGHT_COLOR_FIRE
+ color = COLOR_OFF_WHITE
+
+ var/wax = 1800 // 30 minutes
+ var/lit = 0
+ var/trashtype = /obj/item/trash/candle
+ var/image/wick
+ var/flickering = 0
+
+/obj/item/candle/New(turf/loc)
+ ..()
+ wick = image(icon, src, "candle-wick")
+ wick.appearance_flags = RESET_COLOR
+ update_icon()
+
+/obj/item/candle/Initialize(mapload)
+ . = ..()
+ if (lit)//pre-mapped lit candles
+ lit = 0
+ light("", TRUE)
+
+/obj/item/candle/extinguish()
+ ..()
+ if(lit)
+ lit = 0
+ update_icon()
+ set_light(0)
+ remove_particles(PS_CANDLE)
+ remove_particles(PS_CANDLE2)
+
+/obj/item/candle/update_icon()
+ . = ..()
+ overlays.len = 0
+ if (wax == initial(wax))
+ icon_state = "candle"
+ else
+ var/i
+ if(wax > 1200)
+ i = 1
+ else if(wax > 600)
+ i = 2
+ else i = 3
+ icon_state = "candle[i]"
+ wick.icon_state = "[icon_state]-wick"
+ overlays += wick
+ if (lit)
+ var/image/I = image(icon, src, "[icon_state]_lit")
+ I.appearance_flags = RESET_COLOR
+ I.blend_mode = BLEND_ADD
+ if (isturf(loc))
+ I.plane = ABOVE_LIGHTING_PLANE
+ else
+ I.plane = ABOVE_HUD_PLANE // inventory
+ overlays += I
+
+/obj/item/candle/dropped()
+ ..()
+ update_icon()
+
+/obj/item/candle/attackby(obj/item/W, mob/user, params)
+ ..()
+ if (lit && heat)
+ if (istype(W, /obj/item/candle))
+ var/obj/item/candle/C = W
+ C.light(span_notice("[user] lights [C] with [src].") )
+ else if (istype(W, /obj/item/clothing/mask/cigarette))
+ var/obj/item/clothing/mask/cigarette/ciggy = W
+ ciggy.light(span_notice("[user] lights \the [ciggy] using \the [src]'s flame.") )
+ if(W.heat)
+ light(span_notice("[user] lights [src] with [W].") )
+
+/obj/item/candle/proc/light(var/flavor_text = span_notice("[usr] lights [src].") , var/quiet = 0)
+ if(!lit)
+ lit = 1
+ if(!quiet)
+ visible_message(flavor_text)
+ set_light(0.7)
+ add_particles(PS_CANDLE)
+ add_particles(PS_CANDLE2)
+ START_PROCESSING(SSobj, src)
+ update_icon()
+
+/obj/item/candle/proc/flicker(var/amount = rand(5, 15))
+ if(flickering)
+ return
+ flickering = 1
+ if(lit)
+ for(var/i = 0; i < amount; i++)
+ if(prob(95))
+ if(prob(30))
+ extinguish()
+ else
+ var/candleflick = pick(0.5, 0.7, 0.9, 1, 1.3, 1.5, 2)
+ set_light(candleflick * 0.7)
+ else
+ set_light(5 * 0.7)
+ if(heat == 0) //only holocandles don't have source temp, using this so I don't add a new var
+ wax = 0.8 * wax //jury rigged so the wax reduction doesn't nuke the holocandles if flickered
+ visible_message(span_warning("\The [src]'s flame starts roaring unnaturally!") )
+ update_icon()
+ sleep(rand(5, 8))
+ set_light(1)
+ lit = 1
+ update_icon()
+ flickering = 0
+
+/obj/item/candle/attack_ghost(mob/user)
+ add_hiddenprint(user)
+ flicker(1)
+
+/obj/item/candle/process()
+ if(!lit)
+ return
+ wax--
+ if(!wax)
+ new trashtype(src.loc, src)
+ if(istype(src.loc, /mob))
+ src.dropped()
+ qdel(src)
+ return
+ update_icon()
+
+/obj/item/candle/attack_self(mob/user as mob)
+ if(lit)
+ extinguish()
+ to_chat(user, span_warning("You pinch \the [src]'s wick.") )
+
+
+
+/obj/item/candle/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ . = ..()
+ if(istype(arrived, /obj/projectile/beam))
+ var/obj/projectile/beam/P = arrived//could be a laser beam or an emitter beam, both feature the get_damage() proc, for now...
+ if(P.damage >= 5)
+ light("", 1)
diff --git a/monkestation/code/modules/bloody_cult/cult/use_blood.dm b/monkestation/code/modules/bloody_cult/cult/use_blood.dm
new file mode 100644
index 000000000000..b03a708ad4a8
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/use_blood.dm
@@ -0,0 +1,461 @@
+
+//When cultists need to pay in blood to use their spells, they have a few options at their disposal:
+// * If their hands are bloody, they can use the few units of blood on them.
+// * If there is a blood splatter on the ground that still has a certain amount of fresh blood in it, they can use that?
+// * If they are grabbing another person, they can stab their nails in their vessels to draw some blood from them
+// * If they are standing above a bleeding person, they can dip their fingers into their wounds.
+// * If they are holding a container that has blood in it (such as a beaker or a blood pack), they can pour/squeeze blood from them
+// * If they are standing above a container that has blood in it, they can dip their fingers into them
+// * Finally if there are no alternative blood sources, you can always use your own blood.
+
+/* get_available_blood
+ user: the mob (generally a cultist) trying to spend blood
+ amount_needed: the amount of blood required
+
+ returns: a /list with information on nearby available blood. For use by use_available_blood().
+*/
+/proc/get_available_blood(var/mob/user, var/amount_needed = 0)
+ var/data = list(
+ BLOODCOST_TARGET_BLEEDER = null,
+ BLOODCOST_AMOUNT_BLEEDER = 0,
+ BLOODCOST_TARGET_GRAB = null,
+ BLOODCOST_AMOUNT_GRAB = 0,
+ BLOODCOST_TARGET_HANDS = null,
+ BLOODCOST_AMOUNT_HANDS = 0,
+ BLOODCOST_TARGET_HELD = null,
+ BLOODCOST_AMOUNT_HELD = 0,
+ BLOODCOST_LID_HELD = 0,
+ BLOODCOST_TARGET_SPLATTER = null,
+ BLOODCOST_AMOUNT_SPLATTER = 0,
+ BLOODCOST_TARGET_BLOODPACK = null,
+ BLOODCOST_AMOUNT_BLOODPACK = 0,
+ BLOODCOST_HOLES_BLOODPACK = 0,
+ BLOODCOST_TARGET_CONTAINER = null,
+ BLOODCOST_AMOUNT_CONTAINER = 0,
+ BLOODCOST_LID_CONTAINER = 0,
+ BLOODCOST_TARGET_USER = null,
+ BLOODCOST_AMOUNT_USER = 0,
+ BLOODCOST_RESULT = "",
+ BLOODCOST_TOTAL = 0,
+ BLOODCOST_USER = null,
+ )
+ var/turf/T = get_turf(user)
+ var/amount_gathered = 0
+
+ data[BLOODCOST_RESULT] = user
+
+ if (amount_needed == 0)//the cost was probably 1u, and already paid for by blood communion from another cultist
+ data[BLOODCOST_RESULT] = BLOODCOST_TRIBUTE
+ return data
+
+ //Are we a construct?
+ if (isconstruct(user))
+ var/mob/living/basic/construct/C_user = user
+ if (!C_user.purge)//Constructs can use runes for free as long as they aren't getting purged by holy water or null rods
+ data[BLOODCOST_TARGET_USER] = C_user
+ data[BLOODCOST_AMOUNT_USER] = amount_needed
+ amount_gathered = amount_needed
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_USER
+ return data
+
+ //Is there blood on our hands?
+ var/mob/living/carbon/human/H_user = user
+ if (istype (H_user))
+ data[BLOODCOST_TARGET_HANDS] = H_user
+ var/blood_gathered = min(amount_needed, H_user.blood_in_hands)
+ data[BLOODCOST_AMOUNT_HANDS] = blood_gathered
+ amount_gathered += blood_gathered
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_HANDS
+ return data
+
+ //Is there a fresh blood splatter on the turf?
+ for (var/obj/effect/decal/cleanable/blood/B in T)
+ var/blood_volume = B.count * 25
+ if (blood_volume)
+ data[BLOODCOST_TARGET_SPLATTER] = B
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_AMOUNT_SPLATTER] = blood_gathered
+ amount_gathered += blood_gathered
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_SPLATTER
+ return data
+
+ if (user.pulling)
+ if(ishuman(user.pulling))
+ var/mob/living/carbon/human/H = user.pulling
+ if(!HAS_TRAIT(H, TRAIT_NOBLOOD))
+ var/blood_volume = H.blood_volume
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_TARGET_GRAB] = H
+ data[BLOODCOST_AMOUNT_GRAB] = blood_gathered
+ amount_gathered += blood_gathered
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_GRAB
+ return data
+
+ //Is there a bleeding mob/corpse on the turf that still has blood in it?
+ for (var/mob/living/carbon/human/H in T)
+ if(HAS_TRAIT(H, TRAIT_NOBLOOD))
+ continue
+ if(user != H)
+ if(H.get_bleed_rate() > 0)
+ var/blood_volume = H.blood_volume
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_TARGET_BLEEDER] = H
+ data[BLOODCOST_AMOUNT_BLEEDER] = blood_gathered
+ amount_gathered += blood_gathered
+ break
+ if (data[BLOODCOST_TARGET_BLEEDER])
+ break
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_BLEEDER
+ return data
+
+ for(var/obj/item/reagent_containers/G_held in H_user.held_items) //Accounts for if the person has multiple grasping organs
+ if (!istype(G_held) || !round(G_held.reagents.get_reagent_amount(/datum/reagent/blood)))
+ continue
+ if(istype(G_held, /obj/item/reagent_containers/blood)) //Bloodbags have their own functionality
+ var/obj/item/reagent_containers/blood/blood_pack = G_held
+ var/blood_volume = round(blood_pack.reagents.get_reagent_amount(/datum/reagent/blood))
+ if (blood_volume)
+ data[BLOODCOST_TARGET_BLOODPACK] = blood_pack
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_AMOUNT_BLOODPACK] = blood_gathered
+ amount_gathered += blood_gathered
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_BLOODPACK
+ return data
+
+ else
+ var/blood_volume = round(G_held.reagents.get_reagent_amount(/datum/reagent/blood))
+ if (blood_volume)
+ data[BLOODCOST_TARGET_HELD] = G_held
+ if (G_held.is_open_container())
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_AMOUNT_HELD] = blood_gathered
+ amount_gathered += blood_gathered
+ else
+ data[BLOODCOST_LID_HELD] = 1
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_HELD
+ return data
+
+
+ //Is there a reagent container on the turf that has blood in it?
+ for (var/obj/item/reagent_containers/G in T)
+ var/blood_volume = round(G.reagents.get_reagent_amount(/datum/reagent/blood))
+ if (blood_volume)
+ data[BLOODCOST_TARGET_CONTAINER] = G
+ if (G.is_open_container())
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_AMOUNT_CONTAINER] = blood_gathered
+ amount_gathered += blood_gathered
+ break
+ else
+ data[BLOODCOST_LID_CONTAINER] = 1
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_CONTAINER
+ return data
+
+ //Does the user have blood? (the user can pay in blood without having to bleed first)
+ if(istype(H_user))
+ if(!HAS_TRAIT(H_user, TRAIT_NO_BLOOD_OVERLAY))
+ var/blood_volume = H_user.blood_volume
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_TARGET_USER] = H_user
+ data[BLOODCOST_AMOUNT_USER] = blood_gathered
+ amount_gathered += blood_gathered
+ else//non-human trying to draw runes eh? let's see...
+ if (ismonkey(user) || isalien(user))
+ var/mob/living/carbon/C_user = user
+ if (!C_user.stat == DEAD)
+ var/blood_volume = round(max(0, C_user.health))//Unlike humans, monkeys take oxy damage when blood is taken from them.
+ var/blood_gathered = min(amount_needed-amount_gathered, blood_volume)
+ data[BLOODCOST_TARGET_USER] = C_user
+ data[BLOODCOST_AMOUNT_USER] = blood_gathered
+ amount_gathered += blood_gathered
+
+
+ if (amount_gathered >= amount_needed)
+ data[BLOODCOST_RESULT] = BLOODCOST_TARGET_USER
+ return data
+
+ data[BLOODCOST_RESULT] = BLOODCOST_FAILURE
+ return data
+
+
+/* use_available_blood
+ user: the mob (generally a cultist) trying to spend blood
+ amount_needed: the amount of blood required
+ previous_result: the result of the previous call of this proc if any, to prevent the same flavor text from displaying every single call of this proc in a row
+ tribute: set to 1 when called by a contributor to Blood Communion
+
+ returns: a /list with information on the success/failure of the proc, and in the former case, information the blood that was used (color, type, dna)
+*/
+/proc/use_available_blood(var/mob/user, var/amount_needed = 0, var/previous_result = "", var/tribute = 0, var/feedback = TRUE)
+ //Blood Communion
+ var/communion = 0
+ var/communion_data = null
+ var/total_accumulated = 0
+ var/total_needed = amount_needed
+ if (!tribute && IS_CULTIST(user))
+ var/datum/antagonist/cult/mycultist = user.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (mycultist.blood_pool && (mycultist in GLOB.blood_communion))
+ communion = 1
+ amount_needed = max(1, round(amount_needed * 4 / 5))//saving 20% blood
+ var/list/tributers = list()
+ for (var/datum/antagonist/cult/cultist in GLOB.blood_communion)
+ if (cultist.blood_pool && cultist.owner && cultist.owner.current && iscarbon(cultist.owner.current) && !cultist.owner.current.stat == DEAD)
+ var/mob/living/L = cultist.owner.current
+ if (istype(L) && L != user)
+ tributers.Add(L)
+ var/total_per_tribute = max(1, round(amount_needed/max(1, tributers.len+1)))
+ var/tributer_size = tributers.len
+ for (var/i = 1 to tributer_size)
+ var/mob/living/L = pick(tributers)//so it's not always the first one that pays the first blood unit.
+ tributers.Remove(L)
+ var/data = use_available_blood(L, total_per_tribute, "", 1)
+ if (data[BLOODCOST_RESULT] != BLOODCOST_FAILURE)
+ total_accumulated += data[BLOODCOST_TOTAL]
+ if (total_accumulated >= amount_needed - total_per_tribute)//could happen if the cost is less than 1 per tribute
+ communion_data = data//in which case, the blood will carry the data that paid for it
+ break
+
+ //Getting nearby blood sources
+ var/list/data = get_available_blood(user, amount_needed-total_accumulated)
+
+ var/datum/reagent/blood/blood
+
+ //Flavour text and blood data transfer
+ switch (data[BLOODCOST_RESULT])
+ if (BLOODCOST_TRIBUTE)//if the drop of blood was paid for through blood communion, let's get the reference to the blood they used because we can
+ blood = new()
+ if (communion_data && communion_data[BLOODCOST_RESULT])
+ switch(communion_data[BLOODCOST_RESULT])
+ if (BLOODCOST_TARGET_HANDS)
+ var/mob/living/carbon/human/HU = communion_data[BLOODCOST_USER]
+ blood.color = HU.get_blood_dna_color()
+ var/datum/blood_type/blood_type = HU.get_blood_type()
+ blood.data =blood_type.get_blood_data(HU)
+
+ //can't get virus data from bloody hands because it'd be a pain in the ass to code for minimal use
+ if (BLOODCOST_TARGET_SPLATTER)
+ var/obj/effect/decal/cleanable/blood/B = communion_data[BLOODCOST_TARGET_SPLATTER]
+ blood = new()
+ blood.color = B.color
+ if (BLOODCOST_TARGET_GRAB)
+ var/mob/living/carbon/CA = communion_data[BLOODCOST_TARGET_GRAB]
+ var/datum/blood_type/blood_type = CA.get_blood_type()
+ blood.data = blood_type.get_blood_data(CA)
+
+ if (BLOODCOST_TARGET_BLEEDER)
+ var/mob/living/carbon/CA = communion_data[BLOODCOST_TARGET_BLEEDER]
+ if (isliving(CA))
+ var/datum/blood_type/blood_type = CA.get_blood_type()
+ blood.data = blood_type.get_blood_data(CA)
+ if (BLOODCOST_TARGET_HELD)
+ var/obj/item/reagent_containers/G = communion_data[BLOODCOST_TARGET_HELD]
+ blood = locate() in G.reagents.reagent_list
+ if (BLOODCOST_TARGET_BLOODPACK)
+ var/obj/item/reagent_containers/blood/B = communion_data[BLOODCOST_TARGET_BLOODPACK]
+ blood = locate() in B.reagents.reagent_list
+ if (BLOODCOST_TARGET_CONTAINER)
+ var/obj/item/reagent_containers/G = communion_data[BLOODCOST_TARGET_CONTAINER]
+ blood = locate() in G.reagents.reagent_list
+ if (BLOODCOST_TARGET_USER)
+ var/mob/living/carbon/CA = communion_data[BLOODCOST_USER]
+ if (iscarbon(CA))
+ var/datum/blood_type/blood_type = CA.get_blood_type()
+ blood.data =blood_type.get_blood_data(CA)
+ if (isconstruct(CA))//constructs can't get the blood communion tattoo but just in case they do later
+ blood.data["blood_colour"] = "#CC0E00"
+ if (feedback && !tribute && previous_result != BLOODCOST_TRIBUTE)
+ user.visible_message(span_warning("Drips of blood seem to appear out of thin air around \the [user], and fall onto the floor!") ,
+ span_rose("An ally has lent you a drip of their blood for your ritual.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_HANDS)
+ var/mob/living/carbon/human/H = user
+ blood = new()
+ if (H.blood_in_hands > 0)
+ var/datum/blood_type/blood_type = H.get_blood_type()
+ blood.data =blood_type.get_blood_data(H)
+ //can't get virus data from bloody hands because it'd be a pain in the ass to code for minimal use
+ if (feedback && !tribute && previous_result != BLOODCOST_TARGET_HANDS)
+ user.visible_message(span_warning("The blood on \the [user]'s hands drips onto the floor!") ,
+ span_rose("You let the blood smeared on your hands join the pool of your summoning.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_SPLATTER)
+ var/obj/effect/decal/cleanable/blood/B = data[BLOODCOST_TARGET_SPLATTER]
+ blood = new()
+ blood.color = B.color
+ if (feedback && !tribute && previous_result != BLOODCOST_TARGET_SPLATTER)
+ user.visible_message(span_warning("The blood on the floor below \the [user] starts moving!") ,
+ span_rose("You redirect the flow of blood inside the splatters on the floor toward the pool of your summoning.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_GRAB)
+ var/mob/living/carbon/C = data[BLOODCOST_TARGET_GRAB]
+ if (iscarbon(C))
+ blood = new()
+ var/datum/blood_type/blood_type = C.get_blood_type()
+ blood.data =blood_type.get_blood_data(C)
+ if (feedback && !tribute && previous_result != BLOODCOST_TARGET_GRAB)
+ user.visible_message(span_warning("\The [user] stabs their nails inside \the [data[BLOODCOST_TARGET_GRAB]], drawing blood from them!") ,
+ span_rose("You stab your nails inside \the [data[BLOODCOST_TARGET_GRAB]] to draw some blood from them.") ,
+ span_warning("You hear a liquid flowing.") )
+
+ if (BLOODCOST_TARGET_BLEEDER)
+ var/mob/living/carbon/C = data[BLOODCOST_TARGET_BLEEDER]
+ if (iscarbon(C))
+ blood = new()
+ var/datum/blood_type/blood_type = C.get_blood_type()
+ blood.data =blood_type.get_blood_data(C)
+ if (feedback && !tribute && previous_result != BLOODCOST_TARGET_BLEEDER)
+ user.visible_message(span_warning("\The [user] dips their fingers inside \the [data[BLOODCOST_TARGET_BLEEDER]]'s wounds!") ,
+ span_rose("You dip your fingers inside \the [data[BLOODCOST_TARGET_BLEEDER]]'s wounds to draw some blood from them.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_HELD)
+ var/obj/item/reagent_containers/G = data[BLOODCOST_TARGET_HELD]
+ blood = locate() in G.reagents.reagent_list
+ if (!tribute && previous_result != BLOODCOST_TARGET_HELD)
+ user.visible_message(span_warning("\The [user] tips \the [data[BLOODCOST_TARGET_HELD]], pouring blood!") ,
+ span_rose("You tip \the [data[BLOODCOST_TARGET_HELD]] to pour the blood contained inside.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_BLOODPACK)
+ var/obj/item/reagent_containers/blood/B = data[BLOODCOST_TARGET_BLOODPACK]
+ blood = locate() in B.reagents.reagent_list
+ if (feedback && !tribute && previous_result != BLOODCOST_TARGET_BLOODPACK)
+ user.visible_message(span_warning("\The [user] squeezes \the [data[BLOODCOST_TARGET_BLOODPACK]], pouring blood!") ,
+ span_rose("You squeeze \the [data[BLOODCOST_TARGET_BLOODPACK]] to pour the blood contained inside.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_CONTAINER)
+ var/obj/item/reagent_containers/G = data[BLOODCOST_TARGET_CONTAINER]
+ blood = locate() in G.reagents.reagent_list
+ if (feedback && !tribute && previous_result != BLOODCOST_TARGET_CONTAINER)
+ user.visible_message(span_warning("\The [user] dips their fingers inside \the [data[BLOODCOST_TARGET_CONTAINER]], covering them in blood!") ,
+ span_rose("You dip your fingers inside \the [data[BLOODCOST_TARGET_CONTAINER]], covering them in blood.") ,
+ span_warning("You hear a liquid flowing.") )
+ if (BLOODCOST_TARGET_USER)
+ blood = new()
+ if (!tribute)
+ if (data[BLOODCOST_HOLES_BLOODPACK])
+ to_chat(user, span_warning("You must puncture \the [data[BLOODCOST_TARGET_BLOODPACK]] before you can squeeze blood from it!") )
+ else if (data[BLOODCOST_LID_HELD])
+ to_chat(user, span_warning("Remove \the [data[BLOODCOST_TARGET_HELD]]'s lid first!") )
+ else if (data[BLOODCOST_LID_CONTAINER])
+ to_chat(user, span_warning("Remove \the [data[BLOODCOST_TARGET_CONTAINER]]'s lid first!") )
+ if (iscarbon(user))
+ var/mob/living/carbon/C_user = user
+ var/datum/blood_type/blood_type = C_user.get_blood_type()
+ blood.data =blood_type.get_blood_data(C_user)
+
+ if (isconstruct(user))
+ blood.data["blood_colour"] = "#CC0E00"//not like constructs can write runes by themselves currently, but they might do at some point
+
+ if (feedback && !tribute && (previous_result != BLOODCOST_TARGET_USER))
+ if (iscarbon(user))//if the user is holding a sharp weapon, they get a custom message
+ var/obj/item/weapon/W = user.get_active_held_item()
+ if (W && W.sharpness == SHARP_POINTY)
+ to_chat(user, span_rose("You slice open your finger with \the [W] to let a bit of blood flow.") )
+ else
+ var/obj/item/weapon/W2 = user.get_inactive_held_item()
+ if (W2 && W2.sharpness == SHARP_POINTY)
+ to_chat(user, span_rose("You slice open your finger with \the [W] to let a bit of blood flow.") )
+ else
+ to_chat(user, span_rose("You bite your finger and let the blood pearl up.") )
+ else if (isconstruct(user))
+ to_chat(user, span_rose("Your shell's connection past the veil lets you perform the ritual without the need for a local source of blood.") )
+ if (BLOODCOST_FAILURE)
+ if (!tribute)
+ if (data[BLOODCOST_HOLES_BLOODPACK])
+ to_chat(user, span_danger("You must puncture \the [data[BLOODCOST_TARGET_BLOODPACK]] before you can squeeze blood from it!") )
+ else if (data[BLOODCOST_LID_HELD])
+ to_chat(user, span_danger("Remove \the [data[BLOODCOST_TARGET_HELD]]'s lid first!") )
+ else if (data[BLOODCOST_LID_CONTAINER])
+ to_chat(user, span_danger("Remove \the [data[BLOODCOST_TARGET_HELD]]'s lid first!") )
+ else
+ to_chat(user, span_danger("There is no blood available. Not even in your own body!") )
+
+ //Blood is only consumed if there is enough of it
+ if (!data[BLOODCOST_FAILURE])
+ if (data[BLOODCOST_TARGET_HANDS])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_HANDS]
+ var/mob/living/carbon/human/H = user
+ H.blood_in_hands = max(0, (H.blood_in_hands * 3) - data[BLOODCOST_AMOUNT_HANDS])
+ if (!H.blood_in_hands)
+ H.blood_in_hands= 0
+ H.update_worn_gloves()
+ if (data[BLOODCOST_TARGET_SPLATTER])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_SPLATTER]
+ var/obj/effect/decal/cleanable/blood/B = data[BLOODCOST_TARGET_SPLATTER]
+ B.count = max(0 , B.count * 5 - data[BLOODCOST_AMOUNT_SPLATTER])
+ if (data[BLOODCOST_TARGET_GRAB])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_GRAB]
+ if (ishuman(data[BLOODCOST_TARGET_GRAB]))
+ var/mob/living/carbon/human/H = data[BLOODCOST_TARGET_GRAB]
+ H.blood_volume -= data[BLOODCOST_AMOUNT_GRAB]
+ H.take_overall_damage(data[BLOODCOST_AMOUNT_GRAB] ? 0.1 : 0)
+ if (data[BLOODCOST_TARGET_BLEEDER])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_BLEEDER]
+ var/mob/living/carbon/human/H = data[BLOODCOST_TARGET_BLEEDER]
+ H.blood_volume -= data[BLOODCOST_AMOUNT_BLEEDER]
+ H.take_overall_damage(data[BLOODCOST_AMOUNT_BLEEDER] ? 0.1 : 0)
+ if (data[BLOODCOST_TARGET_HELD])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_HELD]
+ var/obj/item/reagent_containers/G = data[BLOODCOST_TARGET_HELD]
+ G.reagents.remove_reagent(/datum/reagent/blood, data[BLOODCOST_AMOUNT_HELD])
+ if (data[BLOODCOST_TARGET_BLOODPACK])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_BLOODPACK]
+ var/obj/item/reagent_containers/G = data[BLOODCOST_TARGET_BLOODPACK]
+ G.reagents.remove_reagent(/datum/reagent/blood, data[BLOODCOST_AMOUNT_BLOODPACK])
+ if (data[BLOODCOST_TARGET_CONTAINER])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_CONTAINER]
+ var/obj/item/reagent_containers/G = data[BLOODCOST_TARGET_CONTAINER]
+ G.reagents.remove_reagent(/datum/reagent/blood, data[BLOODCOST_AMOUNT_CONTAINER])
+ if (data[BLOODCOST_TARGET_USER])
+ data[BLOODCOST_TOTAL] += data[BLOODCOST_AMOUNT_USER]
+ if (ishuman(user))
+ var/mob/living/carbon/human/H = user
+ var/blood_before = H.blood_volume
+ H.blood_volume -= data[BLOODCOST_AMOUNT_USER] * 3 //doing this yourself is worse then communion
+ var/blood_after = H.blood_volume
+ if (blood_before > BLOOD_VOLUME_SAFE && blood_after < BLOOD_VOLUME_SAFE)
+ to_chat(user, span_cult("You start looking pale.") )
+ else if (blood_before > BLOOD_VOLUME_OKAY && blood_after < BLOOD_VOLUME_OKAY)
+ to_chat(user, span_cult("You are about to pass out from the lack of blood.") )
+ else if (blood_before > BLOOD_VOLUME_BAD && blood_after < BLOOD_VOLUME_BAD)
+ to_chat(user, span_cult("You have trouble focusing, things will go bad if you keep using your blood.") )
+ else if (blood_before > BLOOD_VOLUME_SURVIVE && blood_after < BLOOD_VOLUME_SURVIVE)
+ to_chat(user, span_cult("It will be all over soon.") )
+ H.take_overall_damage(data[BLOODCOST_AMOUNT_USER] ? 0.1 : 0)
+ else if (ismonkey(user) || isalien(user))
+ var/mob/living/carbon/C = user
+ var/blood_before = C.health
+ if (ismonkey(C))
+ C.adjustOxyLoss(data[BLOODCOST_AMOUNT_USER])
+ else if (isalien(C))
+ C.adjustBruteLoss(data[BLOODCOST_AMOUNT_USER])
+ C.updatehealth()
+ var/blood_after = C.health
+ if (blood_before > (C.maxHealth*5/6) && blood_after < (C.maxHealth*5/6))
+ to_chat(user, span_cult("You start looking pale.") )
+ else if (blood_before > (C.maxHealth*4/6) && blood_after < (C.maxHealth*4/6))
+ to_chat(user, span_cult("You feel weak from the lack of blood.") )
+ else if (blood_before > (C.maxHealth*3/6) && blood_after < (C.maxHealth*3/6))
+ to_chat(user, span_cult("You are about to pass out from the lack of blood.") )
+ else if (blood_before > (C.maxHealth*2/6) && blood_after < (C.maxHealth*2/6))
+ to_chat(user, span_cult("You have trouble focusing, things will go bad if you keep using your blood.") )
+ else if (blood_before > (C.maxHealth*1/6) && blood_after < (C.maxHealth*1/6))
+ to_chat(user, span_cult("It will be all over soon.") )
+
+
+ if (communion && data[BLOODCOST_TOTAL] + total_accumulated >= amount_needed)
+ data[BLOODCOST_TOTAL] = max(data[BLOODCOST_TOTAL], total_needed)
+ data["blood"] = blood
+ return data
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/_TESTING_ITEMS.dm b/monkestation/code/modules/bloody_cult/cult/weapons/_TESTING_ITEMS.dm
new file mode 100644
index 000000000000..1fd96b6c5979
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/_TESTING_ITEMS.dm
@@ -0,0 +1,21 @@
+/obj/item/weapon/bloodcult_jaunter
+ name = "test jaunter"
+ desc = ""
+ icon = 'icons/obj/wizard.dmi'
+ icon_state = "soulstone"
+ var/obj/structure/bloodcult_jaunt_target/target = null
+
+/obj/item/weapon/bloodcult_jaunter/New()
+ ..()
+ target = new(loc)
+
+/obj/item/weapon/bloodcult_jaunter/attack_self(var/mob/user)
+ new /obj/effect/bloodcult_jaunt(get_turf(src), user, get_turf(target))
+
+/obj/structure/bloodcult_jaunt_target
+ name = "test target"
+ desc = ""
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state ="pylon"
+ anchored = 1
+ density = 0
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/arcane_tome.dm b/monkestation/code/modules/bloody_cult/cult/weapons/arcane_tome.dm
new file mode 100644
index 000000000000..628ca34a80f6
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/arcane_tome.dm
@@ -0,0 +1,322 @@
+#define PAGE_FOREWORD 0
+#define PAGE_LORE1 101
+#define PAGE_LORE2 102
+#define PAGE_LORE3 103
+
+var/list/arcane_tomes=list()
+
+///////////////////////////////////////ARCANE TOME////////////////////////////////////////////////
+/obj/item/weapon/tome
+ name = "arcane tome"
+ desc = "A dark, dusty tome with frayed edges and a sinister cover. Its surface is hard and cold to the touch."
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "tome"
+ throw_speed = 1
+ throw_range = 5
+ w_class = WEIGHT_CLASS_SMALL
+ slot_flags = ITEM_SLOT_BELT
+ var/state = TOME_CLOSED
+ var/can_flick = 1
+ var/list/talismans = list()
+ var/current_page = PAGE_FOREWORD
+
+/obj/item/weapon/tome/New()
+ ..()
+ arcane_tomes.Add(src)
+
+/obj/item/weapon/tome/salt_act()
+ fire_act(1000, 200)
+
+/obj/item/weapon/tome/Destroy()
+ arcane_tomes.Remove(src)
+ for(var/obj/O in talismans)
+ talismans.Remove(O)
+ qdel(O)
+ talismans=list()
+ ..()
+
+/obj/item/weapon/tome/suicide_act(mob/living/user)
+ var/datum/antagonist/cult/cult_datum=user.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ playsound(get_turf(user), 'monkestation/code/modules/bloody_cult/sound/edibles.ogg', 75, 0)
+ anim(target=user, a_icon='monkestation/code/modules/bloody_cult/icons/cult.dmi', a_icon_state="build", lay=BELOW_OBJ_LAYER, plane=GAME_PLANE, sleeptime=20)
+ user.Stun(10)
+ icon_state="tome-open"
+ flick("tome-flickopen", src)
+ playsound(user, "pageturn", 50, 1, -2)
+ state=TOME_OPEN
+ to_chat(viewers(user), span_danger("[user] starts repeating arcane words while holding \the [src] open. Blood begins to spill from their nose, their eyes, ears, and every other orfices! It looks like \he's trying to commit suicide.") )
+ sleep(10)
+ cult_datum.gain_devotion(500, DEVOTION_TIER_4, "suicide_tome", user)
+ anim(target=user, a_icon='icons/effects/effects.dmi', flick_anim="rune_sac", lay=ABOVE_MOB_LAYER, plane=GAME_PLANE_UPPER)
+ to_chat(user, span_cult("You offer this shell of flesh to Nar-Sie.") )
+ sleep(4)
+ user.gib()
+ else
+ return ..()
+
+
+/obj/item/weapon/tome/proc/tome_text()
+ var/page_data=null
+ var/dat={"arcane tome
+
+
+
+
"}
+
+ var i=1
+ for(var/subtype in subtypesof(/datum/rune_spell))
+ var/datum/rune_spell/instance=subtype
+ if (initial(instance.secret))
+ continue
+ dat += "
"}
+
+ if (page_data)
+ dat += page_data
+ else
+ dat += page_special()
+
+ dat += {"
"}
+
+ return dat
+
+/obj/item/weapon/tome/proc/page_special()
+ var/dat=null
+ switch (current_page)
+ if (PAGE_FOREWORD)
+ dat={"
Foreword
"}
+ dat += "Written over the ages by a collection of arch-cultists, under the guidance of the geometer himself.\
+
Touch a chapter to read it."
+ if (PAGE_LORE1)
+ dat={"
Addendum I: "From the other side of the veil"
"}
+ dat += "It is by chance that humanity stumbled upon the realm of Nar-Sie some centuries ago, \
+ although while some of those so called wizards called it a happy little accident, few of them know that the dice was loaded from the start.\
+
Nar-Sie threw some artifacts adrift in the bluespace, waiting for some intelligent life to pick them up and trace their way back to him. \
+ For you see, Nar-Sie loves two things about humans, the blood that flows from their veins, and the dramatic circumstances around which said blood ends up flowing from their gaping wounds.\
+
How did he know about humanity's existence before they even reached him you might ask? It's quite simple, he could hear the drumming of our heartbeats all the way from the other side of the veil."
+ if (PAGE_LORE2)
+ dat={"
Addendum II: "From whom the blood spills"
"}
+ dat += "After contact was made between the planes, it was a matter of time before some people would appear who would actively seek Nar-Sie.\
+
Either because his love of drama and chaos resonated with them, and they wanted to become his heralds, performing sacrifices for his amusement, \
+ or because they were in awe with his... \"otherworldlyness?\" People who had lived until now grounded in reality, and became quite fascinated with something mystic, yet tangible.\
+
And of course, then came those who seeked to defy him. Either in the name of their own gods, or out of their own sense of morality, but little do those know, \
+ Nar-Sie loves them equally, and doesn't care too much from whom the blood spills."
+ if (PAGE_LORE3)
+ dat={"
Addendum III: "The geometer's calling card"
"}
+ dat += "A common misconception about Nar-Sie is about his title, why is he the Geometer of Blood? Nobody dared ask him directly by fear of offending him, so for a long time, \
+ many cultists just assumed that he was really into geometry, and his powers manifesting from blood drawings of precise patterns would corroborate this hypothesis.\
+
Some cultists eventually took it upon themselves to commune with him to get an answer, after performing some sacrifices for good measure. The answer was unexpected, and shed more light on the cult's origins. \
+ They learned that after the wizards cut their way into his plane, it took some time for them to run into him, just like humans aren't aware of every single ant living in their garden. \
+ But when they did arrive upon him, his gigantic form twisted upon the scenery gave them the image of a geometer moth.\
+
And just like moths tend to be attracted by light, they saw that Nar-Sie was attracted by blood, so they called him the Geometer of Blood, a title very much to his liking.\
+
As humanity ventures deeper and deeper into the darkness of space and toys with powers they understand less and less, Nar-Sie feels them coming closer and closer to him, and wants now to hasten the process. \
+ His cult sends heralds to let humanity know how much he likes them (their blood mostly), and until he's ready to invite them into his realm, they leave blood-splattered space stations across the stars as his calling card."
+ return dat
+
+/obj/item/weapon/tome/Topic(href, href_list)
+ if (..())
+ return
+ if(!usr.held_items.Find(src))
+ return
+ if(href_list["page"])
+ current_page=text2num(href_list["page"])
+ flick("tome-flick", src)
+ playsound(usr, "pageturn", 50, 1, -5)
+
+ if(href_list["talisman"])
+ var/obj/item/weapon/talisman/T=locate(href_list["talisman"])
+ if(!talismans.Find(T))
+ return
+ T.trigger(usr)
+
+ if(href_list["remove"])
+ var/obj/item/weapon/talisman/T=locate(href_list["remove"])
+ if(!talismans.Find(T))
+ return
+ talismans.Remove(T)
+ usr.put_in_hands(T)
+
+ usr << browse(tome_text(), "window=arcanetome;size=900x600")
+
+/obj/item/weapon/tome/attack(var/mob/living/M, var/mob/living/user)
+ /*
+ M.attack_log += text("\[[time_stamp()]\] Has had the [name] used on him by [user.name] ([user.ckey])")
+ user.attack_log += text("\[[time_stamp()]\] Used [name] on [M.name] ([M.ckey])")
+ msg_admin_attack("[user.name] ([user.ckey]) used [name] on [M.name] ([M.ckey]) (JMP)")
+
+ M.assaulted_by(user)
+ */
+
+ if(!istype(M))
+ return
+
+ if(IS_CULTIST(M))//don't want to harm our team mates using tomes
+ return
+
+ ..()
+
+ if (!M.stat == DEAD)
+ M.adjustOrganLoss(ORGAN_SLOT_STOMACH, rand(1, 10))
+ to_chat(M, span_warning("You feel a searing heat inside of you!") )
+ var/datum/antagonist/cult/cult_datum=user.mind.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ if (M.mind)
+ cult_datum.gain_devotion(30, DEVOTION_TIER_3, "attack_tome", M)
+ else
+ cult_datum.gain_devotion(30, DEVOTION_TIER_2, "attack_tome_nomind", M)
+
+/obj/item/weapon/tome/attack_hand(var/mob/living/user)
+ if(!IS_CULTIST(user) && state == TOME_OPEN)
+ to_chat(user, span_warning("As you reach to pick up \the [src], you feel a searing heat inside of you!") )
+ playsound(loc, 'sound/effects/sparks2.ogg', 50, 1, 0, 0, 0)
+ user.Knockdown(5)
+ user.Stun(5)
+ flick("tome-stun", src)
+ state=TOME_CLOSED
+ return
+ ..()
+
+/obj/item/weapon/tome/pickup(var/mob/user)
+ .=..()
+ if(IS_CULTIST(user) && state == TOME_OPEN)
+ usr << browse(tome_text(), "window=arcanetome;size=900x600")
+
+/obj/item/weapon/tome/dropped(var/mob/user)
+ .=..()
+ usr << browse(null, "window=arcanetome")
+
+/obj/item/weapon/tome/attack_self(var/mob/living/user)
+ if(!IS_CULTIST(user))//Too dumb to live.
+ to_chat(user, span_warning("You try to peek inside \the [src], only to feel a discharge of energy and a searing heat inside of you!") )
+ playsound(loc, 'sound/effects/sparks2.ogg', 50, 1, 0, 0, 0)
+ user.Knockdown(5)
+ user.Stun(5)
+ if (state == TOME_OPEN)
+ icon_state="tome"
+ flick("tome-stun", src)
+ state=TOME_CLOSED
+ else
+ flick("tome-stun2", src)
+ return
+ else
+ if (state == TOME_CLOSED)
+ icon_state="tome-open"
+ flick("tome-flickopen", src)
+ playsound(user, "pageturn", 50, 1, -5)
+ state=TOME_OPEN
+ usr << browse(tome_text(), "window=arcanetome;size=900x600")
+ else
+ icon_state="tome"
+ flick("tome-flickclose", src)
+ state=TOME_CLOSED
+ usr << browse(null, "window=arcanetome")
+
+//absolutely no use except letting cultists know that you're here.
+/obj/item/weapon/tome/attack_ghost(var/mob/dead/observer/user)
+ if (state == TOME_OPEN && can_flick)
+ if (Adjacent(user))
+ to_chat(user, "You flick a page.")
+ flick("tome-flick", src)
+ playsound(user, "pageturn", 50, 1, -3)
+ can_flick=0
+ spawn(5)
+ can_flick=1
+ else
+ to_chat(user, span_warning("You need to get closer to interact with the pages.") )
+
+/obj/item/weapon/tome/attackby(var/obj/item/I, var/mob/user)
+ if (..())
+ return
+ if (istype(I, /obj/item/weapon/talisman))
+ if (talismans.len < MAX_TALISMAN_PER_TOME)
+ if(user.dropItemToGround(I))
+ talismans.Add(I)
+ I.forceMove(src)
+ to_chat(user, span_notice("You slip \the [I] into \the [src].") )
+ if (state == TOME_OPEN)
+ usr << browse(tome_text(), "window=arcanetome;size=900x600")
+ else
+ to_chat(user, span_warning("This tome cannot contain any more talismans. Use or remove some first.") )
+
+/obj/item/weapon/tome/AltClick(var/mob/user)
+ var/list/choices=list()
+ var/datum/rune_spell/instance
+ var/list/choice_to_talisman=list()
+ var/image/talisman_image
+ var/blood_messages=0
+ var/blanks=0
+ for(var/obj/item/weapon/talisman/T in talismans)
+ talisman_image=new(T)
+ if (T.blood_text)
+ choices += list(list("Bloody Message[blood_messages ? " #[blood_messages+1]" : ""]", talisman_image, "A ghost has scribled a message on this talisman."))
+ choice_to_talisman["Bloody Message[blood_messages ? " #[blood_messages+1]" : ""]"]=T
+ blood_messages++
+ else if (T.spell_type)
+ instance=T.spell_type
+ choices += list(list(T.talisman_name(), talisman_image, initial(instance.desc_talisman)))
+ choice_to_talisman[T.talisman_name()]=T
+ else
+ choices += list(list("Blank Talisman[blanks ? " #[blanks+1]" : ""]", talisman_image, "Just an empty talisman."))
+ choice_to_talisman["Blank Talisman[blanks ? " #[blanks+1]" : ""]"]=T
+ blanks++
+
+
+ var/list/made_choices=list()
+ for(var/list/choice in choices)
+ var/datum/radial_menu_choice/option=new
+ option.image=image(choice[2])
+ option.info=span_boldnotice(choice[3])
+ made_choices[choice[1]]=option
+
+ if (state == TOME_CLOSED)
+ icon_state="tome-open"
+ flick("tome-flickopen", src)
+ playsound(user, "pageturn", 50, 1, -5)
+ state=TOME_OPEN
+
+ var/choice=show_radial_menu(user, loc, made_choices, tooltips=TRUE, radial_icon='monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi')
+ if(!choice)
+ return
+ var/obj/item/weapon/talisman/chosen_talisman=choice_to_talisman[choice]
+ if(!usr.held_items.Find(src))
+ return
+ if (state == TOME_OPEN)
+ icon_state="tome"
+ flick("tome-stun", src)
+ state=TOME_CLOSED
+ talismans.Remove(chosen_talisman)
+ usr.put_in_hands(chosen_talisman)
+
+#undef PAGE_FOREWORD
+#undef PAGE_LORE1
+#undef PAGE_LORE2
+#undef PAGE_LORE3
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/blood_dagger.dm b/monkestation/code/modules/bloody_cult/cult/weapons/blood_dagger.dm
new file mode 100644
index 000000000000..9ab72a542691
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/blood_dagger.dm
@@ -0,0 +1,94 @@
+/obj/item/weapon/melee/blood_dagger
+ name = "blood dagger"
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ lefthand_file = 'monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_l.dmi'
+ righthand_file = 'monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_r.dmi'
+ icon_state = "blood_dagger"
+ inhand_icon_state = "blood_dagger"
+ desc = "A knife-shaped hunk of solidified blood. Can be thrown to pin enemies down."
+ siemens_coefficient = 0.2
+ sharpness = SHARP_EDGED
+ force = 15.0
+ w_class = WEIGHT_CLASS_GIGANTIC//don't want it stored anywhere case
+ var/mob/originator = null
+ var/obj/abstract/mind_ui_element/hoverable/bloodcult_spell/dagger/linked_ui
+ var/stacks = 0
+ var/absorbed = 0
+
+/obj/item/weapon/melee/blood_dagger/Destroy()
+ if(linked_ui)
+ linked_ui.dagger = null
+ linked_ui.UpdateIcon()
+ linked_ui = null
+ var/turf/T = get_turf(src)
+ playsound(T, 'monkestation/code/modules/bloody_cult/sound/forge_over.ogg', 100, 0, -2)
+ if (!absorbed && !locate(/obj/effect/decal/cleanable/blood/splatter) in T)
+ var/obj/effect/decal/cleanable/blood/splatter/S = new (T)//splash
+ if (color)
+ S.color = color
+ S.update_icon()
+ ..()
+
+/obj/item/weapon/melee/blood_dagger/suicide_act(var/mob/living/user)
+ to_chat(viewers(user), span_danger("[user] is slitting \his throat with \the [src]! It looks like \he's trying to commit suicide.") )
+
+/obj/item/weapon/melee/blood_dagger/dropped(var/mob/user)
+ ..()
+ qdel(src)
+
+/obj/item/weapon/melee/blood_dagger/attack(var/mob/living/target, var/mob/living/carbon/human/user)
+ if(target == user)
+ if (stacks < 5)
+ user.blood_volume -= 5
+ stacks++
+ playsound(user, 'sound/weapons/bladeslice.ogg', 30, 0, -2)
+ to_chat(user, span_warning("\The [src] takes a bit of your blood.") )
+ return
+ if (IS_CULTIST(user) && !IS_CULTIST(target) && !target.stat == DEAD)
+ var/datum/antagonist/cult/cult_datum = user.mind.has_antag_datum(/datum/antagonist/cult)
+ if (target.mind)
+ cult_datum.gain_devotion(30, DEVOTION_TIER_3, "attack_blooddagger", target)
+ else
+ cult_datum.gain_devotion(30, DEVOTION_TIER_2, "attack_blooddagger_nomind", target)
+ ..()
+/obj/item/weapon/melee/blood_dagger/attack_hand(var/mob/living/user)
+ if(!ismob(loc))
+ qdel(src)
+ return
+ ..()
+
+/obj/item/weapon/melee/blood_dagger/attack_self(var/mob/user)
+ if (ishuman(user) && IS_CULTIST(user))
+ var/mob/living/carbon/human/H = user
+ if (!HAS_TRAIT(H, TRAIT_NOBLOOD))
+ to_chat(user, span_notice("You sheath \the [src] back inside your body[stacks ? ", along with the stolen blood" : ""].") )
+ H.blood_volume += 5 + stacks * 5
+ else
+ to_chat(user, span_notice("You sheath \the [src] inside your body, but the blood fails to find vessels to occupy.") )
+ absorbed = 1
+ playsound(H, 'monkestation/code/modules/bloody_cult/sound/bloodyslice.ogg', 30, 0, -2)
+ qdel(src)
+
+
+/obj/item/weapon/melee/blood_dagger/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = MOVE_FORCE_STRONG, gentle = FALSE, quickstart = TRUE)
+ var/turf/starting = get_turf(thrower)
+ var/obj/projectile/blooddagger/dagger = new (starting)
+ dagger.stacks = stacks
+ dagger.damage = 5 + stacks * 5
+ dagger.icon_state = icon_state
+ dagger.color = color
+ dagger.preparePixelProjectile(target, starting)
+ dagger.fire(direct_target = target)
+ dagger.process()
+ qdel(src)
+
+/obj/item/weapon/melee/blood_dagger/attack(mob/living/attacked, mob/living/carbon/human/user)
+ . = ..()
+ if (ismob(attacked))
+ var/mob/living/M = attacked
+ if (iscarbon(M))
+ var/mob/living/carbon/C = M
+ C.blood_volume -= 5
+ if (stacks < 5)
+ stacks++
+ to_chat(user, span_warning("\The [src] steals a bit of their blood.") )
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/blood_pamphlet.dm b/monkestation/code/modules/bloody_cult/cult/weapons/blood_pamphlet.dm
new file mode 100644
index 000000000000..9b537c5cd5f2
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/blood_pamphlet.dm
@@ -0,0 +1,23 @@
+/obj/item/weapon/bloodcult_pamphlet
+ name = "cult of Nar-Sie pamphlet"
+ desc = "Looks like a page torn from one of those cultist tomes. It is titled \"Ten reasons why Nar-Sie can improve your life!\""
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state ="pamphlet"
+ throwforce = 0
+ w_class = WEIGHT_CLASS_TINY
+ throw_range = 1
+ throw_speed = 1
+
+/obj/item/weapon/bloodcult_pamphlet/attack_self(mob/user, modifiers)
+ if (IS_CULTIST(user))
+ return
+ var/datum/antagonist/cult/new_cultist = new /datum/antagonist/cult()
+ new_cultist.cult_team = new_cultist.get_team()
+ user.mind.add_antag_datum(new_cultist)
+
+/obj/item/weapon/bloodcult_pamphlet/oneuse/attack_self(mob/user, modifiers)
+ ..()
+ qdel(src)
+
+/obj/item/weapon/bloodcult_pamphlet/salt_act()
+ fire_act(1000, 200)
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/blood_talisman.dm b/monkestation/code/modules/bloody_cult/cult/weapons/blood_talisman.dm
new file mode 100644
index 000000000000..98468408c5e0
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/blood_talisman.dm
@@ -0,0 +1,206 @@
+/obj/item/weapon/talisman
+ name = "talisman"
+ desc = "A tattered parchment. You feel a dark energy emanating from it."
+ gender = NEUTER
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "talisman"
+ throwforce = 0
+ w_class = WEIGHT_CLASS_TINY
+ throw_range = 1
+ throw_speed = 1
+ pressure_resistance = 1
+ var/obj/abstract/mind_ui_element/hoverable/bloodcult_spell/talisman/linked_ui
+ var/blood_text = ""
+ var/obj/effect/new_rune/attuned_rune = null
+ var/spell_type = null
+ var/uses = 1
+
+/obj/item/weapon/talisman/New()
+ ..()
+ pixel_x = 0
+ pixel_y = 0
+
+
+/obj/item/weapon/talisman/salt_act()
+ if (attuned_rune && attuned_rune.active_spell)
+ attuned_rune.active_spell.salt_act(get_turf(src))
+ fire_act(1000, 200)
+
+
+/obj/item/weapon/talisman/proc/talisman_name()
+ var/datum/rune_spell/instance = spell_type
+ if (blood_text)
+ return "\[blood message\]"
+ if (instance)
+ return initial(instance.name)
+ else
+ return "\[blank\]"
+
+/obj/item/weapon/talisman/suicide_act(mob/living/user)
+ to_chat(viewers(user), span_danger("[user] swallows \a [src] and appears to be choking on it! It looks like \he's trying to commit suicide.") )
+
+/obj/item/weapon/talisman/examine(var/mob/user)
+ ..()
+ if (blood_text)
+ user << browse("[name][blood_text]", "window = [name]")
+ onclose(user, "[name]")
+ return
+
+ if (!spell_type)
+ to_chat(user, span_info("This one, however, seems pretty unremarkable.") )
+ return
+
+ var/datum/rune_spell/instance = spell_type
+
+ if (IS_CULTIST(user) || isobserver(user))
+ if (attuned_rune)
+ to_chat(user, span_info("This one was attuned to a [initial(instance.name)] rune. [initial(instance.desc_talisman)]") )
+ else
+ to_chat(user, span_info("This one was imbued with a [initial(instance.name)] rune. [initial(instance.desc_talisman)]") )
+ if (uses > 1)
+ to_chat(user, span_info("Its powers can be used [uses] more times.") )
+ else
+ to_chat(user, span_info("This one was some arcane drawings on it. You cannot read them.") )
+
+/obj/item/weapon/talisman/attack_self(var/mob/living/user)
+ if (blood_text)
+ user << browse("[name][blood_text]", "window = [name]")
+ onclose(user, "[name]")
+ onclose(user, "[name]")
+ return
+
+ if (IS_CULTIST(user))
+ trigger(user)
+
+/obj/item/weapon/talisman/attack(var/mob/living/target, var/mob/living/user)
+ if(IS_CULTIST(user) && spell_type)
+ var/datum/rune_spell/instance = spell_type
+ if (initial(instance.touch_cast))
+ new spell_type(user, src, "touch", target)
+ qdel(src)
+ return
+ ..()
+
+/obj/item/weapon/talisman/proc/trigger(var/mob/user)
+ if (!user)
+ return
+
+ if (blood_text)
+ user << browse("[name][blood_text]", "window = [name]")
+ onclose(user, "[name]")
+ return
+
+ if (!spell_type)
+ if (!(src in user.held_items))//triggering an empty rune from a tome removes it.
+ if (istype(loc, /obj/item/weapon/tome))
+ var/obj/item/weapon/tome/T = loc
+ T.talismans.Remove(src)
+ user << browse(T.tome_text(), "window = arcanetome;size = 900x600")
+ user.put_in_hands(src)
+ return
+
+ if(iscarbon(user))
+ var/mob/living/carbon/C = user
+ if (C.occult_muted())
+ to_chat(user, span_danger("You find yourself unable to focus your mind on the arcane words of the talisman.") )
+ return
+
+ if (attuned_rune)
+ if (attuned_rune.loc)
+ attuned_rune.trigger(user, 1)
+ else//darn, the rune got destroyed one way or another
+ attuned_rune = null
+ to_chat(user, span_warning("The talisman disappears into dust. The rune it was attuned to appears to no longer exist.") )
+ else
+ new spell_type(user, src)
+
+ uses--
+ if (uses > 0)
+ return
+
+ if (istype(loc, /obj/item/weapon/tome))
+ var/obj/item/weapon/tome/T = loc
+ T.talismans.Remove(src)
+ if (linked_ui)
+ linked_ui.talisman = null
+ qdel(src)
+
+/obj/item/weapon/talisman/proc/imbue(var/mob/user, var/obj/effect/new_rune/R)
+ if (!user || !R)
+ return
+
+ if (blood_text)
+ to_chat(user, span_warning("You can't imbue a talisman that has been written on.") )
+ return
+
+ var/datum/rune_spell/spell = get_rune_spell(user, null, "examine", R.word1, R.word2, R.word3)
+ if(initial(spell.talisman_absorb) == RUNE_CANNOT)//placing a talisman on a Conjure Talisman rune to try and fax it
+ user.dropItemToGround(src)
+ src.forceMove(get_turf(R))
+ R.attack_hand(user)
+ else
+ if (attuned_rune)
+ to_chat(user, span_warning("\The [src] is already imbued with the power of a rune.") )
+ return
+
+ if (!spell)
+ to_chat(user, span_warning("There is no power in those runes. \The [src] isn't reacting to it.") )
+ return
+
+ //blood markings
+ overlays += image(icon, "talisman-[R.word1.icon_state]a")
+ overlays += image(icon, "talisman-[R.word2.icon_state]a")
+ overlays += image(icon, "talisman-[R.word3.icon_state]a")
+ //black markings
+ overlays += image(icon, "talisman-[R.word1.icon_state]")
+ overlays += image(icon, "talisman-[R.word2.icon_state]")
+ overlays += image(icon, "talisman-[R.word3.icon_state]")
+
+ spell_type = spell
+ uses = initial(spell.talisman_uses)
+
+ var/talisman_interaction = initial(spell.talisman_absorb)
+ var/datum/rune_spell/active_spell = R.active_spell
+ if(!istype(R))
+ return
+ if (active_spell)//some runes may change their interaction type dynamically (ie: Path Exit runes)
+ talisman_interaction = active_spell.talisman_absorb
+ if (istype(active_spell, /datum/rune_spell/portalentrance))
+ var/datum/rune_spell/portalentrance/entrance = active_spell
+ if (entrance.network)
+ word_pulse(GLOB.rune_words[entrance.network])
+ else if (istype(active_spell, /datum/rune_spell/portalexit))
+ var/datum/rune_spell/portalentrance/exit = active_spell
+ if (exit.network)
+ word_pulse(GLOB.rune_words[exit.network])
+
+ switch(talisman_interaction)
+ if (RUNE_CAN_ATTUNE)
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/talisman_attune.ogg', 50, 0, -5)
+ to_chat(user, span_notice("\The [src] can now remotely trigger the [initial(spell.name)] rune.") )
+ attuned_rune = R
+ if (RUNE_CAN_IMBUE)
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/talisman_imbue.ogg', 50, 0, -5)
+ to_chat(user, span_notice("\The [src] absorbs the power of the [initial(spell.name)] rune.") )
+ qdel(R)
+ if (RUNE_CANNOT)//like, that shouldn't even be possible because of the earlier if() check, but just in case.
+ message_admins("Error! ([key_name(user)]) managed to imbue a Conjure Talisman rune. That shouldn't be possible!")
+ return
+
+/obj/item/weapon/talisman/proc/word_pulse(var/datum/rune_word/W)
+ var/image/I1 = image(icon, "talisman-[W.icon_state]a")
+ animate(I1, color = list(2, 0.67, 0.27, 0, 0.27, 2, 0.67, 0, 0.67, 0.27, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 5, loop = -1)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ overlays += I1
+ var/image/I2 = image(icon, "talisman-[W.icon_state]")
+ animate(I2, color = list(2, 0.67, 0.27, 0, 0.27, 2, 0.67, 0, 0.67, 0.27, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 5, loop = -1)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.625, 0.35, 0.06, 0, 0.06, 1.625, 0.35, 0, 0.35, 0.06, 1.625, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.75, 0.45, 0.12, 0, 0.12, 1.75, 0.45, 0, 0.45, 0.12, 1.75, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ animate(color = list(1.875, 0.56, 0.19, 0, 0.19, 1.875, 0.56, 0, 0.56, 0.19, 1.875, 0, 0, 0, 0, 1, 0, 0, 0, 0), time = 1)
+ overlays += I2
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/cult_blade.dm b/monkestation/code/modules/bloody_cult/cult/weapons/cult_blade.dm
new file mode 100644
index 000000000000..66813a47a609
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/cult_blade.dm
@@ -0,0 +1,115 @@
+/obj/item/weapon/melee/cultblade
+ name = "cult blade"
+ desc = "An arcane weapon wielded by the followers of Nar-Sie. It features a nice round socket at the base of its obsidian blade."
+ inhand_icon_state = "cultblade"
+ worn_icon_state = "cultblade"
+ lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/64x64_righthand.dmi'
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult.dmi'
+ icon_state = "cultblade"
+ inhand_x_dimension = 64
+ inhand_y_dimension = 64
+ w_class = WEIGHT_CLASS_BULKY
+ force = 30
+ throwforce = 10
+ sharpness = SHARP_EDGED
+ block_chance = 50 // now it's officially a cult esword
+ wound_bonus = -50
+ bare_wound_bonus = 20
+ hitsound = 'sound/weapons/bladeslice.ogg'
+ block_sound = 'sound/weapons/parry.ogg'
+ attack_verb_continuous = list("attacks", "slashes", "stabs", "slices", "tears", "lacerates", "rips", "dices", "rends")
+ attack_verb_simple = list("attack", "slash", "stab", "slice", "tear", "lacerate", "rip", "dice", "rend")
+
+ var/checkcult = 1
+
+
+/obj/item/weapon/melee/cultblade/salt_act()
+ new /obj/item/weapon/melee/cultblade/nocult(loc)
+ qdel(src)
+
+/obj/item/weapon/melee/cultblade/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/butchering, \
+ speed = 4 SECONDS, \
+ effectiveness = 100, \
+ )
+
+/obj/item/weapon/melee/cultblade/narsie_act()
+ return
+
+/obj/item/weapon/melee/cultblade/attack(var/mob/living/target, var/mob/living/carbon/human/user)
+ if(!checkcult)
+ return ..()
+ if (IS_CULTIST(user))
+ if (!IS_CULTIST(target) && target.stat != DEAD)
+ var/datum/antagonist/cult/cult_datum = user.mind.has_antag_datum(/datum/antagonist/cult)
+ if (target.mind)
+ cult_datum.gain_devotion(30, DEVOTION_TIER_3, "attack_cultblade", target)
+ else
+ cult_datum.gain_devotion(30, DEVOTION_TIER_2, "attack_cultblade_nomind", target)
+ if (ishuman(target) && target.resting)
+ var/obj/structure/cult/altar/altar = locate() in target.loc
+ if (altar)
+ altar.attackby(src, user)
+ return
+ else
+ return ..()
+ else
+ return ..()
+ else
+ user.Paralyze(0.5 SECONDS)
+ user.dropItemToGround(src, TRUE)
+ to_chat(user, span_warning("An unexplicable force powerfully repels \the [src] from [target]!") )
+
+/obj/item/weapon/melee/cultblade/pickup(mob/living/user)
+ . = ..()
+ if(checkcult && !IS_CULTIST(user))
+ to_chat(user, span_warning("An overwhelming feeling of dread comes over you as you pick up \the [src]. It would be wise to rid yourself of this, quickly.") )
+ user.set_dizzy(12 SECONDS)
+
+
+/obj/item/weapon/melee/cultblade/attackby(var/obj/item/I, var/mob/user)
+ if(istype(I, /obj/item/soulstone/gem))
+ var/turf/T = get_turf(user)
+ playsound(T, 'sound/items/Deconstruct.ogg', 50, 1)
+ user.dropItemToGround(src)
+ var/obj/item/weapon/melee/soulblade/SB = new (T)
+ spawn(1)
+ user.put_in_active_hand(SB)
+ if (IS_CULTIST(user))
+ SB.linked_cultist = user
+ to_chat(SB.shade, "You have made contact with [user]. As long as you remain within 5 tiles of them, you can move by yourself without losing blood, and regenerate blood passively at a faster rate.")
+ var/obj/item/soulstone/gem/sgem = I
+ var/mob/living/basic/shade/shadeMob = locate(/mob/living/basic/shade) in sgem.contents
+ if (shadeMob)
+ shadeMob.forceMove(SB)
+ SB.shade = shadeMob
+ sgem.contents -= shadeMob
+ if (shadeMob.mind)
+ shadeMob.give_blade_powers()
+ else
+ to_chat(user, span_warning("Although the game appears to hold a shade, it somehow doesn't appear to have a mind capable of manipulating the blade.") )
+ to_chat(user, span_danger("(that's a bug, call Deity, and tell him exactly how you obtained that shade).") )
+ message_admins("[key_name(usr)] somehow placed a soul gem containing a shade with no mind inside a soul blade.")
+ SB.update_icon()
+ qdel(sgem)
+ qdel(src)
+ return 1
+ if(istype(I, /obj/item/soulstone))
+ to_chat(user, span_warning("\The [I] doesn't fit in \the [src]'s socket.") )
+ return 1
+ ..()
+
+
+/obj/item/weapon/melee/cultblade/nocult
+ name = "broken cult blade"
+ desc = "What remains of an arcane weapon wielded by the followers of Nar-Sie. In this state, it can be held mostly without risks."
+ icon_state = "cultblade-broken"
+ checkcult = 0
+ force = 15
+
+/obj/item/weapon/melee/cultblade/nocult/attackby(var/obj/item/I, var/mob/user)
+ if(istype(I, /obj/item/weapon/talisman) || istype(I, /obj/item/paper))
+ return 1
+ ..()
diff --git a/code/modules/antagonists/cult/cult_bastard_sword.dm b/monkestation/code/modules/bloody_cult/cult/weapons/cult_heretic_sword.dm
similarity index 96%
rename from code/modules/antagonists/cult/cult_bastard_sword.dm
rename to monkestation/code/modules/bloody_cult/cult/weapons/cult_heretic_sword.dm
index 73b0433c6acd..690f4db2ddee 100644
--- a/code/modules/antagonists/cult/cult_bastard_sword.dm
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/cult_heretic_sword.dm
@@ -33,7 +33,7 @@
set_light(4)
AddComponent(/datum/component/butchering, 50, 80)
AddComponent(/datum/component/two_handed, require_twohands = TRUE)
- AddComponent(/datum/component/soul_stealer, soulstone_type = /obj/item/soulstone)
+ AddComponent(/datum/component/soul_stealer, soulstone_type = /obj/item/soulstone) //weaker so we can't make perfect constructs.
AddComponent( \
/datum/component/spin2win, \
spin_cooldown_time = 25 SECONDS, \
@@ -73,7 +73,7 @@
. = ..()
if(!IS_CULTIST(user))
if(!IS_HERETIC(user))
- to_chat(user, "\"I wouldn't advise that.\"")
+ to_chat(user, span_cultlarge("\"I wouldn't advise that.\"") )
force = 5
return
else
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/ritual_blade.dm b/monkestation/code/modules/bloody_cult/cult/weapons/ritual_blade.dm
new file mode 100644
index 000000000000..e656dcb001e4
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/ritual_blade.dm
@@ -0,0 +1,44 @@
+/obj/item/knife/ritual
+ name = "ritual dagger"
+ desc = "A strange dagger said to be used by sinister groups for \"preparing\" a corpse before sacrificing it to their dark gods."
+ icon = 'icons/obj/cult/items_and_weapons.dmi'
+ icon_state = "render"
+ inhand_icon_state = "cultdagger"
+ worn_icon_state = "render"
+ lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
+ inhand_x_dimension = 32
+ inhand_y_dimension = 32
+ w_class = WEIGHT_CLASS_SMALL
+ force = 15
+ throwforce = 25
+ block_chance = 25
+ wound_bonus = -10
+ bare_wound_bonus = 20
+ armour_penetration = 35
+ block_sound = 'sound/weapons/parry.ogg'
+
+/obj/item/knife/ritual/Initialize(mapload)
+ . = ..()
+ var/image/silicon_image = image(icon = 'icons/effects/blood.dmi' , icon_state = null, loc = src)
+ silicon_image.override = TRUE
+ add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/silicons, "cult_dagger", silicon_image)
+
+ var/examine_text = {"Cult Girders will be destroyed in a single blow.
+Striking another cultist with it will purge all holy water from them and transform it into unholy water.
+Striking a noncultist, however, will tear their flesh."}
+
+ AddComponent(/datum/component/cult_ritual_item, span_cult(examine_text))
+
+
+/obj/item/knife/ritual/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
+ var/block_message = "[owner] parries [attack_text] with [src]"
+ if(owner.get_active_held_item() != src)
+ block_message = "[owner] parries [attack_text] with [src] in their offhand"
+
+ if(IS_CULTIST(owner) && prob(final_block_chance) && attack_type != PROJECTILE_ATTACK)
+ new /obj/effect/temp_visual/cult/sparks(get_turf(owner))
+ owner.visible_message(span_danger("[block_message]"))
+ return TRUE
+ else
+ return FALSE
diff --git a/monkestation/code/modules/bloody_cult/cult/weapons/soulblade.dm b/monkestation/code/modules/bloody_cult/cult/weapons/soulblade.dm
new file mode 100644
index 000000000000..66e879640857
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/cult/weapons/soulblade.dm
@@ -0,0 +1,321 @@
+/obj/item/weapon/melee/soulblade
+ name = "soul blade"
+ desc = "An obsidian blade fitted with a soul gem, giving it soul catching properties."
+ icon = 'monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi'
+ lefthand_file = 'monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_l.dmi'
+ righthand_file = 'monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_r.dmi'
+ inhand_icon_state = "soulblade"
+ SET_BASE_PIXEL(-16, -16)
+ icon_state = "soulblade"
+ w_class = WEIGHT_CLASS_BULKY
+ force = 30//30 brute, plus 5 burn
+ throwforce = 20
+ sharpness = SHARP_EDGED
+ var/mob/living/basic/shade/shade = null
+ var/blood = 0
+ var/passivebloodregen = 0//increments every Life() proc of the Shade inside, and increases blood by 1 once it reaches the current blood count/3
+ var/maxblood = 100
+ var/movespeed = 2//smaller = faster
+ max_integrity = 60
+ var/reflector = FALSE
+ var/mob/living/linked_cultist = null
+
+/obj/item/weapon/melee/soulblade/Destroy()
+ var/turf/T = get_turf(src)
+ if (istype(loc, /obj/projectile))
+ qdel(loc)
+
+ if (shade)
+ shade.remove_blade_powers()
+ if (T)
+ shade.soulblade_ritual = FALSE
+ shade.forceMove(T)
+ shade.status_flags &= ~GODMODE
+ //shade.canmove = 1
+ shade.cancel_camera()
+ var/datum/control/C = shade.control_object[src]
+ if(C)
+ C.break_control()
+ qdel(C)
+ else
+ qdel(shade)
+
+
+ if (T)
+ var/obj/item/weapon/melee/cultblade/nocult/B = new (T)
+ B.Move(get_step_rand(T))
+ new /obj/item/soulstone(T)
+ shade = null
+ ..()
+
+/obj/item/weapon/melee/soulblade/attack_hand(var/mob/living/user)
+ if (shade)
+ if (IS_CULTIST(user) && (linked_cultist != user))
+ linked_cultist = user
+ to_chat(shade, "You have made contact with [user]. As long as you remain within 5 tiles of them, you can move by yourself without losing blood, and regenerate blood passively at a faster rate.")
+ ..()
+
+/obj/item/weapon/melee/soulblade/salt_act()
+ qdel(src)
+
+
+/obj/item/weapon/melee/soulblade/examine(var/mob/user)
+ . = ..()
+ if (areYouWorthy(user))
+ . += span_info("blade blood: [blood]%")
+ . += span_info("blade health: [round((atom_integrity/max_integrity)*100)]%")
+
+
+/obj/item/weapon/melee/soulblade/narsie_act()
+ return
+
+/obj/item/weapon/melee/soulblade/attack_self(var/mob/living/user)
+ var/choices = list(
+ list("Give Blood", "radial_giveblood", "Transfer some of your blood to \the [src] to repair it and refuel its blood level, or you could just slash someone."),
+ list("Remove Gem", "radial_removegem", "Remove the soul gem from the blade."),
+ )
+
+ if (!areYouWorthy(user))
+ choices = list(
+ list("Remove Gem", "radial_removegem", "Remove the soul gem from \the [src]."),
+ )
+
+ var/list/made_choices = list()
+ for(var/list/choice in choices)
+ var/datum/radial_menu_choice/option = new
+ option.image = image(icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi', icon_state = choice[2])
+ option.info = span_boldnotice(choice[3])
+ made_choices[choice[1]] = option
+
+ var/task = show_radial_menu(user, user, made_choices, tooltips = TRUE, radial_icon = 'monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi')//spawning on loc so we aren't offset by pixel_x/pixel_y, or affected by animate()
+ var/obj/item/active_hand_item = user.get_active_held_item()
+ if (active_hand_item != src)
+ to_chat(user, span_warning("You must hold \the [src] in your active hand.") )
+ return
+ switch (task)
+ if ("Give Blood")
+ var/data = use_available_blood(user, 10)
+ if (data[BLOODCOST_RESULT] != BLOODCOST_FAILURE)
+ blood = min(maxblood, blood+35)//reminder that the blade cannot give blood back to their wielder, so this should prevent some exploits
+ atom_integrity = min(atom_integrity, max_integrity+10)
+ update_icon()
+ if ("Remove Gem")
+ if (!areYouWorthy(user) && shade && ((IS_CULTIST(shade) && !IS_CULTIST(user))))
+ shade.say("Dedo ol'btoh!")
+ user.take_overall_damage(25, 25)
+ if (iscarbon(user))
+ user.bodytemperature += 60
+ playsound(user.loc, 'monkestation/code/modules/bloody_cult/sound/bloodboil.ogg', 50, 0, -1)
+ to_chat(user, span_danger("You manage to pluck the gem out of \the [src], but a surge of the blade's occult energies makes your blood boil!") )
+ var/turf/T = get_turf(user)
+ playsound(T, 'sound/items/Deconstruct.ogg', 50, 0, -3)
+ user.dropItemToGround(src)
+ var/obj/item/weapon/melee/cultblade/CB = new (T)
+ var/obj/item/soulstone/gem/SG = new (T)
+ user.put_in_active_hand(CB)
+ user.put_in_inactive_hand(SG)
+ if (shade)
+ shade.forceMove(SG)
+ SG.contents += shade
+ //shade.remove_blade_powers()
+ shade.soulblade_ritual = FALSE
+ SG.icon_state = "soulstone2"
+ SG.name = "Soul Gem: [shade.real_name]"
+ shade = null
+ loc = null//so we won't drop a broken blade and shard
+ qdel(src)
+
+/obj/item/weapon/melee/soulblade/attack(var/mob/living/target, var/mob/living/carbon/human/user)
+ if(!areYouWorthy(user))
+ user.Paralyze(5)
+ to_chat(user, span_warning("An unexplicable force powerfully repels \the [src] from \the [target]!") )
+ user.adjustFireLoss(5)
+ return
+ if (IS_CULTIST(user) && !IS_CULTIST(target) && !target.stat == DEAD)
+ var/datum/antagonist/cult/cult_datum = user.mind.has_antag_datum(/datum/antagonist/cult)
+ if (target.mind)
+ cult_datum.gain_devotion(30, DEVOTION_TIER_3, "attack_soulblade", target)
+ else
+ cult_datum.gain_devotion(30, DEVOTION_TIER_2, "attack_soulblade_nomind", target)
+ if (ishuman(target) && target.resting)
+ var/obj/structure/cult/altar/altar = locate() in target.loc
+ if (altar)
+ altar.attackby(src, user)
+ return
+ ..()
+
+
+/obj/item/weapon/melee/soulblade/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
+ if(proximity_flag)
+ return
+ if (HAS_TRAIT(user, TRAIT_PACIFISM))
+ return
+
+ if (blood >= 5)
+ blood = max(0, blood-5)
+ update_icon()
+ var/turf/starting = get_turf(user)
+ var/obj/projectile/bloodslash/blood_slash = new (starting)
+ blood_slash.preparePixelProjectile(target, user)
+ if(user.zone_selected)
+ blood_slash.def_zone = user.zone_selected
+ else
+ blood_slash.def_zone = BODY_ZONE_CHEST
+ blood_slash.fire(direct_target = target)
+ playsound(starting, 'monkestation/code/modules/bloody_cult/sound/forge.ogg', 100, 1)
+ blood_slash.process()
+
+/obj/item/weapon/melee/soulblade/attack(mob/living/attacked, mob/living/carbon/human/user)
+ . = ..()
+ if (ismob(attacked))
+ var/mob/living/M = attacked
+ M.adjustFireLoss(5)
+ if (iscarbon(M))
+ var/mob/living/carbon/C = M
+ if (C.stat != DEAD)
+ C.blood_volume -= 10
+ blood = min(100, blood+20)
+ to_chat(user, span_warning("You steal some of their blood!") )
+ else
+ C.blood_volume -= 5
+ blood = min(100, blood+10)
+ to_chat(user, span_warning("You steal a bit of their blood, but not much.") )
+ update_icon()
+ if (shade)
+ shade.DisplayUI("Soulblade")
+ else if (M.blood_volume)
+ var/mob/living/simple_animal/SA = M
+ if (SA.stat != DEAD)
+ blood = min(100, blood+20)
+ to_chat(user, span_warning("You steal some of their blood!") )
+ else
+ blood = min(100, blood+10)
+ to_chat(user, span_warning("You steal a bit of their blood, but not much.") )
+ update_icon()
+ if (shade)
+ shade.DisplayUI("Soulblade")
+
+
+/obj/item/weapon/melee/soulblade/pickup(var/mob/living/user)
+ ..()
+ if(!areYouWorthy(user))
+ to_chat(user, span_warning("An overwhelming feeling of dread comes over you as you pick up \the [src]. It would be wise to rid yourself of this, quickly.") )
+ user.adjust_dizzy(120)
+ else
+ user.adjust_dizzy(-120)
+ update_icon()
+
+/obj/item/weapon/melee/soulblade/proc/areYouWorthy(var/mob/living/user)
+ if (IS_CULTIST(user))
+ return TRUE
+ else if (!shade)
+ return FALSE
+ else if (user == shade)
+ return TRUE
+ return TRUE
+
+/obj/item/weapon/melee/soulblade/dropped(var/mob/user)
+ ..()
+ update_icon()
+
+/obj/item/weapon/melee/soulblade/update_icon()
+ . = ..()
+ overlays.len = 0
+ animate(src, pixel_y = -16 * 1, time = 3, easing = SINE_EASING)
+ shade = locate() in src
+ if (shade)
+ plane = HUD_PLANE//let's keep going and see where this takes us
+ icon_state = "soulblade-full"
+ animate(src, pixel_y = -8 * 1 , time = 7, loop = -1, easing = SINE_EASING)
+ animate(pixel_y = -12 * 1, time = 7, loop = -1, easing = SINE_EASING)
+ else
+ if (!ismob(loc))
+ plane = initial(plane)
+ layer = initial(layer)
+ icon_state = "soulblade"
+
+ if (istype(loc, /mob/living/carbon))
+ var/mob/living/carbon/C = loc
+ if (areYouWorthy(C))
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/hud.dmi', src, "consthealth[10*round((blood/maxblood)*10)]")
+ I.pixel_x = 16
+ I.pixel_y = 16
+ overlays += I
+
+
+/obj/item/weapon/melee/soulblade/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = MOVE_FORCE_STRONG, gentle = FALSE, quickstart = TRUE)
+ var/turf/starting = get_turf(src)
+ var/turf/second_target = target
+ var/obj/projectile/soulbullet/soul_bullet = new (starting)
+ soul_bullet.firer = thrower
+ soul_bullet.def_zone = ran_zone(thrower.zone_selected)
+ soul_bullet.preparePixelProjectile(target, starting)
+ soul_bullet.secondary_target = second_target
+ soul_bullet.shade = shade
+ soul_bullet.blade = src
+ src.forceMove(soul_bullet)
+ soul_bullet.fire()
+ soul_bullet.process()
+
+/obj/item/weapon/melee/soulblade/ex_act(var/severity)
+ switch(severity)
+ if (1)
+ takeDamage(100)
+ if (2)
+ takeDamage(40)
+ if (3)
+ takeDamage(20)
+
+/obj/item/weapon/melee/soulblade/proc/takeDamage(var/damage)
+ if (!damage)
+ return
+ atom_integrity -= damage
+ if (atom_integrity <= 0)
+ playsound(loc, 'sound/effects/hit_on_shattered_glass.ogg', 70, 1)
+ qdel(src)
+ else
+ playsound(loc, "trayhit", 70, 1)
+
+/obj/item/weapon/melee/soulblade/attackby(var/obj/item/I, var/mob/user)
+ if (HAS_TRAIT(user, TRAIT_PACIFISM))
+ return
+ if(I.force)
+ var/damage = I.force
+ takeDamage(damage)
+ user.visible_message(span_danger("\The [src] has been attacked with \the [I] by \the [user]. ") )
+
+/obj/item/weapon/melee/soulblade/hitby(atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ if(.)
+ return
+
+ visible_message(span_warning("\The [src] was hit by \the [hitting_atom].") , 1)
+ if (isobj(hitting_atom))
+ var/obj/O = hitting_atom
+ takeDamage(O.throwforce)
+
+/obj/item/weapon/melee/soulblade/proc/capture_shade(var/mob/living/basic/shade/target, var/mob/user)
+
+ if(shade)
+ to_chat(user, "Capture failed!: \The [src] already has a shade! Remove its soul gem if you wish to harm this shade nonetheless.")
+ else
+ target.forceMove(src) //put shade in blade
+ target.status_flags |= GODMODE
+ target.health = target.maxHealth//full heal
+ target.give_blade_powers()
+ shade = target
+ dir = NORTH
+ update_icon()
+ to_chat(target, "Your soul has been captured by the soul blade, its arcane energies are reknitting your ethereal form, healing you.")
+ to_chat(user, "Capture successful!: [target.real_name]'s has been captured and stored within the gem on your blade.")
+ target.master = user
+
+ //Is our user a cultist? Then you're a cultist too now!
+ if (IS_CULTIST(user) && !IS_CULTIST(target))
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult && !cult.CanConvert())
+ to_chat(user, span_danger("The cult has too many members already. But this shade will obey you nonetheless.") )
+ return
+ var/datum/antagonist/cult/newCultist = new(target.mind)
+ cult.HandleRecruitedRole(newCultist)
+ //newCultist.Greet(GREET_SOULSTONE)
+ newCultist.conversion["soulstone"] = user
diff --git a/monkestation/code/modules/bloody_cult/deluxe_flat_icon.dm b/monkestation/code/modules/bloody_cult/deluxe_flat_icon.dm
new file mode 100644
index 000000000000..4da3faa3b813
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/deluxe_flat_icon.dm
@@ -0,0 +1,244 @@
+
+/* *********************************************************************************
+ _____ _ ______ _ _ _____ _
+ / ____| | | | ____| | | | |_ _| | |
+ | | __ ___| |_ | |__ | | __ _| |_ | | ___ ___ _ __ __| |_ __
+ | | |_ |/ _ \ __| | __| | |/ _` | __| | | / __/ _ \| '_ \ / _` \ \/ /
+ | |__| | __/ |_ | | | | (_| | |_ _| || (_| (_) | | | | | (_| |> <
+ \_____|\___|\__| |_| |_|\__,_|\__| |_____\___\___/|_| |_| \__,_/_/\_\
+
+ Created by Deity, loosely based on David "DarkCampainger" Braun's own proc
+
+ It's open source, so use it how you want I guess
+
+ Version 2.0 - June 18, 2021
+
+ See camera_get_icon_deluxe() in photography.dm for an example usage
+
+*///////////////////////////////////////////////////////////////////////////////////
+/***************
+pros:
+* handles better the capture of multiple overlapping atoms as overlays with custom planes will appear properly relatively to other atoms
+* fixes a bunch of byond bugs that occur when trying to render an icon using a dir that it doesn't have without resorting to big lists full of snowflakey exceptions
+* added exceptions for human icons so that they appear with the direction that they are actually facing
+
+cons:
+* no cache, which would otherwise become expontentially inefficient as more and more atoms are captured at once. shouldn't be much of an issue anyway given how the proc is used
+* no exact arg, if you want a reference picture of a single mob, use the old Get Flat Icon instead. It'd probably be more efficient too for a single atom.
+****************/
+
+#define GFI_DX_ATOM 1 // This is either the icon's source atom, or in the case of an overlay the atom this icon is an overlay of
+#define GFI_DX_ACTUAL_ATOM 2 // Same as above but always the source atom
+#define GFI_DX_ICON 3 // Can be a dmi, or an icon object, depending on whether there is also a state. Can also be null in the case of overlays, so the parent's icon is used.
+#define GFI_DX_STATE 4 // The icon_state.
+#define GFI_DX_DIR 5 // The dir of the parent atom
+#define GFI_DX_PLANE 6 // The plane of the atom, or the parent atom's +0.1 in the case of overlays
+#define GFI_DX_LAYER 7
+#define GFI_DX_COLOR 8 // The parent's color always overrides
+#define GFI_DX_ALPHA 9 // The image's alpha, or that of its parent if said parent's alpha isn't 255
+#define GFI_DX_PIXEL_X 10 // The combined offset of the overlay image and every datum it's part of, including parent images and the base atom
+#define GFI_DX_PIXEL_Y 11 // The combined offset of the overlay image and every datum it's part of, including parent images and the base atom
+#define GFI_DX_COORD_X 12 // Because the proc can be slow and the atom might move, we memorize its location right when the picture is taken
+#define GFI_DX_COORD_Y 13 // Because the proc can be slow and the atom might move, we memorize its location right when the picture is taken
+
+#define GFI_DX_MAX 13 // Remember to keep this updated should you need to keep track of more variables
+
+
+/proc/getFlatIconDeluxe(list/image_datas, var/turf/center, var/radius = 0, var/override_dir = 0, var/ignore_spawn_items = FALSE, var/large_canvas = FALSE)
+
+ var/icon/flat // Final flattened icon
+ if (large_canvas)
+ flat = icon('icons/effects/224x224.dmi',"empty")
+ else
+ flat = icon('monkestation/code/modules/bloody_cult/icons/32x32.dmi', "blank")
+ var/icon/add // Icon of overlay being added
+
+ for(var/data in image_datas)
+ if (!data[GFI_DX_ICON] && !data[GFI_DX_STATE]) // no icon nor icon_state? we're probably not meant to draw that. Possibly a blank icon while we're only interested in its overlays.
+ continue
+ var/atom/atom = data[GFI_DX_ACTUAL_ATOM]
+ if (atom.blend_mode == BLEND_ADD)//additive overlays don't show up properly, especially if they're partly transparent
+ continue
+ var/blending = atom.blend_mode
+ if (blending == BLEND_MULTIPLY)
+ blending = BLEND_DEFAULT
+ if (ignore_spawn_items)
+ // looks like we're getting some reference pics for an ID picture, let's ignore the items held on spawn
+ var/list/icon_states_to_ignore = list(
+ "plasticbag",
+ "bookVirologyGuide",
+ "firstaid",
+ "clipboard",
+ "handdrill",
+ "toolbox_blue",
+ "briefcase-centcomm",
+ "thurible",
+ )
+ if(data[GFI_DX_STATE] in icon_states_to_ignore)
+ continue
+
+ if (override_dir)
+ data[GFI_DX_DIR] = override_dir
+
+ CHECK_TICK
+
+ if (!data[GFI_DX_STATE] || data[GFI_DX_STATE] == "body_m_s")//this fixes human bodies always facing south
+ add = icon(data[GFI_DX_ICON], dir = data[GFI_DX_DIR], frame = 1, moving = 0)
+ else
+ if (!data[GFI_DX_ICON] && data[GFI_DX_ATOM])
+ data[GFI_DX_ICON] = data[GFI_DX_ATOM]:icon
+ //making sure that our icon can turn
+ var/dir = data[GFI_DX_DIR]
+ if (dir != SOUTH) // south-facing atoms shouldn't pose any problem
+ data[GFI_DX_DIR] = SOUTH
+
+ add = icon(data[GFI_DX_ICON]
+ , data[GFI_DX_STATE]
+ , data[GFI_DX_DIR]
+ , 1
+ , 0)
+
+ if(!override_dir && iscarbon(data[GFI_DX_ATOM]))
+ var/mob/living/carbon/C = data[1]
+ if(C.body_position == LYING_DOWN && !isalienadult(C))//because adult aliens have their own resting sprite
+ add.Turn(90)
+
+ if(isobserver(data[GFI_DX_ATOM]))
+ add.ChangeOpacity(0.5)
+
+ // Apply any color or alpha settings
+ if(data[GFI_DX_COLOR] || data[GFI_DX_ALPHA] != 255)
+ if (islist(data[GFI_DX_COLOR]))
+ var/list/color_matrix = data[GFI_DX_COLOR]
+ if (color_matrix.len >= 16)//getting the color for hair and facial hair out of the matrix
+ data[GFI_DX_COLOR] = rgb(color_matrix[13]*255, color_matrix[14]*255, color_matrix[15]*255)
+ var/rgba = (data[GFI_DX_COLOR] || "#FFFFFF") + copytext(rgb(0,0,0,data[GFI_DX_ALPHA]), 8)
+ add.Blend(rgba, ICON_ADD)//adding color onto black hair sprites
+ else
+ var/rgba = (data[GFI_DX_COLOR] || "#FFFFFF")
+ if (length(rgba) < 8)
+ rgba += copytext(rgb(0,0,0,data[GFI_DX_ALPHA]), 8)
+ add.Blend(rgba, ICON_MULTIPLY)
+
+ // Blend the overlay into the flattened icon
+
+ if (center)
+ flat.Blend(add,blendMode2iconMode(blending),1+data[GFI_DX_PIXEL_X]+32*(data[GFI_DX_COORD_X]-center.x+radius),1+data[GFI_DX_PIXEL_Y]+32*(data[GFI_DX_COORD_Y]-center.y+radius))
+ else // if there is no center that means we're probably dealing with a single atom, so we only care about the pixel offset
+ flat.Blend(add,blendMode2iconMode(blending),1+data[GFI_DX_PIXEL_X],1+data[GFI_DX_PIXEL_Y])
+
+ if (!large_canvas)
+ flat.Crop(1,1,32,32)
+
+ return flat
+
+///////////////////////////////////////////////////////////////////////////////////////
+
+// to_sort might be either an atom or an image, returns its image data relative to its parent if there is one
+/proc/get_image_data(var/to_sort,var/list/parent, var/is_underlay = FALSE)
+ var/data[GFI_DX_MAX]
+ data[GFI_DX_ATOM] = to_sort
+ data[GFI_DX_ACTUAL_ATOM] = to_sort
+ data[GFI_DX_ICON] = to_sort:icon
+ data[GFI_DX_STATE] = to_sort:icon_state
+ data[GFI_DX_DIR] = to_sort:dir
+ if (to_sort:plane > 10000)
+ data[GFI_DX_PLANE] = to_sort:plane + FLOAT_PLANE - 2
+ else if (to_sort:plane < -10000)
+ data[GFI_DX_PLANE] = to_sort:plane - FLOAT_PLANE
+ else
+ data[GFI_DX_PLANE] = to_sort:plane
+ data[GFI_DX_LAYER] = to_sort:layer
+ data[GFI_DX_COLOR] = to_sort:color
+ data[GFI_DX_ALPHA] = to_sort:alpha
+ data[GFI_DX_PIXEL_X] = to_sort:pixel_x
+ data[GFI_DX_PIXEL_Y] = to_sort:pixel_y
+ if (isatom(to_sort))
+ data[GFI_DX_COORD_X] = to_sort:x
+ data[GFI_DX_COORD_Y] = to_sort:y
+ if (parent?.len)
+ var/atom/parent_atom = parent[GFI_DX_ATOM]
+ data[GFI_DX_ATOM] = parent_atom // the first entry always has to be the top level atom so we can track things like mobs lying down or their position
+ data[GFI_DX_COORD_X] = parent_atom:x
+ data[GFI_DX_COORD_Y] = parent_atom:y
+ if (to_sort:plane == FLOAT_PLANE)
+ if (is_underlay)
+ data[GFI_DX_PLANE] = parent[GFI_DX_PLANE] - 0.1
+ else
+ data[GFI_DX_PLANE] = parent[GFI_DX_PLANE] + 0.1
+ data[GFI_DX_COLOR] = COLOR_BLOOD
+ else if (isitem(parent_atom) && (to_sort:name == "blood_overlay")) // just a blood-covered item
+ data[GFI_DX_COLOR] = to_sort:color
+ else if (parent[GFI_DX_COLOR] != null)
+ data[GFI_DX_COLOR] = parent[GFI_DX_COLOR]
+ if (parent[GFI_DX_ALPHA] != 255)
+ data[GFI_DX_ALPHA] = parent[GFI_DX_ALPHA]
+ data[GFI_DX_PIXEL_X] += parent[GFI_DX_PIXEL_X]
+ data[GFI_DX_PIXEL_Y] += parent[GFI_DX_PIXEL_Y]
+ return data
+
+// fetches the image data of to_sort, as well as those of its overlays and underlays
+/proc/get_content_image_datas(var/to_sort,var/list/parent, var/is_underlay = FALSE)
+ var/content_data = list()
+ var/list/my_data = get_image_data(to_sort,parent, is_underlay)
+ if (!my_data)
+ return
+ content_data = list(my_data)
+ var/list/underlays = to_sort:underlays
+ var/list/overlays = to_sort:overlays
+ for(var/underlay in underlays)
+ var/list/L = get_content_image_datas(underlay,my_data, is_underlay = TRUE)
+ if (L)
+ content_data += L
+ for(var/overlay in overlays)
+ var/list/L = get_content_image_datas(overlay,my_data)
+ if (L)
+ content_data += L
+
+ return content_data
+
+// fetches the image datas of all atoms in a turf, including itself
+/proc/get_turf_image_datas(var/turf/T,var/obj/item/camera/camera)
+ var/list/turf_image_datas = list()
+ turf_image_datas = get_content_image_datas(T)
+ for(var/atom/A in T.contents)
+ if (A.invisibility)
+ if (!isobserver(A) || !camera || !camera.see_ghosts)
+ continue
+ var/list/L = get_content_image_datas(A)
+ if (L)
+ turf_image_datas += L
+
+ return turf_image_datas
+
+// sort image datas according to their planes/layers
+/proc/sort_image_datas(var/list/datas_to_sort)
+ if (!datas_to_sort?.len)
+ return
+ var/list/sorted = list()
+ for(var/list/current_data in datas_to_sort)
+ var/compare_index
+ for(compare_index = sorted.len, compare_index > 0, --compare_index)
+ var/list/compare_data = sorted[compare_index]
+ if(compare_data[GFI_DX_PLANE] < current_data[GFI_DX_PLANE])
+ break
+ else if((compare_data[GFI_DX_PLANE] == current_data[GFI_DX_PLANE]) && (compare_data[GFI_DX_LAYER] <= current_data[GFI_DX_LAYER]))
+ break
+ sorted.Insert(compare_index+1, list(current_data))
+ return sorted
+
+#undef GFI_DX_ATOM
+#undef GFI_DX_ACTUAL_ATOM
+#undef GFI_DX_ICON
+#undef GFI_DX_STATE
+#undef GFI_DX_DIR
+#undef GFI_DX_PLANE
+#undef GFI_DX_LAYER
+#undef GFI_DX_COLOR
+#undef GFI_DX_ALPHA
+#undef GFI_DX_PIXEL_X
+#undef GFI_DX_PIXEL_Y
+#undef GFI_DX_COORD_X
+#undef GFI_DX_COORD_Y
+
+#undef GFI_DX_MAX
diff --git a/monkestation/code/modules/bloody_cult/font/youmurdererbb_reg.ttf b/monkestation/code/modules/bloody_cult/font/youmurdererbb_reg.ttf
new file mode 100644
index 000000000000..91e402bc3596
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/font/youmurdererbb_reg.ttf differ
diff --git a/monkestation/code/modules/bloody_cult/gods_power/cross_boomerang.dm b/monkestation/code/modules/bloody_cult/gods_power/cross_boomerang.dm
new file mode 100644
index 000000000000..c33e27b4f100
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/gods_power/cross_boomerang.dm
@@ -0,0 +1,243 @@
+/obj/projectile/boomerang
+ name = "boomerang"
+ icon = 'monkestation/code/modules/bloody_cult/icons/boomerang.dmi'
+ icon_state = "boomerang-spin"
+ damage = 20
+ damage_type = BRUTE
+ speed = 0.66
+ var/obj/item/nullrod/cross_boomerang/boomerang
+ var/list/hit_atoms = list()
+
+
+/obj/projectile/boomerang/Bump(atom/A)
+ . = ..()
+ if (!(A in hit_atoms))
+ hit_atoms += A
+ if (boomerang)
+ boomerang.throw_impact(A, null)
+ if (boomerang.loc != src)//boomerang got grabbed most likely
+ boomerang.originator = null
+ boomerang = null
+ qdel(src)
+ return
+ else if (iscarbon(A))
+ boomerang.apply_status_effects(A)
+ forceMove(A.loc)
+ A.Bumped(boomerang)
+ qdel(src)
+ return
+ A.Bumped(boomerang)
+ return ..(A)
+
+/obj/projectile/boomerang/Destroy()
+ if(boomerang)
+ return_to_sender()
+ . = ..()
+
+/obj/projectile/boomerang/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if(!boomerang)
+ qdel(src)
+
+/obj/projectile/boomerang/proc/return_to_sender()
+ if (!boomerang)
+ qdel(src)
+ return
+ var/turf/T = get_turf(src)
+ if (!boomerang.return_check())
+ boomerang.forceMove(T)
+ boomerang.thrown = FALSE
+ boomerang.dropped()
+ boomerang = null
+ return
+ //if there is no air, no return trip
+ var/datum/gas_mixture/current_air = T.return_air()
+ var/atmosphere = 0
+ if(current_air)
+ atmosphere = current_air.return_pressure()
+
+ if (atmosphere < ONE_ATMOSPHERE/2)
+ visible_message("\The [boomerang] dramatically fails to come back due to the lack of air pressure.")
+ boomerang.forceMove(T)
+ boomerang.thrown = FALSE
+ boomerang.dropped()
+ boomerang = null
+ return
+
+ var/atom/return_target
+ if (firer)
+ if (isturf(firer.loc) && (firer.z == z) && (get_dist(firer,src) <= 26))
+ return_target = firer
+
+ if (!return_target)
+ return_target = starting
+
+ var/obj/effect/tracker/boomerang/Tr = new (T)
+ Tr.target = return_target
+ Tr.appearance = appearance
+ Tr.refresh = speed
+ Tr.luminosity = luminosity
+ Tr.boomerang = boomerang
+ Tr.hit_atoms = hit_atoms.Copy()
+ boomerang.forceMove(Tr)
+ boomerang = null
+
+/obj/effect/tracker/boomerang
+ name = "boomerang"
+ icon = 'monkestation/code/modules/bloody_cult/icons/boomerang.dmi'
+ icon_state = "boomerang-spin"
+ mouse_opacity = 1
+ density = 1
+ pass_flags = PASSTABLE
+ var/obj/item/nullrod/cross_boomerang/boomerang
+ var/list/hit_atoms = list()
+
+/obj/effect/tracker/boomerang/Destroy()
+ var/turf/T = get_turf(src)
+ if (T && boomerang)
+ boomerang.forceMove(T)
+ boomerang.thrown = FALSE
+ boomerang.dropped()
+ boomerang.originator = null
+ boomerang = null
+ ..()
+
+/obj/effect/tracker/boomerang/on_step()
+ if (boomerang && !QDELETED(boomerang))
+ boomerang.on_step(src)
+ else
+ qdel(src)
+
+/obj/effect/tracker/boomerang/Bumped(var/atom/movable/AM)
+ make_contact(AM)
+
+/obj/effect/tracker/boomerang/proc/make_contact(var/atom/Obstacle)
+ if (boomerang)
+ if (!(Obstacle in hit_atoms))
+ hit_atoms += Obstacle
+ if (Obstacle == boomerang.originator)
+ if (on_expire(FALSE))
+ qdel(src)
+ return TRUE
+ boomerang.throw_impact(Obstacle,boomerang.throw_speed,boomerang.originator)
+ if (boomerang.loc != src)//boomerang got grabbed most likely
+ boomerang.originator = null
+ boomerang = null
+ qdel(src)
+ return TRUE
+ else if (iscarbon(Obstacle))
+ boomerang.apply_status_effects(Obstacle)
+ return FALSE
+ Obstacle.Bumped(boomerang)
+ if (!ismob(Obstacle))
+ on_expire(TRUE)
+ qdel(src)
+ return TRUE
+ return FALSE
+ else
+ qdel(src)
+ return FALSE
+
+/obj/effect/tracker/boomerang/on_expire(var/bumped_atom = FALSE)
+ if (boomerang && boomerang.originator && Adjacent(boomerang.originator))
+ if (boomerang.on_return())
+ if (boomerang)
+ boomerang.originator = null
+ boomerang = null
+ return TRUE
+ return FALSE
+
+/obj/item/nullrod/cross_boomerang
+ name = "battle cross"
+ desc = "A holy silver cross that dispels evil and smites unholy creatures."
+ throwforce = 20
+
+ icon = 'monkestation/code/modules/bloody_cult/icons/boomerang.dmi'
+ icon_state = "cross_modern"
+ var/thrown = FALSE
+
+ var/flickering = 0
+ var/classic = FALSE
+ var/mob/living/carbon/originator
+ COOLDOWN_DECLARE(last_sound_loop)
+
+ var/sound_throw = 'monkestation/code/modules/bloody_cult/sound/boomerang_cross_start.ogg'
+ var/sound_loop = 'monkestation/code/modules/bloody_cult/sound/boomerang_cross_loop.ogg'
+
+/obj/item/nullrod/cross_boomerang/Initialize(mapload)
+ . = ..()
+ update_appearance()
+
+/obj/item/nullrod/cross_boomerang/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, "[icon_state]-moody", src)
+
+/obj/item/nullrod/cross_boomerang/throw_at(atom/target, range, speed, mob/thrower, spin, diagonals_first, datum/callback/callback, force, gentle, quickstart)
+ thrown = TRUE
+ playsound(loc, sound_throw, 70, 0)
+ if (thrower)
+ originator = thrower
+
+ SET_PLANE_EXPLICIT(src, ABOVE_LIGHTING_PLANE, thrower)
+
+ var/turf/starting = get_turf(src)
+ target = get_turf(target)
+ var/obj/projectile/boomerang/rang = new (starting)
+ rang.boomerang = src
+ rang.firer = thrower
+ rang.def_zone = ran_zone(thrower.zone_selected)
+ rang.preparePixelProjectile(target, thrower)
+ rang.icon_state = "[icon_state]-spin"
+ rang.overlays += overlays
+ rang.plane = plane
+ rang.stun = 1 SECONDS
+
+ forceMove(rang)
+ rang.fire()
+ rang.process()
+
+/obj/item/nullrod/cross_boomerang/proc/on_step(var/obj/O)
+ if (COOLDOWN_FINISHED(src, last_sound_loop))
+ COOLDOWN_START(src, last_sound_loop, 1 SECONDS)
+ playsound(loc,sound_loop, 35, 0)
+ dir = turn(dir, 45)
+ var/obj/effect/afterimage/A = new(O.loc, O, fadout = 5, initial_alpha = 100, pla = ABOVE_LIGHTING_PLANE)
+ A.layer = O.layer - 1
+ A.color = "#1E45FF"
+ if (istype(O,/obj/effect/tracker))//only display those particles on the way back
+ A.add_particles(PS_CROSS_DUST)
+ A.add_particles(PS_CROSS_ORB)
+
+ flickering = (flickering + 1) % 4
+ if (flickering > 1)
+ O.color = "#53A6FF"
+ else
+ O.color = null
+
+/obj/item/nullrod/cross_boomerang/proc/return_check()//lets you add conditions for the boomerang to come back
+ return TRUE
+
+/obj/item/nullrod/cross_boomerang/proc/apply_status_effects(var/mob/living/carbon/C, var/minimal_effect = 0)
+ C.Stun(max(minimal_effect, 1 SECONDS))
+
+/obj/item/nullrod/cross_boomerang/proc/on_return()
+ return (istype(originator) && originator.put_in_hands(src))
+
+/obj/item/nullrod/cross_boomerang/pickup(mob/user)
+ . = ..()
+ thrown = FALSE
+
+/obj/item/nullrod/cross_boomerang/dropped(mob/user, silent)
+ . = ..()
+ SET_PLANE_EXPLICIT(src, initial(plane), user)
+
+/obj/item/nullrod/cross_boomerang/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ if(istype(hit_atom,/obj/machinery/computer/arcade))
+ playsound(hit_atom,'monkestation/code/modules/bloody_cult/sound/boomerang_cross_transform.ogg', 30, 0)
+ classic = !classic
+ icon_state = "[classic ? "cross_classic" : "cross_modern"]"
+ if (istype(loc,/obj))
+ var/obj/O = loc
+ O.icon_state = "[icon_state]-spin"
+ update_appearance()
+ . = ..()
diff --git a/monkestation/code/modules/bloody_cult/gods_power/deconversion_ritual.dm b/monkestation/code/modules/bloody_cult/gods_power/deconversion_ritual.dm
new file mode 100644
index 000000000000..170b3e9c81ca
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/gods_power/deconversion_ritual.dm
@@ -0,0 +1,189 @@
+/obj/item/book/bible/attack(mob/living/target_mob, mob/living/carbon/human/user, params, heal_mode)
+ . = ..()
+ //they have holy water in them? deconversion mode activate! anyone can do it. 'cept cultists O B V I O U S L Y
+ if (!IS_CULTIST(user) && target_mob.reagents?.has_reagent(/datum/reagent/water/holywater) && !(user.istate & ISTATE_HARM))
+ playsound(src, "punch", 25, 1, -1)
+ if (target_mob.stat == DEAD)
+ to_chat(user, span_warning("You cannot deconvert the dead!") )
+ return 1
+ if (target_mob.health < 20)
+ to_chat(user, span_warning("\The [target_mob] is too weak to handle the deconversion ritual, patch them up a bit first.") )
+ return 1
+ var/datum/antagonist/cult/cultist
+ if(IS_CULTIST(target_mob))
+ cultist = target_mob.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cultist.deconversion)
+ to_chat(user, span_warning("There is already a deconversion attempt undergoing!") )
+ return 1
+ else
+ to_chat(target_mob, span_userdanger("They are trying to deconvert you!") )
+ cultist.deconversion = 1//arbitrary non-null value to prevent deconversion-shade spam, will get replaced with a /datum/deconversion_ritual 5 seconds later
+
+ if (do_after(user, 5 SECONDS, target_mob))
+ if(cultist)
+ to_chat(user, span_warning("In the name of [deity_name], Nar-Sie forsake this body and soul!") )
+ user.visible_message(span_warning("\The [target_mob] begins to radiate with light.") )
+ new /datum/deconversion_ritual(user, target_mob, src)
+ else
+ to_chat(user, span_warning("In the name of [deity_name], Nar-Sie forsake this body and soul!") )
+ user.visible_message(span_warning("...but nothing unusual happens.") )
+ else
+ cultist.deconversion = null//deconversion attempt got interrupted, you can now try again
+ return 1
+
+/datum/deconversion_ritual
+ var/datum/antagonist/cult/cultist = null
+ var/cult_chaplain = FALSE
+ var/last_cultist = FALSE
+ var/success = DECONVERSION_ACCEPT
+
+/datum/deconversion_ritual/New(mob/living/deconverter, mob/living/deconvertee, obj/item/book/bible/bible)
+ ..()
+ if (!bible || !bible.deity_name || !deconverter || !deconvertee || !IS_CULTIST(deconvertee))
+ qdel(src)
+ return
+ var/mob/target
+ deconvertee.overlays += image('monkestation/code/modules/bloody_cult/icons/effects.dmi', src, "deconversion")
+ playsound(deconvertee, 'monkestation/code/modules/bloody_cult/sound/deconversion_start.ogg', 50, 0, -4)
+ cultist = IS_CULTIST(deconvertee)
+ cultist.deconversion = src
+
+ deconvertee.adjust_dizzy(30)
+ deconvertee.adjust_stutter(10)
+ deconvertee.adjust_jitter(30)
+ deconvertee.Paralyze(10 SECONDS)
+
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ var/living_cultists = 0
+ for(var/datum/mind/mind in cult.members)
+ if (mind.current.stat != DEAD)
+ living_cultists++
+ if (living_cultists <= 1)
+ last_cultist = TRUE
+
+ spawn()
+ spawn()
+ if (alert(deconvertee, "You are being compelled by the powers of [bible.deity_name][cult_chaplain ? " (wait what?)" : ""] to give up on serving the Cult of Nar-Sie[cult_chaplain ? " (huh!?)" : ""]", "You have 10 seconds to decide", "[!cult_chaplain ? "Abandon the Cult" : "I am so confused right now, ok I guess?"]", "[!cult_chaplain ? "Resist!" : "This is obviously a trick! Resist!"]") == "[!cult_chaplain ? "Abandon the Cult" : "I am so confused right now, ok I guess?"]")
+ success = DECONVERSION_ACCEPT
+ if (!target && !last_cultist)//no threats if nobody remains to carry them out.
+ to_chat(deconvertee, span_cult("[cult_chaplain ? "WERE YOU DECEIVED THAT EASILY? SO BE IT THEN." : "THERE WILL BE A PRICE."]") )
+ else
+ success = DECONVERSION_REFUSE
+ if (!target)
+ to_chat(deconvertee, span_warning("You block the sweet promises of forgiveness from your mind.") )
+ new /obj/effect/bible_spin(get_turf(deconvertee), deconverter ,bible)
+ sleep(10 SECONDS)
+ if (!deconvertee || !IS_CULTIST(deconvertee))
+ qdel(src)
+ return
+ deconvertee.take_overall_damage(10)//it's a painful process no matter what.
+ var/turf/T = get_turf(deconvertee)
+ anim(target = deconvertee, a_icon = 'monkestation/code/modules/bloody_cult/icons/effects.dmi', flick_anim = "cult_jaunt_land", plane = GAME_PLANE_UPPER)
+
+ switch(success)
+ if (DECONVERSION_ACCEPT)
+ var/mob/living/basic/shade/redshade_A = new(T)
+ var/mob/living/basic/shade/redshade_B = new(T)
+ var/list/adjacent_turfs = list()
+ for (var/turf/U in orange(1, T))
+ adjacent_turfs += U
+ playsound(deconvertee, 'monkestation/code/modules/bloody_cult/sound/deconversion_complete.ogg', 50, 0, -4)
+ deconvertee.visible_message(span_notice("You see [deconvertee]'s eyes become clear. Through the blessing of [cult_chaplain ? "some fanfic headcanon version of [bible.deity_name]" : "[bible.deity_name]"] they have renounced Nar-Sie.") , span_notice("You were forgiven by [bible.deity_name][cult_chaplain ? " (YEAH RIGHT...)" : ""]. You no longer share the cult's goals.") )
+ deconvertee.visible_message(span_userdanger("A pair of shades manifests from the occult energies that left them and start attacking them.") )
+ cultist.owner.current.add_particles(PS_CULT_HALO)
+ cultist.owner.current.adjust_particles(PVAR_COLOR, "#00000066", PS_CULT_HALO)
+ cultist.owner.current.adjust_particles(PVAR_ICON_STATE, "cult_halo[cultist.get_devotion_rank()]", PS_CULT_HALO)
+ cultist.owner.remove_antag_datum(/datum/antagonist/cult)
+ var/list/speak = list("...you shall give back the blood we gave you [deconvertee]...", "...one does not simply turn their back on our gift...", "...if you won't dedicate your heart to Nar-Sie, you don't need it anymore...")
+ redshade_A.say(pick(speak))
+ redshade_B.say(pick(speak))
+ target = deconvertee
+ spawn(1)
+ redshade_A.forceMove(get_turf(pick(adjacent_turfs)))
+ redshade_B.forceMove(get_turf(pick(adjacent_turfs)))
+ redshade_A.melee_attack(deconvertee)
+ redshade_B.melee_attack(deconvertee)
+ animate(redshade_A, alpha = 0, time = 2.5 SECONDS)
+ animate(redshade_B, alpha = 0, time = 2.5 SECONDS)
+ QDEL_IN(redshade_A, 3 SECONDS)
+ QDEL_IN(redshade_B, 3 SECONDS)
+ deconvertee.blood_volume -= 100 // ouch
+
+ if (DECONVERSION_REFUSE)
+ playsound(deconvertee, 'monkestation/code/modules/bloody_cult/sound/deconversion_failed.ogg', 50, 0, -4)
+ to_chat(deconvertee, span_notice("You manage to block out the exorcism.") )
+ deconvertee.visible_message(span_userdanger("The ritual was resisted!") , span_warning("The energies you mustered take their toll on your body...") )
+ deconvertee.overlays -= image('monkestation/code/modules/bloody_cult/icons/effects.dmi', src, "deconversion")
+ qdel(src)
+
+/datum/deconversion_ritual/Destroy()
+ if (cultist)
+ cultist.deconversion = null
+ cultist = null
+ ..()
+
+// Belmont Bible Spin
+
+/obj/effect/bible_spin
+ var/mob/living/owner
+ var/obj/item/book/bible/source
+ var/image/bible_image
+ var/current_spin = 0
+ var/lifetime = 10 SECONDS
+ var/lifetime_max = 10 SECONDS
+ var/distance = 0
+ var/distance_min = 8
+ var/distance_amplitude = 24
+ var/spin_speed = 30
+
+/obj/effect/bible_spin/New(var/turf/loc, var/_owner, var/_source)
+ ..()
+ if (!_owner || !_source)
+ qdel(src)
+ return
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/bible_throw.ogg', 70, 0)
+ owner = _owner
+ source = _source
+ source.forceMove(src)
+ //owner.lock_atom(src)
+ current_spin = dir2angle(owner.dir)
+ bible_image = image(source.icon, source, source.icon_state)
+ bible_image.plane = ABOVE_LIGHTING_PLANE
+ overlays += bible_image
+ spawn()
+ process_spin()
+
+/obj/effect/bible_spin/proc/process_spin()
+ set waitfor = 0
+
+ while(owner && !QDELETED(owner)&& source && !QDELETED(source) && (source.loc == src) && lifetime > 0)
+ update_spin()
+ var/obj/effect/afterimage/A = new(loc, null, 15)
+ animate(A)
+ A.appearance = appearance
+ A.pixel_x = pixel_x
+ A.pixel_y = pixel_y
+ animate(A, alpha = 0, time = 10)
+ A.layer--
+ A.add_particles(PS_BIBLE_PAGE)
+ A.adjust_particles(PVAR_VELOCITY, list(pixel_x/2, pixel_y/2), PS_BIBLE_PAGE)
+ A.adjust_particles(PVAR_SPAWNING, 2, PS_BIBLE_PAGE)
+ if ((lifetime % 10) == 0)
+ playsound(src, 'monkestation/code/modules/bloody_cult/sound/bible_spin.ogg', 50, 0)
+ for (var/mob/living/L in range(1, src))
+ source.throw_impact(L, source.throw_speed*2, owner)
+ lifetime--
+ spawn(1)//making sure we're only spawning one page per afterimage
+ A.adjust_particles(PVAR_SPAWNING, 0, PS_BIBLE_PAGE)
+ sleep(1)
+
+ if (source && !QDELETED(source))
+ source.forceMove(loc)
+ if (owner)
+ owner.put_in_hands(source)
+ qdel(src)
+
+/obj/effect/bible_spin/proc/update_spin()
+ current_spin += spin_speed
+ distance = distance_min + distance_amplitude*sin(180*(lifetime/lifetime_max))
+ animate(src, pixel_x = distance*cos(current_spin), pixel_y = distance*sin(current_spin), time = 1)
diff --git a/monkestation/code/modules/bloody_cult/holomap_additions/cult_markers.dm b/monkestation/code/modules/bloody_cult/holomap_additions/cult_markers.dm
new file mode 100644
index 000000000000..41e164028693
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/holomap_additions/cult_markers.dm
@@ -0,0 +1,115 @@
+/atom/movable/screen/fullscreen/blind/above_hud
+ plane = 41
+
+/datum/controller/subsystem/holomaps/proc/generate_cult_maps()
+ for(var/z_level in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ var/icon/map_base = icon(extra_holomaps["[HOLOMAP_EXTRA_STATIONMAPAREAS]_[z_level]"])
+ if(!map_base)
+ continue
+ var/icon/canvas = icon('monkestation/code/modules/bloody_cult/icons/cult_map.dmi', "cultmap")
+ map_base.Blend("#E30000", ICON_MULTIPLY)
+ canvas.Blend(map_base, ICON_OVERLAY)
+ extra_holomaps["[HOLOMAP_EXTRA_CULTMAP]_[z_level]"] = canvas
+
+/datum/controller/subsystem/holomaps/proc/update_cult_map(mob/user, atom/source_object)
+ if(!("[HOLOMAP_EXTRA_CULTMAP]_[user.z]" in extra_holomaps))
+ generate_cult_maps()
+ var/image/map_base = image(extra_holomaps["[HOLOMAP_EXTRA_CULTMAP]_[user.z]"])
+ if(!map_base)
+ return
+
+ map_base.cut_overlays()
+
+ var/datum/holomap_marker/located_marker = source_object.return_attached_holomap_markers()
+ for(var/datum/holomap_marker/marker as anything in holomap_markers)
+ if(!(marker.filter & HOLOMAP_FILTER_CULT))
+ continue
+ var/image/marker_image = image(marker.icon, icon_state = marker.icon_state)
+ if(located_marker == marker)
+ marker_image.icon_state = "[marker.icon_state]-here"
+ marker_image.pixel_x = marker.x + HOLOMAP_CENTER_X
+ marker_image.pixel_y = marker.y + HOLOMAP_CENTER_Y
+ map_base.overlays += marker_image
+
+ var/datum/team/cult/located_cult = locate_team(/datum/team/cult)
+ for(var/datum/mind/mind as anything in located_cult?.members)
+ var/mob/living/current_mob = mind.current
+ if(current_mob.z != user.z)
+ continue
+ var/image/cultist_image = image('monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi', icon_state = "mau1")
+ cultist_image.pixel_x = current_mob.x + HOLOMAP_CENTER_X
+ cultist_image.pixel_y = current_mob.y + HOLOMAP_CENTER_Y
+ map_base.overlays += cultist_image
+
+ return map_base
+
+/datum/controller/subsystem/holomaps/proc/show_cult_map(mob/user, atom/source_object, break_on_move = TRUE)
+ if(user.hud_used.holomap in user.client.screen)
+ return
+ var/image/cult_map = update_cult_map(user, source_object)
+ if(!cult_map)
+ return
+ user.hud_used.holomap.used_base_map = cult_map
+
+
+ if(break_on_move)
+ RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(hide_cult_map), cult_map)
+
+ user.hud_used.holomap.plane = 41
+ cult_map.loc = user.hud_used.holomap
+ user.client.screen |= user.hud_used.holomap
+ user.client.images |= cult_map
+ user.overlay_fullscreen("map_blocker", /atom/movable/screen/fullscreen/blind/above_hud)
+ user.update_fullscreen_alpha("map_blocker", 255, 0)
+
+/datum/controller/subsystem/holomaps/proc/hide_cult_map(mob/user, image/cult_map)
+ user.client.screen -= user.hud_used.holomap
+ user.client.images -= user.hud_used.holomap.used_base_map
+ user.hud_used.holomap.used_base_map = cult_map
+ user.hud_used.holomap.plane = 40
+ user.clear_fullscreen("map_blocker", 10)
+ UnregisterSignal(user, COMSIG_MOVABLE_MOVED)
+
+/datum/controller/subsystem/holomaps/proc/live_update_cult_map(mob/user)
+ var/image/cult_map = update_cult_map(user, null)
+ user.client.images -= user.hud_used.holomap.used_base_map
+ user.hud_used.holomap.used_base_map = cult_map
+ cult_map.loc = user.hud_used.holomap
+ user.client.images |= cult_map
+
+/datum/holomap_marker
+ var/x
+ var/y
+ var/z
+
+ ///this is a filter used when quickly searching for all matching markers IE FILTER_CULT will bring all cult markers up
+ var/filter
+ ///this is the id we use when locating them inside the list of markers
+ var/id
+
+ ///this is our map icon
+ var/icon
+ ///this is our map icon_state
+ var/icon_state
+
+ var/atom/reference_atom
+
+/datum/holomap_marker/New(atom/host)
+ . = ..()
+ SSholomaps.holomap_markers |= src
+ if(host)
+ RegisterSignal(host, COMSIG_QDELETING, PROC_REF(clear_marker))
+
+/datum/holomap_marker/proc/clear_marker()
+ SSholomaps.holomap_markers -= src
+ reference_atom = null
+ qdel(src)
+
+/atom/proc/return_attached_holomap_markers()
+ for(var/datum/holomap_marker/marker as anything in SSholomaps.holomap_markers)
+ if(!marker.reference_atom)
+ continue
+ if(marker.reference_atom != src)
+ continue
+ return marker
+ return null
diff --git a/monkestation/code/modules/bloody_cult/icons/160x160.dmi b/monkestation/code/modules/bloody_cult/icons/160x160.dmi
new file mode 100644
index 000000000000..516222489a1e
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/160x160.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/192x192.dmi b/monkestation/code/modules/bloody_cult/icons/192x192.dmi
new file mode 100644
index 000000000000..5eb2107b927b
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/192x192.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/224x224.dmi b/monkestation/code/modules/bloody_cult/icons/224x224.dmi
new file mode 100644
index 000000000000..5ab4c75a50af
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/224x224.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/32x32.dmi b/monkestation/code/modules/bloody_cult/icons/32x32.dmi
new file mode 100644
index 000000000000..26ac2db41ded
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/32x32.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/352x352.dmi b/monkestation/code/modules/bloody_cult/icons/352x352.dmi
new file mode 100644
index 000000000000..6da3e5cf5c75
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/352x352.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/480x480.dmi b/monkestation/code/modules/bloody_cult/icons/480x480.dmi
new file mode 100644
index 000000000000..fb91249d35b3
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/480x480.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/64x64.dmi b/monkestation/code/modules/bloody_cult/icons/64x64.dmi
new file mode 100644
index 000000000000..9badf6eb570b
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/64x64.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/96x96.dmi b/monkestation/code/modules/bloody_cult/icons/96x96.dmi
new file mode 100644
index 000000000000..9feb39da3c26
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/96x96.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/background.dmi b/monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/background.dmi
new file mode 100644
index 000000000000..917558e2a1a4
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/background.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi b/monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi
new file mode 100644
index 000000000000..1ac0f14668ce
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/background.dmi b/monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/background.dmi
new file mode 100644
index 000000000000..535a03056142
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/background.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi b/monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi
new file mode 100644
index 000000000000..e117a40d8cdc
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/background.dmi b/monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/background.dmi
new file mode 100644
index 000000000000..e0a03fa50335
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/background.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi b/monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi
new file mode 100644
index 000000000000..924451af96bf
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/animal.dmi b/monkestation/code/modules/bloody_cult/icons/animal.dmi
new file mode 100644
index 000000000000..b85448785243
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/animal.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/104x40.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/104x40.dmi
new file mode 100644
index 000000000000..fc22b47f660a
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/104x40.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/16x16.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/16x16.dmi
new file mode 100644
index 000000000000..cb0a834323c9
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/16x16.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/16x32.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/16x32.dmi
new file mode 100644
index 000000000000..cbd53f7ca617
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/16x32.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/192x192.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/192x192.dmi
new file mode 100644
index 000000000000..472ea813e878
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/192x192.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/223x37.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/223x37.dmi
new file mode 100644
index 000000000000..847c74774dc8
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/223x37.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/24x24.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/24x24.dmi
new file mode 100644
index 000000000000..c8e92ca98b45
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/24x24.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/288x16.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/288x16.dmi
new file mode 100644
index 000000000000..d9bd11a8cdf6
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/288x16.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/32x121.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/32x121.dmi
new file mode 100644
index 000000000000..2b0d7f98b589
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/32x121.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/32x32.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/32x32.dmi
new file mode 100644
index 000000000000..8c2fc8dfdf6a
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/32x32.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/362x229.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/362x229.dmi
new file mode 100644
index 000000000000..e6e92da8192f
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/362x229.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bloodcult/40x40.dmi b/monkestation/code/modules/bloody_cult/icons/bloodcult/40x40.dmi
new file mode 100644
index 000000000000..7a922842d28e
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bloodcult/40x40.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/boomerang.dmi b/monkestation/code/modules/bloody_cult/icons/boomerang.dmi
new file mode 100644
index 000000000000..c373549a5fe0
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/boomerang.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/bus.dmi b/monkestation/code/modules/bloody_cult/icons/bus.dmi
new file mode 100644
index 000000000000..2b1b39882aed
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/bus.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/candle.dmi b/monkestation/code/modules/bloody_cult/icons/candle.dmi
new file mode 100644
index 000000000000..06235312f700
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/candle.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult.dmi b/monkestation/code/modules/bloody_cult/icons/cult.dmi
new file mode 100644
index 000000000000..76d2a647d614
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi b/monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi
new file mode 100644
index 000000000000..1a1ac5b840cb
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult_64x64.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi b/monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi
new file mode 100644
index 000000000000..7499bfd93f58
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult_96x96.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult_map.dmi b/monkestation/code/modules/bloody_cult/icons/cult_map.dmi
new file mode 100644
index 000000000000..fc127624da42
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult_map.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult_radial.dmi b/monkestation/code/modules/bloody_cult/icons/cult_radial.dmi
new file mode 100644
index 000000000000..aea1677a8281
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult_radial.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult_radial2.dmi b/monkestation/code/modules/bloody_cult/icons/cult_radial2.dmi
new file mode 100644
index 000000000000..5d585622611b
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult_radial2.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi b/monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi
new file mode 100644
index 000000000000..06f29f65e9ff
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/cult_radial3.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/deityrunes.dmi b/monkestation/code/modules/bloody_cult/icons/deityrunes.dmi
new file mode 100644
index 000000000000..8d712957771a
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/deityrunes.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi b/monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi
new file mode 100644
index 000000000000..5910ccf2c005
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/doafter_icon.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/effects.dmi b/monkestation/code/modules/bloody_cult/icons/effects.dmi
new file mode 100644
index 000000000000..748d52088386
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/effects.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/effects_particles.dmi b/monkestation/code/modules/bloody_cult/icons/effects_particles.dmi
new file mode 100644
index 000000000000..71c30a936ef4
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/effects_particles.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/font_16x16.dmi b/monkestation/code/modules/bloody_cult/icons/font_16x16.dmi
new file mode 100644
index 000000000000..52cbf78fc1a1
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/font_16x16.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/font_8x8.dmi b/monkestation/code/modules/bloody_cult/icons/font_8x8.dmi
new file mode 100644
index 000000000000..28a144f76498
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/font_8x8.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi b/monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi
new file mode 100644
index 000000000000..67128206fe71
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/holomap_markers.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/holomap_markers_32x32.dmi b/monkestation/code/modules/bloody_cult/icons/holomap_markers_32x32.dmi
new file mode 100644
index 000000000000..452444387498
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/holomap_markers_32x32.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/hud.dmi b/monkestation/code/modules/bloody_cult/icons/hud.dmi
new file mode 100644
index 000000000000..f3daddda9866
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/hud.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_l.dmi b/monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_l.dmi
new file mode 100644
index 000000000000..cbc90e39647c
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_l.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_r.dmi b/monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_r.dmi
new file mode 100644
index 000000000000..408d35372f82
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/in_hands/swords_axes_r.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/logos.dmi b/monkestation/code/modules/bloody_cult/icons/logos.dmi
new file mode 100644
index 000000000000..8d4bef1843dd
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/logos.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/mind_ui.dmi b/monkestation/code/modules/bloody_cult/icons/mind_ui.dmi
new file mode 100644
index 000000000000..874404b297e9
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/mind_ui.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/mob.dmi b/monkestation/code/modules/bloody_cult/icons/mob.dmi
new file mode 100644
index 000000000000..54cf7699664a
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/mob.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/projectiles.dmi b/monkestation/code/modules/bloody_cult/icons/projectiles.dmi
new file mode 100644
index 000000000000..0c198a425040
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/projectiles.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/projectiles_experimental.dmi b/monkestation/code/modules/bloody_cult/icons/projectiles_experimental.dmi
new file mode 100644
index 000000000000..f29db8427011
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/projectiles_experimental.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/reagent_containers.dmi b/monkestation/code/modules/bloody_cult/icons/reagent_containers.dmi
new file mode 100644
index 000000000000..e032361c5e70
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/reagent_containers.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/reagentfillings.dmi b/monkestation/code/modules/bloody_cult/icons/reagentfillings.dmi
new file mode 100644
index 000000000000..8818fbb8cc83
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/reagentfillings.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/riftbox.dmi b/monkestation/code/modules/bloody_cult/icons/riftbox.dmi
new file mode 100644
index 000000000000..8bef200fa795
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/riftbox.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/screen1.dmi b/monkestation/code/modules/bloody_cult/icons/screen1.dmi
new file mode 100644
index 000000000000..c295eae7ad92
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/screen1.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/screen1_cult.dmi b/monkestation/code/modules/bloody_cult/icons/screen1_cult.dmi
new file mode 100644
index 000000000000..8e5442a6b84f
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/screen1_cult.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/screen1_full.dmi b/monkestation/code/modules/bloody_cult/icons/screen1_full.dmi
new file mode 100644
index 000000000000..ee22d106919c
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/screen1_full.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/singulo_chain.dmi b/monkestation/code/modules/bloody_cult/icons/singulo_chain.dmi
new file mode 100644
index 000000000000..4e38c5e51c60
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/singulo_chain.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/soulblade/18x200.dmi b/monkestation/code/modules/bloody_cult/icons/soulblade/18x200.dmi
new file mode 100644
index 000000000000..1045ccbbaf88
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/soulblade/18x200.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/soulblade/21x246.dmi b/monkestation/code/modules/bloody_cult/icons/soulblade/21x246.dmi
new file mode 100644
index 000000000000..3e067983f869
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/soulblade/21x246.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/soulblade/32x32.dmi b/monkestation/code/modules/bloody_cult/icons/soulblade/32x32.dmi
new file mode 100644
index 000000000000..3a7f6eafe536
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/soulblade/32x32.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/spells.dmi b/monkestation/code/modules/bloody_cult/icons/spells.dmi
new file mode 100644
index 000000000000..8c3dc43e52b2
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/spells.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/storage.dmi b/monkestation/code/modules/bloody_cult/icons/storage.dmi
new file mode 100644
index 000000000000..aa136f10be06
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/storage.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/uristrunes.dmi b/monkestation/code/modules/bloody_cult/icons/uristrunes.dmi
new file mode 100644
index 000000000000..99c0844e4a6b
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/uristrunes.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/worn/head.dmi b/monkestation/code/modules/bloody_cult/icons/worn/head.dmi
new file mode 100644
index 000000000000..df9b4fd73db7
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/worn/head.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/icons/worn/suit.dmi b/monkestation/code/modules/bloody_cult/icons/worn/suit.dmi
new file mode 100644
index 000000000000..007aac721d6d
Binary files /dev/null and b/monkestation/code/modules/bloody_cult/icons/worn/suit.dmi differ
diff --git a/monkestation/code/modules/bloody_cult/mind_ui.dm b/monkestation/code/modules/bloody_cult/mind_ui.dm
new file mode 100644
index 000000000000..c70f491f8411
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/mind_ui.dm
@@ -0,0 +1,516 @@
+/*
+ mind_ui, started on 2021/07/22 by Deity.
+
+ instead of being stored in the mob like /datum/hud, this one is stored in a mob's /datum/mind 's active_uis list
+
+ A mind can store several separate mind_uis, think of each one as its own menu/pop-up, that in turns contains a list of /obj/mind_ui_element,
+ or other /datum/mind_ui that
+
+ * mind datums and their elements should avoid holding references to atoms in the real world.
+*/
+
+
+/obj/abstract/proc/get_view_size()
+ if(usr && usr.client)
+ . = usr.client.view
+ else
+ . = world.view
+
+
+// During game setup we fill a list with the IDs and types of every /datum/mind_ui subtypes
+GLOBAL_VAR_INIT(mind_ui_init, FALSE)
+GLOBAL_LIST_INIT(mind_ui_id_to_type, list())
+
+/proc/init_mind_ui()
+ if (GLOB.mind_ui_init)
+ return
+ GLOB.mind_ui_init = TRUE
+ for (var/mind_ui_type in subtypesof(/datum/mind_ui))
+ var/datum/mind_ui/ui = mind_ui_type
+ GLOB.mind_ui_id_to_type[initial(ui.uniqueID)] = mind_ui_type
+
+//////////////////////MIND UI PROCS/////////////////////////////
+
+/datum/mind
+ var/list/active_uis = list()
+
+/datum/mind/Destroy()
+ . = ..()
+ RemoveAllUIs()
+ QDEL_NULL(active_uis)
+
+/datum/mind/proc/ResendAllUIs() // Re-sends all mind uis to client.screen, called on mob/living/Login()
+ for (var/mind_ui in active_uis)
+ var/datum/mind_ui/ui = active_uis[mind_ui]
+ ui.SendToClient()
+
+/datum/mind/proc/RemoveAllUIs() // Removes all mind uis from client.screen, called on mob/Logout()
+ for (var/mind_ui in active_uis)
+ var/datum/mind_ui/ui = active_uis[mind_ui]
+ ui.RemoveFromClient()
+
+
+/datum/mind/proc/DisplayUI(ui_ID)
+ var/datum/mind_ui/ui
+ if (ui_ID in active_uis)
+ ui = active_uis[ui_ID]
+ else
+ if (!(ui_ID in GLOB.mind_ui_id_to_type))
+ return
+ var/ui_type = GLOB.mind_ui_id_to_type[ui_ID]
+ ui = new ui_type(src)
+ if(!ui.Valid())
+ ui.Hide()
+ else
+ ui.Display()
+
+/datum/mind/proc/HideUI(var/ui_ID)
+ if (ui_ID in active_uis)
+ var/datum/mind_ui/ui = active_uis[ui_ID]
+ ui.Hide()
+
+/datum/mind/proc/UpdateUIScreenLoc()
+ for (var/mind_ui in active_uis)
+ var/datum/mind_ui/ui = active_uis[mind_ui]
+ ui.UpdateUIScreenLoc()
+
+//////////////////////MOB SHORTCUT PROCS////////////////////////
+
+/mob/proc/ResendAllUIs()
+ if (mind)
+ mind.ResendAllUIs()
+
+/mob/proc/RemoveAllUIs()
+ if (mind)
+ mind.RemoveAllUIs()
+
+/mob/proc/DisplayUI(var/ui_ID)
+ if (mind)
+ mind.DisplayUI(ui_ID)
+
+/mob/proc/HideUI(var/ui_ID)
+ if (mind)
+ mind.HideUI(ui_ID)
+
+/mob/proc/UpdateUIScreenLoc()
+ if (mind)
+ mind.UpdateUIScreenLoc()
+
+/mob/proc/UpdateUIElementIcon(var/element_type)
+ if (client)
+ var/obj/abstract/mind_ui_element/element = locate(element_type) in client.screen
+ if (element)
+ element.UpdateIcon()
+
+/mob/proc/UpdateAllElementIcons()
+ if (client)
+ for (var/obj/abstract/mind_ui_element/element in client.screen)
+ element.UpdateIcon()
+
+
+////////////////////////////////////////////////////////////////////
+// //
+// MIND UI //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui
+ var/uniqueID = "Default"
+ var/datum/mind/mind
+ var/list/elements = list() // the objects displayed by the UI. Those can be both non-interactable objects (background/fluff images, foreground shaders) and clickable interace buttons.
+ var/list/subUIs = list() // children UI. Closing the parent UI closes all the children.
+ var/datum/mind_ui/parent = null
+ var/offset_x = 0 //KEEP THESE AT 0, they are set by /obj/abstract/mind_ui_element/hoverable/movable/
+ var/offset_y = 0
+ var/offset_layer = MIND_UI_GROUP_A
+
+ var/x = "CENTER"
+ var/y = "CENTER"
+
+ var/list/element_types_to_spawn = list()
+ var/list/sub_uis_to_spawn = list()
+
+ var/display_with_parent = FALSE
+ var/never_move = FALSE
+
+ var/active = TRUE
+
+ var/obj/abstract/mind_ui_element/failsafe/failsafe // All mind UI datums include one of those so we can detect if the elements somehow disappeared from client.screen
+
+/datum/mind_ui/New(var/datum/mind/M)
+ if (!istype(M))
+ qdel(src)
+ return
+ mind = M
+ mind.active_uis[uniqueID] = src
+ ..()
+ SpawnElements()
+ for (var/ui_type in sub_uis_to_spawn)
+ var/datum/mind_ui/child = new ui_type(mind)
+ subUIs += child
+ child.parent = src
+ SendToClient()
+
+/datum/mind_ui/proc/SpawnElements()
+ failsafe = new (null, src)
+ elements += failsafe
+ for (var/element_type in element_types_to_spawn)
+ elements += new element_type(null, src)
+
+// Send every element to the client, called on Login() and when the UI is first added to a mind
+/datum/mind_ui/proc/SendToClient()
+ if (mind.current)
+ var/mob/M = mind.current
+ if (!M.client)
+ return
+
+ if (!Valid() || !display_with_parent) // Makes sure the UI isn't still active when we should have lost it (such as coming out of a mecha while disconnected)
+ Hide(TRUE)
+
+ for (var/obj/abstract/mind_ui_element/element in elements)
+ mind.current.client.screen |= element
+
+// Removes every element from the client, called on Logout()
+/datum/mind_ui/proc/RemoveFromClient()
+ if (mind.current)
+ var/mob/M = mind.current
+ if (!M.client)
+ return
+
+ mind.current.client.screen -= elements
+
+// Makes every element visible
+/datum/mind_ui/proc/Display()
+ if (!Valid())
+ Hide(TRUE)
+ return
+ active = TRUE
+
+ var/mob/M = mind.current
+ if (failsafe && M.client && !(failsafe in M.client.screen))
+ SendToClient() // The elements disappeared from the client screen due to some fuckery, send them back!
+
+ for (var/obj/abstract/mind_ui_element/element in elements)
+ element.Appear()
+ for (var/datum/mind_ui/child in subUIs)
+ if (child.display_with_parent)
+ if(child.Valid())
+ child.Display()
+ else
+ child.Hide()
+
+/datum/mind_ui/proc/Hide(var/override = FALSE)
+ active = FALSE
+ HideChildren(override)
+ HideElements(override)
+
+/datum/mind_ui/proc/HideChildren(var/override = FALSE)
+ for (var/datum/mind_ui/child in subUIs)
+ child.Hide(override)
+
+/datum/mind_ui/proc/HideElements(var/override = FALSE)
+ for (var/obj/abstract/mind_ui_element/element in elements)
+ if (override)
+ element.invisibility = 101
+ else
+ element.Hide()
+
+/datum/mind_ui/proc/Valid()
+ return TRUE
+
+/datum/mind_ui/proc/UpdateUIScreenLoc()
+ for (var/obj/abstract/mind_ui_element/element in elements)
+ element.UpdateUIScreenLoc()
+
+/datum/mind_ui/proc/HideParent(var/levels=0)
+ if (levels <= 0)
+ var/datum/mind_ui/ancestor = GetAncestor()
+ ancestor.Hide()
+ return
+ else
+ var/datum/mind_ui/to_hide = src
+ while (levels > 0)
+ if (to_hide.parent)
+ levels--
+ to_hide = to_hide.parent
+ else
+ break
+ to_hide.Hide()
+
+/datum/mind_ui/proc/GetAncestor()
+ if (parent)
+ return parent.GetAncestor()
+ else
+ return src
+
+/datum/mind_ui/proc/GetUser()
+ ASSERT(mind && mind.current)
+ return mind.current
+
+////////////////////////////////////////////////////////////////////
+// //
+// UI ELEMENT //
+// //
+////////////////////////////////////////////////////////////////////
+
+/obj/abstract/mind_ui_element
+ name = "Undefined UI Element"
+ icon = 'monkestation/code/modules/bloody_cult/icons/32x32.dmi'
+ icon_state = ""
+ mouse_opacity = 1
+ plane = HUD_PLANE
+
+ var/datum/mind_ui/parent = null
+ var/element_flags = 0
+ //MINDUI_FLAG_PROCESSING - Adds the element to processing_objects and calls process()
+ //MINDUI_FLAG_TOOLTIP - Displays a tooltip upon mouse hovering (only for /obj/abstract/mind_ui_element/hoverable !)
+
+ var/offset_x = 0
+ var/offset_y = 0
+
+/obj/abstract/mind_ui_element/New(turf/loc, var/datum/mind_ui/P)
+ if (!istype(P))
+ qdel(src)
+ return
+ ..()
+ base_icon_state = icon_state
+ parent = P
+ UpdateUIScreenLoc()
+
+ if (element_flags & MINDUI_FLAG_PROCESSING)
+ START_PROCESSING(SSobj, src)
+
+/obj/abstract/mind_ui_element/Destroy()
+ if (element_flags & MINDUI_FLAG_PROCESSING)
+ STOP_PROCESSING(SSobj, src)
+ ..()
+
+/obj/abstract/mind_ui_element/proc/CanAppear()
+ return TRUE
+
+/obj/abstract/mind_ui_element/proc/Appear()
+ if (invisibility)
+ invisibility = 0
+ UpdateIcon(TRUE)
+ else
+ invisibility = 0
+ UpdateIcon()
+
+/obj/abstract/mind_ui_element/proc/Hide()
+ if (!parent.active) // we check again for it due to potential spawn() use, and inconsistencies caused by quick UI toggling
+ invisibility = 101
+
+//In case we want to make a specific element disappear, and not because the mind UI is inactive.
+/obj/abstract/mind_ui_element/proc/Disappear()
+ invisibility = 101
+
+/obj/abstract/mind_ui_element/proc/GetUser()
+ ASSERT(parent && parent.mind && parent.mind.current)
+ return parent.mind.current
+
+/obj/abstract/mind_ui_element/proc/UpdateUIScreenLoc()
+ screen_loc = "[parent.x]:[offset_x + parent.offset_x],[parent.y]:[offset_y+parent.offset_y]"
+ layer = initial(layer) + parent.offset_layer
+
+/obj/abstract/mind_ui_element/proc/UpdateIcon(var/appear = FALSE)
+ return
+
+/obj/abstract/mind_ui_element/proc/String2Image(var/string,var/spacing=6,var/image_font='monkestation/code/modules/bloody_cult/icons/font_8x8.dmi',var/_color="#FFFFFF",var/_pixel_x = 0,var/_pixel_y = 0) // only supports numbers right now
+ if (!string)
+ return image(image_font,"")
+
+ var/image/result = image(image_font,"")
+ for (var/i = 1 to length(string))
+ var/image/I = image(image_font,copytext(string,i,i+1))
+ I.pixel_x = (i - 1) * spacing
+ result.overlays += I
+ result.color = _color
+ result.pixel_x = _pixel_x
+ result.pixel_y = _pixel_y
+ return result
+
+/obj/abstract/mind_ui_element/proc/String2Maptext(var/string,var/font="Consolas",var/font_size="8pt",var/_color="#FFFFFF",var/_pixel_x = 0,var/_pixel_y = 0)
+ if (!string)
+ return image(icon = null)
+
+ var/image/I_shadow = image(icon = null)
+ I_shadow.maptext = {"[string]"}
+ I_shadow.maptext_height = 512
+ I_shadow.maptext_width = 512
+ I_shadow.maptext_x = 1 + _pixel_x
+ I_shadow.maptext_y = -1 + _pixel_y
+ var/image/I = image(icon = null)
+ I.maptext = {"[string]"}
+ I.maptext_height = 512
+ I.maptext_width = 512
+ I.maptext_x = _pixel_x
+ I.maptext_y = _pixel_y
+
+ overlays += I_shadow
+ overlays += I
+
+
+/obj/abstract/mind_ui_element/proc/SlideUIElement(var/new_x = 0, var/new_y = 0, var/duration, var/layer = MIND_UI_BACK, var/hide_after = FALSE)
+ invisibility = 101
+ var/image/ui_image = image(icon, src, icon_state, layer)
+ ui_image.overlays = overlays
+ var/mob/U = GetUser()
+ U.client.images |= ui_image
+ animate(ui_image, pixel_x = new_x - offset_x, pixel_y = new_y - offset_y, time = duration)
+ spawn(duration)
+ offset_x = new_x
+ offset_y = new_y
+ UpdateUIScreenLoc()
+ U.client.images -= ui_image
+ if(!hide_after)
+ invisibility = 0
+
+/obj/abstract/mind_ui_element/failsafe
+ icon_state = "blank"
+ mouse_opacity = 0
+
+////////////////// HOVERABLE ////////////////////////
+// Make use of MouseEntered/MouseExited to allow for effects and behaviours related to simply hovering above the element
+
+/obj/abstract/mind_ui_element/hoverable
+ var/hovering = FALSE
+ var/tooltip_title = "Undefined UI Element"
+ var/tooltip_content = ""
+ var/tooltip_theme = "default"
+ var/hover_state = TRUE
+
+/obj/abstract/mind_ui_element/hoverable/MouseEntered(location,control,params)
+ StartHovering(location,control,params)
+ hovering = 1
+
+/obj/abstract/mind_ui_element/hoverable/MouseExited()
+ StopHovering()
+ hovering = 0
+
+/obj/abstract/mind_ui_element/hoverable/Hide()
+ ..()
+ StopHovering()
+ hovering = 0
+
+/obj/abstract/mind_ui_element/hoverable/Disappear()
+ ..()
+ StopHovering()
+ hovering = 0
+
+/obj/abstract/mind_ui_element/hoverable/proc/StartHovering(var/location,var/control,var/params)
+ if (hover_state)
+ icon_state = "[base_icon_state]-hover"
+ if (element_flags & MINDUI_FLAG_TOOLTIP)
+ var/mob/M = GetUser()
+ if (M)
+ //I hate this, I hate this, but somehow the tooltips won't appear in the right place unless I do this black magic
+ //this only happens with mindUI elements, but the more offset from the center the elements are, tooltips become even more offset.
+ //this code corrects this extra offset.
+ var/list/param_list = params2list(params)
+ var/screenloc = param_list["screen-loc"]
+ var/x_index = findtext(screenloc, ":", 1, 0)
+ var/comma_index = findtext(screenloc,",", x_index, 0)
+ var/y_index = findtext(screenloc,":", comma_index, 0)
+ var/x_loc = text2num(copytext(screenloc, 1, x_index))
+ var/y_loc = text2num(copytext(screenloc, comma_index+1, y_index))
+ if (x_loc <= 7)
+ x_loc = 7
+ else
+ x_loc = 9
+ if (y_loc <= 7)
+ y_loc = 7
+ else
+ y_loc = 9
+ openToolTip(M,src,"icon-x=1;icon-y=1;screen-loc=[x_loc]:1,[y_loc]:1",title = tooltip_title,content = tooltip_content,theme = tooltip_theme)
+
+/obj/abstract/mind_ui_element/hoverable/proc/StopHovering()
+ if (hover_state)
+ icon_state = "[base_icon_state]"
+ if (element_flags & MINDUI_FLAG_TOOLTIP)
+ var/mob/M = GetUser()
+ if (M)
+ closeToolTip(M)
+
+
+////////////////// MOVABLE ////////////////////////
+// Make use of MouseDown/MouseUp/MouseDrop to allow for relocating of the element
+// By setting "move_whole_ui" to TRUE, the element will cause its entire parent UI to move with it.
+
+/obj/abstract/mind_ui_element/hoverable/movable
+ var/move_whole_ui = FALSE
+ var/moving = FALSE
+ var/icon/movement
+
+/obj/abstract/mind_ui_element/hoverable/movable/AltClick(mob/user) // Alt+Click defaults to reset the offset
+ ResetLoc()
+
+/obj/abstract/mind_ui_element/hoverable/movable/MouseDown(location, control, params)
+ if (!movement)
+ var/icon/I = new(icon, icon_state)
+ I.Blend('monkestation/code/modules/bloody_cult/icons/mind_ui.dmi', ICON_OVERLAY, I.Width()/2-16, I.Height()/2-16)
+ I.Scale(2* I.Width(),2* I.Height()) // doubling the size to account for players generally having more or less a 960x960 resolution
+ var/rgba = "#FFFFFF" + copytext(rgb(0,0,0,191), 8)
+ I.Blend(rgba, ICON_MULTIPLY)
+ movement = I
+
+ var/mob/M = GetUser()
+ M.client.mouse_pointer_icon = movement
+ moving = TRUE
+
+/obj/abstract/mind_ui_element/hoverable/movable/MouseUp(location, control, params)
+ var/mob/M = GetUser()
+ M.client.mouse_pointer_icon = initial(M.client.mouse_pointer_icon)
+ if (moving)
+ MoveLoc(params)
+
+/obj/abstract/mind_ui_element/hoverable/movable/MouseDrop(over_object, src_location, over_location, src_control, over_control, params)
+ var/mob/M = GetUser()
+ M.client.mouse_pointer_icon = initial(M.client.mouse_pointer_icon)
+ MoveLoc(params)
+
+/obj/abstract/mind_ui_element/hoverable/movable/proc/MoveLoc(var/params)
+ moving = FALSE
+ var/list/PM = params2list(params)
+ if(!PM || !PM["screen-loc"])
+ return
+
+ //first we need the x and y coordinates in pixels of the element relative to the bottom left corner of the screen
+ var/icon/I = new(icon,icon_state)
+ var/view = get_view_size()
+ var/list/offsets = screen_loc_to_offset(screen_loc, view)
+ var/start_x_val = offsets[1]
+ var/start_y_val = offsets[2]
+
+ //now we get those of the place where we released the mouse button
+ var/list/dest_loc_params = splittext(PM["screen-loc"], ",")
+ var/list/dest_loc_X = splittext(dest_loc_params[1],":")
+ var/list/dest_loc_Y = splittext(dest_loc_params[2],":")
+ var/dest_pix_x = text2num(dest_loc_X[2]) - round(I.Width()/2)
+ var/dest_pix_y = text2num(dest_loc_Y[2]) - round(I.Height()/2)
+ var/dest_x_val = text2num(dest_loc_X[1])*32 + dest_pix_x
+ var/dest_y_val = text2num(dest_loc_Y[1])*32 + dest_pix_y
+
+ //and calculate the offset between the two, which we can then add to either the element or the whole UI
+ if (move_whole_ui)
+ parent.offset_x += dest_x_val - start_x_val
+ parent.offset_y += dest_y_val - start_y_val
+ parent.UpdateUIScreenLoc()
+ for (var/datum/mind_ui/sub in parent.subUIs)
+ if (!sub.never_move)
+ sub.offset_x += dest_x_val - start_x_val
+ sub.offset_y += dest_y_val - start_y_val
+ sub.UpdateUIScreenLoc()
+ else
+ offset_x += dest_x_val - start_x_val
+ offset_y += dest_y_val - start_y_val
+ UpdateUIScreenLoc()
+
+/obj/abstract/mind_ui_element/hoverable/movable/proc/ResetLoc()
+ if (move_whole_ui)
+ parent.offset_x = 0
+ parent.offset_y = 0
+ parent.UpdateUIScreenLoc()
+ else
+ offset_x = initial(offset_x)
+ offset_y = initial(offset_y)
+ UpdateUIScreenLoc()
diff --git a/monkestation/code/modules/bloody_cult/mind_ui_system.dm b/monkestation/code/modules/bloody_cult/mind_ui_system.dm
new file mode 100644
index 000000000000..18c73bdae3f4
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/mind_ui_system.dm
@@ -0,0 +1,9 @@
+PROCESSING_SUBSYSTEM_DEF(mind_ui)
+ name = "Mind UI Processing"
+ init_order = INIT_ORDER_AIR
+ flags = SS_NO_FIRE
+ wait = 1 SECONDS
+
+/datum/controller/subsystem/processing/mind_ui/Initialize()
+ init_mind_ui()
+ return SS_INIT_SUCCESS
diff --git a/monkestation/code/modules/bloody_cult/mind_uis/adminbus.dm b/monkestation/code/modules/bloody_cult/mind_uis/adminbus.dm
new file mode 100644
index 000000000000..1cbb81e74bd2
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/mind_uis/adminbus.dm
@@ -0,0 +1,808 @@
+
+/datum/mind_ui/adminbus
+ uniqueID = "Adminbus"
+ sub_uis_to_spawn = list(
+ /datum/mind_ui/adminbus_top_panel,
+ /datum/mind_ui/adminbus_left_panel,
+ /datum/mind_ui/adminbus_bottom_panel,
+ )
+
+/datum/mind_ui/adminbus/Valid()
+ var/mob/M = mind.current
+ if (!M)
+ return FALSE
+ if(istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ return TRUE
+ return FALSE
+
+////////////////////////////////////////////////////////////////////
+// //
+// TOP PANEL //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui/adminbus_top_panel
+ uniqueID = "Adminbus Top Panel"
+ y = "TOP"
+ element_types_to_spawn = list(
+ /obj/abstract/mind_ui_element/adminbus_top_panel,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_give_bombs,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_delete_bombs,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_give_guns,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_delete_guns,
+ /obj/abstract/mind_ui_element/adminbus_release,
+ /obj/abstract/mind_ui_element/adminbus_send_home,
+ /obj/abstract/mind_ui_element/adminbus_antag_madness,
+ )
+ display_with_parent = TRUE
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_top_panel
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/background.dmi'
+ icon_state = "panel"
+ layer = MIND_UI_BACK
+ offset_x = -221
+ offset_y = 0
+
+/obj/abstract/mind_ui_element/adminbus_top_panel/UpdateIcon()
+ overlays.len = 0
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ for(var/i = 1 to 16)
+ var/image/I = image('monkestation/code/modules/bloody_cult/icons/32x32.dmi', src, "blank")
+ I.pixel_x = 365 - (16 * i)
+ I.pixel_y = 38
+ I.dir = SOUTH
+ if(i <= A.passengers.len)
+ var/atom/passenger = A.passengers[i]
+ var/image/image = image(passenger)
+ image.dir = SOUTH
+ I.overlays += image
+ overlays += I
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_split
+ name = "Split the Passengers between the two Thunderdome Teams"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_tdarena"
+ layer = MIND_UI_BUTTON
+ offset_x = -188
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_split/Click()
+ flick("[base_icon_state]-push", src)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_red
+ name = "Send Passengers to the Thunderdome's Red Team"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_tdred"
+ layer = MIND_UI_BUTTON
+ offset_x = -206
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_red/Click()
+ flick("[base_icon_state]-push", src)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_green
+ name = "Send Passengers to the Thunderdome's Green Team"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_tdgreen"
+ layer = MIND_UI_BUTTON
+ offset_x = -154
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_green/Click()
+ flick("[base_icon_state]-push", src)
+
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_obs
+ name = "Send Passengers to the Thunderdome's Observers' Lodge"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_tdobs"
+ layer = MIND_UI_BUTTON
+ offset_x = -188
+ offset_y = -4
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_thunderdome_obs/Click()
+ flick("[base_icon_state]-push", src)
+
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_give_bombs
+ name = "Give Fuse-Bombs to the Passengers"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_givebombs"
+ layer = MIND_UI_BUTTON
+ offset_x = 66
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_give_bombs/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.give_bombs(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete_bombs
+ name = "Delete the given Fuse-Bombs"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_delgiven"
+ layer = MIND_UI_BUTTON
+ offset_x = 50
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete_bombs/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.delete_bombs(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_give_guns
+ name = "Give Infinite Laser Guns to the Passengers"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_givelasers"
+ layer = MIND_UI_BUTTON
+ offset_x = -35
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_give_guns/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.give_lasers(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete_guns
+ name = "Delete the given Infinite Laser Guns"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_delgiven"
+ layer = MIND_UI_BUTTON
+ offset_x = -51
+ offset_y = -38
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete_guns/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.delete_lasers(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_release
+ name = "Release Passengers"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_free"
+ layer = MIND_UI_BUTTON
+ offset_x = 169
+ offset_y = -12
+
+/obj/abstract/mind_ui_element/adminbus_release/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.release_passengers(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_send_home
+ name = "Send Passengers Back Home"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_home"
+ layer = MIND_UI_BUTTON
+ offset_x = 198
+ offset_y = -12
+
+/obj/abstract/mind_ui_element/adminbus_send_home/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.Send_Home(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_antag_madness
+ name = "Antag Madness!"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/top_panel/buttons.dmi'
+ icon_state = "icon_antag"
+ layer = MIND_UI_BUTTON
+ offset_x = 227
+ offset_y = -12
+
+/obj/abstract/mind_ui_element/adminbus_antag_madness/Click()
+ flick("[base_icon_state]-push", src)
+
+ alert(usr, "This button still hasn't been updated to use Role Datums. Sorry.")
+
+
+////////////////////////////////////////////////////////////////////
+// //
+// LEFT PANEL //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui/adminbus_left_panel
+ uniqueID = "Adminbus Left Panel"
+ x = "LEFT"
+ element_types_to_spawn = list(
+ /obj/abstract/mind_ui_element/adminbus_left_panel,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_delete_mobs,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spawn_clowns,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spawn_carps,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spawn_bears,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spawn_trees,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spawn_spiders,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spawn_alien,
+ )
+ display_with_parent = TRUE
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_left_panel
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/background.dmi'
+ icon_state = "panel"
+ layer = MIND_UI_BACK
+ offset_x = 0
+ offset_y = -82
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete_mobs
+ name = "Delete all mobs spawned by the Adminbus"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_delmobs"
+ layer = MIND_UI_BUTTON
+ offset_x = 6
+ offset_y = -82
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete_mobs/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.remove_mobs(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_clowns
+ name = "Spawn 5 Clowns"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_spclown"
+ layer = MIND_UI_BUTTON
+ offset_x = 8
+ offset_y = -50
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_clowns/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.spawn_mob(M, 1, 5)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_carps
+ name = "Spawn 5 Carps"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_spcarp"
+ layer = MIND_UI_BUTTON
+ offset_x = 8
+ offset_y = -22
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_carps/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.spawn_mob(M, 2, 5)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_bears
+ name = "Spawn 5 Bears"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_spbear"
+ layer = MIND_UI_BUTTON
+ offset_x = 8
+ offset_y = 6
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_bears/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.spawn_mob(M, 3, 5)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_trees
+ name = "Spawn 5 Trees"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_sptree"
+ layer = MIND_UI_BUTTON
+ offset_x = 8
+ offset_y = 34
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_trees/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.spawn_mob(M, 4, 5)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_spiders
+ name = "Spawn 5 Giant Spiders"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_spspider"
+ layer = MIND_UI_BUTTON
+ offset_x = 8
+ offset_y = 62
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_spiders/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.spawn_mob(M, 5, 5)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_alien
+ name = "Spawn a Large Alien Queen"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/left_panel/buttons.dmi'
+ icon_state = "icon_spalien"
+ layer = MIND_UI_BUTTON
+ offset_x = 7
+ offset_y = 90
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spawn_alien/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.spawn_mob(M, 6, 1)
+
+////////////////////////////////////////////////////////////////////
+// //
+// BOTTOM PANEL //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui/adminbus_bottom_panel
+ uniqueID = "Adminbus Bottom Panel"
+ y = "BOTTOM"
+ element_types_to_spawn = list(
+ /obj/abstract/mind_ui_element/adminbus_bottom_panel,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_money,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_spares,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_healing,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_repair,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_hook,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_jukebox,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_teleport,
+ /obj/abstract/mind_ui_element/adminbus_bumpers_low,
+ /obj/abstract/mind_ui_element/adminbus_bumpers_mid,
+ /obj/abstract/mind_ui_element/adminbus_bumpers_high,
+ /obj/abstract/mind_ui_element/adminbus_door_closed,
+ /obj/abstract/mind_ui_element/adminbus_door_open,
+ /obj/abstract/mind_ui_element/adminbus_roadlights_low,
+ /obj/abstract/mind_ui_element/adminbus_roadlights_mid,
+ /obj/abstract/mind_ui_element/adminbus_roadlights_high,
+ /obj/abstract/mind_ui_element/hoverable/adminbus_delete,
+ )
+ display_with_parent = TRUE
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_bottom_panel
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/background.dmi'
+ icon_state = "panel"
+ layer = MIND_UI_BACK
+ offset_x = -96
+ offset_y = 0
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_money
+ name = "Spawn Loads of Money"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_loadsmone"
+ layer = MIND_UI_BUTTON
+ offset_x = -96
+ offset_y = 69
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_money/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.loadsa_goodies(M, 2)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spares
+ name = "Spawn Loads of Captain Spare IDs"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_loadsids"
+ layer = MIND_UI_BUTTON
+ offset_x = -96
+ offset_y = 41
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_spares/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.loadsa_goodies(M, 1)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_healing
+ name = "Mass Rejuvination"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_massrejuv"
+ layer = MIND_UI_BUTTON
+ offset_x = -61
+ offset_y = 69
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_healing/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.mass_rejuvenate(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_repair
+ name = "Repair Surroundings"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_massrepair"
+ layer = MIND_UI_BUTTON
+ offset_x = -61
+ offset_y = 41
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_repair/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.Mass_Repair(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_hook
+ name = "Singularity Hook"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_hook"
+ layer = MIND_UI_BUTTON
+ offset_x = 64
+ offset_y = 71
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_hook/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if(!A.hook && !A.singulo)
+ icon_state = "icon_hook-push"
+ base_icon_state = "icon_hook-push"
+ else if (A.singulo)
+ icon_state = "icon_singulo"
+ base_icon_state = "icon_singulo"
+ else
+ icon_state = "icon_hook"
+ base_icon_state = "icon_hook"
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_hook/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.throw_hookshot(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_jukebox
+ name = "Adminbus-mounted Jukebox"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_jukebox"
+ layer = MIND_UI_BUTTON
+ offset_x = 107
+ offset_y = 71
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_jukebox/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.Mounted_Jukebox(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_teleport
+ name = "Teleportation"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_teleport"
+ layer = MIND_UI_BUTTON
+ offset_x = 150
+ offset_y = 71
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_teleport/Click()
+ flick("[base_icon_state]-push", src)
+
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.Teleportation(M)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_low
+ name = "Capture Mobs"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_bumpers_1-on"
+ layer = MIND_UI_BUTTON
+ offset_x = 53
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_low/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.bumpers == 1)
+ icon_state = "icon_bumpers_1-on"
+ base_icon_state = "icon_bumpers_1-on"
+ else
+ icon_state = "icon_bumpers_1-off"
+ base_icon_state = "icon_bumpers_1-off"
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_low/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_bumpers(M, 1)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_mid
+ name = "Hit Mobs"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_bumpers_2-off"
+ layer = MIND_UI_BUTTON
+ offset_x = 69
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_mid/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.bumpers == 2)
+ icon_state = "icon_bumpers_2-on"
+ base_icon_state = "icon_bumpers_2-on"
+ else
+ icon_state = "icon_bumpers_2-off"
+ base_icon_state = "icon_bumpers_2-off"
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_mid/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_bumpers(M, 2)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_high
+ name = "Gib Mobs"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_bumpers_3-off"
+ layer = MIND_UI_BUTTON
+ offset_x = 85
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_high/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.bumpers == 3)
+ icon_state = "icon_bumpers_3-on"
+ base_icon_state = "icon_bumpers_3-on"
+ else
+ icon_state = "icon_bumpers_3-off"
+ base_icon_state = "icon_bumpers_3-off"
+
+/obj/abstract/mind_ui_element/adminbus_bumpers_high/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_bumpers(M, 3)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_door_closed
+ name = "Close Door"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_door_0-on"
+ layer = MIND_UI_BUTTON
+ offset_x = 107
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_door_closed/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.door_mode == 0)
+ icon_state = "icon_door_0-on"
+ base_icon_state = "icon_door_0-on"
+ else
+ icon_state = "icon_door_0-off"
+ base_icon_state = "icon_door_0-off"
+
+/obj/abstract/mind_ui_element/adminbus_door_closed/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_door(M, 0)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_door_open
+ name = "Open Door"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_door_1-off"
+ layer = MIND_UI_BUTTON
+ offset_x = 123
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_door_open/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.door_mode == 1)
+ icon_state = "icon_door_1-on"
+ base_icon_state = "icon_door_1-on"
+ else
+ icon_state = "icon_door_1-off"
+ base_icon_state = "icon_door_1-off"
+
+/obj/abstract/mind_ui_element/adminbus_door_open/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_door(M, 1)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_low
+ name = "Turn Off Headlights"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_lights_0-on"
+ layer = MIND_UI_BUTTON
+ offset_x = 145
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_low/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.roadlights == 0)
+ icon_state = "icon_lights_0-on"
+ base_icon_state = "icon_lights_0-on"
+ else
+ icon_state = "icon_lights_0-off"
+ base_icon_state = "icon_lights_0-off"
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_low/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_lights(M, 0)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_mid
+ name = "Dipped Headlights"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_lights_1-off"
+ layer = MIND_UI_BUTTON
+ offset_x = 161
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_mid/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.roadlights == 1)
+ icon_state = "icon_lights_1-on"
+ base_icon_state = "icon_lights_1-on"
+ else
+ icon_state = "icon_lights_1-off"
+ base_icon_state = "icon_lights_1-off"
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_mid/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_lights(M, 1)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_high
+ name = "Main Headlights"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_lights_2-off"
+ layer = MIND_UI_BUTTON
+ offset_x = 177
+ offset_y = 46
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_high/UpdateIcon()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ if (A.roadlights == 2)
+ icon_state = "icon_lights_2-on"
+ base_icon_state = "icon_lights_2-on"
+ else
+ icon_state = "icon_lights_2-off"
+ base_icon_state = "icon_lights_2-off"
+
+/obj/abstract/mind_ui_element/adminbus_roadlights_high/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.toggle_lights(M, 2)
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete
+ name = "Delete Bus"
+ icon = 'monkestation/code/modules/bloody_cult/icons/adminbus/bottom_panel/buttons.dmi'
+ icon_state = "icon_delete"
+ layer = MIND_UI_BUTTON
+ offset_x = 127
+ offset_y = 6
+
+/obj/abstract/mind_ui_element/hoverable/adminbus_delete/Click()
+ var/mob/M = GetUser()
+ if (M && istype(M.buckled, /obj/vehicle/ridden/adminbus))
+ var/obj/vehicle/ridden/adminbus/A = M.buckled
+ A.Adminbus_Deletion(M)
+
+//------------------------------------------------------------
diff --git a/monkestation/code/modules/bloody_cult/mind_uis/blood_cult.dm b/monkestation/code/modules/bloody_cult/mind_uis/blood_cult.dm
new file mode 100644
index 000000000000..4e2e86266ca1
--- /dev/null
+++ b/monkestation/code/modules/bloody_cult/mind_uis/blood_cult.dm
@@ -0,0 +1,2242 @@
+GLOBAL_LIST_INIT(blood_communion, list())
+////////////////////////////////////////////////////////////////////
+// //
+// BLOODCULT - CULTIST //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui/bloodcult_cultist
+ uniqueID = "Cultist"
+ sub_uis_to_spawn = list(
+ /datum/mind_ui/bloodcult_cultist_panel,
+ /datum/mind_ui/bloodcult_left_panel,
+ /datum/mind_ui/bloodcult_right_panel,
+ )
+ display_with_parent = TRUE
+ y = "BOTTOM"
+
+/datum/mind_ui/bloodcult_cultist/Valid()
+ var/mob/M = mind.current
+ if (!M)
+ return FALSE
+ if(IS_CULTIST(M))
+ return TRUE
+ return FALSE
+
+
+////////////////////////////////////////////////////////////////////
+// //
+// BLOODCULT - RUNEDRAW //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui/bloodcult_cultist_panel
+ uniqueID = "Cultist Panel"
+ element_types_to_spawn = list(
+ /obj/abstract/mind_ui_element/hoverable/draw_runes_manual,
+ /obj/abstract/mind_ui_element/hoverable/draw_runes_guided,
+ /obj/abstract/mind_ui_element/hoverable/erase_runes,
+ /obj/abstract/mind_ui_element/hoverable/movable/cultist,
+ )
+ sub_uis_to_spawn = list(
+ /datum/mind_ui/bloodcult_runes,
+ )
+ display_with_parent = TRUE
+ offset_layer = MIND_UI_GROUP_C
+ y = "BOTTOM"
+
+/datum/mind_ui/bloodcult_cultist_panel/Valid()
+ var/mob/M = mind.current
+ if (!M)
+ return FALSE
+ if(IS_CULTIST(M) && iscarbon(M))
+ return TRUE
+ return FALSE
+
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/draw_runes_manual
+ name = "Trace Runes Manually"
+ desc = "(1 BLOOD PER WORD) Use available blood to write down words. Three words form a rune."
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/32x32.dmi'
+ icon_state = "rune_manual"
+ layer = MIND_UI_BUTTON
+ offset_x = 111
+ offset_y = 39
+ mouse_opacity = 1
+
+/obj/abstract/mind_ui_element/hoverable/draw_runes_manual/Click()
+ flick("rune_manual-click", src)
+ var/mob/M = GetUser()
+ if (M)
+ var/datum/antagonist/cult/cult_datum = M.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ cult_datum.verbose = TRUE
+ M.DisplayUI("Bloodcult Runes")
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/draw_runes_guided
+ name = "Trace Rune with a Guide"
+ desc = "(1 BLOOD PER WORD) Use available blood to write down words. Three words form a rune. Access a list of the well known runes."
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/32x32.dmi'
+ icon_state = "rune_guide"
+ layer = MIND_UI_BUTTON
+ offset_x = 111
+ offset_y = 39
+ mouse_opacity = 1
+
+
+/obj/abstract/mind_ui_element/hoverable/draw_runes_guided/Appear()
+ ..()
+ var/datum/team/cult/cult = locate_team(/datum/team/cult)
+ if (cult.stage == BLOODCULT_STAGE_DEFEATED)
+ icon_state = "rune_guide-broken"
+ hover_state = FALSE
+
+
+/obj/abstract/mind_ui_element/hoverable/draw_runes_guided/Click()
+ if (!hover_state)
+ return
+ flick("rune_guide-click", src)
+ var/mob/M = GetUser()
+ if (M)
+
+ var/list/available_runes = list()
+ var/i = 1
+ for(var/blood_spell in subtypesof(/datum/rune_spell))
+ var/datum/rune_spell/instance = blood_spell
+ if (initial(instance.secret))
+ continue
+ available_runes.Add("[initial(instance.name)] - \Roman[i]")
+ available_runes["[initial(instance.name)] - \Roman[i]"] = instance
+ i++
+ var/spell_name = input(M, "Remember how to trace a given rune.", "Trace Rune with a Guide", null) as null|anything in available_runes
+
+ if (spell_name)
+ for(var/datum/mind_ui/bloodcult_runes/BR in parent.subUIs)
+ BR.queued_rune = available_runes[spell_name]
+
+ var/datum/antagonist/cult/cult_datum = M.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ cult_datum.verbose = TRUE
+ M.DisplayUI("Bloodcult Runes")
+ break
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/erase_runes
+ name = "Erase Rune"
+ desc = "Remove the last word written of the rune you're standing above."
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/16x32.dmi'
+ icon_state = "rune_erase"
+ layer = MIND_UI_BUTTON
+ offset_x = 95
+ offset_y = 39
+ mouse_opacity = 1
+
+/obj/abstract/mind_ui_element/hoverable/erase_runes/Click()
+ flick("rune_erase-click", src)
+ var/mob/M = GetUser()
+ if (M)
+ var/datum/antagonist/cult/cult_datum = M.mind?.has_antag_datum(/datum/antagonist/cult)
+ if (cult_datum)
+ cult_datum.erase_rune()
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/movable/cultist
+ name = "Move Interface (Click and Drag)"
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/16x32.dmi'
+ icon_state = "rune_move"
+ layer = MIND_UI_BUTTON
+ offset_x = 143
+ offset_y = 39
+ mouse_opacity = 1
+
+ move_whole_ui = TRUE
+
+
+
+//////////////////////////////////////////////////Valid//////////////////
+// //
+// BLOODCULT - RIGHT PANEL //
+// //
+////////////////////////////////////////////////////////////////////
+
+/datum/mind_ui/bloodcult_right_panel
+ uniqueID = "Cultist Right Panel"
+ element_types_to_spawn = list(
+ /obj/abstract/mind_ui_element/bloodcult_spells_background,
+ /obj/abstract/mind_ui_element/bloodcult_spells_background_artificer,
+ /obj/abstract/mind_ui_element/hoverable/bloodcult_devotion_counter,
+ /obj/abstract/mind_ui_element/hoverable/bloodcult_devotion_counter/solo,
+ /obj/abstract/mind_ui_element/hoverable/bloodcult_spell/pool,
+ /obj/abstract/mind_ui_element/hoverable/bloodcult_spell/dagger,
+ /obj/abstract/mind_ui_element/hoverable/bloodcult_spell/talisman,
+ /obj/abstract/mind_ui_element/hoverable/bloodcult_spell/sigil,
+ /obj/abstract/mind_ui_element/hoverable/movable/cult_spells,
+ )
+
+ sub_uis_to_spawn = list(
+ /datum/mind_ui/hex_controller/first,
+ /datum/mind_ui/hex_controller/second,
+ )
+
+
+ display_with_parent = TRUE
+ offset_layer = MIND_UI_GROUP_C
+
+/datum/mind_ui/bloodcult_right_panel/Valid()
+ var/mob/M = mind.current
+ if (!M)
+ return FALSE
+ if(IS_CULTIST(M))
+ return TRUE
+ return FALSE
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/bloodcult_spells_background
+ name = "Cult Powers"
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/32x121.dmi'
+ icon_state = "powers_bg"
+ offset_x = 192
+ offset_y = -96
+ layer = MIND_UI_BACK
+
+/obj/abstract/mind_ui_element/bloodcult_spells_background/CanAppear()
+ var/mob/living/M = GetUser()
+ return iscarbon(M)
+
+/obj/abstract/mind_ui_element/bloodcult_spells_background/Appear()
+ if(!CanAppear())
+ invisibility = 101
+ return
+ ..()
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/bloodcult_spells_background_artificer
+ name = "Hex Control Panel"
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/32x121.dmi'
+ icon_state = "artificer_bg"
+ offset_x = 192
+ offset_y = -96
+ layer = MIND_UI_BACK
+
+/obj/abstract/mind_ui_element/bloodcult_spells_background_artificer/CanAppear()
+ var/mob/living/M = GetUser()
+ return istype(M, /mob/living/basic/construct/artificer/perfect)
+
+/obj/abstract/mind_ui_element/bloodcult_spells_background_artificer/Appear()
+ if(!CanAppear())
+ invisibility = 101
+ return
+ ..()
+
+//------------------------------------------------------------
+
+/obj/abstract/mind_ui_element/hoverable/bloodcult_devotion_counter
+ name = "Devotion"
+ icon = 'monkestation/code/modules/bloody_cult/icons/bloodcult/32x32.dmi'
+ icon_state = "devotion_counter"
+ offset_x = 192
+ offset_y = -110
+ layer = MIND_UI_BACK+0.5
+
+ hover_state = FALSE
+ element_flags = MINDUI_FLAG_TOOLTIP|MINDUI_FLAG_PROCESSING
+ tooltip_title = "Devotion"
+ tooltip_content = "Performing cult activities generates devotion, which hastens the coming of the Eclipse and rewards you with new powers.
Cult activities range from using most runes, to harming living beings with cult weapons.