diff --git a/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
index 1fad6ae212c..bcfea6d4063 100644
--- a/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
+++ b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
@@ -1402,6 +1402,14 @@
},
/turf/simulated/mineral/volcanic/lava_land_surface,
/area/lavaland/surface/outdoors)
+"lh" = (
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/decal/cleanable/ashrune,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
"zz" = (
/obj/effect/mapping_helpers/no_lava,
/obj/structure/stone_tile{
@@ -1606,7 +1614,7 @@ ak
ak
cg
cb
-cg
+lh
cn
bL
dr
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 234865da160..6eae818ec05 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -210,6 +210,8 @@
#define isskeleton(A) (is_species(A, /datum/species/skeleton))
#define ishumanbasic(A) (is_species(A, /datum/species/human))
#define isunathi(A) (is_species(A, /datum/species/unathi))
+#define isashwalker(A) (is_species(A, /datum/species/unathi/ashwalker))
+#define isashwalkershaman(A) (is_species(A, /datum/species/unathi/ashwalker/shaman))
#define istajaran(A) (is_species(A, /datum/species/tajaran))
#define isvulpkanin(A) (is_species(A, /datum/species/vulpkanin))
#define isskrell(A) (is_species(A, /datum/species/skrell))
diff --git a/code/__DEFINES/rituals.dm b/code/__DEFINES/rituals.dm
new file mode 100644
index 00000000000..c33d531225c
--- /dev/null
+++ b/code/__DEFINES/rituals.dm
@@ -0,0 +1,18 @@
+/// Used in ritual variables
+#define DEFAULT_RITUAL_RANGE_FIND 1
+#define DEFAULT_RITUAL_COOLDOWN (100 SECONDS)
+#define DEFAULT_RITUAL_DISASTER_PROB 10
+#define DEFAULT_RITUAL_FAIL_PROB 10
+/// Ritual object bitflags
+#define RITUAL_STARTED (1<<0)
+#define RITUAL_ENDED (1<<1)
+#define RITUAL_FAILED (1<<2)
+/// Ritual datum bitflags
+#define RITUAL_SUCCESSFUL (1<<0)
+/// Invocation checks, should not be used in extra checks.
+#define RITUAL_FAILED_INVALID_SPECIES (1<<1)
+#define RITUAL_FAILED_EXTRA_INVOKERS (1<<2)
+#define RITUAL_FAILED_MISSED_REQUIREMENTS (1<<3)
+#define RITUAL_FAILED_ON_PROCEED (1<<4)
+#define RITUAL_FAILED_INVALID_SPECIAL_ROLE (1<<5)
+
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 29e33ed9265..b48227307a6 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -94,6 +94,8 @@
#define STATUS_EFFECT_CRUSHERMARK /datum/status_effect/crusher_mark //if struck with a proto-kinetic crusher, takes a ton of damage
+#define STATUS_EFFECT_FANG_EXHAUSTION /datum/status_effect/fang_exhaust // called by poison fang (crusher trophy)
+
#define STATUS_EFFECT_SAWBLEED /datum/status_effect/saw_bleed //if the bleed builds up enough, takes a ton of damage
#define STATUS_EFFECT_BLOODLETTING /datum/status_effect/saw_bleed/bloodletting //nerfed version
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index f8907ab3667..539019a41c8 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -313,6 +313,7 @@
prize_list["Miscellaneous"] = list(
EQUIPMENT("Absinthe", /obj/item/reagent_containers/food/drinks/bottle/absinthe/premium, 500),
EQUIPMENT("Alien Toy", /obj/item/clothing/mask/facehugger/toy, 300),
+ EQUIPMENT("Richard & Co cigarettes", /obj/item/storage/fancy/cigarettes/cigpack_richard, 400),
EQUIPMENT("Cigar", /obj/item/clothing/mask/cigarette/cigar/havana, 300),
EQUIPMENT("GAR Meson Scanners", /obj/item/clothing/glasses/meson/gar, 800),
EQUIPMENT("GPS upgrade", /obj/item/gpsupgrade, 1500),
@@ -342,6 +343,7 @@
EQUIPMENT("Absinthe", /obj/item/reagent_containers/food/drinks/bottle/absinthe/premium, 250),
EQUIPMENT("Cigarettes", /obj/item/storage/fancy/cigarettes, 100),
EQUIPMENT("Medical Marijuana", /obj/item/storage/fancy/cigarettes/cigpack_med, 250),
+ EQUIPMENT("Richard & Co cigarettes", /obj/item/storage/fancy/cigarettes/cigpack_richard, 400),
EQUIPMENT("Cigar", /obj/item/clothing/mask/cigarette/cigar/havana, 150),
EQUIPMENT("Box of matches", /obj/item/storage/box/matches, 50),
EQUIPMENT("Cheeseburger", /obj/item/reagent_containers/food/snacks/cheeseburger, 150),
diff --git a/code/datums/components/ritual_object.dm b/code/datums/components/ritual_object.dm
new file mode 100644
index 00000000000..31064e438db
--- /dev/null
+++ b/code/datums/components/ritual_object.dm
@@ -0,0 +1,119 @@
+/datum/component/ritual_object
+ /// Pre-defined rituals list
+ var/list/rituals = list()
+ /// We define rituals from this.
+ var/list/allowed_categories
+ /// Required species to activate ritual object
+ var/list/allowed_species
+ /// Required special role to activate ritual object
+ var/list/allowed_special_role
+ /// Prevents from multiple uses
+ var/active_ui = FALSE
+
+/datum/component/ritual_object/Initialize(
+ allowed_categories = /datum/ritual,
+ list/allowed_species,
+ list/allowed_special_role
+)
+
+ if(!isobj(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.allowed_categories = allowed_categories
+ src.allowed_species = allowed_species
+ src.allowed_special_role = allowed_special_role
+ get_rituals()
+
+/datum/component/ritual_object/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(attackby))
+
+/datum/component/ritual_object/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND)
+
+/datum/component/ritual_object/proc/get_rituals() // We'll get all rituals for flexibility.
+ LAZYCLEARLIST(rituals)
+
+ for(var/datum/ritual/ritual as anything in typecacheof(allowed_categories))
+ if(!ritual.name)
+ continue
+
+ rituals += new ritual
+
+ for(var/datum/ritual/ritual as anything in rituals)
+ ritual.ritual_object = parent
+
+ return
+
+/datum/component/ritual_object/Destroy(force)
+ LAZYNULL(rituals)
+ return ..()
+
+/datum/component/ritual_object/proc/attackby(datum/source, mob/user)
+ SIGNAL_HANDLER
+
+ if(active_ui)
+ return
+
+ if(!ishuman(user))
+ return
+
+ var/mob/living/carbon/human/human = user
+
+ if(allowed_species && !is_type_in_list(human.dna.species, allowed_species))
+ return
+
+ if(allowed_special_role && !is_type_in_list(human.mind?.special_role, allowed_special_role))
+ return
+
+ active_ui = TRUE
+ INVOKE_ASYNC(src, PROC_REF(open_ritual_ui), human)
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/datum/component/ritual_object/proc/open_ritual_ui(mob/living/carbon/human/human)
+ var/list/rituals_list = get_available_rituals(human)
+
+ if(!LAZYLEN(rituals_list))
+ active_ui = FALSE
+ to_chat(human, "Не имеется доступных для выполнения ритуалов.")
+ return
+
+ var/choosen_ritual = tgui_input_list(human, "Выберите ритуал", "Ритуалы", rituals_list)
+
+ if(!choosen_ritual)
+ active_ui = FALSE
+ return
+
+ var/ritual_status
+
+ for(var/datum/ritual/ritual as anything in rituals)
+ if(choosen_ritual != ritual.name)
+ continue
+
+ ritual_status = ritual.pre_ritual_check(human)
+ break
+
+ if(ritual_status)
+ active_ui = FALSE
+
+ return FALSE
+
+/datum/component/ritual_object/proc/get_available_rituals(mob/living/carbon/human/human)
+ var/list/rituals_list = list()
+
+ for(var/datum/ritual/ritual as anything in rituals)
+ if(ritual.charges == 0)
+ continue
+
+ if(!COOLDOWN_FINISHED(ritual, ritual_cooldown))
+ continue
+
+ if(ritual.allowed_species && !is_type_in_list(human.dna.species, ritual.allowed_species))
+ continue
+
+ if(ritual.allowed_special_role && !is_type_in_list(human.mind?.special_role, ritual.allowed_special_role))
+ continue
+
+ LAZYADD(rituals_list, ritual.name)
+
+ return rituals_list
diff --git a/code/datums/rituals.dm b/code/datums/rituals.dm
new file mode 100644
index 00000000000..4c3049d00fe
--- /dev/null
+++ b/code/datums/rituals.dm
@@ -0,0 +1,1255 @@
+/datum/ritual
+ /// Linked object
+ var/obj/ritual_object
+ /// Name of our ritual
+ var/name
+ /// If ritual requires more than one invoker
+ var/extra_invokers = 0
+ /// If invoker species isn't in allowed - he won't do ritual.
+ var/list/allowed_species
+ /// If invoker special role isn't in allowed - he won't do ritual.
+ var/list/allowed_special_role
+ /// Required to ritual invoke things are located here
+ var/list/required_things
+ /// If true - only whitelisted species will be added as invokers
+ var/require_allowed_species = TRUE
+ /// Same as require_allowed_species, but requires special role to be counted as invoker.
+ var/require_allowed_special_role = FALSE
+ /// We search for humans in that radius
+ var/finding_range = DEFAULT_RITUAL_RANGE_FIND
+ /// Amount of maximum ritual uses.
+ var/charges = -1
+ /// Cooldown for one ritual
+ COOLDOWN_DECLARE(ritual_cooldown)
+ /// Our cooldown after we casted ritual.
+ var/cooldown_after_cast = DEFAULT_RITUAL_COOLDOWN
+ /// If our ritual failed on proceed - we'll try to cause disaster.
+ var/disaster_prob = DEFAULT_RITUAL_DISASTER_PROB
+ /// A chance of failing our ritual.
+ var/fail_chance = DEFAULT_RITUAL_FAIL_PROB
+ /// After successful ritual we'll destroy used things.
+ var/ritual_should_del_things = TRUE
+ /// After failed ritual proceed - we'll delete items.
+ var/ritual_should_del_things_on_fail = FALSE
+ /// Temporary list of objects, which we will delete. Or use in transformations! Then clear list.
+ var/list/used_things = list()
+ /// Temporary list of invokers.
+ var/list/invokers = list()
+ /// If defined - do_after will be added to your ritual
+ var/cast_time
+
+/datum/ritual/Destroy(force)
+ ritual_object = null
+ LAZYNULL(used_things)
+ LAZYNULL(required_things)
+ LAZYNULL(invokers)
+ return ..()
+
+/datum/ritual/proc/pre_ritual_check(mob/living/carbon/human/invoker)
+ var/failed = FALSE
+ var/cause_disaster = FALSE
+
+ var/del_things = FALSE
+ var/start_cooldown = FALSE
+
+ handle_ritual_object(RITUAL_STARTED)
+
+ . = ritual_invoke_check(invoker)
+ switch(.)
+ if(RITUAL_SUCCESSFUL)
+ start_cooldown = TRUE
+ addtimer(CALLBACK(src, PROC_REF(handle_ritual_object), RITUAL_ENDED), 1 SECONDS)
+ charges--
+ if(RITUAL_FAILED_INVALID_SPECIES)
+ failed = TRUE
+ if(RITUAL_FAILED_EXTRA_INVOKERS)
+ failed = TRUE
+ if(RITUAL_FAILED_MISSED_REQUIREMENTS)
+ failed = TRUE
+ if(RITUAL_FAILED_INVALID_SPECIAL_ROLE)
+ failed = TRUE
+ if(RITUAL_FAILED_ON_PROCEED)
+ failed = TRUE
+ cause_disaster = TRUE
+ start_cooldown = TRUE
+ if(NONE)
+ failed = TRUE
+
+ if(start_cooldown)
+ COOLDOWN_START(src, ritual_cooldown, cooldown_after_cast)
+
+ if(cause_disaster && prob(disaster_prob))
+ disaster(invoker)
+
+ if((. & RITUAL_SUCCESSFUL) && (ritual_should_del_things))
+ del_things = TRUE
+
+ if((. & RITUAL_FAILED_ON_PROCEED) && (ritual_should_del_things_on_fail))
+ del_things = TRUE
+
+ if(del_things)
+ del_things()
+
+ if(failed)
+ addtimer(CALLBACK(src, PROC_REF(handle_ritual_object), RITUAL_FAILED), 2 SECONDS)
+
+ /// We use pre-defines
+ LAZYCLEARLIST(invokers)
+ LAZYCLEARLIST(used_things)
+
+ return .
+
+/datum/ritual/proc/handle_ritual_object(bitflags, silent = FALSE)
+ switch(bitflags)
+ if(RITUAL_STARTED)
+ . = RITUAL_STARTED
+ if(!silent)
+ playsound(ritual_object.loc, 'sound/effects/ghost2.ogg', 50, TRUE)
+ if(RITUAL_ENDED)
+ . = RITUAL_ENDED
+ if(!silent)
+ playsound(ritual_object.loc, 'sound/effects/phasein.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ . = RITUAL_FAILED
+ if(!silent)
+ playsound(ritual_object.loc, 'sound/effects/empulse.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/proc/del_things() // This is a neutral variant with item delete. Override it to change.
+ for(var/obj/item/thing in used_things)
+ qdel(thing)
+
+ return
+
+/datum/ritual/proc/ritual_invoke_check(mob/living/carbon/human/invoker)
+ if(!COOLDOWN_FINISHED(src, ritual_cooldown))
+ return NONE
+
+ if(charges == 0)
+ return NONE
+
+ if(allowed_special_role && !is_type_in_list(invoker.mind?.special_role, allowed_special_role))
+ return RITUAL_FAILED_INVALID_SPECIAL_ROLE
+
+ if(allowed_species && !is_type_in_list(invoker.dna.species, allowed_species)) // double check to avoid funny situations
+ return RITUAL_FAILED_INVALID_SPECIES
+
+ if(!check_invokers(invoker))
+ return RITUAL_FAILED_EXTRA_INVOKERS
+
+ if(required_things && !check_contents(invoker))
+ return RITUAL_FAILED_MISSED_REQUIREMENTS
+
+ if(prob(fail_chance))
+ return RITUAL_FAILED_ON_PROCEED
+
+ if(cast_time && !cast(invoker))
+ return RITUAL_FAILED_ON_PROCEED
+
+ return do_ritual(invoker)
+
+/datum/ritual/proc/cast(mob/living/carbon/human/invoker)
+ . = TRUE
+ LAZYADD(invokers, invoker)
+
+ for(var/mob/living/carbon/human/human as anything in invokers)
+ if(!do_after(human, cast_time, ritual_object, extra_checks = CALLBACK(src, PROC_REF(action_check_contents))))
+ . = FALSE
+
+ LAZYREMOVE(invokers, invoker)
+
+ return .
+
+/datum/ritual/proc/check_invokers(mob/living/carbon/human/invoker)
+ if(!extra_invokers)
+ return TRUE
+
+ for(var/mob/living/carbon/human/human in range(finding_range, ritual_object))
+ if(human == invoker)
+ continue
+
+ if(require_allowed_species && !is_type_in_list(human.dna.species, allowed_species))
+ continue
+
+ if(require_allowed_special_role && !is_type_in_list(human.mind?.special_role, allowed_special_role))
+ continue
+
+ LAZYADD(invokers, human)
+
+ if(LAZYLEN(invokers) >= extra_invokers)
+ break
+
+ if(LAZYLEN(invokers) < extra_invokers)
+ ritual_object.balloon_alert(invoker, "требуется больше участников!")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/proc/check_contents(mob/living/carbon/human/invoker)
+ var/list/atom/movable/atoms = list()
+
+ for(var/atom/obj as anything in range(finding_range, ritual_object))
+ if(isitem(obj))
+ var/obj/item/close_item = obj
+ if(close_item.item_flags & ABSTRACT)
+ continue
+
+ if(obj.invisibility)
+ continue
+
+ if(obj == invoker)
+ continue
+
+ if(obj == ritual_object)
+ continue
+
+ if(locate(obj) in invokers)
+ continue
+
+ LAZYADD(atoms, obj)
+
+ var/list/requirements = required_things.Copy()
+ for(var/atom/atom as anything in atoms)
+ for(var/req_type in requirements)
+ if(requirements[req_type] <= 0)
+ continue
+
+ if(!istype(atom, req_type))
+ continue
+
+ LAZYADD(used_things, atom)
+
+ if(isstack(atom))
+ var/obj/item/stack/picked_stack = atom
+ LAZYREMOVE(requirements[req_type], picked_stack.amount)
+ else
+ requirements[req_type]--
+
+ var/list/what_are_we_missing = list()
+ for(var/req_type in requirements)
+ var/number_of_things = requirements[req_type]
+
+ if(number_of_things <= 0)
+ continue
+
+ LAZYADD(what_are_we_missing, req_type)
+
+ if(LAZYLEN(what_are_we_missing))
+ ritual_object.balloon_alert(invoker, "требуется больше компонентов!")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/proc/action_check_contents()
+ for(var/atom/atom as anything in used_things)
+ if(QDELETED(atom))
+ return FALSE
+
+ if(!(atom in range(finding_range, ritual_object)))
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/proc/do_ritual(mob/living/carbon/human/invoker) // Do ritual stuff.
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/proc/disaster(mob/living/carbon/human/invoker)
+ return
+
+/datum/ritual/ashwalker
+ /// If ritual requires extra shaman invokers
+ var/extra_shaman_invokers = 0
+ /// If ritual can be invoked only by shaman
+ var/shaman_only = FALSE
+ allowed_species = list(/datum/species/unathi/ashwalker, /datum/species/unathi/draconid)
+
+/datum/ritual/ashwalker/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(shaman_only && !isashwalkershaman(invoker))
+ to_chat(invoker, span_warning("Только шаман может выполнить данный ритуал!"))
+ return FALSE
+
+ var/list/shaman_invokers = list()
+
+ if(extra_shaman_invokers)
+ for(var/mob/living/carbon/human/human as anything in invokers)
+ if(human == invoker)
+ continue
+
+ if(isashwalkershaman(human))
+ LAZYADD(shaman_invokers, human)
+
+ if(LAZYLEN(shaman_invokers) >= extra_shaman_invokers)
+ break
+
+ if(LAZYLEN(shaman_invokers) < extra_shaman_invokers)
+ ritual_object.balloon_alert(invoker, "требуется больше шаманов!")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/summon_ashstorm
+ name = "Ash storm summon"
+ shaman_only = TRUE
+ disaster_prob = 20
+ charges = 2
+ cooldown_after_cast = 1200 SECONDS
+ cast_time = 100 SECONDS
+ fail_chance = 20
+ extra_invokers = 2
+ required_things = list(
+ /mob/living/simple_animal/hostile/asteroid/goldgrub = 1
+ )
+
+/datum/ritual/ashwalker/summon_ashstorm/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/living in used_things)
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/summon_ashstorm/del_things()
+ . = ..()
+
+ for(var/mob/living/living in used_things)
+ living.gib()
+
+ return
+
+/datum/ritual/ashwalker/summon_ashstorm/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(!invoker.fire_stacks)
+ to_chat(invoker, "Инициатор ритуала должнен быть в воспламеняемой субстанции.")
+ return FALSE
+
+ for(var/mob/living/carbon/human/human as anything in invokers)
+ if(!human.fire_stacks)
+ to_chat(invoker, "Участники ритуала должны быть в воспламеняемой субстанции.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/summon_ashstorm/do_ritual(mob/living/carbon/human/invoker)
+ SSweather.run_weather(/datum/weather/ash_storm)
+ message_admins("[key_name(invoker)] accomplished ashstorm ritual and summoned ashstorm")
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/summon_ashstorm/disaster(mob/living/carbon/human/invoker)
+ var/list/targets = list()
+
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(isashwalker(human))
+ LAZYADD(targets, human)
+
+ if(!LAZYLEN(targets))
+ return
+
+ var/mob/living/carbon/human/human = pick(targets)
+ var/datum/disease/virus/cadaver/cadaver = new
+ cadaver.Contract(human)
+
+ return
+
+/datum/ritual/ashwalker/summon_ashstorm/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/fleshtostone.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/invoke_general.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/castsummon.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/transformation
+ name = "Transformation ritual"
+ disaster_prob = 30
+ fail_chance = 50
+ extra_invokers = 1
+ cooldown_after_cast = 480 SECONDS
+ cast_time = 70 SECONDS
+ ritual_should_del_things_on_fail = TRUE
+ required_things = list(
+ /obj/item/twohanded/spear = 3,
+ /obj/item/organ/internal/regenerative_core = 1,
+ /mob/living/carbon/human = 1
+ )
+
+/datum/ritual/ashwalker/transformation/do_ritual(mob/living/carbon/human/invoker)
+ var/mob/living/carbon/human/human = locate() in used_things
+
+ if(!human || !human.mind || !human.ckey)
+ return RITUAL_FAILED_ON_PROCEED // Your punishment
+
+ human.set_species(/datum/species/unathi/ashwalker)
+ human.mind.store_memory("Теперь вы пеплоходец, вы часть племени! Вы довольно смутно помните о прошлой жизни, и вы не помните, как пользоваться технологиями!")
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/transformation/disaster(mob/living/carbon/human/invoker)
+ invoker.adjustBrainLoss(15)
+ invoker.SetKnockdown(5 SECONDS)
+
+ var/mob/living/carbon/human/human = locate() in used_things
+
+ if(QDELETED(human))
+ return
+
+ var/list/destinations = list()
+
+ for(var/obj/item/radio/beacon/beacon in GLOB.global_radios)
+ LAZYADD(destinations, get_turf(beacon))
+
+ human.forceMove(safepick(destinations))
+ playsound(get_turf(human), 'sound/magic/invoke_general.ogg', 50, TRUE)
+
+ return
+
+/datum/ritual/ashwalker/transformation/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ if(. == RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/effects/clone_jutsu.ogg', 50, TRUE)
+ return
+
+ . = ..(bitflags)
+ return .
+
+/datum/ritual/ashwalker/summon
+ name = "Summoning ritual"
+ disaster_prob = 30
+ fail_chance = 30
+ shaman_only = TRUE
+ cooldown_after_cast = 900 SECONDS
+ cast_time = 50 SECONDS
+ extra_invokers = 1
+
+/datum/ritual/ashwalker/summon/do_ritual(mob/living/carbon/human/invoker)
+ var/list/ready_for_summoning = list()
+
+ for(var/mob/living/carbon/human/human in GLOB.mob_list)
+ if(isashwalker(human))
+ LAZYADD(ready_for_summoning, human)
+
+ if(!LAZYLEN(ready_for_summoning))
+ return RITUAL_FAILED_ON_PROCEED
+
+ var/mob/living/carbon/human/human = tgui_input_list(invoker, "Who will be summoned?", "Summon ritual", ready_for_summoning)
+
+ if(!human)
+ return RITUAL_FAILED_ON_PROCEED
+
+ LAZYADD(invokers, invoker)
+
+ for(var/mob/living/carbon/human/summoner as anything in invokers)
+ summoner.blood_volume -= (summoner.blood_volume * 0.20)
+ summoner.apply_damage(25, def_zone = pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+
+ human.forceMove(ritual_object)
+ human.vomit()
+ human.Weaken(10 SECONDS)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/summon/disaster(mob/living/carbon/human/invoker)
+ if(!prob(70))
+ return
+
+ var/obj/item/organ/external/limb = invoker.get_organ(pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG))
+ limb?.droplimb()
+
+ return
+
+/datum/ritual/ashwalker/summon/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/weapons/zapbang.ogg', 50, TRUE)
+ var/datum/effect_system/smoke_spread/smoke = new
+ smoke.set_up(5, FALSE, ritual_object.loc)
+ smoke.start()
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/forcewall.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/invoke_general.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/curse
+ name = "Curse ritual"
+ disaster_prob = 30
+ fail_chance = 30
+ cooldown_after_cast = 600 SECONDS
+ cast_time = 60 SECONDS
+ charges = 3
+ shaman_only = TRUE
+ extra_invokers = 2
+ required_things = list(
+ /mob/living/carbon/human = 3
+ )
+
+/datum/ritual/ashwalker/curse/del_things()
+ for(var/mob/living/carbon/human/human as anything in used_things)
+ human.gib()
+
+ return
+
+/datum/ritual/ashwalker/curse/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/carbon/human/human as anything in used_things)
+ if(human.stat != DEAD)
+ to_chat(invoker, "Гуманоиды должны быть мертвы.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/curse/do_ritual(mob/living/carbon/human/invoker)
+ var/list/humans = list()
+
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human))
+ LAZYADD(humans, human)
+
+ if(!LAZYLEN(humans))
+ return RITUAL_FAILED_ON_PROCEED
+
+ var/mob/living/carbon/human/human = pick(humans)
+ var/datum/disease/vampire/disease = new
+
+ if(!disease.Contract(human))
+ return RITUAL_FAILED_ON_PROCEED
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/curse/disaster(mob/living/carbon/human/invoker)
+ var/list/targets = list()
+
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(isashwalker(human))
+ LAZYADD(targets, human)
+
+ if(!LAZYLEN(targets))
+ return
+
+ var/mob/living/carbon/human/human = pick(targets)
+ human.monkeyize()
+
+ return
+
+/datum/ritual/ashwalker/power
+ name = "Power ritual"
+ disaster_prob = 40
+ fail_chance = 40
+ charges = 1
+ cooldown_after_cast = 800 SECONDS
+ cast_time = 80 SECONDS
+ shaman_only = TRUE
+ extra_invokers = 4
+ required_things = list(
+ /mob/living/simple_animal/hostile/asteroid/goliath = 3,
+ /obj/item/organ/internal/regenerative_core = 3
+ )
+
+/datum/ritual/ashwalker/power/del_things()
+ for(var/mob/living/living in used_things)
+ living.gib()
+
+ return
+
+/datum/ritual/ashwalker/power/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/living in used_things)
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/power/do_ritual(mob/living/carbon/human/invoker)
+ LAZYADD(invokers, invoker)
+
+ for(var/mob/living/carbon/human/human as anything in invokers)
+ if(LAZYIN(human.dna?.default_blocks, GLOB.weakblock))
+ human.force_gene_block(GLOB.weakblock)
+
+ human.force_gene_block(GLOB.strongblock, TRUE)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/power/disaster(mob/living/carbon/human/invoker)
+ var/list/targets = list()
+
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(isashwalker(human))
+ LAZYADD(targets, human)
+
+ if(!LAZYLEN(targets))
+ return
+
+ invoker.force_gene_block(pick(GLOB.bad_blocks), TRUE)
+ for(var/mob/living/carbon/human/human as anything in invokers)
+ human.force_gene_block(pick(GLOB.bad_blocks), TRUE)
+
+ var/mob/living/carbon/human/human = pick(targets)
+ human.force_gene_block(pick(GLOB.bad_blocks), TRUE)
+
+ return
+
+/datum/ritual/ashwalker/power/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/castsummon.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/smoke.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/strings.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/resurrection
+ name = "Resurrection ritual"
+ charges = 3
+ extra_invokers = 2
+ cooldown_after_cast = 180 SECONDS
+ cast_time = 100 SECONDS
+ shaman_only = TRUE
+ disaster_prob = 25
+ fail_chance = 35
+ required_things = list(
+ /obj/item/organ/internal/regenerative_core = 2,
+ /mob/living/carbon/human = 1,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/fireblossom = 4,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/cactus_fruit = 1
+ )
+
+/datum/ritual/ashwalker/resurrection/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/living in used_things)
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы.")
+ return FALSE
+
+ var/mob/living/carbon/human/human = locate() in used_things
+
+ if(!human.mind || !human.ckey)
+ return FALSE
+
+ if(!isashwalker(human))
+ fail_chance = 15
+
+ return TRUE
+
+/datum/ritual/ashwalker/resurrection/do_ritual(mob/living/carbon/human/invoker)
+ var/mob/living/carbon/human/human = locate() in used_things
+ human.revive()
+ human.adjustBrainLoss(20)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/resurrection/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in range(10, ritual_object))
+ if(!isashwalker(human) || human.stat == DEAD)
+ continue
+
+ human.adjustBrainLoss(15)
+
+ return
+
+/datum/ritual/ashwalker/resurrection/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/clockwork/reconstruct.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/disable_tech.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/invoke_general.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/recharge
+ name = "Recharge rituals"
+ extra_invokers = 3
+ disaster_prob = 30
+ fail_chance = 50
+ cooldown_after_cast = 360 SECONDS
+ cast_time = 90 SECONDS
+ shaman_only = TRUE
+ required_things = list(
+ /mob/living/simple_animal/hostile/asteroid/basilisk/watcher = 1,
+ /mob/living/simple_animal/hostile/asteroid/goliath = 1,
+ /obj/item/organ/internal/regenerative_core = 1,
+ /mob/living/simple_animal/hostile/asteroid/goldgrub = 1
+ )
+ var/list/blacklisted_rituals = list(/datum/ritual/ashwalker/power)
+
+/datum/ritual/ashwalker/recharge/del_things()
+ . = ..()
+
+ for(var/mob/living/living in used_things)
+ living.gib()
+
+ return
+
+/datum/ritual/ashwalker/recharge/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/living in used_things)
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/recharge/do_ritual(mob/living/carbon/human/invoker)
+ var/datum/component/ritual_object/component = ritual_object.GetComponent(/datum/component/ritual_object)
+
+ if(!component)
+ return RITUAL_FAILED_ON_PROCEED
+
+ for(var/datum/ritual/ritual as anything in component.rituals)
+ if(is_type_in_list(ritual, blacklisted_rituals))
+ continue
+
+ if(ritual.charges < 0)
+ continue
+
+ ritual.charges++
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/recharge/disaster(mob/living/carbon/human/invoker)
+ var/list/targets = list()
+
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(isashwalker(human))
+ LAZYADD(targets, human)
+
+ if(!LAZYLEN(targets))
+ return
+
+ var/mob/living/carbon/human/human = pick(targets)
+ new /obj/item/organ/internal/legion_tumour(human)
+
+ return
+
+/datum/ritual/ashwalker/recharge/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/castsummon.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/cult_spell.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/invoke_general.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/population
+ name = "Population ritual"
+ extra_invokers = 2
+ charges = 1
+ cooldown_after_cast = 120 SECONDS
+ cast_time = 40 SECONDS
+ ritual_should_del_things_on_fail = TRUE
+ required_things = list(
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/cactus_fruit = 1,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/fireblossom = 1,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/mushroom_stem = 1,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/mushroom_leaf = 1,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/mushroom_cap = 1,
+ /obj/item/reagent_containers/food/snacks/grown/ash_flora/shavings = 1
+ )
+
+/datum/ritual/ashwalker/population/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(!isashwalkershaman(invoker))
+ disaster_prob = 40
+ fail_chance = 40
+
+ return TRUE
+
+/datum/ritual/ashwalker/population/del_things()
+ for(var/mob/living/living as anything in used_things)
+ living.gib()
+
+ return
+
+/datum/ritual/ashwalker/population/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/living as anything in used_things)
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/population/do_ritual(mob/living/carbon/human/invoker)
+ new /obj/effect/mob_spawn/human/ash_walker/shaman(ritual_object.loc)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/population/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human) || !prob(disaster_prob))
+ continue
+
+ if(!isturf(human.loc))
+ continue
+
+ var/datum/effect_system/smoke_spread/smoke = new
+ smoke.set_up(5, FALSE, get_turf(human.loc))
+ smoke.start()
+
+ for(var/obj/item/obj as anything in human.get_equipped_items(TRUE, TRUE))
+ human.drop_item_ground(obj)
+
+ return
+
+/datum/ritual/ashwalker/population/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/demon_consume.ogg', 50, TRUE)
+ var/datum/effect_system/smoke_spread/smoke = new
+ smoke.set_up(5, FALSE, get_turf(ritual_object.loc))
+ smoke.start()
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/cult_spell.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/teleport_diss.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/soul
+ name = "Soul ritual"
+ extra_invokers = 3
+ cooldown_after_cast = 1200 SECONDS
+ cast_time = 60 SECONDS
+ required_things = list(
+ /mob/living/carbon/human = 3,
+ /obj/item/stack/sheet/animalhide/ashdrake = 1
+ )
+
+/datum/ritual/ashwalker/soul/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(!isashwalkershaman(invoker))
+ disaster_prob = 40
+ fail_chance = 70
+
+ return TRUE
+
+/datum/ritual/ashwalker/population/del_things()
+ var/obj/item/stack/sheet/animalhide/ashdrake/stack = locate() in used_things
+ stack.use(1)
+
+ for(var/mob/living/living in used_things)
+ living.gib()
+
+ return
+
+/datum/ritual/ashwalker/soul/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/living in used_things)
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/soul/do_ritual(mob/living/carbon/human/invoker)
+ var/datum/effect_system/smoke_spread/smoke = new
+ smoke.set_up(5, FALSE, get_turf(invoker.loc))
+ smoke.start()
+ invoker.set_species(/datum/species/unathi/draconid)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/soul/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human) || !prob(disaster_prob))
+ continue
+
+ if(!isturf(human.loc))
+ continue
+
+ human.SetKnockdown(10 SECONDS)
+ var/turf/turf = human.loc
+ new /obj/effect/hotspot(turf)
+ turf.hotspot_expose(700, 50, 1)
+
+ return
+
+/datum/ritual/ashwalker/soul/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/effects/whoosh.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/effects/bamf.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/effects/blobattack.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/transmutation
+ name = "Transmutation ritual"
+ cooldown_after_cast = 120 SECONDS
+ cast_time = 10 SECONDS
+ required_things = list(
+ /obj/item/stack/ore = 10
+ )
+
+/datum/ritual/ashwalker/transmutation/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(!isashwalkershaman(invoker))
+ disaster_prob = 30
+ fail_chance = 50
+
+ return TRUE
+
+/datum/ritual/ashwalker/transmutation/do_ritual(mob/living/carbon/human/invoker)
+ var/list/ore_types = list()
+
+ for(var/obj/item/stack/ore/ore as anything in subtypesof(/obj/item/stack/ore))
+ LAZYADD(ore_types, ore)
+
+ var/obj/item/stack/ore/ore = pick(ore_types)
+ ore = new(get_turf(ritual_object))
+ ore.add(10)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/transmutation/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human) || !prob(disaster_prob))
+ continue
+
+ if(!isturf(human.loc))
+ continue
+
+ human.SetKnockdown(10 SECONDS)
+ var/turf/turf = human.loc
+ new /obj/effect/hotspot(turf)
+ turf.hotspot_expose(700, 50, 1)
+
+ return
+
+/datum/ritual/ashwalker/transmutation/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/effects/bin_close.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/cult_spell.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/knock.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/interrogation
+ name = "Interrogation ritual"
+ cooldown_after_cast = 50 SECONDS
+ shaman_only = TRUE
+ cast_time = 10 SECONDS
+ required_things = list(
+ /mob/living/carbon/human = 1
+ )
+
+/datum/ritual/ashwalker/interrogation/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ if(invoker.health > 10)
+ disaster_prob = 30
+ fail_chance = 30
+
+ return TRUE
+
+/datum/ritual/ashwalker/interrogation/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ var/mob/living/carbon/human/human = locate() in used_things
+ if(!human || QDELETED(human))
+ return RITUAL_FAILED_ON_PROCEED
+
+ if(human.stat == DEAD || !human.mind)
+ to_chat(invoker, "Гуманоид должен быть жив и иметь разум.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/interrogation/do_ritual(mob/living/carbon/human/invoker)
+ var/obj/effect/proc_holder/spell/empath/empath = new
+ if(!empath.cast(used_things, invoker))
+ return RITUAL_FAILED_ON_PROCEED
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/interrogation/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human))
+ continue
+
+ if(!isturf(human.loc))
+ continue
+
+ var/turf/turf = human.loc
+ to_chat(human, "HONK")
+ SEND_SOUND(turf, sound('sound/items/airhorn.ogg'))
+ human.AdjustHallucinate(150 SECONDS)
+ human.EyeBlind(5 SECONDS)
+ var/datum/effect_system/smoke_spread/smoke = new
+ smoke.set_up(5, FALSE, turf)
+ smoke.start()
+
+ return
+
+/datum/ritual/ashwalker/interrogation/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/effects/anvil_start.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/effects/hulk_hit_airlock.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/effects/forge_destroy.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/creation
+ name = "Creation ritual"
+ cooldown_after_cast = 150 SECONDS
+ shaman_only = TRUE
+ extra_invokers = 2
+ cast_time = 60 SECONDS
+ required_things = list(
+ /mob/living/carbon/human = 2
+ )
+
+/datum/ritual/ashwalker/creation/check_invokers(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/carbon/human/human as anything in invokers)
+ if(human.stat != UNCONSCIOUS)
+ disaster_prob += 20
+ fail_chance += 20
+
+ return TRUE
+
+/datum/ritual/ashwalker/creation/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/carbon/human/human as anything in used_things)
+ if(human.stat != DEAD)
+ to_chat(invoker, "Гуманоиды должны быть мертвы.")
+ return FALSE
+
+ if(!isashwalker(human))
+ to_chat(invoker, "Гуманоиды должны быть пеплоходцами.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/creation/do_ritual(mob/living/carbon/human/invoker)
+ for(var/mob/living/mob as anything in subtypesof(/mob/living/simple_animal/hostile/asteroid))
+ if(prob(30))
+ mob = new(get_turf(ritual_object))
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/creation/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human) || !prob(disaster_prob))
+ continue
+
+ if(!isturf(human.loc))
+ continue
+
+ human.SetKnockdown(10 SECONDS)
+ var/turf/turf = human.loc
+ new /obj/effect/hotspot(turf)
+ turf.hotspot_expose(700, 50, 1)
+
+ return
+
+/datum/ritual/ashwalker/creation/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/demon_consume.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/blind.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/castsummon.ogg', 50, TRUE)
+
+ return .
+
+/datum/ritual/ashwalker/command
+ name = "Command ritual"
+ cooldown_after_cast = 150 SECONDS
+ shaman_only = TRUE
+ disaster_prob = 35
+ extra_invokers = 1
+ cast_time = 60 SECONDS
+ required_things = list(
+ /mob/living/simple_animal = 1,
+ /obj/item/organ/internal/regenerative_core = 1,
+ /obj/item/reagent_containers/food/snacks/monstermeat/spiderleg = 1
+ )
+
+/datum/ritual/ashwalker/command/check_contents(mob/living/carbon/human/invoker)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ for(var/mob/living/simple_animal/living as anything in used_things)
+ if(living.client)
+ to_chat(invoker, "Существо должно быть бездушным.")
+ return FALSE
+
+ if(living.sentience_type == SENTIENCE_BOSS)
+ to_chat(invoker, "Ритуал не может воздействовать на мегафауну.")
+ return FALSE
+
+ if(living.stat != DEAD)
+ to_chat(invoker, "Существа должны быть мертвы.")
+ return FALSE
+
+ return TRUE
+
+/datum/ritual/ashwalker/command/do_ritual(mob/living/carbon/human/invoker)
+ var/mob/living/simple_animal/animal = locate() in used_things
+
+ if(QDELETED(animal))
+ return RITUAL_FAILED_ON_PROCEED
+
+ animal.faction = invoker.faction
+ animal.revive()
+ var/list/candidates = SSghost_spawns.poll_candidates("Вы хотите сыграть за раба пеплоходцев?", ROLE_SENTIENT, TRUE, source = animal)
+
+ if(!LAZYLEN(candidates) || QDELETED(animal)) // no travelling into nullspace
+ return RITUAL_FAILED_ON_PROCEED // no mercy guys. But you got friendly creature
+
+ var/mob/mob = pick(candidates)
+ animal.key = mob.key
+ animal.universal_speak = 1
+ animal.sentience_act()
+ animal.can_collar = 1
+ animal.maxHealth = max(animal.maxHealth, 200)
+ animal.del_on_death = FALSE
+ animal.master_commander = invoker
+
+ animal.mind.store_memory("Мой хозяин [invoker.name], выполню [genderize_ru(invoker.gender, "его", "её", "этого", "их")] цели любой ценой!")
+ to_chat(animal, chat_box_green("Вы - раб пеплоходцев. Всегда подчиняйтесь и помогайте им."))
+ add_game_logs("стал питомцем игрока [key_name(invoker)]", animal)
+
+ return RITUAL_SUCCESSFUL
+
+/datum/ritual/ashwalker/command/disaster(mob/living/carbon/human/invoker)
+ for(var/mob/living/carbon/human/human in SSmobs.clients_by_zlevel[invoker.z])
+ if(!isashwalker(human) || !prob(disaster_prob))
+ continue
+
+ if(!isturf(human.loc))
+ continue
+
+ var/datum/effect_system/smoke_spread/smoke = new
+ smoke.set_up(5, FALSE, get_turf(human.loc))
+ smoke.start()
+
+ var/mob/living/simple_animal/mob = locate() in used_things
+ qdel(mob)
+
+ new /mob/living/simple_animal/hostile/asteroid/goliath/beast/ancient(get_turf(ritual_object))
+
+ return
+
+/datum/ritual/ashwalker/command/handle_ritual_object(bitflags, silent = FALSE)
+ . = ..(bitflags, TRUE)
+
+ switch(.)
+ if(RITUAL_ENDED)
+ playsound(ritual_object.loc, 'sound/magic/demon_consume.ogg', 50, TRUE)
+ if(RITUAL_STARTED)
+ playsound(ritual_object.loc, 'sound/magic/invoke_general.ogg', 50, TRUE)
+ if(RITUAL_FAILED)
+ playsound(ritual_object.loc, 'sound/magic/castsummon.ogg', 50, TRUE)
+
+ return .
+
diff --git a/code/datums/status_effects/debuffs.dm b/code/datums/status_effects/debuffs.dm
index 839a3cdf909..d95b87db1f1 100644
--- a/code/datums/status_effects/debuffs.dm
+++ b/code/datums/status_effects/debuffs.dm
@@ -57,6 +57,40 @@
/datum/status_effect/pacifism/on_remove()
REMOVE_TRAIT(owner, TRAIT_PACIFISM, id)
+/datum/status_effect/fang_exhaust
+ id = "fang_exhaust"
+ alert_type = null
+ duration = 2 SECONDS
+ var/modifier
+
+/datum/status_effect/fang_exhaust/on_creation(mob/living/simple_animal/new_owner, modifier = 1.1)
+ if(!istype(new_owner))
+ return FALSE
+
+ src.modifier = modifier
+ return ..()
+
+/datum/status_effect/fang_exhaust/on_apply()
+ var/mob/living/simple_animal/new_owner = owner
+
+ for(var/thing in new_owner.damage_coeff)
+ if(!new_owner.damage_coeff[thing])
+ continue
+
+ new_owner.damage_coeff[thing] *= modifier
+
+ return ..()
+
+/datum/status_effect/fang_exhaust/on_remove()
+ var/mob/living/simple_animal/new_owner = owner
+
+ for(var/thing in new_owner.damage_coeff)
+ if(!new_owner.damage_coeff[thing])
+ continue
+
+ new_owner.damage_coeff[thing] /= modifier
+
+ return ..()
/datum/status_effect/shadow_boxing
id = "shadow barrage"
diff --git a/code/game/objects/effects/decals/Cleanable/misc.dm b/code/game/objects/effects/decals/Cleanable/misc.dm
index 4c9df1cec23..82e17d98a83 100644
--- a/code/game/objects/effects/decals/Cleanable/misc.dm
+++ b/code/game/objects/effects/decals/Cleanable/misc.dm
@@ -257,3 +257,21 @@
/obj/effect/decal/cleanable/glass/plasma
icon_state = "plasmatiny"
+
+/obj/effect/decal/cleanable/ashrune
+ name = "Ash rune"
+ desc = "A rune drawn in ash."
+ icon = 'icons/effects/ashwalker_rune.dmi'
+ icon_state = "AshRuneFilled"
+ anchored = TRUE
+ mergeable_decal = FALSE
+ mouse_opacity = MOUSE_OPACITY_ICON
+
+/obj/effect/decal/cleanable/ashrune/ComponentInitialize()
+ AddComponent( \
+ /datum/component/ritual_object, \
+ /datum/ritual/ashwalker, \
+ )
+
+/obj/effect/decal/cleanable/ashrune/is_cleanable()
+ return FALSE
diff --git a/code/game/objects/items/weapons/cigs.dm b/code/game/objects/items/weapons/cigs.dm
index 3b1b3cc915c..a548b051206 100644
--- a/code/game/objects/items/weapons/cigs.dm
+++ b/code/game/objects/items/weapons/cigs.dm
@@ -331,6 +331,9 @@ LIGHTERS ARE IN LIGHTERS.DM
/obj/item/clothing/mask/cigarette/shadyjims
list_reagents = list("nicotine" = 40, "lipolicide" = 7.5, "ammonia" = 2, "atrazine" = 1, "toxin" = 1.5)
+/obj/item/clothing/mask/cigarette/richard
+ list_reagents = list("nicotine" = 40, "epinephrine" = 5, "absinthe" = 5)
+
/obj/item/clothing/mask/cigarette/rollie
name = "rollie"
desc = "A roll of dried plant matter wrapped in thin paper."
diff --git a/code/game/objects/items/weapons/storage/fancy.dm b/code/game/objects/items/weapons/storage/fancy.dm
index c491bd96b2a..b1c8f7b30f9 100644
--- a/code/game/objects/items/weapons/storage/fancy.dm
+++ b/code/game/objects/items/weapons/storage/fancy.dm
@@ -335,6 +335,11 @@
item_state = "upliftpacket"
cigarette_type = /obj/item/clothing/mask/cigarette/menthol
+/obj/item/storage/fancy/cigarettes/cigpack_richard
+ name = "\improper Richard & Co cigarettes"
+ desc = "Курят только отчаянные."
+ cigarette_type = /obj/item/clothing/mask/cigarette/richard
+
/obj/item/storage/fancy/cigarettes/cigpack_robust
name = "\improper Robust packet"
desc = "Smoked by the robust."
diff --git a/code/modules/crafting/recipes.dm b/code/modules/crafting/recipes.dm
index cd2fd7af4fd..4dcd10981ba 100644
--- a/code/modules/crafting/recipes.dm
+++ b/code/modules/crafting/recipes.dm
@@ -1519,6 +1519,39 @@
subcategory = CAT_WEAPON
always_availible = FALSE
+/datum/crafting_recipe/pickaxe
+ name = "Iron pickaxe"
+ reqs = list(
+ /obj/item/stack/sheet/wood = 2,
+ /obj/item/stack/sheet/metal = 5
+ )
+ result = list(/obj/item/pickaxe)
+ category = CAT_PRIMAL
+
+/datum/crafting_recipe/pickaxe/silver
+ name = "Silver pickaxe"
+ reqs = list(
+ /obj/item/stack/sheet/wood = 2,
+ /obj/item/stack/sheet/mineral/silver = 5
+ )
+ result = list(/obj/item/pickaxe/silver)
+
+/datum/crafting_recipe/pickaxe/golden
+ name = "Golden pickaxe"
+ reqs = list(
+ /obj/item/stack/sheet/wood = 2,
+ /obj/item/stack/sheet/mineral/gold = 5
+ )
+ result = list(/obj/item/pickaxe/gold)
+
+/datum/crafting_recipe/pickaxe/diamond
+ name = "Diamond pickaxe"
+ reqs = list(
+ /obj/item/stack/sheet/wood = 2,
+ /obj/item/stack/sheet/mineral/diamond = 5
+ )
+ result = list(/obj/item/pickaxe/diamond)
+
/datum/crafting_recipe/drone
name = "Inactive Drone"
result = list(/obj/item/inactive_drone)
diff --git a/code/modules/mining/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm
index 978864203c5..822765b8858 100644
--- a/code/modules/mining/equipment/kinetic_crusher.dm
+++ b/code/modules/mining/equipment/kinetic_crusher.dm
@@ -361,6 +361,70 @@
if(.)
H.charge_time += bonus_value
+/// Massive eyed tentacle
+/obj/item/crusher_trophy/eyed_tentacle
+ name = "Massive eyed tentacle"
+ desc = "Большое и глазастое щупальце древнего голиафа. Может быть установлено как трофей крашера."
+ icon_state = "ancient_goliath_tentacle"
+ denied_type = /obj/item/crusher_trophy/eyed_tentacle
+ bonus_value = 1
+
+/obj/item/crusher_trophy/eyed_tentacle/effect_desc()
+ return "causes kinetic crusher to deal 50% more damage if target has more than 90% HP"
+
+/obj/item/crusher_trophy/eyed_tentacle/on_melee_hit(mob/living/target, mob/living/user)
+ var/procent = (target.health / target.maxHealth) * 100
+ if(procent < 90)
+ return
+
+ var/obj/item/twohanded/kinetic_crusher/crusher = user.get_active_hand()
+ if(!crusher)
+ return
+
+ target.apply_damage(crusher.force * bonus_value, crusher.damtype, user.zone_selected)
+
+/// Poison fang
+/obj/item/crusher_trophy/fang
+ name = "Poison fang"
+ desc = "Уродливый и отравленный коготь. Может быть установлен как трофей крашера."
+ icon_state = "ob_gniga"
+ denied_type = /obj/item/crusher_trophy/fang
+ bonus_value = 1.1
+
+/obj/item/crusher_trophy/fang/effect_desc()
+ return "causes fauna to get 10% more damage after mark destroyed for 2 seconds"
+
+/obj/item/crusher_trophy/fang/on_mark_detonation(mob/living/target, mob/living/user)
+ target.apply_status_effect(STATUS_EFFECT_FANG_EXHAUSTION, bonus_value)
+
+/// Frost gland
+/obj/item/crusher_trophy/gland
+ name = "Frost gland"
+ desc = "Замороженная железа. Может быть установлена как трофей крашера."
+ icon_state = "ice_gniga"
+ denied_type = /obj/item/crusher_trophy/gland
+ bonus_value = 0.9
+
+/obj/item/crusher_trophy/gland/effect_desc()
+ return "causes fauna to deal 10% less damage when marked"
+
+/obj/item/crusher_trophy/gland/on_mark_application(mob/living/simple_animal/target, datum/status_effect/crusher_mark/mark, had_mark)
+ if(had_mark)
+ return
+
+ if(!istype(target))
+ return
+
+ target.melee_damage_lower *= bonus_value
+ target.melee_damage_upper *= bonus_value
+
+/obj/item/crusher_trophy/gland/on_mark_detonation(mob/living/simple_animal/target, mob/living/user)
+ if(!istype(target)) // double check
+ return
+
+ target.melee_damage_lower /= bonus_value
+ target.melee_damage_upper /= bonus_value
+
//blood-drunk hunter
/obj/item/crusher_trophy/miner_eye
name = "eye of a blood-drunk hunter"
diff --git a/code/modules/mob/living/simple_animal/hostile/mining/goliath.dm b/code/modules/mob/living/simple_animal/hostile/mining/goliath.dm
index da1aa23923b..b9a6169ab4b 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining/goliath.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining/goliath.dm
@@ -244,6 +244,7 @@
speed = 4
pre_attack_icon = "Goliath_preattack"
throw_message = "does nothing to the rocky hide of the"
+ crusher_loot = /obj/item/crusher_trophy/eyed_tentacle
loot = list(/obj/item/stack/sheet/animalhide/goliath_hide) //A throwback to the asteroid days
butcher_results = list(/obj/item/reagent_containers/food/snacks/monstermeat/goliath = 2, /obj/item/stack/sheet/bone = 2)
crusher_drop_mod = 30
diff --git a/code/modules/mob/living/simple_animal/hostile/mining/marrow_weaver.dm b/code/modules/mob/living/simple_animal/hostile/mining/marrow_weaver.dm
index c70328f1574..2fd8b9fe08b 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining/marrow_weaver.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining/marrow_weaver.dm
@@ -7,6 +7,7 @@
icon_aggro = "weaver"
icon_dead = "weaver_dead"
throw_message = "bounces harmlessly off the"
+ crusher_loot = /obj/item/crusher_trophy/fang
butcher_results = list(/obj/item/stack/ore/uranium = 2, /obj/item/stack/sheet/bone = 2, /obj/item/stack/sheet/sinew = 1, /obj/item/stack/sheet/animalhide/weaver_chitin = 3, /obj/item/reagent_containers/food/snacks/monstermeat/spiderleg = 2)
loot = list()
attacktext = "кусает" //can we revert all translation in our code?
@@ -136,15 +137,20 @@
/mob/living/simple_animal/hostile/asteroid/marrowweaver/frost
name = "frostbite weaver"
desc = "A big, angry, venomous ice spider. It likes to snack on bone marrow. Its preferred food source is you."
+
icon_state = "weaver_ice"
icon_living = "weaver_ice"
icon_aggro = "weaver_ice"
icon_dead = "weaver_ice_dead"
+
melee_damage_lower = 10 //stronger venom, but weaker attack.
melee_damage_upper = 13
+
poison_type = "frostoil"
poison_per_bite = 5
+ crusher_loot = /obj/item/crusher_trophy/gland
+
/mob/living/simple_animal/hostile/asteroid/marrowweaver/tendril
fromtendril = TRUE
diff --git a/icons/effects/ashwalker_rune.dmi b/icons/effects/ashwalker_rune.dmi
new file mode 100644
index 00000000000..3c703403738
Binary files /dev/null and b/icons/effects/ashwalker_rune.dmi differ
diff --git a/icons/obj/lavaland/artefacts.dmi b/icons/obj/lavaland/artefacts.dmi
index 178610b7424..4fc1f5cba29 100644
Binary files a/icons/obj/lavaland/artefacts.dmi and b/icons/obj/lavaland/artefacts.dmi differ
diff --git a/paradise.dme b/paradise.dme
index e79b1cd77ea..a57e67814ea 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -39,6 +39,7 @@
#include "code\__DEFINES\blob.dm"
#include "code\__DEFINES\borer.dm"
#include "code\__DEFINES\bots.dm"
+#include "code\__DEFINES\rituals.dm"
#include "code\__DEFINES\byond_tracy.dm"
#include "code\__DEFINES\callbacks.dm"
#include "code\__DEFINES\cargo_quests.dm"
@@ -367,6 +368,7 @@
#include "code\controllers\subsystem\tickets\mentor_tickets.dm"
#include "code\controllers\subsystem\tickets\tickets.dm"
#include "code\datums\action.dm"
+#include "code\datums\rituals.dm"
#include "code\datums\ai_law_sets.dm"
#include "code\datums\ai_laws.dm"
#include "code\datums\armor.dm"
@@ -444,6 +446,7 @@
#include "code\datums\components\conveyor_movement.dm"
#include "code\datums\components\cross_shock.dm"
#include "code\datums\components\decal.dm"
+#include "code\datums\components\ritual_object.dm"
#include "code\datums\components\defibrillator.dm"
#include "code\datums\components\drift.dm"
#include "code\datums\components\ducttape.dm"