diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index 4c79c9ad814..b565b32aa49 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -277,6 +277,9 @@
#define COMSIG_LIVING_GRAB "living_grab"
// Return COMPONENT_CANCEL_ATTACK_CHAIN / COMPONENT_SKIP_ATTACK_CHAIN to stop the grab
+///Called when living finish eat (/datum/component/edible/proc/On_Consume)
+#define COMSIG_LIVING_FINISH_EAT "living_finish_eat"
+
/// From /datum/element/basic_eating/try_eating()
#define COMSIG_MOB_PRE_EAT "mob_pre_eat"
///cancel eating attempt
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index a309c76ecfe..a2925ee2e5b 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -1173,6 +1173,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Trait given to anything linked to, not necessarily allied to, the mansus
#define TRAIT_MANSUS_TOUCHED "mansus_touched"
+/// Appiled when wizard buy (/datum/spellbook_entry/perks/spalls_lottery) perk.
+/// Give 50/25% chance not spend a spellbook charge on 1/2 cost spell.
+/// Appiled it wizard can't refund any spells.
+#define TRAIT_SPELLS_LOTTERY "spell_for_sale"
+
/// Trait given to mobs wearing the clown mask
#define TRAIT_PERCEIVED_AS_CLOWN "perceived_as_clown"
/// Does this item bypass ranged armor checks?
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 01f3b05baed..d098e1c11fb 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -452,6 +452,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_SPARRING" = TRAIT_SPARRING,
"TRAIT_SPEAKS_CLEARLY" = TRAIT_SPEAKS_CLEARLY,
"TRAIT_SPECIAL_TRAUMA_BOOST" = TRAIT_SPECIAL_TRAUMA_BOOST,
+ "TRAIT_SPELLS_LOTTERY" = TRAIT_SPELLS_LOTTERY,
"TRAIT_SPIDER_CONSUMED" = TRAIT_SPIDER_CONSUMED,
"TRAIT_SPIRITUAL" = TRAIT_SPIRITUAL,
"TRAIT_SPRAY_PAINTABLE" = TRAIT_SPRAY_PAINTABLE,
diff --git a/code/datums/components/dejavu.dm b/code/datums/components/dejavu.dm
index c7b3e3f2ff5..8a1902526c4 100644
--- a/code/datums/components/dejavu.dm
+++ b/code/datums/components/dejavu.dm
@@ -85,7 +85,7 @@
master.forceMove(starting_turf)
rewinds_remaining --
- if(rewinds_remaining)
+ if(rewinds_remaining || rewinds_remaining < 0)
addtimer(CALLBACK(src, rewind_type), rewind_interval)
else
to_chat(parent, span_notice(no_rewinds_message))
diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm
index d4b80ea1dd8..056f1e5791e 100644
--- a/code/datums/components/food/edible.dm
+++ b/code/datums/components/food/edible.dm
@@ -641,6 +641,7 @@ Behavior that's still missing from this component that original food items had t
///Delete the item when it is fully eaten
/datum/component/edible/proc/On_Consume(mob/living/eater, mob/living/feeder)
SEND_SIGNAL(parent, COMSIG_FOOD_CONSUMED, eater, feeder)
+ SEND_SIGNAL(eater, COMSIG_LIVING_FINISH_EAT, parent, feeder)
on_consume?.Invoke(eater, feeder)
if (QDELETED(parent)) // might be destroyed by the callback
diff --git a/code/datums/components/heart_eater.dm b/code/datums/components/heart_eater.dm
new file mode 100644
index 00000000000..5b73c40c954
--- /dev/null
+++ b/code/datums/components/heart_eater.dm
@@ -0,0 +1,118 @@
+/datum/component/heart_eater
+ /// Check if we fully ate whole heart and reset when we start eat new one.
+ var/bites_taken = 0
+ /// Remember the number of species damage_modifier.
+ var/remember_modifier = 0
+ /// Remember last heart we ate and reset bites_taken counter if we start eat new one
+ var/datum/weakref/last_heart_we_ate
+ /// List of all mutations allowed to get.
+ var/static/list/datum/mutation/human/mutations_list = list(
+ /datum/mutation/human/adaptation/cold,
+ /datum/mutation/human/adaptation/heat,
+ /datum/mutation/human/adaptation/pressure,
+ /datum/mutation/human/adaptation/thermal,
+ /datum/mutation/human/chameleon,
+ /datum/mutation/human/cryokinesis,
+ /datum/mutation/human/cryokinesis/pyrokinesis,
+ /datum/mutation/human/dwarfism,
+ /datum/mutation/human/geladikinesis/ash,
+ /datum/mutation/human/insulated,
+ /datum/mutation/human/telekinesis,
+ /datum/mutation/human/telepathy,
+ /datum/mutation/human/thermal,
+ /datum/mutation/human/tongue_spike,
+ /datum/mutation/human/webbing,
+ /datum/mutation/human/xray,
+ )
+
+/datum/component/heart_eater/Initialize(...)
+ . = ..()
+ if(!ishuman(parent))
+ return COMPONENT_INCOMPATIBLE
+ prepare_species(parent)
+
+/datum/component/heart_eater/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_SPECIES_GAIN, PROC_REF(on_species_change))
+ RegisterSignal(parent, COMSIG_LIVING_FINISH_EAT, PROC_REF(eat_eat_eat))
+
+/datum/component/heart_eater/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, COMSIG_LIVING_FINISH_EAT)
+ UnregisterSignal(parent, COMSIG_SPECIES_GAIN)
+
+/datum/component/heart_eater/proc/prepare_species(mob/living/carbon/human/eater)
+ if(eater.get_liked_foodtypes() & GORE)
+ return
+ var/obj/item/organ/internal/tongue/eater_tongue = eater.get_organ_slot(ORGAN_SLOT_TONGUE)
+ if(!eater_tongue)
+ return
+ eater_tongue.disliked_foodtypes &= ~GORE
+ eater_tongue.liked_foodtypes |= GORE
+
+/datum/component/heart_eater/proc/on_species_change(mob/living/carbon/human/eater, datum/species/new_species, datum/species/old_species)
+ SIGNAL_HANDLER
+
+ eater.dna?.species?.damage_modifier += remember_modifier
+ prepare_species(eater)
+
+/// Proc called when we finish eat somthing.
+/datum/component/heart_eater/proc/eat_eat_eat(mob/living/carbon/human/eater, datum/what_we_ate)
+ SIGNAL_HANDLER
+
+ if(get_area(eater) == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ if(!istype(what_we_ate, /obj/item/organ/internal/heart))
+ return
+ var/obj/item/organ/internal/heart/we_ate_heart = what_we_ate
+ var/obj/item/organ/internal/heart/previous_heart = last_heart_we_ate?.resolve()
+ if(we_ate_heart == previous_heart)
+ return
+ bites_taken = 0
+
+ last_heart_we_ate = WEAKREF(we_ate_heart)
+ bites_taken++
+ if(bites_taken < (we_ate_heart.reagents.total_volume/2))
+ return
+ if(prob(50))
+ perfect_heart(eater)
+ return
+ not_perfect_heart(eater)
+
+///Perfect heart give our +10 damage modifier(Max. 80).
+/datum/component/heart_eater/proc/perfect_heart(mob/living/carbon/human/eater)
+ if(eater.dna?.species?.damage_modifier >= 80)
+ healing_heart(eater)
+ return
+ eater.dna?.species?.damage_modifier += 10
+ remember_modifier += 10
+ healing_heart(eater)
+ to_chat(eater, span_warning("This heart is perfect. You feel a surge of vital energy."))
+
+///Not Perfect heart give random mutation.
+/datum/component/heart_eater/proc/not_perfect_heart(mob/living/carbon/human/eater)
+ var/datum/mutation/human/new_mutation
+ var/list/datum/mutation/human/shuffle_mutation_list = shuffle(mutations_list)
+ for(var/mutation_in_list in shuffle_mutation_list)
+ if(is_type_in_list(mutation_in_list, eater.dna.mutations))
+ continue
+ new_mutation = mutation_in_list
+ break
+ if(isnull(new_mutation))
+ healing_heart(eater)
+ return
+ eater.dna.add_mutation(new_mutation)
+ healing_heart(eater)
+ to_chat(eater, span_warning("This heart is not right for you. You now have [new_mutation.name] mutation."))
+
+///Heart eater give also strong healing from hearts.
+/datum/component/heart_eater/proc/healing_heart(mob/living/carbon/human/eater)
+ for(var/heal_organ in eater.organs)
+ eater.adjustOrganLoss(heal_organ, -50)
+ for(var/datum/wound/heal_wound in eater.all_wounds)
+ heal_wound.remove_wound()
+ eater.adjustBruteLoss(-50)
+ eater.adjustFireLoss(-50)
+ eater.adjustToxLoss(-50)
+ eater.adjustOxyLoss(-50)
+ eater.adjustStaminaLoss(-50)
diff --git a/code/datums/components/slime_friends.dm b/code/datums/components/slime_friends.dm
new file mode 100644
index 00000000000..d2b751d092e
--- /dev/null
+++ b/code/datums/components/slime_friends.dm
@@ -0,0 +1,61 @@
+/datum/component/slime_friends
+ /// Slime maker timer.
+ var/timer
+ /// List to pick from when we need slime colour.
+ var/static/colours = list(
+ /datum/slime_type/adamantine,
+ /datum/slime_type/black,
+ /datum/slime_type/blue,
+ /datum/slime_type/bluespace,
+ /datum/slime_type/cerulean,
+ /datum/slime_type/darkblue,
+ /datum/slime_type/darkpurple,
+ /datum/slime_type/gold,
+ /datum/slime_type/green,
+ /datum/slime_type/grey,
+ /datum/slime_type/lightpink,
+ /datum/slime_type/metal,
+ /datum/slime_type/oil,
+ /datum/slime_type/orange,
+ /datum/slime_type/pink,
+ /datum/slime_type/purple,
+ /datum/slime_type/pyrite,
+ /datum/slime_type/rainbow,
+ /datum/slime_type/red,
+ /datum/slime_type/sepia,
+ /datum/slime_type/silver,
+ /datum/slime_type/yellow,
+ )
+
+/datum/component/slime_friends/Initialize(...)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/mob/living/living_parent = parent
+ living_parent.faction |= FACTION_SLIME
+ RegisterSignal(living_parent, COMSIG_ENTER_AREA, PROC_REF(start_slime_prodaction))
+
+/datum/component/slime_friends/Destroy(force)
+ . = ..()
+ var/mob/living/living_parent = parent
+ living_parent.faction -= FACTION_SLIME
+ timer = null
+
+/// Start slime prodaction when we leave wizden.
+/datum/component/slime_friends/proc/start_slime_prodaction(mob/living/friend, area/new_area)
+ if(new_area == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ timer = addtimer(CALLBACK(src, PROC_REF(make_slime_friend), friend), 20 SECONDS)
+ UnregisterSignal(friend, COMSIG_ENTER_AREA)
+
+/// Slime prodactor proc.
+/datum/component/slime_friends/proc/make_slime_friend(mob/living/friend)
+ timer = addtimer(CALLBACK(src, PROC_REF(make_slime_friend), friend), 20 SECONDS)
+ if(get_area(friend) == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ var/turf/where = get_turf(friend)
+ var/new_colour = pick(colours)
+ var/mob/living/basic/slime/new_friend = new(where, new_colour, SLIME_LIFE_STAGE_ADULT)
+ new_friend.faction = friend.faction.Copy()
+ new_friend.set_enraged_behaviour()
+ friend.nutrition -= 50
diff --git a/code/datums/components/wormborn.dm b/code/datums/components/wormborn.dm
new file mode 100644
index 00000000000..1841dbf38cc
--- /dev/null
+++ b/code/datums/components/wormborn.dm
@@ -0,0 +1,74 @@
+/datum/component/wormborn
+
+/datum/component/wormborn/Initialize(...)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/wormborn/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(second_breath))
+
+/datum/component/wormborn/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, COMSIG_LIVING_DEATH)
+
+/datum/component/wormborn/proc/second_breath(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(get_area(source) == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ source.buckled?.unbuckle_mob(source, force = TRUE)
+
+ if(source.movement_type & VENTCRAWLING)
+ source.forceMove(get_turf(source))
+
+ var/mob/living/worm = new /mob/living/basic/wizard_worm(get_turf(source))
+ source.mind?.transfer_to(worm)
+ source.forceMove(worm)
+
+/mob/living/basic/wizard_worm
+ name = "Magic Worm"
+ desc = "Large blue worm. What happens if you put your hand in his mouth?."
+ icon = 'icons/mob/simple/mob.dmi'
+ icon_state = "wizard_start"
+ icon_living = "wizard_start"
+ base_icon_state = "wizard"
+ maxHealth = 800
+ health = 800
+ melee_damage_lower = 20
+ melee_damage_upper = 30
+ obj_damage = 200
+ speed = 0
+ move_force = MOVE_FORCE_OVERPOWERING
+ move_resist = MOVE_FORCE_OVERPOWERING
+ pull_force = MOVE_FORCE_OVERPOWERING
+ mob_size = MOB_SIZE_HUGE
+ sentience_type = SENTIENCE_BOSS
+ mob_biotypes = MOB_ORGANIC|MOB_SPECIAL
+
+/mob/living/basic/wizard_worm/has_gravity(turf/gravity_turf)
+ return TRUE
+
+/mob/living/basic/wizard_worm/can_be_pulled()
+ return FALSE
+
+/mob/living/basic/wizard_worm/Initialize(mapload, spawn_bodyparts = TRUE)
+ . = ..()
+ AddElement(/datum/element/wall_tearer)
+
+ if(spawn_bodyparts)
+ build_tail()
+
+/mob/living/basic/wizard_worm/proc/build_tail(mob/living/tail)
+ AddComponent(/datum/component/mob_chain, vary_icon_state = TRUE)
+ var/mob/living/basic/wizard_worm/prev = src
+ for(var/i in 1 to 5)
+ prev = new_segment(behind = prev)
+ update_appearance(UPDATE_ICON_STATE)
+
+/mob/living/basic/wizard_worm/proc/new_segment(mob/living/basic/wizard_worm/behind)
+ var/mob/living/segment = new type(drop_location(), FALSE)
+ ADD_TRAIT(segment, TRAIT_PERMANENTLY_MORTAL, INNATE_TRAIT)
+ segment.AddComponent(/datum/component/mob_chain, front = behind, vary_icon_state = TRUE)
+ return segment
diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm
index 7dc220d516f..18e2dae715c 100644
--- a/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm
+++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm
@@ -70,6 +70,15 @@
for(var/spell in user.actions)
if(is_type_in_typecache(spell, no_coexistance_typecache))
return FALSE
+ var/datum/antagonist/wizard/wizard_datum = user.mind.has_antag_datum(/datum/antagonist/wizard)
+ if(!wizard_datum)
+ return TRUE
+ for(var/perks in wizard_datum.perks)
+ if(is_type_in_typecache(perks, no_coexistance_typecache))
+ return FALSE
+ if(is_type_in_list(src, wizard_datum.perks))
+ to_chat(user, span_warning("This perk already learned!"))
+ return FALSE
return TRUE
/**
@@ -137,6 +146,9 @@
* Return TRUE if it can refunded, FALSE otherwise
*/
/datum/spellbook_entry/proc/can_refund(mob/living/carbon/human/user, obj/item/spellbook/book)
+ if(HAS_TRAIT(user, TRAIT_SPELLS_LOTTERY))
+ to_chat(user, span_notice("No refund."))
+ return FALSE
if(!refundable)
return FALSE
if(!book.refunds_allowed)
diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm
index a66d99c21c8..e7c204a39e2 100644
--- a/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm
+++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm
@@ -55,7 +55,7 @@
it will become easier for others to find your item of power."
spell_type = /datum/action/cooldown/spell/lichdom
category = SPELLBOOK_CATEGORY_DEFENSIVE
- no_coexistance_typecache = list(/datum/action/cooldown/spell/splattercasting)
+ no_coexistance_typecache = list(/datum/action/cooldown/spell/splattercasting, /datum/spellbook_entry/perks/wormborn)
/datum/spellbook_entry/chuunibyou
name = "Chuuni Invocations"
diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm
new file mode 100644
index 00000000000..6cebe8fa1e9
--- /dev/null
+++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm
@@ -0,0 +1,184 @@
+#define SPELLBOOK_CATEGORY_PERKS "Perks"
+
+/datum/spellbook_entry/perks
+ desc = "Main node of perks"
+ category = SPELLBOOK_CATEGORY_PERKS
+ refundable = FALSE // no refund
+ requires_wizard_garb = FALSE
+
+/datum/spellbook_entry/perks/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ var/datum/antagonist/wizard/wizard_datum = user.mind.has_antag_datum(/datum/antagonist/wizard)
+ if(wizard_datum)
+ wizard_datum.perks += src
+ to_chat(user, span_notice("You got a new perk: [src.name]."))
+ return TRUE
+
+/datum/spellbook_entry/perks/fourhands
+ name = "Four Hands"
+ desc = "Gives you even more hands to perform magic"
+
+/datum/spellbook_entry/perks/fourhands/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ user.change_number_of_hands(4)
+
+/datum/spellbook_entry/perks/wormborn
+ name = "Worm Born"
+ desc = "Your soul is infested with mana worms. When you die, you will be reborn as a large worm. \
+ When the worm dies, it has no such luck. Parasitic infection prevents you from binding your soul to objects."
+ no_coexistance_typecache = list(/datum/action/cooldown/spell/lichdom)
+
+/datum/spellbook_entry/perks/wormborn/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ user.AddComponent(/datum/component/wormborn)
+
+/datum/spellbook_entry/perks/dejavu
+ name = "Déjà vu"
+ desc = "Every 60 seconds returns you to the place where you were 60 seconds ago with the same amount of health as you had 60 seconds ago."
+
+/datum/spellbook_entry/perks/dejavu/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ RegisterSignal(user, COMSIG_ENTER_AREA, PROC_REF(give_dejavu))
+
+/datum/spellbook_entry/perks/dejavu/proc/give_dejavu(mob/living/carbon/human/wizard, area/new_area)
+ SIGNAL_HANDLER
+
+ if(new_area == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ wizard.AddComponent(/datum/component/dejavu/timeline, -1, 60 SECONDS)
+ UnregisterSignal(wizard, COMSIG_ENTER_AREA)
+
+/datum/spellbook_entry/perks/spell_lottery
+ name = "Spells Lottery"
+ desc = "Spells Lottery gives you the chance to get something from the book absolutely free, but you can no longer refund any purchases."
+
+/datum/spellbook_entry/perks/spell_lottery/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ ADD_TRAIT(user, TRAIT_SPELLS_LOTTERY, REF(src))
+
+/datum/spellbook_entry/perks/gamble
+ name = "Gamble"
+ desc = "You get 2 random perks."
+
+/datum/spellbook_entry/perks/gamble/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ var/datum/antagonist/wizard/check_perks = user.mind.has_antag_datum(/datum/antagonist/wizard)
+ var/perks_allocated = 0
+ var/list/taking_perks = list()
+ for(var/datum/spellbook_entry/perks/generate_perk in book.entries)
+ if(istype(generate_perk, src))
+ continue
+ if(check_perks && is_type_in_list(generate_perk, check_perks.perks))
+ continue
+ taking_perks += generate_perk
+ perks_allocated++
+ if(perks_allocated >= 2)
+ break
+ if(taking_perks.len < 1)
+ to_chat(user, span_warning("Gamble cannot give 2 perks, so points are returned"))
+ return FALSE
+ taking_perks = shuffle(taking_perks)
+ for(var/datum/spellbook_entry/perks/perks_ready in taking_perks)
+ perks_ready.buy_spell(user, book, log_buy)
+
+/datum/spellbook_entry/perks/heart_eater
+ name = "Heart Eater"
+ desc = "Gives you ability to obtain a person's life force by eating their heart. \
+ By eating someone's heart you can increase your damage resistance or gain random mutation. \
+ Heart also give strong healing buff."
+
+/datum/spellbook_entry/perks/heart_eater/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ user.AddComponent(/datum/component/heart_eater)
+
+/datum/spellbook_entry/perks/slime_friends
+ name = "Slime Friends"
+ desc = "Slimes are your friends. \
+ Every 15 seconds you lose some nutriments and summon a random evil slime to fight on your side."
+
+/datum/spellbook_entry/perks/slime_friends/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ user.AddComponent(/datum/component/slime_friends)
+
+/datum/spellbook_entry/perks/transparence
+ name = "Transparence"
+ desc = "You become a little closer to the world of the dead. \
+ Projectiles pass through you, but you lose 25% of your health and you are hunted by a terrible curse which wants to return you to the afterlife."
+
+/datum/spellbook_entry/perks/transparence/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ user.maxHealth *= 0.75
+ user.alpha = 125
+ ADD_TRAIT(user, TRAIT_UNHITTABLE_BY_PROJECTILES, REF(src))
+ RegisterSignal(user, COMSIG_ENTER_AREA, PROC_REF(make_stalker))
+
+/datum/spellbook_entry/perks/transparence/proc/make_stalker(mob/living/carbon/human/wizard, area/new_area)
+ SIGNAL_HANDLER
+
+ if(new_area == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ wizard.gain_trauma(/datum/brain_trauma/magic/stalker)
+ UnregisterSignal(wizard, COMSIG_ENTER_AREA)
+
+/datum/spellbook_entry/perks/magnetism
+ name = "Magnetism"
+ desc = "You get a small gravity anomaly that orbit around you. \
+ Nearby things will be attracted to you."
+
+/datum/spellbook_entry/perks/magnetism/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book, log_buy)
+ . = ..()
+ var/atom/movable/magnitizm = new /obj/effect/wizard_magnetism(get_turf(user))
+ magnitizm.orbit(user, 20)
+
+/obj/effect/wizard_magnetism
+ name = "magnetic anomaly"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "shield2"
+ /// We need to orbit around someone.
+ var/datum/weakref/owner
+
+/obj/effect/wizard_magnetism/New(loc, ...)
+ . = ..()
+ transform *= 0.4
+
+/obj/effect/wizard_magnetism/orbit(atom/new_owner, radius, clockwise, rotation_speed, rotation_segments, pre_rotation)
+ . = ..()
+ if(!isliving(new_owner))
+ return
+ owner = WEAKREF(new_owner)
+ RegisterSignal(new_owner, COMSIG_ENTER_AREA, PROC_REF(check_area))
+ RegisterSignal(new_owner, COMSIG_LIVING_DEATH, PROC_REF(on_owner_death))
+
+/obj/effect/wizard_magnetism/proc/check_area(mob/living/wizard, area/new_area)
+ SIGNAL_HANDLER
+
+ if(new_area == GLOB.areas_by_type[/area/centcom/wizard_station])
+ return
+ START_PROCESSING(SSprocessing, src)
+ UnregisterSignal(wizard, COMSIG_ENTER_AREA)
+
+/obj/effect/wizard_magnetism/proc/on_owner_death()
+ SIGNAL_HANDLER
+
+ stop_orbit()
+
+/obj/effect/wizard_magnetism/process(seconds_per_tick)
+ if(isnull(owner))
+ stop_orbit()
+ return
+ var/mob/living/wizard = owner.resolve()
+ var/list/things_in_range = orange(5, wizard) - orange(1, wizard)
+ for(var/obj/take_object in things_in_range)
+ if(!take_object.anchored)
+ step_towards(take_object, wizard)
+ for(var/mob/living/living_mov in things_in_range)
+ if(wizard)
+ if(living_mov == wizard)
+ continue
+ if(!living_mov.mob_negates_gravity())
+ step_towards(living_mov, wizard)
+
+/obj/effect/wizard_magnetism/stop_orbit()
+ STOP_PROCESSING(SSprocessing, src)
+ qdel(src)
+
+#undef SPELLBOOK_CATEGORY_PERKS
diff --git a/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm b/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm
index f13b53b12ed..b0016bffe5f 100644
--- a/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm
+++ b/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm
@@ -229,6 +229,10 @@
return FALSE
to_buy.times++
+ if(HAS_TRAIT(user, TRAIT_SPELLS_LOTTERY))
+ if(prob(50 / to_buy.cost))
+ to_chat(user, span_notice("This spell was given to you for free!"))
+ return TRUE
uses -= to_buy.cost
return TRUE
diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm
index e6da5195b02..335c934a320 100644
--- a/code/modules/antagonists/wizard/wizard.dm
+++ b/code/modules/antagonists/wizard/wizard.dm
@@ -25,6 +25,8 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key)
show_to_ghosts = TRUE
/// This mob's Grand Ritual ability
var/datum/action/cooldown/grand_ritual/ritual
+ /// Perks that wizard learn
+ var/list/perks = list()
/datum/antagonist/wizard_minion
name = "Wizard Minion"
diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm
index fdfbce9c761..992d60142bd 100644
--- a/code/modules/surgery/bodyparts/parts.dm
+++ b/code/modules/surgery/bodyparts/parts.dm
@@ -182,7 +182,7 @@
return
var/atom/movable/screen/inventory/hand/hand = new_owner.hud_used.hand_slots["[held_index]"]
- hand.update_appearance()
+ hand?.update_appearance()
/obj/item/bodypart/arm/left
name = "left arm"
diff --git a/code/modules/surgery/organs/external/wings/functional_wings.dm b/code/modules/surgery/organs/external/wings/functional_wings.dm
index 23f897bce95..f4a5a23bf69 100644
--- a/code/modules/surgery/organs/external/wings/functional_wings.dm
+++ b/code/modules/surgery/organs/external/wings/functional_wings.dm
@@ -25,6 +25,8 @@
///Are our wings open or closed?
var/wings_open = FALSE
+ ///We cant hide this wings in suit
+ var/cant_hide = FALSE
// grind_results = list(/datum/reagent/flightpotion = 5)
food_reagents = list(/datum/reagent/flightpotion = 5)
@@ -66,7 +68,7 @@
if(human.stat || human.body_position == LYING_DOWN)
return FALSE
//Jumpsuits have tail holes, so it makes sense they have wing holes too
- if(human.wear_suit && ((human.wear_suit.flags_inv & HIDEJUMPSUIT) && (!human.wear_suit.species_exception || !is_type_in_list(src, human.wear_suit.species_exception))))
+ if(!cant_hide && human.wear_suit && ((human.wear_suit.flags_inv & HIDEJUMPSUIT) && (!human.wear_suit.species_exception || !is_type_in_list(src, human.wear_suit.species_exception))))
to_chat(human, span_warning("Your suit blocks your wings from extending!"))
return FALSE
var/turf/location = get_turf(human)
diff --git a/icons/mob/nonhuman-player/eldritch_mobs.dmi b/icons/mob/nonhuman-player/eldritch_mobs.dmi
index 18e50d727ae..3f2aa454f0e 100644
Binary files a/icons/mob/nonhuman-player/eldritch_mobs.dmi and b/icons/mob/nonhuman-player/eldritch_mobs.dmi differ
diff --git a/icons/mob/simple/mob.dmi b/icons/mob/simple/mob.dmi
index e8fb3c59b8f..142481e08e9 100644
Binary files a/icons/mob/simple/mob.dmi and b/icons/mob/simple/mob.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 0e8f10e3f13..e0fb60d58cf 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1114,6 +1114,7 @@
#include "code\datums\components\hazard_area.dm"
#include "code\datums\components\healing_touch.dm"
#include "code\datums\components\health_scaling_effects.dm"
+#include "code\datums\components\heart_eater.dm"
#include "code\datums\components\heirloom.dm"
#include "code\datums\components\hide_highest_offset.dm"
#include "code\datums\components\holderloving.dm"
@@ -1203,6 +1204,7 @@
#include "code\datums\components\sisyphus_awarder.dm"
#include "code\datums\components\sitcomlaughter.dm"
#include "code\datums\components\sizzle.dm"
+#include "code\datums\components\slime_friends.dm"
#include "code\datums\components\slippery.dm"
#include "code\datums\components\smooth_tunes.dm"
#include "code\datums\components\soul_stealer.dm"
@@ -1266,6 +1268,7 @@
#include "code\datums\components\wearertargeting.dm"
#include "code\datums\components\weatherannouncer.dm"
#include "code\datums\components\wet_floor.dm"
+#include "code\datums\components\wormborn.dm"
#include "code\datums\components\container_item\container_item.dm"
#include "code\datums\components\container_item\tank_holder.dm"
#include "code\datums\components\crafting\_recipes.dm"
@@ -3319,6 +3322,7 @@
#include "code\modules\antagonists\wizard\equipment\spellbook_entries\defensive.dm"
#include "code\modules\antagonists\wizard\equipment\spellbook_entries\mobility.dm"
#include "code\modules\antagonists\wizard\equipment\spellbook_entries\offensive.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\perks.dm"
#include "code\modules\antagonists\wizard\equipment\spellbook_entries\summons.dm"
#include "code\modules\antagonists\wizard\grand_ritual\fluff.dm"
#include "code\modules\antagonists\wizard\grand_ritual\grand_ritual.dm"
diff --git a/tgui/packages/tgui/interfaces/Spellbook.tsx b/tgui/packages/tgui/interfaces/Spellbook.tsx
index c6addf2857e..3d6929dd212 100644
--- a/tgui/packages/tgui/interfaces/Spellbook.tsx
+++ b/tgui/packages/tgui/interfaces/Spellbook.tsx
@@ -22,6 +22,7 @@ enum SpellCategory {
Mobility = 'Mobility',
Assistance = 'Assistance',
Rituals = 'Rituals',
+ Perks = 'Perks',
}
type byondRef = string;
@@ -124,6 +125,16 @@ const TAB2NAME: TabType[] = [
"If you didn't like the loadouts offered, you can embrace chaos. Not recommended for newer wizards.",
component: () => ,
},
+ {
+ title: 'Perks',
+ blurb:
+ 'Perks are useful (and not so useful) improvements to the soul and body collected from all corners of the universe.',
+ scrollable: true,
+ },
+ {
+ title: 'Table of Contents',
+ component: () => ,
+ },
];
enum Buywords {
@@ -158,7 +169,7 @@ const EnscribedName = (props) => {
);
};
-const lineHeightToc = '34.6px';
+const lineHeightToc = '30.6px';
const TableOfContents = (props) => {
const [tabIndex, setTabIndex] = useLocalState('tab-index', 1);
@@ -238,6 +249,14 @@ const TableOfContents = (props) => {
content="Arcane Randomizer"
onClick={() => setTabIndex(9)}
/>
+
+