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)} /> + +