diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 59f6ce1471d..d997a325b4c 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -376,6 +376,7 @@ #define COLOR_CARP_PURPLE "#aba2ff" +#define COLOR_CARP_DARK_PURPLE "#4c486e" #define COLOR_CARP_PINK "#da77a8" #define COLOR_CARP_GREEN "#70ff25" #define COLOR_CARP_GRAPE "#df0afb" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index dd75bf32ba1..f5e8f847a7c 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -198,6 +198,7 @@ ///Called when an armor plate is successfully applied to an object #define COMSIG_ARMOR_PLATED "armor_plated" + ///Called when an item gets recharged by the ammo powerup #define COMSIG_ITEM_RECHARGED "item_recharged" ///Called when an item is being offered, from [/obj/item/proc/on_offered(mob/living/offerer)] @@ -377,6 +378,8 @@ ///called in /obj/item/firing_pin/proc/gun_remove(mob/living/user): (obj/item/firing_pin/pin, mob/living/user) #define COMSIG_GUN_PIN_REMOVED "gun_pin_removed" +///called when an empty rechargeable gun gives itself more ammo +#define COMSIG_GUN_REPLENISHED_CHARGE "gun_replenished_charge" // Jetpack things // Please kill me diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm index 3e9622b79d0..3855ac9b345 100644 --- a/code/_globalvars/traits/admin_tooling.dm +++ b/code/_globalvars/traits/admin_tooling.dm @@ -20,6 +20,7 @@ GLOBAL_LIST_INIT(admin_visible_traits, list( "TRAIT_MOVE_PHASING" = TRAIT_MOVE_PHASING, "TRAIT_MOVE_VENTCRAWLING" = TRAIT_MOVE_VENTCRAWLING, "TRAIT_MOVE_UPSIDE_DOWN" = TRAIT_MOVE_UPSIDE_DOWN, + "TRAIT_NO_TELEPORT" = TRAIT_NO_TELEPORT, "TRAIT_RAINSTORM_IMMUNE" = TRAIT_RAINSTORM_IMMUNE, "TRAIT_RUNECHAT_HIDDEN" = TRAIT_RUNECHAT_HIDDEN, "TRAIT_SANDSTORM_IMMUNE" = TRAIT_SANDSTORM_IMMUNE, @@ -366,7 +367,6 @@ GLOBAL_LIST_INIT(admin_visible_traits, list( "TRAIT_GERM_SENSITIVE" = TRAIT_GERM_SENSITIVE, "TRAIT_HAUNTED" = TRAIT_HAUNTED, "TRAIT_NO_STORAGE_INSERT" = TRAIT_NO_STORAGE_INSERT, - "TRAIT_NO_TELEPORT" = TRAIT_NO_TELEPORT, "TRAIT_NODROP" = TRAIT_NODROP, "TRAIT_T_RAY_VISIBLE" = TRAIT_T_RAY_VISIBLE, "TRAIT_UNCATCHABLE" = TRAIT_UNCATCHABLE, diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/clear_key.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/clear_key.dm new file mode 100644 index 00000000000..ae0711789fa --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/clear_key.dm @@ -0,0 +1,14 @@ +/// Clears a blackboard key (or keys), simply if you want to do this after an action without making a subtype +/datum/ai_behavior/clear_key + +/datum/ai_behavior/clear_key/perform(seconds_per_tick, datum/ai_controller/controller, list/to_clear) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/clear_key/finish_action(datum/ai_controller/controller, succeeded, list/to_clear) + . = ..() + if (!to_clear) + return + if (!islist(to_clear)) + to_clear = list(to_clear) + for (var/key in to_clear) + controller.clear_blackboard_key(key) diff --git a/code/datums/ai/basic_mobs/targeting_strategies/_targeting_strategy.dm b/code/datums/ai/basic_mobs/targeting_strategies/_targeting_strategy.dm index dbd2028081b..5720602c6fc 100644 --- a/code/datums/ai/basic_mobs/targeting_strategies/_targeting_strategy.dm +++ b/code/datums/ai/basic_mobs/targeting_strategies/_targeting_strategy.dm @@ -13,6 +13,12 @@ target_hiding_location = target.loc return target_hiding_location +/// Simply always returns true if you have a target, so only use this if you're pre-checking the targets somewhere else +/datum/targeting_strategy/anything + +/datum/targeting_strategy/anything/can_attack(mob/living/living_mob, atom/target, vision_range) + return !!target + ///A very simple targeting strategy that checks that the target is a valid fishing spot. /datum/targeting_strategy/fishing diff --git a/code/datums/components/hat_stabilizer.dm b/code/datums/components/hat_stabilizer.dm index b7d1a4a3ce2..909eba12a2f 100644 --- a/code/datums/components/hat_stabilizer.dm +++ b/code/datums/components/hat_stabilizer.dm @@ -195,7 +195,7 @@ /datum/component/hat_stabilizer/proc/on_hat_movement(obj/hat, mob/user) SIGNAL_HANDLER - remove_hat(user) + remove_hat(ismob(user) ? user : null) /datum/component/hat_stabilizer/proc/on_secondary_attack_hand(datum/source, mob/user) SIGNAL_HANDLER diff --git a/code/datums/diseases/verminous_plague.dm b/code/datums/diseases/verminous_plague.dm new file mode 100644 index 00000000000..8e56f7fcc08 --- /dev/null +++ b/code/datums/diseases/verminous_plague.dm @@ -0,0 +1,76 @@ +GLOBAL_LIST_INIT(cursed_vermin_by_stage, list( + "1" = list( // Mostly harmless + /mob/living/basic/butterfly = 1, + /mob/living/basic/cockroach = 4, + /mob/living/basic/cockroach/bloodroach = 1, + /mob/living/basic/frog = 2, + /mob/living/basic/mouse = 2, + /mob/living/basic/snail = 2, + /mob/living/basic/spider/maintenance = 1, + ), + "2" = list( // Deal damage but not that dangerous + /mob/living/basic/cockroach/bloodroach = 1, + /mob/living/basic/cockroach/sewer = 1, + /mob/living/basic/bee = 4, + /mob/living/basic/frog = 3, + /mob/living/basic/frog/crazy = 2, + /mob/living/basic/mouse/rat = 4, + /mob/living/basic/snail/angry = 2, + ), + "3" = list( // More annoying + /mob/living/basic/bee/toxin = 4, + /mob/living/basic/cockroach/hauberoach = 3, + /mob/living/basic/frog/crazy = 3, + /mob/living/basic/mouse/rat = 3, + /mob/living/basic/spider/growing/spiderling/hunter = 1, + ), +)) + +/// Makes you cough out rats, bugs, etc +/datum/disease/verminous_plague + name = "Verminous Plague" + desc = "You can't stop germinating small animals." + form = "Affliction" + agent = "Wizard's Curse" + cure_text = "Exorcism" + viable_mobtypes = list(/mob/living/carbon/human) + cures = list(/datum/reagent/consumable/salt, /datum/reagent/water/holywater) + spread_flags = DISEASE_SPREAD_CONTACT_SKIN + severity = DISEASE_SEVERITY_MEDIUM + max_stages = 3 + stage_prob = 2 + needs_all_cures = FALSE + /// Chance to spawn something + var/spawn_chance = 5 + +/datum/disease/verminous_plague/stage_act(seconds_per_tick) + . = ..() + if (SPT_PROB(spawn_chance, seconds_per_tick)) + spawn_mob() + +/// Creates a pest +/datum/disease/verminous_plague/proc/spawn_mob() + var/creating_type = pick_weight(GLOB.cursed_vermin_by_stage["[stage]"]) + var/mob/living/created = new creating_type(affected_mob.drop_location()) + if (QDELETED(created)) + return + + var/obj/item/organ/lungs = affected_mob.get_organ_slot(ORGAN_SLOT_LUNGS) + if (lungs && affected_mob.emote("cough")) + affected_mob.visible_message(span_warning("[affected_mob] coughs out [created]!")) + lungs.apply_organ_damage(stage * 2) + else + affected_mob.vomit(VOMIT_CATEGORY_BLOOD_STUNLESS | MOB_VOMIT_FORCE) + affected_mob.visible_message(span_warning("[affected_mob] vomits out [created]!")) + + if (stage == 1 || !created.ai_controller) + return + + created.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, affected_mob) + created.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, affected_mob) + +/datum/disease/verminous_plague/update_stage(new_stage) + var/was_stage = stage + . = ..() + if (new_stage > was_stage) + spawn_mob() diff --git a/code/datums/storage/storage.dm b/code/datums/storage/storage.dm index 4198d41535b..7779f559e90 100644 --- a/code/datums/storage/storage.dm +++ b/code/datums/storage/storage.dm @@ -499,7 +499,8 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches) to_insert.forceMove(real_location) if(get(real_location, /mob) != user) to_insert.do_pickup_animation(real_location, user) - item_insertion_feedback(user, to_insert, override) + if (messages) + item_insertion_feedback(user, to_insert, override) parent.update_appearance() return TRUE diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 50d9dab5244..020a7bbe118 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -466,7 +466,8 @@ worn_icon_state = "soulstonebelt" storage_type = /datum/storage/wands_belt -/obj/item/storage/belt/wands/full/PopulateContents() +/// Put some wands in that bad boy +/obj/item/storage/belt/wands/full/proc/create_wands() new /obj/item/gun/magic/wand/death(src) new /obj/item/gun/magic/wand/resurrection(src) new /obj/item/gun/magic/wand/polymorph(src) @@ -475,10 +476,96 @@ new /obj/item/gun/magic/wand/fireball(src) new /obj/item/gun/magic/wand/shrink(src) +/obj/item/storage/belt/wands/full/PopulateContents() + create_wands() for(var/obj/item/gun/magic/wand/W in contents) //All wands in this pack come in the best possible condition W.max_charges = initial(W.max_charges) W.charges = W.max_charges +/// Filled with budget wands instead of cool ones +/obj/item/storage/belt/wands/full/discount + /// Which wands can appear? + var/static/list/possible_options = list( + /obj/item/gun/magic/wand/animate, + /obj/item/gun/magic/wand/babel, + /obj/item/gun/magic/wand/bald, + /obj/item/gun/magic/wand/door, + /obj/item/gun/magic/wand/fireball, + /obj/item/gun/magic/wand/freeze, + /obj/item/gun/magic/wand/hallucination, + /obj/item/gun/magic/wand/levitate, + /obj/item/gun/magic/wand/pax, + /obj/item/gun/magic/wand/pizza, + /obj/item/gun/magic/wand/plague, + /obj/item/gun/magic/wand/prank, + /obj/item/gun/magic/wand/rebel, + /obj/item/gun/magic/wand/repulse, + /obj/item/gun/magic/wand/swap, + /obj/item/gun/magic/wand/teleport, + /obj/item/gun/magic/wand/tentacles, + /obj/item/gun/magic/wand/zap, + ) + +/obj/item/storage/belt/wands/full/discount/create_wands() + var/list/available_options = possible_options.Copy() + for (var/i in 1 to 6) + if (!length(available_options)) + break + var/wand_path = pick_n_take(available_options) + new wand_path(src) + +/// Not a subtype of bandolier because it acts pretty differently +/obj/item/storage/belt/wand_bandolier + name = "wand bandolier" + desc = "A bandolier for holding a whole lot of wands. If worn on your suit, swaps expended wands for fresh ones on the fly." + icon_state = "bandolier" + inhand_icon_state = "bandolier" + worn_icon_state = "bandolier" + storage_type = /datum/storage/wands_belt + w_class = WEIGHT_CLASS_NORMAL + +/obj/item/storage/belt/wand_bandolier/equipped(mob/user, slot) + . = ..() + if (!(slot & ITEM_SLOT_SUITSTORE)) + return + ADD_CLOTHING_TRAIT(user, TRAIT_GUNFLIP) + RegisterSignal(user, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fired)) + +/obj/item/storage/belt/wand_bandolier/dropped(mob/user) + . = ..() + REMOVE_CLOTHING_TRAIT(user, TRAIT_GUNFLIP) + UnregisterSignal(user, COMSIG_MOB_FIRED_GUN) + +/// After we fire a gun, check if it's a wand. If it is and it's empty, do a swap +/obj/item/storage/belt/wand_bandolier/proc/on_gun_fired(mob/living/wizard, obj/item/gun/magic/wand/old_wand) + SIGNAL_HANDLER + INVOKE_ASYNC(src, PROC_REF(equip_new_wand), wizard, old_wand) + +/obj/item/storage/belt/wand_bandolier/proc/equip_new_wand(mob/living/wizard, obj/item/gun/magic/wand/old_wand) + if (!istype(old_wand)) + return + if (!atom_storage.real_location.contents.len) // no other wands + return + if (old_wand.charges > 1) // It hasn't subtracted yet + return + + var/obj/item/fresh_wand + for (var/obj/item/gun/magic/wand/checked in atom_storage.real_location.contents) + if (checked.charges) + fresh_wand = checked + break + if (!fresh_wand || fresh_wand.on_found()) + return // Nothing to swap with + + wizard.temporarilyRemoveItemFromInventory(old_wand) + if (!wizard.put_in_hands(fresh_wand)) + return + to_chat(wizard, span_notice("You quickly draw [fresh_wand].")) + if (atom_storage.attempt_insert(old_wand, wizard)) + return + old_wand.forceMove(wizard.drop_location()) + to_chat(wizard, span_warning("...and drop [old_wand] on the ground.")) + /obj/item/storage/belt/janitor name = "janibelt" desc = "A belt used to hold most janitorial supplies." diff --git a/code/game/objects/structures/mystery_box.dm b/code/game/objects/structures/mystery_box.dm index 7b71200fb47..28700113381 100644 --- a/code/game/objects/structures/mystery_box.dm +++ b/code/game/objects/structures/mystery_box.dm @@ -77,6 +77,7 @@ GLOBAL_LIST_INIT(mystery_magic, list( /obj/item/gun/magic/wand/door, /obj/item/gun/magic/wand/nothing, /obj/item/storage/belt/wands/full, + /obj/item/storage/belt/wands/full/discount, /obj/item/gun/magic/staff/healing, /obj/item/gun/magic/staff/chaos, /obj/item/gun/magic/staff/door, diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm index 9a55eecd953..609f626bb05 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm @@ -117,13 +117,36 @@ /datum/spellbook_entry/item/wands name = "Wand Assortment" desc = "A collection of wands that allow for a wide variety of utility. \ - Wands have a limited number of charges, so be conservative with their use. Comes in a handy belt." + Wands have a limited number of charges, so be conservative with their use. \ + Comes in a handy belt, or a fancy bandolier if you are already wearing one." item_path = /obj/item/storage/belt/wands/full category = SPELLBOOK_CATEGORY_DEFENSIVE /datum/spellbook_entry/item/wands/try_equip_item(mob/living/carbon/human/user, obj/item/to_equip) - var/was_equipped = user.equip_to_slot_if_possible(to_equip, ITEM_SLOT_BELT, disable_warning = TRUE) - to_chat(user, span_notice("\A [to_equip.name] has been summoned [was_equipped ? "on your waist" : "at your feet"].")) + if (!istype(user.belt, /obj/item/storage/belt/wands)) + var/was_equipped = user.equip_to_slot_if_possible(to_equip, ITEM_SLOT_BELT, disable_warning = TRUE) + to_chat(user, span_notice("\A [to_equip.name] has been summoned [was_equipped ? "on your waist" : "at your feet"].")) + return + + // If you already have a wand belt you get a cool bandolier instead for your copious amount of wands + var/obj/item/storage/belt/wand_bandolier/bandolier = new(user.drop_location()) + + for (var/obj/item/wand_presumably in to_equip.atom_storage.real_location) + bandolier.atom_storage.attempt_insert(wand_presumably, user, messages = FALSE) + + qdel(to_equip) + + var/was_equipped = user.equip_to_slot_if_possible(bandolier, ITEM_SLOT_SUITSTORE, disable_warning = TRUE) + to_chat(user, span_notice("\A [bandolier.name] has been summoned [was_equipped ? "across your chest" : "at your feet"].")) + +/datum/spellbook_entry/item/wands/discount + name = "Wand Assortment (Bargain Bin)" + desc = "A random collection of wands sourced from apprentice wandmaking studies. \ + You're never quite sure what you're going to get. \ + Comes in a handy belt, or a fancy bandolier if you are already wearing one." + cost = 1 + item_path = /obj/item/storage/belt/wands/full/discount + category = SPELLBOOK_CATEGORY_DEFENSIVE /datum/spellbook_entry/item/armor name = "Mastercrafted Armor Set" diff --git a/code/modules/antagonists/wizard/equipment/wiz_robe.dm b/code/modules/antagonists/wizard/equipment/wiz_robe.dm index cae9a388ae0..26e6d85690a 100644 --- a/code/modules/antagonists/wizard/equipment/wiz_robe.dm +++ b/code/modules/antagonists/wizard/equipment/wiz_robe.dm @@ -119,7 +119,7 @@ inhand_icon_state = "wizrobe" body_parts_covered = CHEST|GROIN|ARMS|LEGS armor_type = /datum/armor/suit_wizrobe - allowed = list(/obj/item/teleportation_scroll, /obj/item/highfrequencyblade/wizard) + allowed = list(/obj/item/highfrequencyblade/wizard, /obj/item/storage/belt/wand_bandolier, /obj/item/teleportation_scroll) flags_inv = HIDEJUMPSUIT strip_delay = 5 SECONDS equip_delay_other = 5 SECONDS diff --git a/code/modules/mob/living/basic/illusion/illusion.dm b/code/modules/mob/living/basic/illusion/illusion.dm index 0a1a5fa3195..97a61e653f0 100644 --- a/code/modules/mob/living/basic/illusion/illusion.dm +++ b/code/modules/mob/living/basic/illusion/illusion.dm @@ -72,12 +72,16 @@ appearance = original.appearance setDir(original.dir) - maxHealth = hp - updatehealth() // re-cap health to new value + if (isnull(hp)) + maxHealth = hp + updatehealth() // re-cap health to new value - melee_damage_lower = damage - melee_damage_upper = damage - multiply_chance = replicate + if (isnull(damage)) + melee_damage_lower = damage + melee_damage_upper = damage + + if (isnull(replicate)) + multiply_chance = replicate remove_faction(FACTION_NEUTRAL) transform = initial(transform) diff --git a/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm b/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm index 854c74d30ec..c6140302bf8 100644 --- a/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm +++ b/code/modules/mob/living/basic/ruin_defender/mimic/mimic.dm @@ -411,10 +411,13 @@ GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list( return TRUE /mob/living/basic/mimic/copy/ranged/CopyObject(obj/item/gun/original, mob/living/creator, destroy_original = 0) - if(..()) - obj_damage = 0 - melee_damage_upper = original.force - melee_damage_lower = original.force - max(0, (original.force / 2)) + . = ..() + if(!.) + return + obj_damage = 0 + melee_damage_upper = original.force + melee_damage_lower = original.force - max(0, (original.force / 2)) + RegisterSignal(original, COMSIG_GUN_REPLENISHED_CHARGE, PROC_REF(on_regained_charge)) /mob/living/basic/mimic/copy/ranged/CopyObjectVisuals(obj/original) name = original.name @@ -424,4 +427,9 @@ GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list( /mob/living/basic/mimic/copy/ranged/can_use_guns(obj/item/gun) return TRUE +/// If our gun ran out of ammo but then regenerated it then we should go back to shooting +/mob/living/basic/mimic/copy/ranged/proc/on_regained_charge() + SIGNAL_HANDLER + ai_controller?.set_blackboard_key(BB_GUNMIMIC_GUN_EMPTY, FALSE) + #undef CANT_INSERT_FULL diff --git a/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm b/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm index e1b664e852a..9e3475b7a6a 100644 --- a/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm +++ b/code/modules/mob/living/basic/ruin_defender/mimic/mimic_ai.dm @@ -87,3 +87,30 @@ return controller.queue_behavior(/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions. + +/// Special subtree for living wands/staffs of animation which will focus on animating more things +/datum/ai_controller/basic_controller/mimic_copy/gun/animator + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_HUNT_TARGETING_STRATEGY = /datum/targeting_strategy/anything, + BB_GUNMIMIC_GUN_EMPTY = FALSE, + ) + planning_subtrees = list( + /datum/ai_planning_subtree/shoot_animatable_objects, + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/random_speech/when_has_target/mimic/gun, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) + +/// Try to find objects and then shoot them +/datum/ai_planning_subtree/shoot_animatable_objects + +/datum/ai_planning_subtree/shoot_animatable_objects/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if(controller.blackboard[BB_GUNMIMIC_GUN_EMPTY]) + return // No charge in our gun + if(!controller.blackboard_key_exists(BB_CURRENT_HUNTING_TARGET)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/animatable, BB_CURRENT_HUNTING_TARGET) + return + controller.queue_behavior(/datum/ai_behavior/ranged_skirmish, BB_CURRENT_HUNTING_TARGET, BB_HUNT_TARGETING_STRATEGY, null, 9, 0) + controller.queue_behavior(/datum/ai_behavior/clear_key, BB_CURRENT_HUNTING_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING diff --git a/code/modules/mob/living/basic/space_fauna/carp/magicarp.dm b/code/modules/mob/living/basic/space_fauna/carp/magicarp.dm index 1eccb697b59..9e2224de1fc 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/magicarp.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/magicarp.dm @@ -2,38 +2,47 @@ GLOBAL_LIST_INIT(magicarp_spell_types, list( /obj/projectile/magic/animate = "dancing", /obj/projectile/magic/arcane_barrage = "arcane", + /obj/projectile/magic/babel = "babbling", /obj/projectile/magic/change = "transforming", /obj/projectile/magic/death = "grim", /obj/projectile/magic/door = "unbarred", /obj/projectile/magic/fireball = "blazing", + /obj/projectile/magic/freeze = "frigid", + /obj/projectile/magic/levitate = "weightless", /obj/projectile/magic/resurrection = "vital", /obj/projectile/magic/spellblade = "vorpal", + /obj/projectile/magic/swap = "quantum", /obj/projectile/magic/teleport = "warping", - /obj/projectile/magic/babel = "babbling", )) /// A reduced list of spells for magicarp spawned in xenobiology, less disruptive GLOBAL_LIST_INIT(xenobiology_magicarp_spell_types, list( /obj/projectile/magic/animate = "dancing", - /obj/projectile/magic/teleport = "warping", + /obj/projectile/magic/arcane_barrage = "arcane", /obj/projectile/magic/door = "unbarred", /obj/projectile/magic/fireball = "blazing", + /obj/projectile/magic/freeze = "frigid", + /obj/projectile/magic/levitate = "weightless", /obj/projectile/magic/spellblade = "vorpal", - /obj/projectile/magic/arcane_barrage = "arcane", + /obj/projectile/magic/swap = "quantum", + /obj/projectile/magic/teleport = "warping", )) /// Associative list of magicarp spells to colours, expand this list if you expand the other lists GLOBAL_LIST_INIT(magicarp_spell_colours, list( /obj/projectile/magic/animate = COLOR_CARP_RUSTY, /obj/projectile/magic/arcane_barrage = COLOR_CARP_PURPLE, + /obj/projectile/magic/babel = COLOR_CARP_BROWN, /obj/projectile/magic/change = COLOR_CARP_PINK, /obj/projectile/magic/death = COLOR_CARP_DARK_BLUE, /obj/projectile/magic/door = COLOR_CARP_GREEN, /obj/projectile/magic/fireball = COLOR_CARP_RED, + /obj/projectile/magic/freeze = COLOR_CARP_TURQUOISE, + /obj/projectile/magic/levitate = COLOR_CARP_DARK_PURPLE, /obj/projectile/magic/resurrection = COLOR_CARP_PALE_GREEN, /obj/projectile/magic/spellblade = COLOR_CARP_SILVER, + /obj/projectile/magic/swap = COLOR_CARP_LIGHT_BLUE, /obj/projectile/magic/teleport = COLOR_CARP_GRAPE, - /obj/projectile/magic/babel = COLOR_CARP_BROWN, )) /** diff --git a/code/modules/mod/mod_theme.dm b/code/modules/mod/mod_theme.dm index 69887ce7ade..3656cb5718f 100644 --- a/code/modules/mod/mod_theme.dm +++ b/code/modules/mod/mod_theme.dm @@ -1551,9 +1551,10 @@ inbuilt_modules = list(/obj/item/mod/module/anti_magic/wizard) hearing_protection = EAR_PROTECTION_NORMAL allowed_suit_storage = list( - /obj/item/teleportation_scroll, - /obj/item/highfrequencyblade/wizard, /obj/item/gun/magic, + /obj/item/highfrequencyblade/wizard, + /obj/item/storage/belt/wand_bandolier, + /obj/item/teleportation_scroll, ) variants = list( "enchanted" = list( diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 6819e359a16..843f0fcb741 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -464,8 +464,9 @@ balloon_alert(user, "trigger locked, firing pin needed!") return FALSE +/// Called to put ammo back in a gun which recharges itself, should call super if successful /obj/item/gun/proc/recharge_newshot() - return + SEND_SIGNAL(src, COMSIG_GUN_REPLENISHED_CHARGE) /obj/item/gun/proc/process_burst(mob/living/user, atom/target, message = TRUE, params=null, zone_override = "", random_spread = 0, burst_spread_mult = 0, iteration = 0) if(!user || !firing_burst) diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm index e7910d37be5..dccee957e4c 100644 --- a/code/modules/projectiles/guns/energy.dm +++ b/code/modules/projectiles/guns/energy.dm @@ -200,6 +200,7 @@ /obj/item/gun/energy/recharge_newshot(no_cyborg_drain) if (!ammo_type || !cell) return + if(use_cyborg_cell && !no_cyborg_drain) if(iscyborg(loc)) var/mob/living/silicon/robot/robot = loc @@ -207,12 +208,15 @@ var/obj/item/ammo_casing/energy/shot = ammo_type[select] //Necessary to find cost of shot if(robot.cell.use(shot.e_cost)) //Take power from the borg... cell.give(shot.e_cost) //... to recharge the shot + return ..() + if(!chambered) var/obj/item/ammo_casing/energy/AC = ammo_type[select] if(cell.charge >= AC.e_cost) //if there's enough power in the cell cell... chambered = AC //...prepare a new shot based on the current ammo type selected if(!chambered.loaded_projectile) chambered.newshot() + return ..() /obj/item/gun/energy/handle_chamber() if(chambered && !chambered.loaded_projectile) //if loaded_projectile is null, i.e the shot has been fired... diff --git a/code/modules/projectiles/guns/magic.dm b/code/modules/projectiles/guns/magic.dm index 2119b904e92..f32d53cae10 100644 --- a/code/modules/projectiles/guns/magic.dm +++ b/code/modules/projectiles/guns/magic.dm @@ -107,8 +107,10 @@ return charges /obj/item/gun/magic/recharge_newshot() - if (charges && chambered && !chambered.loaded_projectile) - chambered.newshot() + if (!charges || !chambered || chambered.loaded_projectile) + return + chambered.newshot() + return ..() /obj/item/gun/magic/handle_chamber() if(chambered && !chambered.loaded_projectile) //if BB is null, i.e the shot has been fired... diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm index 79f5cfb4fd2..308a6c87884 100644 --- a/code/modules/projectiles/guns/magic/staff.dm +++ b/code/modules/projectiles/guns/magic/staff.dm @@ -113,6 +113,12 @@ user.death() // If you got put into crit by the mobs we'll finish you off return MANUAL_SUICIDE +/obj/item/gun/magic/staff/animate/animate_atom_living(mob/living/owner) + var/mob/living/basic/mimic/copy/ranged/living_staff = new(drop_location(), src, owner) + QDEL_NULL(living_staff.ai_controller) + living_staff.ai_controller = new /datum/ai_controller/basic_controller/mimic_copy/gun/animator(living_staff) + return living_staff + /// Heals people and even raises the dead /obj/item/gun/magic/staff/healing name = "staff of healing" @@ -188,6 +194,7 @@ /obj/projectile/magic/animate, /obj/projectile/magic/antimagic, /obj/projectile/magic/arcane_barrage, + /obj/projectile/magic/babel, /obj/projectile/magic/bounty, /obj/projectile/magic/change, /obj/projectile/magic/death, @@ -195,15 +202,20 @@ /obj/projectile/magic/fetch, /obj/projectile/magic/fireball, /obj/projectile/magic/flying, + /obj/projectile/magic/freeze, + /obj/projectile/magic/levitate, /obj/projectile/magic/locker, /obj/projectile/magic/necropotence, + /obj/projectile/magic/plague, + /obj/projectile/magic/rebellion, /obj/projectile/magic/resurrection, - /obj/projectile/magic/babel, + /obj/projectile/magic/shrink, /obj/projectile/magic/spellblade, + /obj/projectile/magic/swap, /obj/projectile/magic/teleport, + /obj/projectile/magic/tentacle, /obj/projectile/magic/wipe, - /obj/projectile/temp/chill, - /obj/projectile/magic/shrink + /obj/projectile/temp/chill ) /obj/item/gun/magic/staff/chaos/unrestricted diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm index 43efeda1e4f..e6280bde5f3 100644 --- a/code/modules/projectiles/guns/magic/wand.dm +++ b/code/modules/projectiles/guns/magic/wand.dm @@ -356,6 +356,19 @@ . = ..() return SHAME +// Animating a nothing wand makes it into an animating wand (and also animates it) +/obj/item/gun/magic/wand/nothing/animate_atom_living(mob/living/owner) + var/obj/item/gun/magic/wand/animate/animated_wand = new() + animated_wand.charges = charges + animated_wand.name = name + "?" + + var/mob/living/basic/mimic/copy/ranged/living_wand = new(drop_location(), animated_wand, owner, TRUE) // It's already got eyes + QDEL_NULL(living_wand.ai_controller) + living_wand.ai_controller = new /datum/ai_controller/basic_controller/mimic_copy/gun/animator(living_wand) + + qdel(src) + return living_wand + /// Also wand of doing fuck all /obj/item/gun/magic/wand/nothing/fake_resurrection name = "holy staff" @@ -365,6 +378,9 @@ base_icon_state = "revivewand" ammo_type = /obj/item/ammo_casing/magic +/obj/item/gun/magic/wand/nothing/fake_resurrection/animate_atom_living(mob/living/owner) + return new /mob/living/basic/mimic/copy/ranged(drop_location(), src, owner) + /// Wand of making things small /obj/item/gun/magic/wand/shrink name = "wand of shrinking" diff --git a/code/modules/projectiles/guns/magic/wands/wand_animate.dm b/code/modules/projectiles/guns/magic/wands/wand_animate.dm new file mode 100644 index 00000000000..367962180ce --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_animate.dm @@ -0,0 +1,56 @@ + +/// Wand of making objects alive +/obj/item/gun/magic/wand/animate + name = "wand of animation" + desc = "This wand is attuned to life and will animate effected objects into destructive servants." + school = SCHOOL_EVOCATION + ammo_type = /obj/item/ammo_casing/magic/animate + icon_state = "animatewand" + base_icon_state = "animatewand" + fire_sound = 'sound/effects/magic/staff_animation.ogg' + max_charges = 10 + +/obj/item/gun/magic/wand/animate/zap_self(mob/living/user, suicide = FALSE) + . = ..() + to_chat(user, span_warning("You animate one of your own possessions!")) + charges-- + var/list/possessions = user.get_equipped_items() + if (!length(possessions)) + to_chat(user, span_notice("...but you don't have any.")) + return + + var/obj/some_item = pick(possessions) + user.dropItemToGround(some_item) + some_item.animate_atom_living(user) + +/obj/item/gun/magic/wand/animate/do_suicide(mob/living/user) + charges-- + if (!iscarbon(user)) + user.visible_message(span_suicide("... but [user] is already animate!")) + return SHAME + var/mob/living/carbon/suicider = user + var/obj/item/animate_part = suicider.get_organ_slot(ORGAN_SLOT_BRAIN) || suicider.get_bodypart(BODY_ZONE_HEAD) + if (!animate_part) + return SHAME + + var/turf/destination = user.drop_location() + + if (isorgan(animate_part)) + var/obj/item/organ/brain = animate_part + brain.Remove(user, special = FALSE) + else + var/obj/item/bodypart/head = animate_part + head.dismember(BRUTE) + + animate_part.forceMove(destination) + animate_part.animate_atom_living() + if (user.stat != DEAD) + return SHAME + user.set_suicide(TRUE) + return MANUAL_SUICIDE + +/obj/item/gun/magic/wand/animate/animate_atom_living(mob/living/owner) + var/mob/living/basic/mimic/copy/ranged/living_wand = new(drop_location(), src, owner, TRUE) // It's already got eyes + QDEL_NULL(living_wand.ai_controller) + living_wand.ai_controller = new /datum/ai_controller/basic_controller/mimic_copy/gun/animator(living_wand) + return living_wand diff --git a/code/modules/projectiles/guns/magic/wands/wand_babel.dm b/code/modules/projectiles/guns/magic/wands/wand_babel.dm new file mode 100644 index 00000000000..973ae6a6b78 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_babel.dm @@ -0,0 +1,24 @@ +/** + * Scrambles the languages of someone you shoot. + */ +/obj/item/gun/magic/wand/babel + name = "rod of babel" + desc = "The incredible power of this wand causes victims to forget all of the languages they know, and learn a new one." + school = SCHOOL_TRANSMUTATION + ammo_type = /obj/item/ammo_casing/magic/babel + icon_state = "babelwand" + base_icon_state = "babelwand" + fire_sound = 'sound/effects/magic/staff_change.ogg' + max_charges = 6 + +/obj/item/gun/magic/wand/babel/zap_self(mob/living/user, suicide = FALSE) + . = ..() + charges-- + if (HAS_TRAIT(user, TRAIT_TOWER_OF_BABEL)) + return + curse_of_babel(user) + +/obj/item/gun/magic/wand/babel/do_suicide(mob/living/user) + . = ..() + user.say("Someone please kill me!", forced = "failed babel wand suicide") + return SHAME diff --git a/code/modules/projectiles/guns/magic/wands/wand_bald.dm b/code/modules/projectiles/guns/magic/wands/wand_bald.dm new file mode 100644 index 00000000000..1d35bd05d8e --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_bald.dm @@ -0,0 +1,137 @@ +/// Makes you bald, or emphasises your baldness +/obj/item/gun/magic/wand/bald + name = "shearing rod" + desc = "A wand commonly used in wizard collegiate hazing, renders victims bald or draws significant attention to their baldness." + school = SCHOOL_TRANSMUTATION + ammo_type = /obj/item/ammo_casing/magic/bald + icon_state = "shavingwand" + base_icon_state = "shavingwand" + fire_sound = 'sound/items/tools/welder2.ogg' + max_charges = 12 + +/obj/item/gun/magic/wand/bald/zap_self(mob/living/user, suicide) + . = ..() + if (!suicide) + visible_message(span_notice("[user] gives [user.p_themselves()] a quick shave.")) + var/obj/projectile/magic/bald/trimmer = new(user.drop_location()) + trimmer.firer = user + user.projectile_hit(trimmer, BODY_ZONE_HEAD) + qdel(trimmer) + +/obj/item/gun/magic/wand/bald/do_suicide(mob/living/carbon/human/user) + var/obj/item/bodypart/head/dome = user.get_bodypart(BODY_ZONE_HEAD) + if (!dome || ((dome.head_flags & HEAD_HAIR) && user.hairstyle != "Bald")) + . = ..() + visible_message(span_suicide("[user] desperately attempts to shave [user.p_themselves()] in a cry for help.")) + return SHAME + + charges-- + user.apply_status_effect(/datum/status_effect/bald_flare) + dome.dismember(wounding_type = WOUND_SLASH, silent = TRUE) + visible_message(span_suicide("As the light fades, it becomes clear that [user] has shaved off [user.p_their()] entire head.")) + +/obj/item/gun/magic/wand/bald/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread) + zone_override = BODY_ZONE_HEAD // Head only + return ..() + +/obj/item/ammo_casing/magic/bald + projectile_type = /obj/projectile/magic/bald + +/// Embaldens people, or turns their bald spot into a reflective surface +/obj/projectile/magic/bald + name = "bolt of baldness" + icon = 'icons/obj/cosmetic.dmi' + icon_state = "razor" + damage = 5 + damage_type = BRUTE + accuracy_falloff = 0 + +// Currently we have no facility for shaving nonhuman mobs +/obj/projectile/magic/bald/on_hit(mob/living/carbon/human/target, blocked, pierce_hit) + if (!isliving(target)) + return ..() + + var/obj/item/bodypart/head/dome = target.get_bodypart(BODY_ZONE_HEAD) + if (iscarbon(target) && !dome) + return BULLET_ACT_FORCE_PIERCE // No head no shave + . = ..() + if (. == BULLET_ACT_BLOCK || !istype(target) || blocked >= 100) + return + + // First look for wigs or hats + var/obj/item/hat = target.head // Head here means what's on it, not what it is + + if (hat) + if (!istype(hat, /obj/item/clothing/head/wig)) // If we're not a wig, check if there's a wig on our hat + var/obj/item/clothing/head/wig/attached_wig = locate() in hat + hat = attached_wig || hat + + if (istype(hat, /obj/item/clothing/head/wig)) + hat.forceMove(target.drop_location()) + hat.deconstruct(FALSE) + if (QDELETED(hat)) // IDK maybe it's disagreed + visible_message(span_warning("[target]'s \the [hat] is shredded by [src]!")) + log_combat(firer, target, "magically destroyed wig", src) + return + + if(hat.flags_inv & HIDEHAIR) + var/obj/item/clothing/clothing_hat = hat // Not all hats are clothing + if (!istype(clothing_hat) || !(clothing_hat.clothing_flags & SNUG_FIT)) + visible_message(span_warning("[hat] is knocked off [target]'s head!")) + target.dropItemToGround(hat) + return + + // Weird mutant hair + var/obj/item/organ/mutant_hair = target.get_organ_slot(ORGAN_SLOT_EXTERNAL_POD_HAIR) + if (mutant_hair) + mutant_hair.Remove(target, special = FALSE) + qdel(mutant_hair) + target.Knockdown(1 SECONDS) + visible_message(span_warning("[target]'s hair is instantly shaved away by [src]!")) + log_combat(firer, target, "magically shaved bald", src) + return + + // Finally normal hair + if ((dome?.head_flags & HEAD_HAIR) && target.hairstyle != "Bald") + target.set_hairstyle("Bald") + target.Knockdown(1 SECONDS) + visible_message(span_warning("[target]'s hair is instantly shaved away by [src]!")) + log_combat(firer, target, "magically shaved bald", src) + return + + target.apply_status_effect(/datum/status_effect/bald_flare) + +#define BALD_RAYS_FILTER "bald_ray" + +/// Mostly just handles animation +/datum/status_effect/bald_flare + id = "bald_flare" + alert_type = null + duration = 1.6 SECONDS + +/datum/status_effect/bald_flare/on_apply() + . = ..() + owner.visible_message(span_warning("[owner]'s bald head releases a bright flash of light!")) + for(var/mob/living/viewers in (viewers(3, owner) - owner)) + viewers.flash_act() + + var/mob/living/carbon/human/human_owner = owner + var/height = max((human_owner.mob_height || 0) - 2, 0) + + owner.add_filter(name = BALD_RAYS_FILTER, priority = 1, params = list( + type = "rays", + y = height, + size = 10, + color = COLOR_VERY_SOFT_YELLOW, + density = 20 + )) + + var/ray_filter = owner.get_filter(BALD_RAYS_FILTER) + animate(ray_filter, size = 30, time = duration * 0.85) + animate(size = 0, time = duration * 0.15) + animate(ray_filter, offset = 1.5, time = duration, flags = ANIMATION_PARALLEL) + +/datum/status_effect/bald_flare/on_remove() + owner.remove_filter(BALD_RAYS_FILTER) + +#undef BALD_RAYS_FILTER diff --git a/code/modules/projectiles/guns/magic/wands/wand_booze.dm b/code/modules/projectiles/guns/magic/wands/wand_booze.dm new file mode 100644 index 00000000000..68ca560eb84 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_booze.dm @@ -0,0 +1,61 @@ +/** + * Wand which makes you drunk, self-explanatory + * As it applies a copious amount of alcohol it's actually surprisingly deadly, but not very fast at being deadly + */ + +/obj/item/gun/magic/wand/booze + name = "party wand" + desc = "A wand which fills its target with copious amounts of alcohol. Use in moderation, unless you are \ + trying to give someone liver damage." + school = SCHOOL_CONJURATION + ammo_type = /obj/item/ammo_casing/magic/booze + icon_state = "beerwand" + base_icon_state = "beerwand" + fire_sound = 'sound/effects/slosh.ogg' + max_charges = 8 + +/obj/item/gun/magic/wand/booze/zap_self(mob/living/user, suicide = FALSE) + playsound(user, fire_sound, 50, TRUE) + if (!suicide) + user.log_message("zapped [user.p_them()]self with a [src]", LOG_ATTACK) + user.visible_message(span_notice("[user] puts the [src] up to [user.p_their()] mouth and starts chugging!")) + var/obj/projectile/magic/booze/splash = new(user.drop_location()) + splash.firer = user + user.projectile_hit(splash, BODY_ZONE_HEAD) + qdel(splash) + charges-- + +/obj/item/gun/magic/wand/booze/do_suicide(mob/living/user) + . = ..() + user.visible_message(span_suicide("[user] pours a constant stream of booze into [user.p_their()] mouth, [user.p_theyre()] about to pop!")) + if (!do_after(user, 3 SECONDS, target = src)) + return SHAME + user.Stun(5 SECONDS, ignore_canstun = TRUE) + user.inflate_gib(DROP_ALL_REMAINS, gib_time = 2.5 SECONDS) + addtimer(CALLBACK(src, PROC_REF(flood_beer), get_turf(user)), 2.5 SECONDS, TIMER_DELETE_ME) + return MANUAL_SUICIDE + +/// Creates a beer blast when someone bursts themselves +/obj/item/gun/magic/wand/booze/proc/flood_beer(turf/flood_point) + if (!flood_point) + return + var/datum/reagents/beer = new /datum/reagents(100) + beer.add_reagent(/datum/reagent/consumable/ethanol/beer, 100) + beer.my_atom = flood_point + beer.create_foam(/datum/effect_system/fluid_spread/foam, 100) + +/obj/item/ammo_casing/magic/booze + projectile_type = /obj/projectile/magic/booze + +/obj/projectile/magic/booze + name = "bolt of inebriation" + icon_state = "energy" + +/obj/projectile/magic/booze/Initialize(mapload) + . = ..() + create_reagents(max_vol = 10) + reagents.add_reagent(/datum/reagent/consumable/ethanol/bacchus_blessing, 10) + +/obj/projectile/magic/booze/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + reagents.trans_to(target, 10, methods = INGEST) diff --git a/code/modules/projectiles/guns/magic/wands/wand_freeze.dm b/code/modules/projectiles/guns/magic/wands/wand_freeze.dm new file mode 100644 index 00000000000..126ccf9e313 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_freeze.dm @@ -0,0 +1,73 @@ +/** + * Freezing wand places you in an ice cube and creates ice turfs + */ +/obj/item/gun/magic/wand/freeze + name = "wand of ice" + desc = "The chilling power of this wand will stop your enemies in their tracks." + school = SCHOOL_EVOCATION + ammo_type = /obj/item/ammo_casing/magic/freeze + icon_state = "icewand" + base_icon_state = "icewand" + fire_sound = 'sound/effects/magic/blink.ogg' + max_charges = 8 + +/obj/item/gun/magic/wand/freeze/zap_self(mob/living/user, suicide = FALSE) + . = ..() + to_chat(user, span_warning("You freeze yourself in a block of ice!")) + var/obj/projectile/magic/freeze/ice = new(user.drop_location()) + ice.firer = user + user.projectile_hit(ice, BODY_ZONE_CHEST) + qdel(ice) + charges-- + +/obj/item/gun/magic/wand/freeze/do_suicide(mob/living/user) + charges-- + playsound(user, fire_sound, 50, TRUE) + var/obj/structure/statue/snow/snowman/snover = new(user.drop_location()) + snover.name = user.real_name + var/obj/item/organ/brain = user.get_organ_slot(ORGAN_SLOT_BRAIN) + if (brain) + brain.Remove(user) + brain.forceMove(snover) + user.unequip_everything() + user.ghostize() + qdel(user) + return MANUAL_SUICIDE + +/obj/item/ammo_casing/magic/freeze + projectile_type = /obj/projectile/magic/freeze + harmful = FALSE + +/obj/projectile/magic/freeze + name = "bolt of freezing" + icon_state = "ice_1" + /// Temperature change to apply to hit mob + var/temperature = -350 + +/obj/projectile/magic/freeze/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/turf/hit_turf = get_turf(target) + if (isfloorturf(hit_turf) && !isspaceturf(hit_turf) && !isindestructiblefloor(hit_turf)) + hit_turf.ChangeTurf(/turf/open/floor/fakeice/slippery, flags = CHANGETURF_INHERIT_AIR) + + if(isobj(target)) + var/obj/thingy = target + + if(thingy.reagents) + var/datum/reagents/reagents = thingy.reagents + reagents?.expose_temperature(temperature) + return + + var/mob/living/victim = target + if (!istype(victim)) + return + + victim.apply_status_effect(/datum/status_effect/ice_block_talisman, 10 SECONDS) + + var/applied_temp = (1 - blocked) * temperature + if(iscarbon(target)) + var/mob/living/carbon/chill_dude = target + var/thermal_protection = 1 - chill_dude.get_insulation_protection(chill_dude.bodytemperature + temperature) + applied_temp = (thermal_protection * temperature) + temperature + + victim.adjust_bodytemperature(applied_temp) diff --git a/code/modules/projectiles/guns/magic/wands/wand_hallucination.dm b/code/modules/projectiles/guns/magic/wands/wand_hallucination.dm new file mode 100644 index 00000000000..87f31314c5c --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_hallucination.dm @@ -0,0 +1,77 @@ +#define WIZARD_WAND_HALLUCINATIONS list(\ + /datum/hallucination/death,\ + /datum/hallucination/death/dust,\ + /datum/hallucination/delusion/preset/carp,\ + /datum/hallucination/delusion/preset/corgi,\ + /datum/hallucination/delusion/preset/cyborg,\ + /datum/hallucination/delusion/preset/ghost,\ + /datum/hallucination/delusion/preset/monkey,\ + /datum/hallucination/delusion/preset/skeleton,\ + /datum/hallucination/delusion/preset/syndies,\ + /datum/hallucination/delusion/preset/zombie,\ + /datum/hallucination/fire,\ + /datum/hallucination/hazard/lava,\ + /datum/hallucination/hazard/chasm,\ + /datum/hallucination/hazard/anomaly,\ + /datum/hallucination/ice,\ + /datum/hallucination/oh_yeah,\ + /datum/hallucination/shock,\ + /datum/hallucination/xeno_attack,\ +) + +/** + * Hallucination wand looks exactly like a bolt of chaos except whatever happens isn't real + * One of the few times people might actually fall for hallucinations of being set on fire or turned into a xenomorph + * Even if they don't, several of these are minor stuns anyway + */ +/obj/item/gun/magic/wand/hallucination + name = "wand of chaos" + desc = "A wand which spits bolts of hallucinogenic magic which can do almost anything, or at least make the victim think so." + school = SCHOOL_FORBIDDEN + ammo_type = /obj/item/ammo_casing/magic/hallucination + icon_state = "chaoswand" + base_icon_state = "chaoswand" + fire_sound = 'sound/effects/magic/staff_chaos.ogg' + max_charges = 20 + +/obj/item/gun/magic/wand/hallucination/zap_self(mob/living/user, suicide = FALSE) + . = ..() + var/datum/hallucination/picked_hallucination = pick(WIZARD_WAND_HALLUCINATIONS) + user.cause_hallucination(picked_hallucination, "wand") + charges-- + +/obj/item/gun/magic/wand/hallucination/do_suicide(mob/living/user) + charges-- + playsound(user, fire_sound, 50, TRUE) + var/mob/living/basic/illusion/mirage/mirage = new(get_turf(src)) + mirage.mock_as(user, 15 SECONDS) + mirage.AddElement(/datum/element/content_barfer) + + qdel(mirage.ai_controller) + mirage.ai_controller = new /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles(mirage) + + var/list/our_stuff = user.unequip_everything() + for (var/atom/movable/thing in our_stuff) + thing.forceMove(mirage) + user.ghostize() + qdel(user) + return MANUAL_SUICIDE + +/obj/item/ammo_casing/magic/hallucination + projectile_type = /obj/projectile/magic/hallucination + harmful = FALSE + +/obj/projectile/magic/hallucination + // Eagle eyed players may be aware that there's no such thing as a real bolt of chaos projectile + name = "bolt of chaos" + icon_state = "ice_1" + +/obj/projectile/magic/hallucination/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/mob/living/carbon/victim = target + if (!istype(victim)) + return + var/datum/hallucination/picked_hallucination = pick(WIZARD_WAND_HALLUCINATIONS) + victim.cause_hallucination(picked_hallucination, "wand") + +#undef WIZARD_WAND_HALLUCINATIONS diff --git a/code/modules/projectiles/guns/magic/wands/wand_levitate.dm b/code/modules/projectiles/guns/magic/wands/wand_levitate.dm new file mode 100644 index 00000000000..4466a7e7425 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_levitate.dm @@ -0,0 +1,68 @@ +/** + * Levitation wand applies anti-gravity to target. + */ +/obj/item/gun/magic/wand/levitate + name = "lifting rod" + desc = "The power of this rod lifts living creatures off the ground, potentially leaving them unable to move." + school = SCHOOL_TRANSLOCATION + ammo_type = /obj/item/ammo_casing/magic/levitate + icon_state = "gravwand" + base_icon_state = "gravwand" + fire_sound = 'sound/effects/magic/repulse.ogg' + max_charges = 12 + +/obj/item/gun/magic/wand/levitate/zap_self(mob/living/user, suicide = FALSE) + . = ..() + user.apply_status_effect(/datum/status_effect/levitate) + +/obj/item/gun/magic/wand/levitate/do_suicide(mob/living/user) + if (!iscarbon(user)) + . = ..() + return SHAME + + charges-- + playsound(user, fire_sound, 50, TRUE) + user.visible_message(span_suicide("[user] inverts gravity inside of [user.p_their()] body!")) + var/mob/living/carbon/organ_haver = user + for (var/obj/item/organ/organ as anything in organ_haver.organs) + organ.Remove(user) + organ.forceMove(user.drop_location()) + organ.AddElement(/datum/element/forced_gravity, -1) + + var/obj/item/bodypart/chest = user.get_bodypart(BODY_ZONE_CHEST) + chest.dismember() + + return MANUAL_SUICIDE // I think every carbon dies if you remove all the organs from its body + +/obj/item/ammo_casing/magic/levitate + projectile_type = /obj/projectile/magic/levitate + harmful = FALSE + +/obj/projectile/magic/levitate + name = "bolt of levitation" + icon_state = "bluespace" + +/obj/projectile/magic/levitate/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/mob/living/victim = target + if (!istype(victim)) + return + victim.apply_status_effect(/datum/status_effect/levitate) + +/datum/status_effect/levitate + id = "levitated" + status_type = STATUS_EFFECT_REPLACE + duration = 2 MINUTES + alert_type = null + +/datum/status_effect/levitate/on_apply() + owner.visible_message(span_warning("[owner] floats into the air!")) + owner.AddElement(/datum/element/forced_gravity, 0) + owner.add_filter("antigrav_glow", 2, list("type" = "outline", "color" = "#de3aff48", "size" = 2)) + return ..() + +/datum/status_effect/levitate/on_remove() + owner.visible_message(span_notice("[owner] gently descends to the ground")) + owner.RemoveElement(/datum/element/forced_gravity, 0) + owner.remove_filter("antigrav_glow") + return ..() diff --git a/code/modules/projectiles/guns/magic/wands/wand_pax.dm b/code/modules/projectiles/guns/magic/wands/wand_pax.dm new file mode 100644 index 00000000000..9bd221056b4 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_pax.dm @@ -0,0 +1,67 @@ +/** + * Pax wand is a minor heal which applies temporary pacifism, gives you time to talk it out? + */ +/obj/item/gun/magic/wand/pax + name = "rod of compassion" + desc = "A wand which supernaturally connects victim and target, which renders both unable to fight and makes them feel a little better." + school = SCHOOL_RESTORATION + ammo_type = /obj/item/ammo_casing/magic/pax + icon_state = "peacewand" + base_icon_state = "peacewand" + fire_sound = 'sound/effects/emotes/kiss.ogg' + max_charges = 12 + +/obj/item/gun/magic/wand/pax/fire_gun(atom/target, mob/living/user, flag, params) + . = ..() + if (!.) + return + user.apply_status_effect(/datum/status_effect/pacify/visible, 30 SECONDS) // Don't miss! + user.adjust_brute_loss(-30) + +/obj/item/gun/magic/wand/pax/zap_self(mob/living/user, suicide = FALSE) + if (!suicide) + user.visible_message(span_notice("[user] tenderly kisses [user.p_their()] own wand.")) + +/obj/item/gun/magic/wand/pax/do_suicide(mob/living/user) + . = ..() + user.visible_message(span_suicide("...but [user] can't bring [user.p_themselves()] to hurt anyone!")) + return SHAME + +/obj/item/ammo_casing/magic/pax + projectile_type = /obj/projectile/magic/pax + harmful = FALSE + +/obj/projectile/magic/pax + name = "bolt of compassion" + icon = 'icons/mob/simple/animal.dmi' + icon_state = "heart" + +/obj/projectile/magic/pax/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/mob/living/victim = target + if (!istype(victim)) + return + + victim.apply_status_effect(/datum/status_effect/pacify/visible, 30 SECONDS) + victim.adjust_brute_loss(-30) + +// Default pacify status effect has no screen alert but I think this should have one +/datum/status_effect/pacify/visible + alert_type = /atom/movable/screen/alert/status_effect/pacified + +/datum/status_effect/pacify/visible/on_apply() + if (!HAS_TRAIT(owner, TRAIT_PACIFISM)) + owner.visible_message(span_notice("[owner] seems to relax."), span_notice("You feel your muscles loosen and your will to fight melt away.")) + return ..() + +/datum/status_effect/pacify/visible/on_remove() + . = ..() + // Might have it from somewhere else + if (HAS_TRAIT(owner, TRAIT_PACIFISM)) + return + owner.visible_message(span_warning("[owner] suddenly tenses up."), span_notice("You suddenly remember that violence is an option.")) + +/atom/movable/screen/alert/status_effect/pacified + name = "Pacified" + desc = "You find yourself temporarily incapable of violence." + icon_state = "in_love" diff --git a/code/modules/projectiles/guns/magic/wands/wand_pizza.dm b/code/modules/projectiles/guns/magic/wands/wand_pizza.dm new file mode 100644 index 00000000000..f067abb2c8d --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_pizza.dm @@ -0,0 +1,87 @@ +/** + * Turns things you hit into pizza. + */ +/obj/item/gun/magic/wand/pizza + name = "wand of snacking" + desc = "The incredible power of this wand transforms certain objects and surfaces into edible pizza." + school = SCHOOL_TRANSMUTATION + ammo_type = /obj/item/ammo_casing/magic/pizza + icon_state = "pizzawand" + base_icon_state = "pizzawand" + fire_sound = 'sound/effects/magic/staff_change.ogg' + max_charges = 20 + +/obj/item/gun/magic/wand/pizza/zap_self(mob/living/user, suicide = FALSE) + to_chat(user, span_notice("You can't bring yourself to commit to a permanent transformation into pizza right now.")) + return + +/obj/item/gun/magic/wand/pizza/do_suicide(mob/living/user) + charges-- + playsound(user, fire_sound, 50, TRUE) + var/turf/user_loc = get_turf(user) + user.unequip_everything() + + new /obj/effect/particle_effect/fluid/smoke(user_loc) + + var/list/slice_types = subtypesof(/obj/item/food/pizzaslice) + var/special_slice = pick(slice_types) + var/obj/item/pizza = new special_slice(user_loc) + + pizza.name = "[user.real_name] slice" + user.ghostize() + qdel(user) + + return MANUAL_SUICIDE + +/obj/item/ammo_casing/magic/pizza + projectile_type = /obj/projectile/magic/pizza + harmful = FALSE + +/obj/projectile/magic/pizza + name = "bolt of snacking" + icon = 'icons/obj/food/pizza.dmi' + icon_state = "pizzamargherita" + +/obj/projectile/magic/pizza/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/turf/hit_turf = get_turf(target) + var/datum/dimension_theme/pizza/converter = new() + if(converter.can_convert(hit_turf)) + converter.apply_theme(hit_turf) + + var/mob/living/lunch_haver = target + if (!istype(lunch_haver)) + return + + var/list/slice_types = subtypesof(/obj/item/food/pizzaslice) + var/special_slice = pick(slice_types) + + var/obj/item/the_piz = new special_slice() + + if (!length(lunch_haver.held_items)) + the_piz.forceMove(lunch_haver.drop_location()) + else + if (!iscarbon(lunch_haver)) + if (lunch_haver.put_in_hands(the_piz)) + if (lunch_haver.get_active_held_item() != the_piz) + lunch_haver.swap_hand() + else if (!lunch_haver.put_in_active_hand(the_piz)) + if (istype(lunch_haver.get_active_held_item(), /obj/item/food/pizzaslice)) + the_piz.forceMove(lunch_haver.drop_location()) + else + // If you've got arms and refused to pick up pizza then woe be upon you + var/obj/item/bodypart/my_arm = lunch_haver.get_active_hand() + + if (my_arm && my_arm.dismember()) + the_piz.forceMove(lunch_haver.drop_location()) + else + // I would love to use plaintext_zone but what if they don't have an arm? + var/hand_index = IS_LEFT_INDEX(lunch_haver.active_hand_index) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM + var/hand_zone = IS_LEFT_INDEX(lunch_haver.active_hand_index) ? "left arm" : "right arm" + + var/mob/living/carbon/edward_pizza_hands = lunch_haver + edward_pizza_hands.make_item_prosthetic(the_piz, hand_index) + edward_pizza_hands.visible_message(span_warning("[edward_pizza_hands]'s [hand_zone] is transformed into \a [the_piz]!")) + + lunch_haver.set_combat_mode(FALSE) // You can't eat pizza if you're on combat mode + the_piz.attack(lunch_haver, lunch_haver) diff --git a/code/modules/projectiles/guns/magic/wands/wand_plague.dm b/code/modules/projectiles/guns/magic/wands/wand_plague.dm new file mode 100644 index 00000000000..8e9991f9f24 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_plague.dm @@ -0,0 +1,50 @@ +/// Makes the target cough out frogs and locusts and such +/obj/item/gun/magic/wand/plague + name = "pestilent wand" + desc = "A vile implement which forces its victims' bodies to disgorge frogs and insects." + school = SCHOOL_CONJURATION + ammo_type = /obj/item/ammo_casing/magic/plague + icon_state = "plaguewand" + base_icon_state = "plaguewand" + fire_sound = 'sound/effects/smoke.ogg' + max_charges = 6 + +/obj/item/gun/magic/wand/plague/zap_self(mob/living/user, suicide) + . = ..() + var/obj/projectile/magic/plague/germ = new(user.drop_location()) + germ.firer = user + user.projectile_hit(germ, BODY_ZONE_CHEST) + qdel(germ) + charges-- + +/obj/item/gun/magic/wand/plague/do_suicide(mob/living/user) + . = ..() + + new /obj/effect/temp_visual/circle_wave/bioscrambler(get_turf(src)) + var/datum/disease/verminous_plague/curse = new() + for(var/mob/living/carbon/human/infectee in (hearers(2, user) - user)) + infectee.ContactContractDisease(curse) + + return TOXLOSS + +/obj/item/ammo_casing/magic/plague + projectile_type = /obj/projectile/magic/plague + +/// Makes the target sick with frogs +/obj/projectile/magic/plague + name = "pestilent bolt" + icon_state = "blastwave" + damage = 20 + damage_type = TOX + +/obj/projectile/magic/plague/on_hit(mob/living/carbon/human/target, blocked, pierce_hit) + . = ..() + if (. == BULLET_ACT_BLOCK || !istype(target) || blocked >= 100) + return + target.ContactContractDisease(new /datum/disease/verminous_plague()) + var/datum/disease/verminous_plague/curse = locate() in target.diseases + if (!QDELETED(curse)) + if (curse.stage < 2) + curse.update_stage(2) + else + curse.spawn_mob() diff --git a/code/modules/projectiles/guns/magic/wands/wand_prank.dm b/code/modules/projectiles/guns/magic/wands/wand_prank.dm new file mode 100644 index 00000000000..955edfaef58 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_prank.dm @@ -0,0 +1,86 @@ +/** + * Prank wand knocks you over as if pied and surrounds you with space lube + */ +/obj/item/gun/magic/wand/prank + name = "wand of pratfalls" + desc = "A comedy wand which is sure to get a laugh out of you, if not its victims." + school = SCHOOL_CONJURATION + ammo_type = /obj/item/ammo_casing/magic/prank + icon_state = "bananawand" + base_icon_state = "bananawand" + fire_sound = 'sound/items/bikehorn.ogg' + max_charges = 10 + +/obj/item/gun/magic/wand/prank/zap_self(mob/living/user, suicide = FALSE) + . = ..() + var/obj/item/food/pie/cream/magical/pie = new(src) + pie.stun_and_blur(user) + charges-- + +/obj/item/gun/magic/wand/prank/do_suicide(mob/living/user) + charges-- + playsound(user, fire_sound, 50, TRUE) + user.visible_message("[user] covers [user.p_themselves()] with magical lube!") + var/datum/reagents/lube = new /datum/reagents(40) + lube.add_reagent(/datum/reagent/lube, 40) + lube.my_atom = get_turf(user) + lube.create_foam(/datum/effect_system/fluid_spread/foam, 40) + user.slip(5 SECONDS) + user.apply_status_effect(/datum/status_effect/slippery_death) + return MANUAL_SUICIDE + +/obj/item/ammo_casing/magic/prank + projectile_type = /obj/projectile/magic/prank + harmful = FALSE + +/obj/projectile/magic/prank + name = "bolt of pratfalls" + icon = 'icons/obj/food/piecake.dmi' + icon_state = "pie" + +/obj/projectile/magic/prank/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/obj/item/food/pie/cream/magical/pie = new() + pie.stun_and_blur(target) + +/// This creates an extremely messy cream pie which your audience will love +/obj/item/food/pie/cream/magical + +/obj/item/food/pie/cream/magical/stun_and_blur(mob/living/victim, can_splat_on) + . = ..() + var/datum/reagents/lube = new /datum/reagents(40) + lube.add_reagent(/datum/reagent/lube, 40) + lube.my_atom = get_turf(victim) + lube.create_foam(/datum/effect_system/fluid_spread/foam, 40) + qdel(lube) + var/static/laugh_sound = list('sound/items/sitcom_laugh/SitcomLaugh1.ogg', 'sound/items/sitcom_laugh/SitcomLaugh2.ogg', 'sound/items/sitcom_laugh/SitcomLaugh3.ogg') + playsound(victim, pick(laugh_sound), 100, FALSE) + +/// Used by the wand suicide, keep slipping until you hit a wall and explode +/datum/status_effect/slippery_death + id = "slippery_death" + alert_type = null + tick_interval = 0.2 SECONDS + /// What direction are we slipping in? + var/direction + +/datum/status_effect/slippery_death/on_creation(mob/living/new_owner, set_duration) + . = ..() + direction = pick(GLOB.cardinals) + new_owner.setDir(direction) + new_owner.Stun(INFINITY, ignore_canstun = TRUE) + ADD_TRAIT(new_owner, TRAIT_IMMOBILIZED, REF(src)) + RegisterSignals(new_owner, list(COMSIG_MOVABLE_Z_CHANGED, COMSIG_LIVING_DEATH, COMSIG_LIVING_MOB_BUMP, COMSIG_LIVING_MOB_BUMPED), PROC_REF(die)) + +/datum/status_effect/slippery_death/tick(seconds_between_ticks) + var/turf/turf = get_step(owner, direction) + if(!turf || turf.is_blocked_turf()) + die() + return + owner.Move(turf) + +/// When we hit anything, die, or change z level we burst +/datum/status_effect/slippery_death/proc/die() + SIGNAL_HANDLER + owner.gib(DROP_ALL_REMAINS) + qdel(src) diff --git a/code/modules/projectiles/guns/magic/wands/wand_rebel.dm b/code/modules/projectiles/guns/magic/wands/wand_rebel.dm new file mode 100644 index 00000000000..74cabaf9d0f --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_rebel.dm @@ -0,0 +1,75 @@ +/** + * Detaches a limb and animates it, so that it attacks its former owner. + * Stop hitting yourself. Stop hitting yourself. + */ +/obj/item/gun/magic/wand/rebel + name = "wand of rebellion" + desc = "The dark power of this wand turns the victim's body against itself." + school = SCHOOL_NECROMANCY + ammo_type = /obj/item/ammo_casing/magic/rebellion + icon_state = "necrowand" + base_icon_state = "necrowand" + fire_sound = 'sound/effects/magic/wandodeath.ogg' + max_charges = 6 + +/obj/item/gun/magic/wand/rebel/zap_self(mob/living/user, suicide = FALSE) + . = ..() + charges-- + var/obj/projectile/magic/rebellion/blast = new() + if (suicide) + blast.valid_zones = list(BODY_ZONE_HEAD) + user.projectile_hit(blast, suicide ? BODY_ZONE_HEAD : BODY_ZONE_CHEST) + qdel(blast) + +/obj/item/gun/magic/wand/rebel/do_suicide(mob/living/user) + . = ..() + if (user.stat != DEAD) + return SHAME // God damn dullahans + return BRUTELOSS + +/obj/item/ammo_casing/magic/rebellion + projectile_type = /obj/projectile/magic/rebellion + +/obj/projectile/magic/rebellion + name = "bolt of rebellion" + icon_state = "soulslash" + damage = 15 + damage_type = BRUTE + /// Valid locations to shoot someone + var/list/valid_zones + +/obj/projectile/magic/rebellion/Initialize(mapload) + . = ..() + valid_zones = GLOB.limb_zones.Copy() // We don't want to mutate the global list + +/obj/projectile/magic/rebellion/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/mob/living/carbon/victim = target + if (!istype(victim)) + if (isliving(victim)) + victim.apply_damage(35, BRUTE, sharpness = SHARP_EDGED) + return + + var/obj/item/bodypart/slapper + + while (!slapper && length(valid_zones)) + var/zone = pick_n_take(valid_zones) + slapper = victim.get_bodypart(zone) + + if (!slapper) + victim.apply_damage(35, BRUTE, wound_bonus = 20, sharpness = SHARP_EDGED) + return + + var/atom/animate_item = slapper + if (slapper.bodypart_flags & BODYPART_PSEUDOPART) + animate_item = victim.get_item_for_held_index(slapper.held_index) + + slapper.dismember(silent = FALSE, wounding_type = WOUND_SLASH) + + if (QDELETED(animate_item)) + return + var/mob/living/bully = animate_item.animate_atom_living(firer) + if (!bully.ai_controller) + return + bully.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, victim) + bully.ai_controller.ai_interact(victim, combat_mode = TRUE) diff --git a/code/modules/projectiles/guns/magic/wands/wand_repulse.dm b/code/modules/projectiles/guns/magic/wands/wand_repulse.dm new file mode 100644 index 00000000000..5a3618559e0 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_repulse.dm @@ -0,0 +1,64 @@ +#define REPULSE_RANGE 5 + +/** + * Repulsion wand throws things backwards, might hurt them if they ram something + */ +/obj/item/gun/magic/wand/repulse + name = "rod of repulsion" + desc = "A wand which blasts things away from you." + school = SCHOOL_TRANSLOCATION + ammo_type = /obj/item/ammo_casing/magic/repulse + icon_state = "repulsionwand" + base_icon_state = "repulsionwand" + fire_sound = 'sound/effects/magic/repulse.ogg' + max_charges = 12 + +/obj/item/gun/magic/wand/repulse/zap_self(mob/living/user, suicide = FALSE) + . = ..() + user.visible_message(span_warning("[user] blasts [user.p_themselves()] into the ground!")) + user.adjust_brute_loss(30) + user.Paralyze(10 SECONDS) + +/obj/item/gun/magic/wand/repulse/suicide_act(mob/living/user) + if (!iscarbon(user)) + . = ..() + return BRUTELOSS + + playsound(user, fire_sound, 50, TRUE) + var/mob/living/carbon/suicider = user + + var/throw_dir = pick(GLOB.alldirs) + var/atom/start_location = user.drop_location() + var/turf/target_turf = get_edge_target_turf(start_location, throw_dir) + + user.Stun(2 SECONDS, ignore_canstun = TRUE) + for (var/obj/item/organ/water_balloon as anything in suicider.organs) + if (QDELETED(user)) + break // They might get dusted when one of these is pulled out or something + var/turf/throw_at = get_ranged_target_turf_direct(start_location, target_turf, range = REPULSE_RANGE, offset = rand(-20, 20)) + water_balloon.Remove(user) + water_balloon.forceMove(start_location) + water_balloon.safe_throw_at(throw_at, REPULSE_RANGE, 3, force = MOVE_FORCE_STRONG) + sleep(0.1 SECONDS) + return BRUTELOSS + +/obj/item/ammo_casing/magic/repulse + projectile_type = /obj/projectile/magic/repulse + +/obj/projectile/magic/repulse + name = "bolt of repulsion" + icon_state = "blastwave" + +/obj/projectile/magic/repulse/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/atom/movable/victim = target + if (!istype(victim)) + return + var/turf/throw_target = get_ranged_target_turf_direct(src, get_edge_target_turf(src, NORTH), REPULSE_RANGE, -angle) + victim.safe_throw_at(throw_target, REPULSE_RANGE, 3, force = MOVE_FORCE_STRONG) + var/mob/living/living_target = target + if (!istype(living_target)) + return + living_target.Paralyze(2 SECONDS) + +#undef REPULSE_RANGE diff --git a/code/modules/projectiles/guns/magic/wands/wand_swap.dm b/code/modules/projectiles/guns/magic/wands/wand_swap.dm new file mode 100644 index 00000000000..944958f6641 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_swap.dm @@ -0,0 +1,58 @@ +/** + * Swaps you with something you hit. + */ +/obj/item/gun/magic/wand/swap + name = "switching rod" + desc = "Exchanges the position of the wielder and an unanchored object they point at." + school = SCHOOL_TRANSLOCATION + ammo_type = /obj/item/ammo_casing/magic/swap + icon_state = "swapwand" + base_icon_state = "swapwand" + fire_sound = 'sound/effects/magic/swap.ogg' + max_charges = 20 + +/obj/item/gun/magic/wand/swap/zap_self(mob/living/user, suicide = FALSE) + . = ..() + to_chat(user, span_notice("You swap places with yourself! Amazing!")) + var/obj/effect/particle_effect/fluid/smoke/poof_in = new (get_turf(user)) + poof_in.lifetime = 5 DECISECONDS + charges-- + +/obj/item/gun/magic/wand/swap/do_suicide(mob/living/user) + . = ..() + user.visible_message(span_suicide("As the smoke clears, [user] is lying completely alive on [get_turf(user)].")) + return SHAME + +/obj/item/ammo_casing/magic/swap + projectile_type = /obj/projectile/magic/swap + harmful = FALSE + +/obj/projectile/magic/swap + name = "bolt of switching" + icon_state = "magicm" + projectile_phasing = PASSTABLE | PASSGLASS | PASSGRILLE + +/obj/projectile/magic/swap/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + var/atom/movable/hit_target = target + if (!istype(hit_target)) + return + if (hit_target.anchored) + do_sparks(3, FALSE, hit_target) + return + if (!firer) + return + + var/turf/my_turf = get_turf(firer) + var/turf/your_turf = get_turf(hit_target) + + if (!check_teleport_valid(firer, your_turf, TELEPORT_CHANNEL_MAGIC, my_turf) || !check_teleport_valid(hit_target, my_turf, TELEPORT_CHANNEL_MAGIC, your_turf)) + do_sparks(3, FALSE, hit_target) + return + + do_teleport(firer, your_turf, asoundin = 'sound/effects/magic/swap.ogg', channel = TELEPORT_CHANNEL_MAGIC) + do_teleport(hit_target, my_turf, asoundin = 'sound/effects/magic/swap.ogg', channel = TELEPORT_CHANNEL_MAGIC) + var/obj/effect/particle_effect/fluid/smoke/poof_in = new (my_turf) + poof_in.lifetime = 5 DECISECONDS + var/obj/effect/particle_effect/fluid/smoke/poof_out = new (your_turf) + poof_out.lifetime = 5 DECISECONDS diff --git a/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm b/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm new file mode 100644 index 00000000000..e01aa152f20 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_tentacle.dm @@ -0,0 +1,165 @@ +#define RETRACT_TIME (0.7 SECONDS) + +/** + * Immobilises people with tentacles. It's not a fetish. + */ +/obj/item/gun/magic/wand/tentacles + name = "restraining rod" + desc = "This wriggling wand binds its victims in a place for a time, although it doesn't stop them from shooting back." + school = SCHOOL_CONJURATION + ammo_type = /obj/item/ammo_casing/magic/tentacle + icon_state = "tentawand" + base_icon_state = "tentawand" + fire_sound = 'sound/effects/splat.ogg' + max_charges = 10 + +/obj/item/gun/magic/wand/tentacles/zap_self(mob/living/user, suicide) + . = ..() + var/tentacle_type = suicide ? /obj/effect/wizard_tentacle/suicide : /obj/effect/wizard_tentacle + var/turf/target_turf = get_turf(user) + if (!(locate(/obj/effect/wizard_tentacle) in target_turf)) + new tentacle_type(target_turf, user) + +/obj/item/gun/magic/wand/tentacles/do_suicide(mob/living/user) + . = ..() + return user.has_status_effect(/datum/status_effect/incapacitating/immobilized/wizard_tentacle/suicide) ? MANUAL_SUICIDE : SHAME + +/obj/item/ammo_casing/magic/tentacle + projectile_type = /obj/projectile/magic/tentacle + +/// Grabs the target for a while in an unwanted hug +/obj/projectile/magic/tentacle + name = "bolt of binding" + icon_state = "tentacle_end" + +/obj/projectile/magic/tentacle/on_hit(mob/living/target, blocked = 0, pierce_hit) + . = ..() + if (. == BULLET_ACT_BLOCK || !istype(target) || blocked >= 100) + return + var/turf/target_turf = get_turf(target) + if (!(locate(/obj/effect/wizard_tentacle) in target_turf)) + new /obj/effect/wizard_tentacle(get_turf(target), target) + +/// Different enough from the goliath tentacle to just be its own subtype +/obj/effect/wizard_tentacle + name = "tentacle" + icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' + icon_state = "goliath_tentacle_wiggle" + layer = BELOW_MOB_LAYER + plane = GAME_PLANE + anchored = TRUE + /// How long do we get grabbed? + var/grab_time = 20 SECONDS + /// Status effect we apply to victims + var/status_applied = /datum/status_effect/incapacitating/immobilized/wizard_tentacle + +/obj/effect/wizard_tentacle/Initialize(mapload, mob/living/victim) + . = ..() + if (isgroundlessturf(loc) || isnull(victim)) + return INITIALIZE_HINT_QDEL + flick("goliath_tentacle_spawn", src) + + if (victim.stat != DEAD && !HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE)) + balloon_alert(victim, "grabbed") + visible_message(span_danger("[src] grabs hold of [victim]!")) + victim.apply_damage(15, BRUTE) + if (victim.apply_status_effect(status_applied, grab_time, src)) + buckle_mob(victim, TRUE) + + if (has_buckled_mobs()) + addtimer(CALLBACK(src, PROC_REF(retract)), grab_time, TIMER_DELETE_ME) + else + retract() + +/obj/effect/wizard_tentacle/user_unbuckle_mob(mob/living/buckled_mob, mob/user) + if (buckled_mob == user) + balloon_alert(user, "can't reach!") + return + return ..() + +/// We're done now +/obj/effect/wizard_tentacle/proc/retract() + if (icon_state == "goliath_tentacle_retract") + return // Already retracting + icon_state = "goliath_tentacle_retract" + unbuckle_all_mobs(force = TRUE) + QDEL_IN(src, RETRACT_TIME) + +/// This one kills you +/obj/effect/wizard_tentacle/suicide + grab_time = 3 SECONDS // Something worse is going to happen + status_applied = /datum/status_effect/incapacitating/immobilized/wizard_tentacle/suicide + +/// Subtype of immobilise which is linked to a tentacle +/datum/status_effect/incapacitating/immobilized/wizard_tentacle + id = "tentacle_immobilized" + /// The tentacle that is tenderly holding us close + var/obj/effect/wizard_tentacle/tentacle + /// Can someone else help us out? + var/removable = TRUE + +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/on_creation(mob/living/new_owner, set_duration, obj/effect/wizard_tentacle/tentacle) + . = ..() + if (!.) + return + src.tentacle = tentacle + +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/on_apply() + . = ..() + if (removable) + RegisterSignal(owner, COMSIG_CARBON_PRE_MISC_HELP, PROC_REF(on_helped)) + RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), PROC_REF(release)) + RegisterSignal(tentacle, COMSIG_QDELETING, PROC_REF(on_tentacle_left)) + +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/on_remove() + . = ..() + UnregisterSignal(owner, list(COMSIG_CARBON_PRE_MISC_HELP, SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE))) + if (isnull(tentacle)) + return + UnregisterSignal(tentacle, COMSIG_QDELETING) + tentacle.retract() + tentacle = null + +/// Some kind soul has rescued us +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/proc/on_helped(mob/source, mob/helping) + SIGNAL_HANDLER + if (helping == owner) + owner.balloon_alert(owner, "can't reach!") + return NONE + source.visible_message(span_notice("[helping] rips [source] from the tentacle's grasp!")) + release() + return COMPONENT_BLOCK_MISC_HELP + +/// Something happened to make the tentacle let go +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/proc/release() + SIGNAL_HANDLER + qdel(src) + +/// Something happened to our associated tentacle +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/proc/on_tentacle_left() + SIGNAL_HANDLER + UnregisterSignal(tentacle, COMSIG_QDELETING) + tentacle = null + release() + +/// This one kills you +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/suicide + removable = FALSE + +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/suicide/on_apply() + . = ..() + owner.Stun(6 SECONDS, ignore_canstun = TRUE) + +/datum/status_effect/incapacitating/immobilized/wizard_tentacle/suicide/on_remove() + var/had_tentacle = !!tentacle + . = ..() + if (!owner || !had_tentacle) + return + owner.visible_message(span_suicide("The tentacle drags [owner] directly to hell!")) + owner.unequip_everything() + animate(owner, transform = matrix() * 0, time = RETRACT_TIME) + animate(owner, pixel_y = -12, time = RETRACT_TIME, flags = ANIMATION_PARALLEL) + addtimer(CALLBACK(owner, TYPE_PROC_REF(/mob, ghostize)), RETRACT_TIME - 1) + QDEL_IN(owner, RETRACT_TIME) + +#undef RETRACT_TIME diff --git a/code/modules/projectiles/guns/magic/wands/wand_zap.dm b/code/modules/projectiles/guns/magic/wands/wand_zap.dm new file mode 100644 index 00000000000..4858b0bf7c2 --- /dev/null +++ b/code/modules/projectiles/guns/magic/wands/wand_zap.dm @@ -0,0 +1,61 @@ +/** + * It's just a classic taser + */ +/obj/item/gun/magic/wand/zap + name = "wand of zapping" + desc = "This wand overloads the nerves of your enemies with potent lightning." + school = SCHOOL_EVOCATION + ammo_type = /obj/item/ammo_casing/magic/zap + icon_state = "teslawand" + base_icon_state = "teslawand" + fire_sound = 'sound/items/weapons/taser.ogg' + max_charges = 8 + +/obj/item/gun/magic/wand/zap/zap_self(mob/living/user, suicide = FALSE) + . = ..() + do_sparks(1, TRUE, src) + var/obj/projectile/magic/zap/spark = new(user.drop_location()) + spark.firer = user + user.projectile_hit(spark, BODY_ZONE_CHEST) + qdel(spark) + charges-- + +/obj/item/gun/magic/wand/zap/do_suicide(mob/living/user) + charges-- + playsound(user, fire_sound, 50, TRUE) + do_sparks(2, TRUE, user) + user.electrocute_act(0, src, siemens_coeff = 1, flags = SHOCK_IGNORE_IMMUNITY|SHOCK_DELAY_STUN|SHOCK_NOGLOVES) + tesla_zap(source = user, zap_range = 5, power = 2.5e4, cutoff = 1e3) + return FIRELOSS + +/obj/item/ammo_casing/magic/zap + projectile_type = /obj/projectile/magic/zap + +/// A magic taser electrode +/obj/projectile/magic/zap + name = "spark" + icon_state = "spark" + color = COLOR_YELLOW + paralyze = 10 SECONDS + stutter = 10 SECONDS + jitter = 40 SECONDS + hitsound = 'sound/items/weapons/taserhit.ogg' + range = 7 + tracer_type = /obj/effect/projectile/tracer/stun + muzzle_type = /obj/effect/projectile/muzzle/stun + impact_type = /obj/effect/projectile/impact/stun + +/obj/projectile/magic/zap/on_hit(mob/living/target, blocked = 0, pierce_hit) + . = ..() + if (!istype(target) || blocked >= 100) + do_sparks(1, TRUE, src) + return + + target.add_mood_event("tased", /datum/mood_event/tased) + SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK) // We really sending this signal from like 8 places huh? + if(!target.check_stun_immunity(CANKNOCKDOWN)) + addtimer(CALLBACK(target, TYPE_PROC_REF(/mob/living, do_jitter_animation), 20), 0.5 SECONDS) + +/obj/projectile/magic/zap/on_range() + do_sparks(1, TRUE, src) + return ..() diff --git a/code/modules/projectiles/guns/special/syringe_gun.dm b/code/modules/projectiles/guns/special/syringe_gun.dm index ca79d6d9800..ceecb89044b 100644 --- a/code/modules/projectiles/guns/special/syringe_gun.dm +++ b/code/modules/projectiles/guns/special/syringe_gun.dm @@ -52,6 +52,7 @@ if(!syringes.len) return chambered.newshot() + return ..() /obj/item/gun/syringe/can_shoot() return syringes.len diff --git a/code/modules/spells/spell_types/right_and_wrong.dm b/code/modules/spells/spell_types/right_and_wrong.dm index 10203bc7bf3..48422a26fdc 100644 --- a/code/modules/spells/spell_types/right_and_wrong.dm +++ b/code/modules/spells/spell_types/right_and_wrong.dm @@ -86,25 +86,42 @@ GLOBAL_LIST_INIT(summoned_magic, list( /obj/item/book/granter/action/spell/charge, /obj/item/book/granter/action/spell/summonitem, /obj/item/book/granter/action/spell/lightningbolt, - /obj/item/gun/magic/wand/nothing, + /obj/item/gun/magic/staff/babel, + /obj/item/gun/magic/staff/door, + /obj/item/gun/magic/staff/healing, + /obj/item/gun/magic/wand/animate, + /obj/item/gun/magic/wand/babel, + /obj/item/gun/magic/wand/bald, /obj/item/gun/magic/wand/death, - /obj/item/gun/magic/wand/resurrection, - /obj/item/gun/magic/wand/polymorph, - /obj/item/gun/magic/wand/teleport, /obj/item/gun/magic/wand/door, /obj/item/gun/magic/wand/fireball, - /obj/item/gun/magic/staff/healing, - /obj/item/gun/magic/staff/door, - /obj/item/gun/magic/staff/babel, + /obj/item/gun/magic/wand/freeze, + /obj/item/gun/magic/wand/hallucination, + /obj/item/gun/magic/wand/levitate, + /obj/item/gun/magic/wand/nothing, + /obj/item/gun/magic/wand/pax, + /obj/item/gun/magic/wand/pizza, + /obj/item/gun/magic/wand/plague, + /obj/item/gun/magic/wand/polymorph, + /obj/item/gun/magic/wand/prank, + /obj/item/gun/magic/wand/rebel, + /obj/item/gun/magic/wand/repulse, + /obj/item/gun/magic/wand/resurrection, + /obj/item/gun/magic/wand/swap, + /obj/item/gun/magic/wand/teleport, + /obj/item/gun/magic/wand/tentacles, + /obj/item/gun/magic/wand/zap, + /obj/item/immortality_talisman, + /obj/item/melee/ghost_sword, /obj/item/scrying, /obj/item/warp_whistle, - /obj/item/immortality_talisman, - /obj/item/melee/ghost_sword)) +)) GLOBAL_LIST_INIT(summoned_special_magic, list( /obj/item/gun/magic/staff/change, /obj/item/gun/magic/staff/animate, /obj/item/storage/belt/wands/full, + /obj/item/storage/belt/wands/full/discount, /obj/item/antag_spawner/contract, /obj/item/gun/magic/staff/chaos, /obj/item/necromantic_stone)) @@ -119,6 +136,7 @@ GLOBAL_LIST_INIT(summoned_magic_objectives, list( /obj/item/scrying, /obj/item/spellbook, /obj/item/storage/belt/wands/full, + /obj/item/storage/belt/wands/full/discount, /obj/item/warp_whistle)) /* diff --git a/code/modules/surgery/bodyparts/head.dm b/code/modules/surgery/bodyparts/head.dm index ce5e8c669e8..034c1f8e752 100644 --- a/code/modules/surgery/bodyparts/head.dm +++ b/code/modules/surgery/bodyparts/head.dm @@ -122,6 +122,13 @@ return null return list(species.skinned_type = 1) +/obj/item/bodypart/head/animate_atom_living(mob/living/owner) + var/mob/living/basic/animated = ..() + animated.attack_vis_effect = ATTACK_EFFECT_BITE + animated.attack_verb_continuous = "bites" + animated.attack_verb_simple = "bite" + return animated + /obj/item/bodypart/head/grind_results() return null diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm index 9b0f56c6a58..1eee423e1a9 100644 --- a/code/modules/surgery/bodyparts/parts.dm +++ b/code/modules/surgery/bodyparts/parts.dm @@ -252,6 +252,13 @@ var/atom/movable/screen/inventory/hand/hand_screen_object = owner.hud_used.hand_slots[held_index] hand_screen_object?.update_appearance() +/obj/item/bodypart/arm/animate_atom_living(mob/living/owner) + var/mob/living/basic/slapper = ..() + slapper.attack_vis_effect = ATTACK_EFFECT_PUNCH + slapper.attack_verb_continuous = "punches" + slapper.attack_verb_simple = "punch" + return slapper + /datum/status_effect/arm_speed_penalty id = "arm_speed_penalty" alert_type = null @@ -539,6 +546,13 @@ else if(!bodypart_disabled) owner.set_usable_legs(owner.usable_legs + 1) +/obj/item/bodypart/leg/animate_atom_living(mob/living/owner) + var/mob/living/basic/kicker = ..() + kicker.attack_vis_effect = ATTACK_EFFECT_KICK + kicker.attack_verb_continuous = "kicks" + kicker.attack_verb_simple = "kick" + return kicker + /obj/item/bodypart/leg/apply_ownership(mob/living/carbon/new_owner) . = ..() if(speed_modifier) diff --git a/icons/mob/clothing/belt_mirror.dmi b/icons/mob/clothing/belt_mirror.dmi index 06b8b4e1d9b..81eef910392 100644 Binary files a/icons/mob/clothing/belt_mirror.dmi and b/icons/mob/clothing/belt_mirror.dmi differ diff --git a/icons/obj/weapons/guns/magic.dmi b/icons/obj/weapons/guns/magic.dmi index 34256a71060..23ef6874986 100644 Binary files a/icons/obj/weapons/guns/magic.dmi and b/icons/obj/weapons/guns/magic.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 1b71e02f1a3..990fe7f869b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -972,6 +972,7 @@ #include "code\datums\ai\basic_mobs\generic_controllers.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\befriend_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\clear_key.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\climb_tree.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\emote_with_target.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\find_parent.dm" @@ -1473,6 +1474,7 @@ #include "code\datums\diseases\rhumba_beat.dm" #include "code\datums\diseases\transformation.dm" #include "code\datums\diseases\tuberculosis.dm" +#include "code\datums\diseases\verminous_plague.dm" #include "code\datums\diseases\weightlessness.dm" #include "code\datums\diseases\wizarditis.dm" #include "code\datums\diseases\advance\advance.dm" @@ -6031,6 +6033,22 @@ #include "code\modules\projectiles\guns\magic\arcane_barrage.dm" #include "code\modules\projectiles\guns\magic\staff.dm" #include "code\modules\projectiles\guns\magic\wand.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_animate.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_babel.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_bald.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_booze.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_freeze.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_hallucination.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_levitate.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_pax.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_pizza.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_plague.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_prank.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_rebel.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_repulse.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_swap.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_tentacle.dm" +#include "code\modules\projectiles\guns\magic\wands\wand_zap.dm" #include "code\modules\projectiles\guns\special\blastcannon.dm" #include "code\modules\projectiles\guns\special\chem_gun.dm" #include "code\modules\projectiles\guns\special\grenade_launcher.dm"