From d5ce7fd526c901d064b8042aa9ea0d248c6a5636 Mon Sep 17 00:00:00 2001 From: Szyszkrzyneczka Date: Thu, 19 Sep 2024 18:20:02 +0200 Subject: [PATCH 01/83] Taken all the files from the base pr Second commit will remove all the errors In the meantime pretty please tell me if you people want anything else to go with this pr --- beestation.dme | 179 +++- code/__DEFINES/actions.dm | 44 + code/__DEFINES/antagonists.dm | 6 + code/__DEFINES/dcs/signals/signals_action.dm | 35 + .../signals/signals_datum/signals_datum.dm | 3 - code/__DEFINES/dcs/signals/signals_heretic.dm | 18 + .../dcs/signals/signals_mob/signals_human.dm | 11 +- .../signals_obj/signals_item/signals_item.dm | 3 - code/__DEFINES/dcs/signals/signals_spell.dm | 93 ++ code/__DEFINES/is_helpers.dm | 3 + code/__DEFINES/magic.dm | 82 ++ code/__DEFINES/span.dm | 179 ++++ code/__DEFINES/traits.dm | 10 + code/__byond_version_compat.dm | 2 +- code/_compile_options.dm | 2 +- code/_globalvars/bitfields.dm | 9 + code/controllers/subsystem/augury.dm | 8 +- code/datums/action.dm | 830 --------------- code/datums/actions/action.dm | 256 +++++ code/datums/actions/cooldown_action.dm | 221 ++++ code/datums/actions/innate_action.dm | 84 ++ code/datums/actions/item_action.dm | 33 + code/datums/actions/items/adjust.dm | 7 + code/datums/actions/{ => items}/beam_rifle.dm | 0 code/datums/actions/items/berserk.dm | 19 + code/datums/actions/items/boot_dash.dm | 10 + code/datums/actions/items/cult_dagger.dm | 37 + code/datums/actions/items/hands_free.dm | 8 + code/datums/actions/{ => items}/ninja.dm | 0 code/datums/actions/items/organ_action.dm | 25 + code/datums/actions/items/set_internals.dm | 12 + code/datums/actions/items/stealth_box.dm | 55 + code/datums/actions/items/summon_stickmen.dm | 6 + code/datums/actions/items/toggles.dm | 112 ++ code/datums/actions/items/vortex_recall.dm | 15 + code/datums/actions/mobs/language_menu.dm | 13 + code/datums/actions/mobs/small_sprite.dm | 54 + code/datums/brain_damage/imaginary_friend.dm | 2 +- code/datums/components/cult_ritual_item.dm | 11 +- code/datums/components/manual_breathing.dm | 2 +- code/datums/components/seclight_attachable.dm | 4 +- code/datums/components/stationloving.dm | 4 +- code/datums/components/storage/storage.dm | 18 +- code/datums/martial/plasma_fist.dm | 5 +- code/datums/mind.dm | 50 - .../__mutations.dm} | 46 +- code/datums/mutations/actions.dm | 291 ------ code/datums/mutations/antenna.dm | 59 ++ code/datums/mutations/autonomy.dm | 43 + code/datums/mutations/cold.dm | 42 +- code/datums/mutations/fire_breath.dm | 96 ++ code/datums/mutations/olfaction.dm | 139 +++ code/datums/mutations/sight.dm | 63 +- code/datums/mutations/telepathy.dm | 10 + code/datums/mutations/tongue_spike.dm | 181 ++++ code/datums/mutations/touch.dm | 66 +- code/datums/mutations/void_magnet.dm | 43 + code/datums/mutations/webbing.dm | 52 + code/datums/proximity_monitor/timestop.dm | 207 ++++ code/datums/status_effects/neutral.dm | 8 +- code/game/objects/effects/decals/cleanable.dm | 14 + code/game/objects/effects/forcefields.dm | 36 +- code/game/objects/effects/phased_mob.dm | 88 ++ code/game/objects/items.dm | 76 +- code/game/objects/items/RCD.dm | 4 + code/game/objects/items/RCL.dm | 10 + code/game/objects/items/cards_ids.dm | 1 + code/game/objects/items/chainsaw.dm | 3 + code/game/objects/items/chromosome.dm | 6 +- code/game/objects/items/devices/multitool.dm | 4 +- code/game/objects/items/devices/spyglasses.dm | 3 + code/game/objects/items/granters.dm | 470 --------- code/game/objects/items/granters/_granters.dm | 106 ++ .../granters/crafting/_crafting_granter.dm | 11 + .../items/granters/crafting/bone_notes.dm | 20 + .../objects/items/granters/crafting/cannon.dm | 19 + .../items/granters/crafting/desserts.dm | 20 + .../items/granters/crafting/pipegun.dm | 19 + .../items/granters/magic/_spell_granter.dm | 93 ++ .../objects/items/granters/magic/barnyard.dm | 34 + .../objects/items/granters/magic/blind.dm | 19 + .../objects/items/granters/magic/charge.dm | 20 + .../objects/items/granters/magic/fireball.dm | 27 + .../objects/items/granters/magic/forcewall.dm | 20 + .../objects/items/granters/magic/knock.dm | 19 + .../game/objects/items/granters/magic/mime.dm | 28 + .../objects/items/granters/magic/mindswap.dm | 57 + .../items/granters/magic/sacredflame.dm | 14 + .../objects/items/granters/magic/smoke.dm | 26 + .../items/granters/magic/summon_item.dm | 19 + .../granters/martial_arts/_martial_arts.dm | 24 + .../items/granters/martial_arts/cqc.dm | 29 + .../granters/martial_arts/plasma_fist.dm | 35 + .../granters/martial_arts/sleeping_carp.dm | 35 + code/game/objects/items/granters/oragami.dm | 33 + code/game/objects/items/implants/implant.dm | 26 +- .../items/implants/implant_abductor.dm | 1 - .../objects/items/implants/implant_camera.dm | 2 +- .../objects/items/implants/implant_chem.dm | 2 +- .../objects/items/implants/implant_clown.dm | 2 +- .../items/implants/implant_deathrattle.dm | 2 +- .../objects/items/implants/implant_exile.dm | 2 +- .../items/implants/implant_explosive.dm | 4 + .../items/implants/implant_krav_maga.dm | 1 - .../items/implants/implant_mindshield.dm | 2 +- .../objects/items/implants/implant_misc.dm | 6 +- .../objects/items/implants/implant_spell.dm | 56 +- .../objects/items/implants/implant_track.dm | 2 +- .../objects/items/robot/robot_upgrades.dm | 2 + code/game/objects/items/scrolls.dm | 79 +- code/game/objects/items/signs.dm | 9 + code/game/objects/items/storage/holsters.dm | 1 + .../game/objects/items/storage/uplink_kits.dm | 6 +- code/game/objects/items/tanks/watertank.dm | 3 + .../structures/crates_lockers/closets.dm | 6 + code/modules/admin/admin_verbs.dm | 121 ++- code/modules/admin/battle_royale.dm | 2 +- .../antagonists/_common/antag_spawner.dm | 8 +- code/modules/antagonists/cult/blood_magic.dm | 86 +- code/modules/antagonists/cult/cult.dm | 60 +- code/modules/antagonists/cult/cult_comms.dm | 358 +++---- .../antagonists/fugitive/fugitive_outfits.dm | 5 +- .../antagonists/heretic/heretic_antag.dm | 21 +- .../antagonists/heretic/heretic_knowledge.dm | 32 +- .../antagonists/heretic/knowledge/ash_lore.dm | 16 +- .../heretic/knowledge/flesh_lore.dm | 11 +- .../heretic/knowledge/rust_lore.dm | 4 +- .../heretic/knowledge/side_flesh_void.dm | 2 +- .../heretic/knowledge/starting_lore.dm | 2 +- .../heretic/knowledge/void_lore.dm | 4 +- .../heretic/magic/aggressive_spread.dm | 53 +- .../heretic/magic/ash_ascension.dm | 201 ++-- .../antagonists/heretic/magic/ash_jaunt.dm | 36 +- .../antagonists/heretic/magic/blood_cleave.dm | 81 +- .../antagonists/heretic/magic/blood_siphon.dm | 95 +- .../heretic/magic/eldritch_blind.dm | 8 +- .../heretic/magic/eldritch_emplosion.dm | 12 +- .../heretic/magic/eldritch_shapeshift.dm | 8 +- .../heretic/magic/eldritch_telepathy.dm | 11 +- .../heretic/magic/flesh_ascension.dm | 110 +- .../heretic/magic/madness_touch.dm | 54 +- .../antagonists/heretic/magic/manse_link.dm | 113 +- .../antagonists/heretic/magic/mansus_grasp.dm | 148 ++- .../heretic/magic/nightwatcher_rebirth.dm | 81 +- .../antagonists/heretic/magic/rust_wave.dm | 72 +- .../antagonists/heretic/magic/void_phase.dm | 77 +- .../antagonists/heretic/magic/void_pull.dm | 73 +- code/modules/antagonists/revenant/revenant.dm | 21 +- .../revenant/revenant_abilities.dm | 478 ++++----- code/modules/antagonists/santa/santa.dm | 3 +- .../antagonists/slaughter/slaughter.dm | 99 +- .../antagonists/slaughter/slaughterevent.dm | 10 +- .../traitor/equipment/Malf_Modules.dm | 163 ++- .../antagonists/wizard/equipment/spellbook.dm | 821 --------------- .../equipment/spellbook_entries/_entry.dm | 232 +++++ .../equipment/spellbook_entries/assistance.dm | 107 ++ .../equipment/spellbook_entries/challenges.dm | 10 + .../equipment/spellbook_entries/defensive.dm | 148 +++ .../equipment/spellbook_entries/mobility.dm | 45 + .../equipment/spellbook_entries/summons.dm | 87 ++ .../wizard/equipment/wizard_spellbook.dm | 329 ++++++ code/modules/antagonists/wizard/wizard.dm | 142 +-- .../awaymissions/mission_code/Academy.dm | 53 +- code/modules/clothing/chameleon.dm | 15 +- code/modules/clothing/clothing.dm | 2 +- code/modules/clothing/glasses/_glasses.dm | 69 +- code/modules/clothing/glasses/hud.dm | 4 + code/modules/clothing/head/hardhat.dm | 7 + code/modules/clothing/masks/hailer.dm | 4 + .../clothing/spacesuits/_spacesuits.dm | 31 +- code/modules/clothing/spacesuits/plasmamen.dm | 9 +- code/modules/events/wizard/aid.dm | 66 +- code/modules/events/wizard/shuffle.dm | 35 +- code/modules/instruments/items.dm | 11 + code/modules/jobs/job_types/mime.dm | 83 +- .../mining/lavaland/necropolis_chests.dm | 4 +- code/modules/mining/minebot.dm | 8 +- code/modules/mob/living/bloodcrawl.dm | 180 ---- code/modules/mob/living/brain/brain.dm | 2 - code/modules/mob/living/carbon/alien/alien.dm | 6 +- .../carbon/alien/humanoid/alien_powers.dm | 501 +++++---- .../carbon/alien/humanoid/caste/drone.dm | 43 +- .../carbon/alien/humanoid/caste/praetorian.dm | 46 +- .../carbon/alien/humanoid/caste/sentinel.dm | 4 +- .../living/carbon/alien/humanoid/humanoid.dm | 6 +- .../mob/living/carbon/alien/humanoid/queen.dm | 160 +-- .../carbon/alien/humanoid/update_icons.dm | 8 +- .../mob/living/carbon/alien/larva/larva.dm | 11 +- .../mob/living/carbon/alien/larva/powers.dm | 136 ++- .../modules/mob/living/carbon/alien/organs.dm | 47 +- code/modules/mob/living/carbon/carbon.dm | 2 +- .../mob/living/carbon/human/inventory.dm | 51 +- .../carbon/human/species_types/golems.dm | 102 +- .../carbon/human/species_types/psyphoza.dm | 6 +- code/modules/mob/living/carbon/inventory.dm | 26 +- code/modules/mob/living/living.dm | 27 - code/modules/mob/living/living_defines.dm | 3 - code/modules/mob/living/login.dm | 3 - code/modules/mob/living/say.dm | 16 +- .../mob/living/simple_animal/constructs.dm | 165 +-- .../simple_animal/friendly/drone/inventory.dm | 6 + .../living/simple_animal/heretic_monsters.dm | 47 +- .../simple_animal/hostile/giant_spider.dm | 369 ++++--- .../living/simple_animal/hostile/statue.dm | 102 +- .../living/simple_animal/hostile/wizard.dm | 71 +- code/modules/mob/mob.dm | 73 +- code/modules/mob/mob_defines.dm | 14 +- code/modules/mob/say.dm | 10 +- .../computers/item/computer.dm | 11 +- code/modules/power/singularity/emitter.dm | 4 +- code/modules/projectiles/projectile/magic.dm | 238 +++-- .../chemistry/reagents/alcohol_reagents.dm | 63 +- .../nanites/nanite_programs/utility.dm | 2 +- .../xenobiology/crossbreeding/_misc.dm | 2 +- .../xenobiology/crossbreeding/_mobs.dm | 34 +- .../xenobiology/crossbreeding/burning.dm | 15 +- code/modules/spells/spell.dm | 979 +++++++----------- code/modules/spells/spell_types/aimed.dm | 184 ---- .../spell_types/aoe_spell/_aoe_spell.dm | 57 + .../spell_types/aoe_spell/area_conversion.dm | 25 + .../spells/spell_types/aoe_spell/knock.dm | 20 + .../spell_types/aoe_spell/magic_missile.dm | 47 + .../spells/spell_types/aoe_spell/repulse.dm | 87 ++ .../spell_types/aoe_spell/sacred_flame.dm | 39 + .../spells/spell_types/area_teleport.dm | 89 -- code/modules/spells/spell_types/barnyard.dm | 50 - code/modules/spells/spell_types/blind.dm | 49 - code/modules/spells/spell_types/bloodcrawl.dm | 36 - code/modules/spells/spell_types/charge.dm | 103 -- code/modules/spells/spell_types/cone/_cone.dm | 123 +++ .../modules/spells/spell_types/cone_spells.dm | 117 --- code/modules/spells/spell_types/conjure.dm | 103 -- .../spells/spell_types/conjure/_conjure.dm | 50 + .../spells/spell_types/conjure/bees.dm | 18 + .../spells/spell_types/conjure/carp.dm | 13 + .../spells/spell_types/conjure/constructs.dm | 20 + .../spells/spell_types/conjure/creatures.dm | 15 + .../spells/spell_types/conjure/cult_turfs.dm | 29 + .../spells/spell_types/conjure/ed_swarm.dm | 22 + .../spell_types/conjure/invisible_chair.dm | 34 + .../spell_types/conjure/invisible_wall.dm | 26 + .../spells/spell_types/conjure/link_words.dm | 15 + .../spells/spell_types/conjure/presents.dm | 14 + .../spells/spell_types/conjure/soulstone.dm | 30 + .../spells/spell_types/conjure/the_traps.dm | 35 + .../spell_types/conjure_item/_conjure_item.dm | 46 + .../spell_types/conjure_item/infinite_guns.dm | 41 + .../spell_types/conjure_item/invisible_box.dm | 42 + .../conjure_item/lightning_packet.dm | 38 + .../spell_types/conjure_item/snowball.dm | 8 + .../spells/spell_types/construct_spells.dm | 353 ------- code/modules/spells/spell_types/emplosion.dm | 17 - .../spells/spell_types/ethereal_jaunt.dm | 123 --- code/modules/spells/spell_types/explosion.dm | 16 - code/modules/spells/spell_types/forcewall.dm | 40 - code/modules/spells/spell_types/genetic.dm | 45 - code/modules/spells/spell_types/godhand.dm | 204 ---- .../spells/spell_types/infinite_guns.dm | 27 - .../spells/spell_types/jaunt/_jaunt.dm | 92 ++ .../spells/spell_types/jaunt/bloodcrawl.dm | 315 ++++++ .../spell_types/jaunt/ethereal_jaunt.dm | 256 +++++ .../spells/spell_types/jaunt/shadow_walk.dm | 82 ++ code/modules/spells/spell_types/knock.dm | 32 - code/modules/spells/spell_types/lichdom.dm | 160 --- code/modules/spells/spell_types/lightning.dm | 86 -- .../spell_types/list_targets/_list_targets.dm | 41 + .../spell_types/list_targets/telepathy.dm | 51 + code/modules/spells/spell_types/mime.dm | 242 ----- .../spells/spell_types/mind_transfer.dm | 108 -- .../spells/spell_types/personality_commune.dm | 36 - code/modules/spells/spell_types/pointed.dm | 75 -- .../spells/spell_types/pointed/_pointed.dm | 181 ++++ .../spell_types/pointed/abyssal_gaze.dm | 53 + .../spells/spell_types/pointed/barnyard.dm | 55 + .../spells/spell_types/pointed/blind.dm | 51 + .../spells/spell_types/pointed/dominate.dm | 49 + .../spells/spell_types/pointed/finger_guns.dm | 48 + .../spells/spell_types/pointed/fireball.dm | 23 + .../spell_types/pointed/lightning_bolt.dm | 43 + .../spell_types/pointed/mind_transfer.dm | 125 +++ .../spells/spell_types/pointed/spell_cards.dm | 82 ++ code/modules/spells/spell_types/projectile.dm | 130 --- .../projectile/_basic_projectile.dm | 29 + .../spell_types/projectile/juggernaut.dm | 12 + .../spells/spell_types/rightandwrong.dm | 18 +- code/modules/spells/spell_types/rod_form.dm | 56 - code/modules/spells/spell_types/santa.dm | 16 - .../spells/spell_types/self/basic_heal.dm | 27 + .../modules/spells/spell_types/self/charge.dm | 58 ++ .../spells/spell_types/self/disable_tech.dm | 30 + .../spells/spell_types/self/forcewall.dm | 66 ++ .../spells/spell_types/self/lichdom.dm | 83 ++ .../spells/spell_types/self/lightning.dm | 128 +++ .../spells/spell_types/self/mime_vow.dm | 24 + .../modules/spells/spell_types/self/mutate.dm | 49 + .../spells/spell_types/self/night_vision.dm | 39 + .../spell_types/self/personality_commune.dm | 54 + .../spells/spell_types/self/rod_form.dm | 149 +++ code/modules/spells/spell_types/self/smoke.dm | 37 + .../spells/spell_types/self/soultap.dm | 63 ++ .../spell_types/self/spacetime_distortion.dm | 168 +++ .../spells/spell_types/self/stop_time.dm | 30 + .../spells/spell_types/self/summon_item.dm | 154 +++ .../spells/spell_types/self/voice_of_god.dm | 50 + .../modules/spells/spell_types/shadow_walk.dm | 101 -- code/modules/spells/spell_types/shapeshift.dm | 185 ---- .../spell_types/shapeshift/_shapeshift.dm | 244 +++++ .../spells/spell_types/shapeshift/dragon.dm | 7 + .../spell_types/shapeshift/polar_bear.dm | 7 + .../spell_types/shapeshift/shapechange.dm | 22 + code/modules/spells/spell_types/soultap.dm | 33 - .../spell_types/spacetime_distortion.dm | 133 --- code/modules/spells/spell_types/summonitem.dm | 110 -- code/modules/spells/spell_types/telepathy.dm | 38 - .../spells/spell_types/touch/_touch.dm | 265 +++++ .../spell_types/touch/duffelbag_curse.dm | 86 ++ .../spell_types/touch/flesh_to_stone.dm | 33 + .../modules/spells/spell_types/touch/smite.dm | 55 + .../spells/spell_types/touch_attacks.dm | 99 -- code/modules/spells/spell_types/trigger.dm | 26 - .../spells/spell_types/turf_teleport.dm | 38 - .../spells/spell_types/voice_of_god.dm | 45 - code/modules/spells/spell_types/wizard.dm | 377 ------- code/modules/unit_tests/_unit_tests.dm | 6 + code/modules/unit_tests/mindbound_actions.dm | 30 + code/modules/unit_tests/spell_invocations.dm | 26 + code/modules/unit_tests/spell_mindswap.dm | 41 + code/modules/unit_tests/spell_names.dm | 32 + code/modules/unit_tests/spell_shapeshift.dm | 20 + code/modules/unit_tests/wizard_loadout.dm | 14 + tgui/packages/tgui/interfaces/Spellbook.js | 581 +++++++++++ .../Scripts/67083_action_granters.dm | 1 + 332 files changed, 13254 insertions(+), 9841 deletions(-) create mode 100644 code/__DEFINES/actions.dm create mode 100644 code/__DEFINES/dcs/signals/signals_action.dm create mode 100644 code/__DEFINES/dcs/signals/signals_heretic.dm create mode 100644 code/__DEFINES/dcs/signals/signals_spell.dm create mode 100644 code/__DEFINES/span.dm delete mode 100644 code/datums/action.dm create mode 100644 code/datums/actions/action.dm create mode 100644 code/datums/actions/cooldown_action.dm create mode 100644 code/datums/actions/innate_action.dm create mode 100644 code/datums/actions/item_action.dm create mode 100644 code/datums/actions/items/adjust.dm rename code/datums/actions/{ => items}/beam_rifle.dm (100%) create mode 100644 code/datums/actions/items/berserk.dm create mode 100644 code/datums/actions/items/boot_dash.dm create mode 100644 code/datums/actions/items/cult_dagger.dm create mode 100644 code/datums/actions/items/hands_free.dm rename code/datums/actions/{ => items}/ninja.dm (100%) create mode 100644 code/datums/actions/items/organ_action.dm create mode 100644 code/datums/actions/items/set_internals.dm create mode 100644 code/datums/actions/items/stealth_box.dm create mode 100644 code/datums/actions/items/summon_stickmen.dm create mode 100644 code/datums/actions/items/toggles.dm create mode 100644 code/datums/actions/items/vortex_recall.dm create mode 100644 code/datums/actions/mobs/language_menu.dm create mode 100644 code/datums/actions/mobs/small_sprite.dm rename code/datums/{mutations.dm => mutations/__mutations.dm} (85%) delete mode 100644 code/datums/mutations/actions.dm create mode 100644 code/datums/mutations/autonomy.dm create mode 100644 code/datums/mutations/fire_breath.dm create mode 100644 code/datums/mutations/olfaction.dm create mode 100644 code/datums/mutations/telepathy.dm create mode 100644 code/datums/mutations/tongue_spike.dm create mode 100644 code/datums/mutations/void_magnet.dm create mode 100644 code/datums/mutations/webbing.dm create mode 100644 code/datums/proximity_monitor/timestop.dm create mode 100644 code/game/objects/effects/phased_mob.dm delete mode 100644 code/game/objects/items/granters.dm create mode 100644 code/game/objects/items/granters/_granters.dm create mode 100644 code/game/objects/items/granters/crafting/_crafting_granter.dm create mode 100644 code/game/objects/items/granters/crafting/bone_notes.dm create mode 100644 code/game/objects/items/granters/crafting/cannon.dm create mode 100644 code/game/objects/items/granters/crafting/desserts.dm create mode 100644 code/game/objects/items/granters/crafting/pipegun.dm create mode 100644 code/game/objects/items/granters/magic/_spell_granter.dm create mode 100644 code/game/objects/items/granters/magic/barnyard.dm create mode 100644 code/game/objects/items/granters/magic/blind.dm create mode 100644 code/game/objects/items/granters/magic/charge.dm create mode 100644 code/game/objects/items/granters/magic/fireball.dm create mode 100644 code/game/objects/items/granters/magic/forcewall.dm create mode 100644 code/game/objects/items/granters/magic/knock.dm create mode 100644 code/game/objects/items/granters/magic/mime.dm create mode 100644 code/game/objects/items/granters/magic/mindswap.dm create mode 100644 code/game/objects/items/granters/magic/sacredflame.dm create mode 100644 code/game/objects/items/granters/magic/smoke.dm create mode 100644 code/game/objects/items/granters/magic/summon_item.dm create mode 100644 code/game/objects/items/granters/martial_arts/_martial_arts.dm create mode 100644 code/game/objects/items/granters/martial_arts/cqc.dm create mode 100644 code/game/objects/items/granters/martial_arts/plasma_fist.dm create mode 100644 code/game/objects/items/granters/martial_arts/sleeping_carp.dm create mode 100644 code/game/objects/items/granters/oragami.dm create mode 100644 code/game/objects/items/storage/holsters.dm delete mode 100644 code/modules/antagonists/wizard/equipment/spellbook.dm create mode 100644 code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm create mode 100644 code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm create mode 100644 code/modules/antagonists/wizard/equipment/spellbook_entries/challenges.dm create mode 100644 code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm create mode 100644 code/modules/antagonists/wizard/equipment/spellbook_entries/mobility.dm create mode 100644 code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm create mode 100644 code/modules/antagonists/wizard/equipment/wizard_spellbook.dm delete mode 100644 code/modules/mob/living/bloodcrawl.dm delete mode 100644 code/modules/spells/spell_types/aimed.dm create mode 100644 code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm create mode 100644 code/modules/spells/spell_types/aoe_spell/area_conversion.dm create mode 100644 code/modules/spells/spell_types/aoe_spell/knock.dm create mode 100644 code/modules/spells/spell_types/aoe_spell/magic_missile.dm create mode 100644 code/modules/spells/spell_types/aoe_spell/repulse.dm create mode 100644 code/modules/spells/spell_types/aoe_spell/sacred_flame.dm delete mode 100644 code/modules/spells/spell_types/area_teleport.dm delete mode 100644 code/modules/spells/spell_types/barnyard.dm delete mode 100644 code/modules/spells/spell_types/blind.dm delete mode 100644 code/modules/spells/spell_types/bloodcrawl.dm delete mode 100644 code/modules/spells/spell_types/charge.dm create mode 100644 code/modules/spells/spell_types/cone/_cone.dm delete mode 100644 code/modules/spells/spell_types/cone_spells.dm delete mode 100644 code/modules/spells/spell_types/conjure.dm create mode 100644 code/modules/spells/spell_types/conjure/_conjure.dm create mode 100644 code/modules/spells/spell_types/conjure/bees.dm create mode 100644 code/modules/spells/spell_types/conjure/carp.dm create mode 100644 code/modules/spells/spell_types/conjure/constructs.dm create mode 100644 code/modules/spells/spell_types/conjure/creatures.dm create mode 100644 code/modules/spells/spell_types/conjure/cult_turfs.dm create mode 100644 code/modules/spells/spell_types/conjure/ed_swarm.dm create mode 100644 code/modules/spells/spell_types/conjure/invisible_chair.dm create mode 100644 code/modules/spells/spell_types/conjure/invisible_wall.dm create mode 100644 code/modules/spells/spell_types/conjure/link_words.dm create mode 100644 code/modules/spells/spell_types/conjure/presents.dm create mode 100644 code/modules/spells/spell_types/conjure/soulstone.dm create mode 100644 code/modules/spells/spell_types/conjure/the_traps.dm create mode 100644 code/modules/spells/spell_types/conjure_item/_conjure_item.dm create mode 100644 code/modules/spells/spell_types/conjure_item/infinite_guns.dm create mode 100644 code/modules/spells/spell_types/conjure_item/invisible_box.dm create mode 100644 code/modules/spells/spell_types/conjure_item/lightning_packet.dm create mode 100644 code/modules/spells/spell_types/conjure_item/snowball.dm delete mode 100644 code/modules/spells/spell_types/construct_spells.dm delete mode 100644 code/modules/spells/spell_types/emplosion.dm delete mode 100644 code/modules/spells/spell_types/ethereal_jaunt.dm delete mode 100644 code/modules/spells/spell_types/explosion.dm delete mode 100644 code/modules/spells/spell_types/forcewall.dm delete mode 100644 code/modules/spells/spell_types/genetic.dm delete mode 100644 code/modules/spells/spell_types/godhand.dm delete mode 100644 code/modules/spells/spell_types/infinite_guns.dm create mode 100644 code/modules/spells/spell_types/jaunt/_jaunt.dm create mode 100644 code/modules/spells/spell_types/jaunt/bloodcrawl.dm create mode 100644 code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm create mode 100644 code/modules/spells/spell_types/jaunt/shadow_walk.dm delete mode 100644 code/modules/spells/spell_types/knock.dm delete mode 100644 code/modules/spells/spell_types/lichdom.dm delete mode 100644 code/modules/spells/spell_types/lightning.dm create mode 100644 code/modules/spells/spell_types/list_targets/_list_targets.dm create mode 100644 code/modules/spells/spell_types/list_targets/telepathy.dm delete mode 100644 code/modules/spells/spell_types/mime.dm delete mode 100644 code/modules/spells/spell_types/mind_transfer.dm delete mode 100644 code/modules/spells/spell_types/personality_commune.dm delete mode 100644 code/modules/spells/spell_types/pointed.dm create mode 100644 code/modules/spells/spell_types/pointed/_pointed.dm create mode 100644 code/modules/spells/spell_types/pointed/abyssal_gaze.dm create mode 100644 code/modules/spells/spell_types/pointed/barnyard.dm create mode 100644 code/modules/spells/spell_types/pointed/blind.dm create mode 100644 code/modules/spells/spell_types/pointed/dominate.dm create mode 100644 code/modules/spells/spell_types/pointed/finger_guns.dm create mode 100644 code/modules/spells/spell_types/pointed/fireball.dm create mode 100644 code/modules/spells/spell_types/pointed/lightning_bolt.dm create mode 100644 code/modules/spells/spell_types/pointed/mind_transfer.dm create mode 100644 code/modules/spells/spell_types/pointed/spell_cards.dm delete mode 100644 code/modules/spells/spell_types/projectile.dm create mode 100644 code/modules/spells/spell_types/projectile/_basic_projectile.dm create mode 100644 code/modules/spells/spell_types/projectile/juggernaut.dm delete mode 100644 code/modules/spells/spell_types/rod_form.dm delete mode 100644 code/modules/spells/spell_types/santa.dm create mode 100644 code/modules/spells/spell_types/self/basic_heal.dm create mode 100644 code/modules/spells/spell_types/self/charge.dm create mode 100644 code/modules/spells/spell_types/self/disable_tech.dm create mode 100644 code/modules/spells/spell_types/self/forcewall.dm create mode 100644 code/modules/spells/spell_types/self/lichdom.dm create mode 100644 code/modules/spells/spell_types/self/lightning.dm create mode 100644 code/modules/spells/spell_types/self/mime_vow.dm create mode 100644 code/modules/spells/spell_types/self/mutate.dm create mode 100644 code/modules/spells/spell_types/self/night_vision.dm create mode 100644 code/modules/spells/spell_types/self/personality_commune.dm create mode 100644 code/modules/spells/spell_types/self/rod_form.dm create mode 100644 code/modules/spells/spell_types/self/smoke.dm create mode 100644 code/modules/spells/spell_types/self/soultap.dm create mode 100644 code/modules/spells/spell_types/self/spacetime_distortion.dm create mode 100644 code/modules/spells/spell_types/self/stop_time.dm create mode 100644 code/modules/spells/spell_types/self/summon_item.dm create mode 100644 code/modules/spells/spell_types/self/voice_of_god.dm delete mode 100644 code/modules/spells/spell_types/shadow_walk.dm delete mode 100644 code/modules/spells/spell_types/shapeshift.dm create mode 100644 code/modules/spells/spell_types/shapeshift/_shapeshift.dm create mode 100644 code/modules/spells/spell_types/shapeshift/dragon.dm create mode 100644 code/modules/spells/spell_types/shapeshift/polar_bear.dm create mode 100644 code/modules/spells/spell_types/shapeshift/shapechange.dm delete mode 100644 code/modules/spells/spell_types/soultap.dm delete mode 100644 code/modules/spells/spell_types/spacetime_distortion.dm delete mode 100644 code/modules/spells/spell_types/summonitem.dm delete mode 100644 code/modules/spells/spell_types/telepathy.dm create mode 100644 code/modules/spells/spell_types/touch/_touch.dm create mode 100644 code/modules/spells/spell_types/touch/duffelbag_curse.dm create mode 100644 code/modules/spells/spell_types/touch/flesh_to_stone.dm create mode 100644 code/modules/spells/spell_types/touch/smite.dm delete mode 100644 code/modules/spells/spell_types/touch_attacks.dm delete mode 100644 code/modules/spells/spell_types/trigger.dm delete mode 100644 code/modules/spells/spell_types/turf_teleport.dm delete mode 100644 code/modules/spells/spell_types/voice_of_god.dm delete mode 100644 code/modules/spells/spell_types/wizard.dm create mode 100644 code/modules/unit_tests/mindbound_actions.dm create mode 100644 code/modules/unit_tests/spell_invocations.dm create mode 100644 code/modules/unit_tests/spell_mindswap.dm create mode 100644 code/modules/unit_tests/spell_names.dm create mode 100644 code/modules/unit_tests/spell_shapeshift.dm create mode 100644 code/modules/unit_tests/wizard_loadout.dm create mode 100644 tgui/packages/tgui/interfaces/Spellbook.js create mode 100644 tools/UpdatePaths/Scripts/67083_action_granters.dm diff --git a/beestation.dme b/beestation.dme index 20b18a84b80fe..6339b1f91f2f2 100644 --- a/beestation.dme +++ b/beestation.dme @@ -27,6 +27,7 @@ #include "code\__DEFINES\_tick.dm" #include "code\__DEFINES\access.dm" #include "code\__DEFINES\achievements.dm" +#include "code\__DEFINES\actions.dm" #include "code\__DEFINES\actionspeed_modification.dm" #include "code\__DEFINES\admin.dm" #include "code\__DEFINES\ai.dm" @@ -159,6 +160,7 @@ #include "code\__DEFINES\sound.dm" #include "code\__DEFINES\space.dm" #include "code\__DEFINES\spaceman_dmm.dm" +#include "code\__DEFINES\span.dm" #include "code\__DEFINES\species.dm" #include "code\__DEFINES\speech_channels.dm" #include "code\__DEFINES\spell.dm" @@ -196,10 +198,13 @@ #include "code\__DEFINES\dcs\flags.dm" #include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals.dm" +#include "code\__DEFINES\dcs\signals\signals_action.dm" #include "code\__DEFINES\dcs\signals\signals_area.dm" #include "code\__DEFINES\dcs\signals\signals_global.dm" +#include "code\__DEFINES\dcs\signals\signals_heretic.dm" #include "code\__DEFINES\dcs\signals\signals_lighting.dm" #include "code\__DEFINES\dcs\signals\signals_movable.dm" +#include "code\__DEFINES\dcs\signals\signals_spell.dm" #include "code\__DEFINES\dcs\signals\signals_turf.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_attack.dm" @@ -508,7 +513,6 @@ #include "code\controllers\subsystem\processing\singulo.dm" #include "code\controllers\subsystem\processing\station.dm" #include "code\controllers\subsystem\processing\wet_floors.dm" -#include "code\datums\action.dm" #include "code\datums\ai_laws.dm" #include "code\datums\alarm.dm" #include "code\datums\armor.dm" @@ -535,7 +539,6 @@ #include "code\datums\mind.dm" #include "code\datums\movement_detector.dm" #include "code\datums\mutable_appearance.dm" -#include "code\datums\mutations.dm" #include "code\datums\numbered_display.dm" #include "code\datums\outfit.dm" #include "code\datums\position_point_vector.dm" @@ -562,8 +565,23 @@ #include "code\datums\achievements\boss_scores.dm" #include "code\datums\achievements\misc_achievements.dm" #include "code\datums\achievements\misc_scores.dm" -#include "code\datums\actions\beam_rifle.dm" -#include "code\datums\actions\ninja.dm" +#include "code\datums\actions\action.dm" +#include "code\datums\actions\innate_action.dm" +#include "code\datums\actions\item_action.dm" +#include "code\datums\actions\items\adjust.dm" +#include "code\datums\actions\items\beam_rifle.dm" +#include "code\datums\actions\items\berserk.dm" +#include "code\datums\actions\items\cult_dagger.dm" +#include "code\datums\actions\items\hands_free.dm" +#include "code\datums\actions\items\ninja.dm" +#include "code\datums\actions\items\organ_action.dm" +#include "code\datums\actions\items\set_internals.dm" +#include "code\datums\actions\items\stealth_box.dm" +#include "code\datums\actions\items\summon_stickmen.dm" +#include "code\datums\actions\items\toggles.dm" +#include "code\datums\actions\items\vortex_recall.dm" +#include "code\datums\actions\mobs\language_menu.dm" +#include "code\datums\actions\mobs\small_sprite.dm" #include "code\datums\ai\_ai_behavior.dm" #include "code\datums\ai\_ai_controller.dm" #include "code\datums\ai\_ai_planning_subtree.dm" @@ -905,20 +923,27 @@ #include "code\datums\mood_events\generic_positive_events.dm" #include "code\datums\mood_events\mood_event.dm" #include "code\datums\mood_events\needs_events.dm" +#include "code\datums\mutations\__mutations.dm" #include "code\datums\mutations\_combined.dm" -#include "code\datums\mutations\actions.dm" #include "code\datums\mutations\antenna.dm" +#include "code\datums\mutations\autonomy.dm" #include "code\datums\mutations\body.dm" #include "code\datums\mutations\chameleon.dm" #include "code\datums\mutations\cluwne.dm" #include "code\datums\mutations\cold.dm" +#include "code\datums\mutations\fire_breath.dm" #include "code\datums\mutations\hulk.dm" +#include "code\datums\mutations\olfaction.dm" #include "code\datums\mutations\radioactive.dm" #include "code\datums\mutations\sight.dm" #include "code\datums\mutations\space_adaptation.dm" #include "code\datums\mutations\speech.dm" #include "code\datums\mutations\telekinesis.dm" +#include "code\datums\mutations\telepathy.dm" +#include "code\datums\mutations\tongue_spike.dm" #include "code\datums\mutations\touch.dm" +#include "code\datums\mutations\void_magnet.dm" +#include "code\datums\mutations\webbing.dm" #include "code\datums\ruins\lavaland.dm" #include "code\datums\ruins\space.dm" #include "code\datums\station_traits\_station_trait.dm" @@ -1227,6 +1252,7 @@ #include "code\game\objects\effects\mines.dm" #include "code\game\objects\effects\misc.dm" #include "code\game\objects\effects\overlays.dm" +#include "code\game\objects\effects\phased_mob.dm" #include "code\game\objects\effects\portals.dm" #include "code\game\objects\effects\proximity.dm" #include "code\game\objects\effects\spiders.dm" @@ -1322,7 +1348,6 @@ #include "code\game\objects\items\fireaxe.dm" #include "code\game\objects\items\flamethrower.dm" #include "code\game\objects\items\gift.dm" -#include "code\game\objects\items\granters.dm" #include "code\game\objects\items\handcuffs.dm" #include "code\game\objects\items\his_grace.dm" #include "code\game\objects\items\holosign_creator.dm" @@ -1448,6 +1473,29 @@ #include "code\game\objects\items\food\spaghetti.dm" #include "code\game\objects\items\food\sweets.dm" #include "code\game\objects\items\food\vegetables.dm" +#include "code\game\objects\items\granters\_granters.dm" +#include "code\game\objects\items\granters\oragami.dm" +#include "code\game\objects\items\granters\crafting\_crafting_granter.dm" +#include "code\game\objects\items\granters\crafting\bone_notes.dm" +#include "code\game\objects\items\granters\crafting\cannon.dm" +#include "code\game\objects\items\granters\crafting\desserts.dm" +#include "code\game\objects\items\granters\crafting\pipegun.dm" +#include "code\game\objects\items\granters\magic\_spell_granter.dm" +#include "code\game\objects\items\granters\magic\barnyard.dm" +#include "code\game\objects\items\granters\magic\blind.dm" +#include "code\game\objects\items\granters\magic\charge.dm" +#include "code\game\objects\items\granters\magic\fireball.dm" +#include "code\game\objects\items\granters\magic\forcewall.dm" +#include "code\game\objects\items\granters\magic\knock.dm" +#include "code\game\objects\items\granters\magic\mime.dm" +#include "code\game\objects\items\granters\magic\mindswap.dm" +#include "code\game\objects\items\granters\magic\sacredflame.dm" +#include "code\game\objects\items\granters\magic\smoke.dm" +#include "code\game\objects\items\granters\magic\summon_item.dm" +#include "code\game\objects\items\granters\martial_arts\_martial_arts.dm" +#include "code\game\objects\items\granters\martial_arts\cqc.dm" +#include "code\game\objects\items\granters\martial_arts\plasma_fist.dm" +#include "code\game\objects\items\granters\martial_arts\sleeping_carp.dm" #include "code\game\objects\items\grenades\_grenade.dm" #include "code\game\objects\items\grenades\antigravity.dm" #include "code\game\objects\items\grenades\chem_grenade.dm" @@ -1533,6 +1581,7 @@ #include "code\game\objects\items\storage\briefcase.dm" #include "code\game\objects\items\storage\fancy.dm" #include "code\game\objects\items\storage\firstaid.dm" +#include "code\game\objects\items\storage\holsters.dm" #include "code\game\objects\items\storage\lockbox.dm" #include "code\game\objects\items\storage\paperbag.dm" #include "code\game\objects\items\storage\secure.dm" @@ -2139,7 +2188,12 @@ #include "code\modules\antagonists\wizard\wizard.dm" #include "code\modules\antagonists\wizard\equipment\artefact.dm" #include "code\modules\antagonists\wizard\equipment\soulstone.dm" -#include "code\modules\antagonists\wizard\equipment\spellbook.dm" +#include "code\modules\antagonists\wizard\equipment\wizard_spellbook.dm" +#include "code\modules\antagonists\wizard\equipment\spellbook_entries\_entry.dm" +#include "code\modules\antagonists\wizard\equipment\spellbook_entries\assistance.dm" +#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\summons.dm" #include "code\modules\antagonists\xeno\xeno.dm" #include "code\modules\aquarium\aquarium.dm" #include "code\modules\aquarium\aquarium_behaviour.dm" @@ -2985,7 +3039,6 @@ #include "code\modules\mob\dead\observer\orbit.dm" #include "code\modules\mob\dead\observer\say.dm" #include "code\modules\mob\living\blood.dm" -#include "code\modules\mob\living\bloodcrawl.dm" #include "code\modules\mob\living\damage_procs.dm" #include "code\modules\mob\living\death.dm" #include "code\modules\mob\living\emote.dm" @@ -3912,51 +3965,81 @@ #include "code\modules\shuttle\super_cruise\shuttle_components\shuttle_console.dm" #include "code\modules\shuttle\super_cruise\shuttle_components\shuttle_docking.dm" #include "code\modules\spells\spell.dm" -#include "code\modules\spells\spell_types\aimed.dm" -#include "code\modules\spells\spell_types\area_teleport.dm" -#include "code\modules\spells\spell_types\barnyard.dm" -#include "code\modules\spells\spell_types\blind.dm" -#include "code\modules\spells\spell_types\bloodcrawl.dm" -#include "code\modules\spells\spell_types\charge.dm" #include "code\modules\spells\spell_types\cluwnecurse.dm" -#include "code\modules\spells\spell_types\cone_spells.dm" -#include "code\modules\spells\spell_types\conjure.dm" -#include "code\modules\spells\spell_types\construct_spells.dm" #include "code\modules\spells\spell_types\curse.dm" #include "code\modules\spells\spell_types\devil.dm" #include "code\modules\spells\spell_types\devil_boons.dm" -#include "code\modules\spells\spell_types\emplosion.dm" -#include "code\modules\spells\spell_types\ethereal_jaunt.dm" -#include "code\modules\spells\spell_types\explosion.dm" -#include "code\modules\spells\spell_types\forcewall.dm" -#include "code\modules\spells\spell_types\genetic.dm" -#include "code\modules\spells\spell_types\godhand.dm" -#include "code\modules\spells\spell_types\infinite_guns.dm" -#include "code\modules\spells\spell_types\knock.dm" #include "code\modules\spells\spell_types\lesserlichdom.dm" -#include "code\modules\spells\spell_types\lichdom.dm" -#include "code\modules\spells\spell_types\lightning.dm" -#include "code\modules\spells\spell_types\mime.dm" -#include "code\modules\spells\spell_types\mind_transfer.dm" -#include "code\modules\spells\spell_types\personality_commune.dm" -#include "code\modules\spells\spell_types\pointed.dm" -#include "code\modules\spells\spell_types\projectile.dm" #include "code\modules\spells\spell_types\rightandwrong.dm" -#include "code\modules\spells\spell_types\rod_form.dm" -#include "code\modules\spells\spell_types\santa.dm" -#include "code\modules\spells\spell_types\shadow_walk.dm" -#include "code\modules\spells\spell_types\shapeshift.dm" -#include "code\modules\spells\spell_types\soultap.dm" -#include "code\modules\spells\spell_types\spacetime_distortion.dm" -#include "code\modules\spells\spell_types\summonitem.dm" #include "code\modules\spells\spell_types\taeclowndo.dm" -#include "code\modules\spells\spell_types\telepathy.dm" #include "code\modules\spells\spell_types\the_traps.dm" -#include "code\modules\spells\spell_types\touch_attacks.dm" -#include "code\modules\spells\spell_types\trigger.dm" -#include "code\modules\spells\spell_types\turf_teleport.dm" -#include "code\modules\spells\spell_types\voice_of_god.dm" -#include "code\modules\spells\spell_types\wizard.dm" +#include "code\modules\spells\spell_types\aoe_spell\_aoe_spell.dm" +#include "code\modules\spells\spell_types\aoe_spell\area_conversion.dm" +#include "code\modules\spells\spell_types\aoe_spell\knock.dm" +#include "code\modules\spells\spell_types\aoe_spell\repulse.dm" +#include "code\modules\spells\spell_types\aoe_spell\sacred_flame.dm" +#include "code\modules\spells\spell_types\cone\_cone.dm" +#include "code\modules\spells\spell_types\conjure\_conjure.dm" +#include "code\modules\spells\spell_types\conjure\bees.dm" +#include "code\modules\spells\spell_types\conjure\carp.dm" +#include "code\modules\spells\spell_types\conjure\constructs.dm" +#include "code\modules\spells\spell_types\conjure\creatures.dm" +#include "code\modules\spells\spell_types\conjure\cult_turfs.dm" +#include "code\modules\spells\spell_types\conjure\ed_swarm.dm" +#include "code\modules\spells\spell_types\conjure\invisible_chair.dm" +#include "code\modules\spells\spell_types\conjure\invisible_wall.dm" +#include "code\modules\spells\spell_types\conjure\link_words.dm" +#include "code\modules\spells\spell_types\conjure\presents.dm" +#include "code\modules\spells\spell_types\conjure\soulstone.dm" +#include "code\modules\spells\spell_types\conjure\the_traps.dm" +#include "code\modules\spells\spell_types\conjure_item\_conjure_item.dm" +#include "code\modules\spells\spell_types\conjure_item\infinite_guns.dm" +#include "code\modules\spells\spell_types\conjure_item\invisible_box.dm" +#include "code\modules\spells\spell_types\conjure_item\lightning_packet.dm" +#include "code\modules\spells\spell_types\conjure_item\snowball.dm" +#include "code\modules\spells\spell_types\jaunt\_jaunt.dm" +#include "code\modules\spells\spell_types\jaunt\bloodcrawl.dm" +#include "code\modules\spells\spell_types\jaunt\ethereal_jaunt.dm" +#include "code\modules\spells\spell_types\jaunt\shadow_walk.dm" +#include "code\modules\spells\spell_types\list_targets\_list_targets.dm" +#include "code\modules\spells\spell_types\list_targets\telepathy.dm" +#include "code\modules\spells\spell_types\pointed\_pointed.dm" +#include "code\modules\spells\spell_types\pointed\abyssal_gaze.dm" +#include "code\modules\spells\spell_types\pointed\barnyard.dm" +#include "code\modules\spells\spell_types\pointed\blind.dm" +#include "code\modules\spells\spell_types\pointed\dominate.dm" +#include "code\modules\spells\spell_types\pointed\finger_guns.dm" +#include "code\modules\spells\spell_types\pointed\fireball.dm" +#include "code\modules\spells\spell_types\pointed\lightning_bolt.dm" +#include "code\modules\spells\spell_types\pointed\mind_transfer.dm" +#include "code\modules\spells\spell_types\pointed\spell_cards.dm" +#include "code\modules\spells\spell_types\projectile\_basic_projectile.dm" +#include "code\modules\spells\spell_types\projectile\juggernaut.dm" +#include "code\modules\spells\spell_types\self\basic_heal.dm" +#include "code\modules\spells\spell_types\self\charge.dm" +#include "code\modules\spells\spell_types\self\disable_tech.dm" +#include "code\modules\spells\spell_types\self\forcewall.dm" +#include "code\modules\spells\spell_types\self\lichdom.dm" +#include "code\modules\spells\spell_types\self\lightning.dm" +#include "code\modules\spells\spell_types\self\mime_vow.dm" +#include "code\modules\spells\spell_types\self\mutate.dm" +#include "code\modules\spells\spell_types\self\night_vision.dm" +#include "code\modules\spells\spell_types\self\personality_commune.dm" +#include "code\modules\spells\spell_types\self\rod_form.dm" +#include "code\modules\spells\spell_types\self\smoke.dm" +#include "code\modules\spells\spell_types\self\soultap.dm" +#include "code\modules\spells\spell_types\self\spacetime_distortion.dm" +#include "code\modules\spells\spell_types\self\stop_time.dm" +#include "code\modules\spells\spell_types\self\summon_item.dm" +#include "code\modules\spells\spell_types\self\voice_of_god.dm" +#include "code\modules\spells\spell_types\shapeshift\_shapeshift.dm" +#include "code\modules\spells\spell_types\shapeshift\dragon.dm" +#include "code\modules\spells\spell_types\shapeshift\polar_bear.dm" +#include "code\modules\spells\spell_types\shapeshift\shapechange.dm" +#include "code\modules\spells\spell_types\touch\_touch.dm" +#include "code\modules\spells\spell_types\touch\duffelbag_curse.dm" +#include "code\modules\spells\spell_types\touch\flesh_to_stone.dm" +#include "code\modules\spells\spell_types\touch\smite.dm" #include "code\modules\station_goals\bluespace_tap.dm" #include "code\modules\station_goals\bsa.dm" #include "code\modules\station_goals\custom_shuttle.dm" @@ -4090,6 +4173,12 @@ #include "code\modules\tgui_panel\tgui_panel.dm" #include "code\modules\tooltip\tooltip.dm" #include "code\modules\unit_tests\_unit_tests.dm" +#include "code\modules\unit_tests\mindbound_actions.dm" +#include "code\modules\unit_tests\spell_invocations.dm" +#include "code\modules\unit_tests\spell_mindswap.dm" +#include "code\modules\unit_tests\spell_names.dm" +#include "code\modules\unit_tests\spell_shapeshift.dm" +#include "code\modules\unit_tests\wizard_loadout.dm" #include "code\modules\uplink\uplink_devices.dm" #include "code\modules\uplink\uplink_items.dm" #include "code\modules\uplink\uplink_purchase_log.dm" diff --git a/code/__DEFINES/actions.dm b/code/__DEFINES/actions.dm new file mode 100644 index 0000000000000..383221dc237ae --- /dev/null +++ b/code/__DEFINES/actions.dm @@ -0,0 +1,44 @@ +///Action button checks if hands are unusable +#define AB_CHECK_HANDS_BLOCKED (1<<0) +///Action button checks if user is immobile +#define AB_CHECK_IMMOBILE (1<<1) +///Action button checks if user is resting +#define AB_CHECK_LYING (1<<2) +///Action button checks if user is conscious +#define AB_CHECK_CONSCIOUS (1<<3) +///Action button checks if user is incapacitated +#define AB_CHECK_INCAPACITATED (1<<4) +///Action button checks if user is jaunting +#define AB_CHECK_PHASED (1<<5) + +//Bitfield is in /_DEFINES/_globablvars/bitfields.dm for reasons + + +///Action button triggered with right click +#define TRIGGER_SECONDARY_ACTION (1<<0) +///Action triggered to ignore any availability checks +#define TRIGGER_FORCE_AVAILABLE (1<<1) + +// Defines for formatting cooldown actions for the stat panel. +/// The stat panel the action is displayed in. +#define PANEL_DISPLAY_PANEL "panel" +/// The status shown in the stat panel. +/// Can be stuff like "ready", "on cooldown", "active", "charges", "charge cost", etc. +#define PANEL_DISPLAY_STATUS "status" +/// The name shown in the stat panel. +#define PANEL_DISPLAY_NAME "name" + +#define ACTION_BUTTON_DEFAULT_BACKGROUND "_use_ui_default_background" + +#define UPDATE_BUTTON_NAME (1<<0) +#define UPDATE_BUTTON_ICON (1<<1) +#define UPDATE_BUTTON_BACKGROUND (1<<2) +#define UPDATE_BUTTON_OVERLAY (1<<3) +#define UPDATE_BUTTON_STATUS (1<<4) + +/// Takes in a typepath of a `/datum/action` and adds it to `src`. +/// Only useful if you want to add the action and never desire to reference it again ever. +#define GRANT_ACTION(typepath) do {\ + var/datum/action/_ability = new typepath(src);\ + _ability.Grant(src);\ +} while (FALSE) diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 3bcd628b099d9..73929cf72ef4a 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -108,6 +108,12 @@ #define CONSTRUCT_WRAITH "Wraith" #define CONSTRUCT_ARTIFICER "Artificer" +/// Used in logging spells for roundend results +#define LOG_SPELL_TYPE "type" +#define LOG_SPELL_AMOUNT "amount" + + + /// How much does it cost to reroll strains? #define BLOB_REROLL_COST 40 diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm new file mode 100644 index 0000000000000..ee98b5a4eb9a8 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_action.dm @@ -0,0 +1,35 @@ +// Action signals + +///from base of datum/action/proc/Trigger(): (datum/action) +#define COMSIG_ACTION_TRIGGER "action_trigger" + // Return to block the trigger from occuring + #define COMPONENT_ACTION_BLOCK_TRIGGER (1<<0) +/// From /datum/action/Grant(): (mob/grant_to) +#define COMSIG_ACTION_GRANTED "action_grant" +/// From /datum/action/Remove(): (mob/removed_from) +#define COMSIG_ACTION_REMOVED "action_removed" + +// Cooldown action signals + +/// From base of /datum/action/cooldown/proc/PreActivate(), sent to the action owner: (datum/action/cooldown/activated) +#define COMSIG_MOB_ABILITY_STARTED "mob_ability_base_started" + /// Return to block the ability from starting / activating + #define COMPONENT_BLOCK_ABILITY_START (1<<0) +/// From base of /datum/action/cooldown/proc/PreActivate(), sent to the action owner: (datum/action/cooldown/finished) +#define COMSIG_MOB_ABILITY_FINISHED "mob_ability_base_finished" + +/// From base of /datum/action/cooldown/proc/set_statpanel_format(): (list/stat_panel_data) +#define COMSIG_ACTION_SET_STATPANEL "ability_set_statpanel" + +// Specific cooldown action signals + +/// From base of /datum/action/cooldown/mob_cooldown/blood_warp/proc/blood_warp(): () +#define COMSIG_BLOOD_WARP "mob_ability_blood_warp" +/// From base of /datum/action/cooldown/mob_cooldown/charge/proc/do_charge(): () +#define COMSIG_STARTED_CHARGE "mob_ability_charge_started" +/// From base of /datum/action/cooldown/mob_cooldown/charge/proc/do_charge(): () +#define COMSIG_FINISHED_CHARGE "mob_ability_charge_finished" +/// From base of /datum/action/cooldown/mob_cooldown/lava_swoop/proc/swoop_attack(): () +#define COMSIG_SWOOP_INVULNERABILITY_STARTED "mob_swoop_invulnerability_started" +/// From base of /datum/action/cooldown/mob_cooldown/lava_swoop/proc/swoop_attack(): () +#define COMSIG_LAVA_ARENA_FAILED "mob_lava_arena_failed" diff --git a/code/__DEFINES/dcs/signals/signals_datum/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum/signals_datum.dm index e9fc41ba61575..9d11f680b93f9 100644 --- a/code/__DEFINES/dcs/signals/signals_datum/signals_datum.dm +++ b/code/__DEFINES/dcs/signals/signals_datum/signals_datum.dm @@ -74,9 +74,6 @@ #define COMPONENT_TWOHANDED_BLOCK_WIELD 1 #define COMSIG_TWOHANDED_UNWIELD "twohanded_unwield" //from base of datum/component/two_handed/proc/unwield(mob/living/carbon/user): (/mob/user) -// /datum/action signals -#define COMSIG_ACTION_TRIGGER "action_trigger" //! from base of datum/action/proc/Trigger(): (datum/action) - #define COMPONENT_ACTION_BLOCK_TRIGGER 1 // /datum/mind signals #define COMSIG_MIND_TRANSFER_TO "mind_transfer_to" // (mob/old, mob/new) diff --git a/code/__DEFINES/dcs/signals/signals_heretic.dm b/code/__DEFINES/dcs/signals/signals_heretic.dm new file mode 100644 index 0000000000000..dbd0b49d7b949 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_heretic.dm @@ -0,0 +1,18 @@ +/// Heretic signals +/// From /obj/item/clothing/mask/madness_mask/process : (amount) +#define COMSIG_HERETIC_MASK_ACT "void_mask_act" + +/// From /obj/item/melee/touch_attack/mansus_fist/on_mob_hit : (mob/living/source, mob/living/target) +#define COMSIG_HERETIC_MANSUS_GRASP_ATTACK "mansus_grasp_attack" +/// Default behavior is to use the hand, so return this to blocks the mansus fist from being consumed after use. + #define COMPONENT_BLOCK_HAND_USE (1<<0) +/// From /obj/item/melee/touch_attack/mansus_fist/afterattack_secondary : (mob/living/source, atom/target) +#define COMSIG_HERETIC_MANSUS_GRASP_ATTACK_SECONDARY "mansus_grasp_attack_secondary" + +/// Default behavior is to continue attack chain and do nothing else, so return this to use up the hand after use. + #define COMPONENT_USE_HAND (1<<0) + +/// From /obj/item/melee/sickly_blade/afterattack (with proximity) : (mob/living/source, mob/living/target) +#define COMSIG_HERETIC_BLADE_ATTACK "blade_attack" +/// From /obj/item/melee/sickly_blade/afterattack (without proximity) : (mob/living/source, mob/living/target) +#define COMSIG_HERETIC_RANGED_BLADE_ATTACK "ranged_blade_attack" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_human.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_human.dm index 9c57150679f92..d135ebe280ddb 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_human.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_human.dm @@ -7,16 +7,9 @@ #define COMSIG_HUMAN_ATTACKED "carbon_attacked" //hit by something that checks shields. //Heretics stuff -#define COMSIG_HERETIC_MASK_ACT "void_mask_act" -/// From /obj/item/melee/touch_attack/mansus_fist/on_mob_hit : (mob/living/source, mob/living/target) -#define COMSIG_HERETIC_MANSUS_GRASP_ATTACK "mansus_grasp_attack" - /// Default behavior is to use a charge, so return this to blocks the mansus fist from being consumed after use. - #define COMPONENT_BLOCK_CHARGE_USE (1<<0) +/// Default behavior is to use a charge, so return this to blocks the mansus fist from being consumed after use. +#define COMPONENT_BLOCK_CHARGE_USE (1<<0) -/// From /obj/item/melee/sickly_blade/afterattack (with proximity) : (mob/living/source, mob/living/target) -#define COMSIG_HERETIC_BLADE_ATTACK "blade_attack" -/// From /obj/item/melee/sickly_blade/afterattack (without proximity) : (mob/living/source, mob/living/target) -#define COMSIG_HERETIC_RANGED_BLADE_ATTACK "ranged_blade_attack" ///called from /obj/effect/proc_holder/spell/cast_check (src) #define COMSIG_MOB_PRE_CAST_SPELL "mob_cast_spell" /// Return to cancel the cast from beginning. diff --git a/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm b/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm index 555630211a3a4..45da823c20c7e 100644 --- a/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm +++ b/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm @@ -24,9 +24,6 @@ #define COMPONENT_ACTION_HANDLED (1<<0) #define COMSIG_ITEM_ATTACK_ZONE "item_attack_zone" //! from base of mob/living/carbon/attacked_by(): (mob/living/carbon/target, mob/living/user, hit_zone) -#define COMSIG_ITEM_IMBUE_SOUL "item_imbue_soul" //! return a truthy value to prevent ensouling, checked in /obj/effect/proc_holder/spell/targeted/lichdom/cast(): (mob/user) -#define COMSIG_ITEM_MARK_RETRIEVAL "item_mark_retrieval" //! called before marking an object for retrieval, checked in /obj/effect/proc_holder/spell/targeted/summonitem/cast() : (mob/user) - #define COMPONENT_BLOCK_MARK_RETRIEVAL 1 #define COMSIG_ITEM_HIT_REACT "item_hit_react" //! from base of obj/item/hit_reaction(): (mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", damage = 0, attack_type = MELEE_ATTACK) #define COMPONENT_HIT_REACTION_BLOCK (1<<0) #define COMSIG_ITEM_SHARPEN_ACT "sharpen_act" //! from base of item/sharpener/attackby(): (amount, max) diff --git a/code/__DEFINES/dcs/signals/signals_spell.dm b/code/__DEFINES/dcs/signals/signals_spell.dm new file mode 100644 index 0000000000000..4d2c7d2993fca --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_spell.dm @@ -0,0 +1,93 @@ +// Signals sent to or by spells + +// Generic spell signals + + +/// Sent from /datum/action/cooldown/spell/before_cast() to the caster: (datum/action/cooldown/spell/spell, atom/cast_on) +#define COMSIG_MOB_BEFORE_SPELL_CAST "mob_spell_pre_cast" +/// Sent from /datum/action/cooldown/spell/before_cast() to the spell: (atom/cast_on) +#define COMSIG_SPELL_BEFORE_CAST "spell_pre_cast" + /// Return to prevent the spell cast from continuing. + #define SPELL_CANCEL_CAST (1 << 0) + /// Return from before cast signals to prevent the spell from giving off sound or invocation. + #define SPELL_NO_FEEDBACK (1 << 1) + /// Return from before cast signals to prevent the spell from going on cooldown before aftercast. + #define SPELL_NO_IMMEDIATE_COOLDOWN (1 << 2) + +/// Sent from /datum/action/cooldown/spell/set_click_ability() to the caster: (datum/action/cooldown/spell/spell) +#define COMSIG_MOB_SPELL_ACTIVATED "mob_spell_active" + /// Same as spell_cancel_cast, as they're able to be used interchangeably + #define SPELL_CANCEL_ACTIVATION SPELL_CANCEL_CAST + +/// Sent from /datum/action/cooldown/spell/cast() to the caster: (datum/action/cooldown/spell/spell, atom/cast_on) +#define COMSIG_MOB_CAST_SPELL "mob_cast_spell" +/// Sent from /datum/action/cooldown/spell/cast() to the spell: (atom/cast_on) +#define COMSIG_SPELL_CAST "spell_cast" +// Sent from /datum/action/cooldown/spell/after_cast() to the caster: (datum/action/cooldown/spell/spell, atom/cast_on) +#define COMSIG_MOB_AFTER_SPELL_CAST "mob_after_spell_cast" +/// Sent from /datum/action/cooldown/spell/after_cast() to the spell: (atom/cast_on) +#define COMSIG_SPELL_AFTER_CAST "spell_after_cast" +/// Sent from /datum/action/cooldown/spell/reset_spell_cooldown() to the spell: () +#define COMSIG_SPELL_CAST_RESET "spell_cast_reset" + +// Spell type signals + +// Pointed projectiles +/// Sent from /datum/action/cooldown/spell/pointed/projectile/on_cast_hit: (atom/hit, atom/firer, obj/projectile/source) +#define COMSIG_SPELL_PROJECTILE_HIT "spell_projectile_hit" + +// AOE spells +/// Sent from /datum/action/cooldown/spell/aoe/cast: (list/atoms_affected, atom/caster) +#define COMSIG_SPELL_AOE_ON_CAST "spell_aoe_cast" + +// Cone spells +/// Sent from /datum/action/cooldown/spell/cone/cast: (list/atoms_affected, atom/caster) +#define COMSIG_SPELL_CONE_ON_CAST "spell_cone_cast" +/// Sent from /datum/action/cooldown/spell/cone/do_cone_effects: (list/atoms_affected, atom/caster, level) +#define COMSIG_SPELL_CONE_ON_LAYER_EFFECT "spell_cone_cast_effect" + +// Touch spells +/// Sent from /datum/action/cooldown/spell/touch/do_hand_hit: (atom/hit, mob/living/carbon/caster, obj/item/melee/touch_attack/hand) +#define COMSIG_SPELL_TOUCH_HAND_HIT "spell_touch_hand_cast" + +// Jaunt Spells +/// Sent from datum/action/cooldown/spell/jaunt/enter_jaunt, to the mob jaunting: (obj/effect/dummy/phased_mob/jaunt, datum/action/cooldown/spell/spell) +#define COMSIG_MOB_ENTER_JAUNT "spell_mob_enter_jaunt" +/// Sent from datum/action/cooldown/spell/jaunt/exit_jaunt, after the mob exited jaunt: (datum/action/cooldown/spell/spell) +#define COMSIG_MOB_AFTER_EXIT_JAUNT "spell_mob_after_exit_jaunt" + +/// Sent from/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/try_enter_jaunt, +/// to any unconscious / critical mobs being dragged when the jaunter enters blood: +/// (datum/action/cooldown/spell/jaunt/bloodcrawl/crawl, mob/living/jaunter, obj/effect/decal/cleanable/blood) +#define COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED "living_pre_consumed_by_bloodcrawl" +/// Sent from/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/consume_victim, +/// to the victim being consumed by the slaughter demon. +/// (datum/action/cooldown/spell/jaunt/bloodcrawl/crawl, mob/living/jaunter) +#define COMSIG_LIVING_BLOOD_CRAWL_CONSUMED "living_consumed_by_bloodcrawl" + /// Return at any point to stop the bloodcrawl "consume" process from continuing. + #define COMPONENT_STOP_CONSUMPTION (1 << 0) + +// Signals for specific spells + +// Lichdom +/// Sent from /datum/action/cooldown/spell/lichdom/cast(), to the item being imbued: (datum/action/cooldown/spell/spell, mob/user) +#define COMSIG_ITEM_IMBUE_SOUL "item_imbue_soul" + /// Return to stop the cast and prevent the soul imbue + #define COMPONENT_BLOCK_IMBUE (1 << 0) + +/// Sent from /datum/action/cooldown/spell/aoe/knock/cast(), to every nearby turf (for connect loc): (datum/action/cooldown/spell/aoe/knock/spell, mob/living/caster) +#define COMSIG_ATOM_MAGICALLY_UNLOCKED "atom_magic_unlock" + +// Instant Summons +/// Sent from /datum/action/cooldown/spell/summonitem/cast(), to the item being marked for recall: (datum/action/cooldown/spell/spell, mob/user) +#define COMSIG_ITEM_MARK_RETRIEVAL "item_mark_retrieval" + /// Return to stop the cast and prevent the item from being marked + #define COMPONENT_BLOCK_MARK_RETRIEVAL (1 << 0) + +// Charge +/// Sent from /datum/action/cooldown/spell/charge/cast(), to the item in hand being charged: (datum/action/cooldown/spell/spell, mob/user) +#define COMSIG_ITEM_MAGICALLY_CHARGED "item_magic_charged" + /// Return if an item was successfuly recharged + #define COMPONENT_ITEM_CHARGED (1 << 0) + /// Return if the item had a negative side effect occur while recharging + #define COMPONENT_ITEM_BURNT_OUT (1 << 1) diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 4411c991873dd..5afbc0d8e4532 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -176,6 +176,9 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list( #define ismimite(A) (istype(A, /mob/living/simple_animal/hostile/mimite)) +#define isspider(A) (istype(A, /mob/living/simple_animal/hostile/giant_spider)) + + //Misc mobs #define isobserver(A) (istype(A, /mob/dead/observer)) diff --git a/code/__DEFINES/magic.dm b/code/__DEFINES/magic.dm index 9708cf2cb5add..8ebf3b122cd19 100644 --- a/code/__DEFINES/magic.dm +++ b/code/__DEFINES/magic.dm @@ -3,6 +3,31 @@ ///Spawns random wands and spellbooks near players and gives some players antag objectives #define SUMMON_MAGIC "magic" +// Magic schools +/// Unset / default / "not actually magic" school. +#define SCHOOL_UNSET "unset" +// GOOD SCHOOLS (allowed by honorbound gods, some of these you can get on station) +/// Holy school (chaplain magic) +#define SCHOOL_HOLY "holy" +/// Mime... school? Mime magic. It counts +#define SCHOOL_MIME "mime" +/// Restoration school, which is mostly healing stuff +#define SCHOOL_RESTORATION "restoration" +// NEUTRAL SPELLS (punished by honorbound gods if you get caught using it) +/// Evocation school, usually involves killing or destroy stuff, usually out of thin air +#define SCHOOL_EVOCATION "evocation" +/// School of transforming stuff into other stuff +#define SCHOOL_TRANSMUTATION "transmutation" +/// School of transolcation, usually movement spells +#define SCHOOL_TRANSLOCATION "translocation" +/// Conjuration spells summon items / mobs / etc somehow +#define SCHOOL_CONJURATION "conjuration" +// EVIL SPELLS (instant smite + banishment) +/// Necromancy spells, usually involves soul / evil / bad stuff +#define SCHOOL_NECROMANCY "necromancy" +/// Other forbidden magics, such as heretic spells +#define SCHOOL_FORBIDDEN "forbidden" + // magical invocation types ///Allows being able to cast the spell without saying anything. #define INVOCATION_NONE "none" @@ -13,3 +38,60 @@ ///Forces the wizard to whisper (and be able to) to cast the spell. #define INVOCATION_WHISPER "whisper" + +// Bitflags for spell requirements +/// Whether the spell requires wizard clothes to cast. +#define SPELL_REQUIRES_WIZARD_GARB (1 << 0) +/// Whether the spell can only be cast by humans (mob type, not species). +/// SPELL_REQUIRES_WIZARD_GARB comes with this flag implied, as carbons and below can't wear clothes. +#define SPELL_REQUIRES_HUMAN (1 << 1) +/// Whether the spell can be cast by mobs who are brains / mmis. +/// When applying, bear in mind most spells will not function for brains out of the box. +#define SPELL_CASTABLE_AS_BRAIN (1 << 2) +/// Whether the spell can be cast while phased, such as blood crawling, ethereal jaunting or using rod form. +#define SPELL_CASTABLE_WHILE_PHASED (1 << 3) +/// Whether the spell can be cast while the user has antimagic on them that corresponds to the spell's own antimagic flags. +#define SPELL_REQUIRES_NO_ANTIMAGIC (1 << 4) +/// Whether the spell can be cast on the centcom z level. +#define SPELL_REQUIRES_OFF_CENTCOM (1 << 5) +/// Whether the spell must be cast by someone with a mind datum. +#define SPELL_REQUIRES_MIND (1 << 6) +/// Whether the spell requires the caster have a mime vow (mindless mobs will succeed this check regardless). +#define SPELL_REQUIRES_MIME_VOW (1 << 7) +/// Whether the spell can be cast, even if the caster is unable to speak the invocation +/// (effectively making the invocation flavor, instead of required). +#define SPELL_CASTABLE_WITHOUT_INVOCATION (1 << 8) + +DEFINE_BITFIELD(spell_requirements, list( + "SPELL_CASTABLE_AS_BRAIN" = SPELL_CASTABLE_AS_BRAIN, + "SPELL_CASTABLE_WHILE_PHASED" = SPELL_CASTABLE_WHILE_PHASED, + "SPELL_CASTABLE_WITHOUT_INVOCATION" = SPELL_CASTABLE_WITHOUT_INVOCATION, + "SPELL_REQUIRES_HUMAN" = SPELL_REQUIRES_HUMAN, + "SPELL_REQUIRES_MIME_VOW" = SPELL_REQUIRES_MIME_VOW, + "SPELL_REQUIRES_MIND" = SPELL_REQUIRES_MIND, + "SPELL_REQUIRES_NO_ANTIMAGIC" = SPELL_REQUIRES_NO_ANTIMAGIC, + "SPELL_REQUIRES_OFF_CENTCOM" = SPELL_REQUIRES_OFF_CENTCOM, + "SPELL_REQUIRES_WIZARD_GARB" = SPELL_REQUIRES_WIZARD_GARB, +)) + +// Bitflags for teleport spells +/// Whether the teleport spell skips over space turfs +#define TELEPORT_SPELL_SKIP_SPACE (1 << 0) +/// Whether the teleport spell skips over dense turfs +#define TELEPORT_SPELL_SKIP_DENSE (1 << 1) +/// Whether the teleport spell skips over blocked turfs +#define TELEPORT_SPELL_SKIP_BLOCKED (1 << 2) + +// Bitflags for magic resistance types +/// Default magic resistance that blocks normal magic (wizard, spells, magical staff projectiles) +#define MAGIC_RESISTANCE (1<<0) +/// Tinfoil hat magic resistance that blocks mental magic (telepathy / mind links, mind curses, abductors) +#define MAGIC_RESISTANCE_MIND (1<<1) +#define MAGIC_RESISTANCE_HOLY (1<<2) + +DEFINE_BITFIELD(antimagic_flags, list( + "MAGIC_RESISTANCE" = MAGIC_RESISTANCE, + "MAGIC_RESISTANCE_HOLY" = MAGIC_RESISTANCE_HOLY, + "MAGIC_RESISTANCE_MIND" = MAGIC_RESISTANCE_MIND, +)) + diff --git a/code/__DEFINES/span.dm b/code/__DEFINES/span.dm new file mode 100644 index 0000000000000..3baac161ae418 --- /dev/null +++ b/code/__DEFINES/span.dm @@ -0,0 +1,179 @@ +// Sorted alphabetically +#define span_abductor(str) ("" + str + "") +#define span_admin(str) ("" + str + "") +#define span_adminhelp(str) ("" + str + "") +#define span_adminnotice(str) ("" + str + "") +#define span_adminobserverooc(str) ("" + str + "") +#define span_adminooc(str) ("" + str + "") +#define span_adminsay(str) ("" + str + "") +#define span_aiprivradio(str) ("" + str + "") +#define span_alert(str) ("" + str + "") +#define span_alertalien(str) ("" + str + "") +#define span_alertealien(str) ("" + str + "") +#define span_alertsyndie(str) ("" + str + "") +#define span_alertwarning(str) ("" + str + "") +#define span_alien(str) ("" + str + "") +#define span_alloy(str) ("" + str + "") +#define span_announce(str) ("" + str + "") +#define span_assimilator(str) ("" + str + "") +#define span_attack(str) ("" + str + "") +#define span_average(str) ("" + str + "") +#define span_bad(str) ("" + str + "") +#define span_big(str) ("" + str + "") +#define span_big_brass(str) ("" + str + "") +#define span_bigassimilator(str) ("" + str + "") +#define span_bigicon(str) ("" + str + "") +#define span_binarysay(str) ("" + str + "") +#define span_blue(str) ("" + str + "") +#define span_blueteamradio(str) ("" + str + "") +#define span_bold(str) ("" + str + "") +#define span_boldannounce(str) ("" + str + "") +#define span_bolddanger(str) ("" + str + "") +#define span_boldnotice(str) ("" + str + "") +#define span_boldwarning(str) ("" + str + "") +#define span_brass(str) ("" + str + "") +#define span_caution(str) ("" + str + "") +#define span_centcomradio(str) ("" + str + "") +#define span_changeling(str) ("" + str + "") +#define span_clown(str) ("" + str + "") +#define span_colossus(str) ("" + str + "") +#define span_command_headset(str) ("" + str + "") +#define span_company(str) ("" + str + "") +#define span_comradio(str) ("" + str + "") +#define span_cult(str) ("" + str + "") +#define span_cultbold(str) ("" + str + "") +#define span_cultboldtalic(str) ("" + str + "") +#define span_cultitalic(str) ("" + str + "") +#define span_cultlarge(str) ("" + str + "") +#define span_cultsmall(str) ("" + str + "") +#define span_danger(str) ("" + str + "") +#define span_dangers(str) ("" + str + "") +#define span_dark(str) ("" + str + "") +#define span_deadsay(str) ("" + str + "") +#define span_deconversion_message(str) ("" + str + "") +#define span_deptradio(str) ("" + str + "") +#define span_disarm(str) ("" + str + "") +#define span_drone(str) ("" + str + "") +#define span_dronesay(str) ("" + str + "") +#define span_engradio(str) ("" + str + "") +#define span_error(str) ("" + str + "") +#define span_extremelybig(str) ("" + str + "") +#define span_ghostalert(str) ("" + str + "") +#define span_good(str) ("" + str + "") +#define span_green(str) ("" + str + "") +#define span_greenannounce(str) ("" + str + "") +#define span_greenteamradio(str) ("" + str + "") +#define span_greentext(str) ("" + str + "") +#define span_header(str) ("" + str + "") +#define span_hear(str) ("" + str + "") +#define span_heavy_brass(str) ("" + str + "") +#define span_hidden(str) ("") +#define span_hierophant(str) ("" + str + "") +#define span_hierophant_warning(str) ("" + str + "") +#define span_highlight(str) ("" + str + "") +#define span_his_grace(str) ("" + str + "") +#define span_holoparasite(str) ("" + str + "") +#define span_hypnophrase(str) ("" + str + "") +#define span_icon(str) ("" + str + "") +#define span_inathneq(str) ("" + str + "") +#define span_inathneq_large(str) ("" + str + "") +#define span_inathneq_small(str) ("" + str + "") +#define span_info(str) ("" + str + "") +#define span_interface(str) ("" + str + "") +#define span_italics(str) ("" + str + "") +#define span_large_brass(str) ("" + str + "") +#define span_linkOff(str) ("" + str + "") +#define span_linkOn(str) ("" + str + "") +#define span_linkify(str) ("" + str + "") +#define span_looc(str) ("" + str + "") +#define span_maptext(str) ("" + str + "") +#define span_marooned(str) ("" + str + "") +#define span_medal(str) ("" + str + "") +#define span_medaltext(str) ("" + str + "") +#define span_medradio(str) ("" + str + "") +#define span_memo(str) ("" + str + "") +#define span_memoedit(str) ("" + str + "") +#define span_mentor(str) ("" + str + "") +#define span_message(str) ("" + str + "") +#define span_mind_control(str) ("" + str + "") +#define span_minorannounce(str) ("" + str + "") +#define span_monkey(str) ("" + str + "") +#define span_monkeyhive(str) ("" + str + "") +#define span_monkeylead(str) ("" + str + "") +#define span_name(str) ("" + str + "") +#define span_narsie(str) ("" + str + "") +#define span_narsiesmall(str) ("" + str + "") +#define span_neovgre(str) ("" + str + "") +#define span_neovgre_small(str) ("" + str + "") +#define span_neutraltext(str) ("" + str + "") +#define span_nezbere(str) ("" + str + "") +#define span_nicegreen(str) ("" + str + "") +#define span_nopositions(str) ("" + str + "") +#define span_notice(str) ("" + str + "") +#define span_noticealien(str) ("" + str + "") +#define span_nzcrentr(str) ("" + str + "") +#define span_nzcrentr_large(str) ("" + str + "") +#define span_nzcrentr_small(str) ("" + str + "") +#define span_ooc(str) ("" + str + "") +#define span_ownerdanger(str) ("" + str + "") +#define span_paper_field(str) ("" + str + "") +#define span_papyrus(str) ("" + str + "") +#define span_phobia(str) ("" + str + "") +#define span_prefix(str) ("" + str + "") +#define span_priority(str) ("" + str + "") +#define span_purple(str) ("" + str + "") +#define span_radio(str) ("" + str + "") +#define span_ratvar(str) ("" + str + "") +#define span_reallybig(str) ("" + str + "") +#define span_reallybigphobia(str) ("" + str + "") +#define span_red(str) ("" + str + "") +#define span_redteamradio(str) ("" + str + "") +#define span_redtext(str) ("" + str + "") +#define span_resonate(str) ("" + str + "") +#define span_revenbignotice(str) ("" + str + "") +#define span_revenboldnotice(str) ("" + str + "") +#define span_revendanger(str) ("" + str + "") +#define span_revenminor(str) ("" + str + "") +#define span_revennotice(str) ("" + str + "") +#define span_revenwarning(str) ("" + str + "") +#define span_robot(str) ("" + str + "") +#define span_robotic(str) ("" + str + "") +#define span_rose(str) ("" + str + "") +#define span_runtime_line(str) ("" + str + "") +#define span_s_company(str) ("" + str + "") +#define span_sans(str) ("" + str + "") +#define span_sciradio(str) ("" + str + "") +#define span_secradio(str) ("" + str + "") +#define span_servradio(str) ("" + str + "") +#define span_sevtug(str) ("" + str + "") +#define span_sevtug_small(str) ("" + str + "") +#define span_shadowling(str) ("" + str + "") +#define span_singing(str) ("" + str + "") +#define span_slime(str) ("" + str + "") +#define span_small(str) ("" + str + "") +#define span_smalldanger(str) ("" + str + "") +#define span_smallnotice(str) ("" + str + "") +#define span_smallnoticeital(str) ("" + str + "") +#define span_span_notify(str) ("" + str + "") +#define span_spider(str) ("" + str + "") +#define span_suicide(str) ("" + str + "") +#define span_suppradio(str) ("" + str + "") +#define span_surrender(str) ("" + str + "") +#define span_swarmer(str) ("" + str + "") +#define span_syndradio(str) ("" + str + "") +#define span_tape_recorder(str) ("" + str + "") +#define span_tinynotice(str) ("" + str + "") +#define span_tinynoticeital(str) ("" + str + "") +#define span_tooltip_container(str) ("" + str + "") +#define span_umbra_emphasis(str) ("" + str + "") +#define span_unclaimed(str) ("" + str + "") +#define span_unconscious(str) ("" + str + "") +#define span_userdanger(str) ("" + str + "") +#define span_usernotice(str) ("" + str + "") +#define span_value(str) ("" + str + "") +#define span_vampirewarning(str) ("" + str + "") +#define span_velvet(str) ("" + str + "") +#define span_warner(str) ("" + str + "") +#define span_warning(str) ("" + str + "") +#define span_yell(str) ("" + str + "") +#define span_yellowteamradio(str) ("" + str + "") diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index eaa87c3b502d6..88d9ebeae62a8 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -276,6 +276,16 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_NO_BLEEDING "no_bleed" // The user can acquire the bleeding status effect, but will no lose blood #define TRAIT_BLOOD_COOLANT "blood_coolant" // Replaces blood with coolant, meaning we overheat instead of losing air +/// Immune to being afflicted by time stop (spell) +#define TRAIT_TIME_STOP_IMMUNE "time_stop_immune" +/// Whether a spider's consumed this mob +#define TRAIT_SPIDER_CONSUMED "spider_consumed" +/// Whether we're sneaking, from the alien sneak ability. +/// Maybe worth generalizing into a general "is sneaky" / "is stealth" trait in the future. +#define TRAIT_ALIEN_SNEAK "sneaking_alien" +/// This mob is phased out of reality from magic, either a jaunt or rod form +#define TRAIT_MAGICALLY_PHASED "magically_phased" + // You can stare into the abyss, but it does not stare back. // You're immune to the hallucination effect of the supermatter, either // through force of will, or equipment. diff --git a/code/__byond_version_compat.dm b/code/__byond_version_compat.dm index 0b6ff6cad451e..6bf42fbc4d0fb 100644 --- a/code/__byond_version_compat.dm +++ b/code/__byond_version_compat.dm @@ -28,7 +28,7 @@ // and so this check is in place to stop serious damage. // That being said, if you really are ready, you can give YES_I_WANT_515 to TGS. #if !defined(YES_I_WANT_515) && DM_VERSION >= 515 -#error We do not yet completely support BYOND 515. +//#error We do not yet completely support BYOND 515. #endif // 515 split call for external libraries into call_ext diff --git a/code/_compile_options.dm b/code/_compile_options.dm index 931aa9e059103..55a03078d83cf 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -104,7 +104,7 @@ #define MAX_COMPILER_VERSION 514 #define MAX_COMPILER_BUILD 1589 #if DM_VERSION > MAX_COMPILER_VERSION || DM_BUILD > MAX_COMPILER_BUILD -#warn WARNING: Your BYOND version is over the recommended version (514.1589)! Stability is not guaranteed. +//#warn WARNING: Your BYOND version is over the recommended version (514.1589)! Stability is not guaranteed. #endif //Log the full sendmaps profile on 514.1556+, any earlier and we get bugs or it not existing #if DM_VERSION >= 514 && DM_BUILD >= 1556 diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index 1cdd42507f746..2c3ce46e5c272 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -299,3 +299,12 @@ DEFINE_BITFIELD(mecha_flags, list( "IS_ENCLOSED" = IS_ENCLOSED, "HAS_LIGHTS" = HAS_LIGHTS, )) + +DEFINE_BITFIELD(check_flags, list(\ + "CHECK IF HANDS BLOCKED" = AB_CHECK_HANDS_BLOCKED, \ + "CHECK IF IMMOBILIZED" = AB_CHECK_IMMOBILE, \ + "CHECK IF LYING DOWN" = AB_CHECK_LYING, \ + "CHECK IF CONSCIOUS" = AB_CHECK_CONSCIOUS, \ + "CHECK IF INCAPACITATED" = AB_CHECK_INCAPACITATED, \ + "CHECK IF TEMPORARILY INCORPOREAL" = AB_CHECK_PHASED, +)) diff --git a/code/controllers/subsystem/augury.dm b/code/controllers/subsystem/augury.dm index e51e511177208..5eec89e91b78c 100644 --- a/code/controllers/subsystem/augury.dm +++ b/code/controllers/subsystem/augury.dm @@ -65,17 +65,17 @@ SUBSYSTEM_DEF(augury) SSaugury.watchers += owner to_chat(owner, "You are now auto-following debris.") active = TRUE - UpdateButtonIcon() + UpdateButtons() /datum/action/innate/augury/Deactivate() SSaugury.watchers -= owner to_chat(owner, "You are no longer auto-following debris.") active = FALSE - UpdateButtonIcon() + UpdateButtons() /datum/action/innate/augury/UpdateButtonIcon(status_only = FALSE, force) ..() if(active) - button.icon_state = "template_active" + button_icon_state = "template_active" else - button.icon_state = "template" + button_icon_state = "template" diff --git a/code/datums/action.dm b/code/datums/action.dm deleted file mode 100644 index 6792456a3f271..0000000000000 --- a/code/datums/action.dm +++ /dev/null @@ -1,830 +0,0 @@ -// Checks to see if the mob is able to use their hands, or if they are blocked by cuffs or stuns -#define AB_CHECK_HANDS_BLOCKED (1<<0) -// Checks to see if the mob is incapacitated by stuns or paralysis effects -#define AB_CHECK_INCAPACITATED (1<<1) -// Checks to see if the mob is standing -#define AB_CHECK_LYING (1<<2) -// Checks to see if the mob in concious -#define AB_CHECK_CONSCIOUS (1<<3) - -/datum/action - var/name = "Generic Action" - var/desc = null - var/obj/target = null - var/check_flags = NONE - var/processing = FALSE - var/atom/movable/screen/movable/action_button/button = null - var/buttontooltipstyle = "" - var/transparent_when_unavailable = TRUE - - var/button_icon = 'icons/mob/actions/backgrounds.dmi' //This is the file for the BACKGROUND icon - var/background_icon_state = ACTION_BUTTON_DEFAULT_BACKGROUND //And this is the state for the background icon - - var/icon_icon = 'icons/mob/actions.dmi' //This is the file for the ACTION icon - var/button_icon_state = "default" //And this is the state for the action icon - var/mob/owner - - var/has_cooldown_timer = FALSE - -/datum/action/New(Target) - link_to(Target) - button = new - button.linked_action = src - button.name = name - button.actiontooltipstyle = buttontooltipstyle - if(desc) - button.desc = desc - -/datum/action/proc/link_to(Target) - target = Target - RegisterSignal(Target, COMSIG_ATOM_UPDATED_ICON, PROC_REF(OnUpdatedIcon)) - -/datum/action/Destroy() - if(owner) - Remove(owner) - target = null - QDEL_NULL(button) - return ..() - -/datum/action/proc/Grant(mob/M) - if(M) - if(owner) - if(owner == M) - return - Remove(owner) - owner = M - RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(owner_deleted)) - - //button id generation - var/counter = 0 - var/bitfield = 0 - for(var/datum/action/A in M.actions) - if(A.name == name && A.button.id) - counter += 1 - bitfield |= A.button.id - bitfield = ~bitfield - var/bitflag = 1 - for(var/i in 1 to (counter + 1)) - if(bitfield & bitflag) - button.id = bitflag - break - bitflag *= 2 - - M.actions += src - if(M.client) - M.client.screen += button - button.locked = M.client.prefs.read_player_preference(/datum/preference/toggle/buttons_locked) || button.id ? M.client.prefs.action_buttons_screen_locs["[name]_[button.id]"] : FALSE //even if it's not defaultly locked we should remember we locked it before - button.moved = button.id ? M.client.prefs.action_buttons_screen_locs["[name]_[button.id]"] : FALSE - var/obj/effect/proc_holder/spell/spell_proc_holder = button.linked_action.target - if(istype(spell_proc_holder) && spell_proc_holder.text_overlay) - M.client.images += spell_proc_holder.text_overlay - M.update_action_buttons() - else - Remove(owner) - -/datum/action/proc/owner_deleted(datum/source) - SIGNAL_HANDLER - - Remove(owner) - -/datum/action/proc/Remove(mob/M) - if(M) - if(M.client) - M.client.screen -= button - M.actions -= src - M.update_action_buttons() - if(owner) - UnregisterSignal(owner, COMSIG_PARENT_QDELETING) - owner = null - button.moved = FALSE //so the button appears in its normal position when given to another owner. - button.locked = FALSE - button.id = null - -/datum/action/proc/Trigger() - if(!IsAvailable()) - return FALSE - if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER) - return FALSE - return TRUE - -/datum/action/proc/IsAvailable() - if(!owner) - return FALSE - if((check_flags & AB_CHECK_HANDS_BLOCKED) && HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED)) - return FALSE - if((check_flags & AB_CHECK_INCAPACITATED) && HAS_TRAIT(owner, TRAIT_INCAPACITATED)) - return FALSE - if((check_flags & AB_CHECK_LYING) && isliving(owner)) - var/mob/living/action_user = owner - if(action_user.body_position == LYING_DOWN) - return FALSE - if((check_flags & AB_CHECK_CONSCIOUS) && owner.stat != CONSCIOUS) - return FALSE - return TRUE - -/datum/action/proc/UpdateButtonIcon(status_only = FALSE, force = FALSE) - if(!button) - return FALSE - - if(!status_only) - button.name = name - button.desc = desc - if(owner?.hud_used && background_icon_state == ACTION_BUTTON_DEFAULT_BACKGROUND) - var/list/settings = owner.hud_used.get_action_buttons_icons() - if(button.icon != settings["bg_icon"]) - button.icon = settings["bg_icon"] - if(button.icon_state != settings["bg_state"]) - button.icon_state = settings["bg_state"] - else - if(button.icon != button_icon) - button.icon = button_icon - if(button.icon_state != background_icon_state) - button.icon_state = background_icon_state - ApplyIcon(button, force) - if(!IsAvailable()) - button.color = has_cooldown_timer ? rgb(219, 219, 219, 255) : transparent_when_unavailable ? rgb(128,0,0,128) : rgb(128,0,0) - else - button.color = rgb(255,255,255,255) - return TRUE - -/datum/action/proc/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force = FALSE) - if(icon_icon && button_icon_state || force) - current_button.cut_overlays() - current_button.add_overlay(mutable_appearance(icon_icon, button_icon_state)) - current_button.button_icon_state = button_icon_state - -/datum/action/proc/OnUpdatedIcon() - SIGNAL_HANDLER - UpdateButtonIcon() - -//Presets for item actions -/datum/action/item_action - check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_INCAPACITATED|AB_CHECK_CONSCIOUS - button_icon_state = null - // If you want to override the normal icon being the item - // then change this to an icon state - -/datum/action/item_action/New(Target) - ..() - var/obj/item/I = target - LAZYINITLIST(I.actions) - I.actions += src - -/datum/action/item_action/Destroy() - var/obj/item/I = target - I?.actions -= src - UNSETEMPTY(I.actions) - return ..() - -/datum/action/item_action/Trigger() - . = ..() - if(!..()) - return FALSE - if(target) - var/obj/item/I = target - I.ui_action_click(owner, src) - return TRUE - -/datum/action/item_action/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force) - if(button_icon && button_icon_state) - // If set, use the custom icon that we set instead - // of the item appearance - ..() - else if((target && current_button.appearance_cache != target.appearance) || force) //replace with /ref comparison if this is not valid. - var/obj/item/I = target - var/old_layer = I.layer - var/old_plane = I.plane - I.layer = FLOAT_LAYER //AAAH - I.plane = FLOAT_PLANE //^ what that guy said - current_button.cut_overlays() - current_button.add_overlay(I) - I.layer = old_layer - I.plane = old_plane - current_button.appearance_cache = I.appearance - -/datum/action/item_action/toggle_light - name = "Toggle Light" - -/datum/action/item_action/toggle_light/Trigger() - if(istype(target, /obj/item/modular_computer)) - var/obj/item/modular_computer/mc = target - mc.toggle_flashlight() - return - ..() - -/datum/action/item_action/toggle_hood - name = "Toggle Hood" - -/datum/action/item_action/toggle_firemode - name = "Toggle Firemode" - -/datum/action/item_action/rcl_col - name = "Change Cable Color" - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "rcl_rainbow" - -/datum/action/item_action/rcl_gui - name = "Toggle Fast Wiring Gui" - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "rcl_gui" - -/datum/action/item_action/startchainsaw - name = "Pull The Starting Cord" - -/datum/action/item_action/toggle_computer_light - name = "Toggle Flashlight" - -/datum/action/item_action/toggle_gunlight - name = "Toggle Gunlight" - -/datum/action/item_action/toggle_mode - name = "Toggle Mode" - -/datum/action/item_action/toggle_barrier_spread - name = "Toggle Barrier Spread" - -/datum/action/item_action/equip_unequip_TED_Gun - name = "Equip/Unequip TED Gun" - -/datum/action/item_action/toggle_paddles - name = "Toggle Paddles" - -/datum/action/item_action/set_internals - name = "Set Internals" - -/datum/action/item_action/set_internals/UpdateButtonIcon(status_only = FALSE, force) - if(..()) //button available - if(iscarbon(owner)) - var/mob/living/carbon/C = owner - if(target == C.internal) - button.icon_state = "template_active" - -/datum/action/item_action/pick_color - name = "Choose A Color" - -/datum/action/item_action/toggle_mister - name = "Toggle Mister" - -/datum/action/item_action/activate_injector - name = "Activate Injector" - -/datum/action/item_action/toggle_helmet_light - name = "Toggle Helmet Light" - -/datum/action/item_action/toggle_welding_screen - name = "Toggle Welding Screen" - -/datum/action/item_action/toggle_welding_screen/Trigger() - var/obj/item/clothing/head/utility/hardhat/welding/H = target - if(istype(H)) - H.toggle_welding_screen(owner) - -/datum/action/item_action/toggle_welding_screen/plasmaman - name = "Toggle Welding Screen" - -/datum/action/item_action/toggle_welding_screen/plasmaman/Trigger() - var/obj/item/clothing/head/helmet/space/plasmaman/H = target - if(istype(H)) - H.toggle_welding_screen(owner) - -/datum/action/item_action/toggle_headphones - name = "Open Music Menu" - desc = "UNTZ UNTZ UNTZ" - -/datum/action/item_action/toggle_headphones/Trigger() - var/obj/item/clothing/ears/headphones/H = target - if(istype(H)) - H.interact(owner) - -/datum/action/item_action/toggle_spacesuit - name = "Toggle Suit Thermal Regulator" - icon_icon = 'icons/mob/actions/actions_spacesuit.dmi' - button_icon_state = "thermal_off" - -/datum/action/item_action/toggle_spacesuit/New(Target) - . = ..() - RegisterSignal(target, COMSIG_SUIT_SPACE_TOGGLE, PROC_REF(toggle)) - -/datum/action/item_action/toggle_spacesuit/Destroy() - UnregisterSignal(target, COMSIG_SUIT_SPACE_TOGGLE) - return ..() - -/datum/action/item_action/toggle_spacesuit/Trigger() - var/obj/item/clothing/suit/space/suit = target - if(!istype(suit)) - return - suit.toggle_spacesuit() - -/// Toggle the action icon for the space suit thermal regulator -/datum/action/item_action/toggle_spacesuit/proc/toggle(obj/item/clothing/suit/space/suit) - button_icon_state = "thermal_[suit.thermal_on ? "on" : "off"]" - UpdateButtonIcon() - -/datum/action/item_action/toggle_unfriendly_fire - name = "Toggle Friendly Fire \[ON\]" - desc = "Toggles if the club's blasts cause friendly fire." - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "vortex_ff_on" - -/datum/action/item_action/toggle_unfriendly_fire/Trigger() - if(..()) - UpdateButtonIcon() - -/datum/action/item_action/toggle_unfriendly_fire/UpdateButtonIcon(status_only = FALSE, force) - if(istype(target, /obj/item/hierophant_club)) - var/obj/item/hierophant_club/H = target - if(H.friendly_fire_check) - button_icon_state = "vortex_ff_off" - name = "Toggle Friendly Fire \[OFF\]" - else - button_icon_state = "vortex_ff_on" - name = "Toggle Friendly Fire \[ON\]" - ..() - -/datum/action/item_action/vortex_recall - name = "Vortex Recall" - desc = "Recall yourself, and anyone nearby, to an attuned hierophant beacon at any time.
If the beacon is still attached, will detach it." - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "vortex_recall" - -/datum/action/item_action/vortex_recall/IsAvailable() - if(istype(target, /obj/item/hierophant_club)) - var/obj/item/hierophant_club/H = target - if(H.teleporting) - return 0 - return ..() - -/datum/action/item_action/clock/hierophant - name = "Hierophant Network" - desc = "Lets you discreetly talk with all other servants. Nearby listeners can hear you whispering, so make sure to do this privately." - button_icon_state = "hierophant_slab" - -/datum/action/item_action/clock/quickbind - name = "Quickbind" - desc = "If you're seeing this, file a bug report." - var/scripture_index = 0 //the index of the scripture we're associated with - -/datum/action/item_action/toggle_helmet_flashlight - name = "Toggle Helmet Flashlight" - -/datum/action/item_action/toggle_helmet_mode - name = "Toggle Helmet Mode" - -/datum/action/item_action/toggle_beacon - name = "Toggle Hardsuit Locator Beacon" - icon_icon = 'icons/mob/actions.dmi' - button_icon_state = "toggle-transmission" - -/datum/action/item_action/toggle_beacon_hud - name = "Toggle Hardsuit Locator HUD" - icon_icon = 'icons/mob/actions.dmi' - button_icon_state = "toggle-hud" - -/datum/action/item_action/toggle_beacon_hud/explorer - button_icon_state = "toggle-hud-explo" - -/datum/action/item_action/toggle_beacon_frequency - name = "Toggle Hardsuit Locator Frequency" - icon_icon = 'icons/mob/actions.dmi' - button_icon_state = "change-code" - -/datum/action/item_action/crew_monitor - name = "Interface With Crew Monitor" - -/datum/action/item_action/toggle - -/datum/action/item_action/toggle/New(Target) - ..() - name = "Toggle [target.name]" - button.name = name - -/datum/action/item_action/halt - name = "HALT!" - -/datum/action/item_action/toggle_voice_box - name = "Toggle Voice Box" - -/datum/action/item_action/change - name = "Change" - -/datum/action/item_action/nano_picket_sign - name = "Retext Nano Picket Sign" - -/datum/action/item_action/nano_picket_sign/Trigger() - if(!istype(target, /obj/item/picket_sign)) - return - var/obj/item/picket_sign/sign = target - sign.retext(owner) - -/datum/action/item_action/adjust - -/datum/action/item_action/adjust/New(Target) - ..() - name = "Adjust [target.name]" - button.name = name - -/datum/action/item_action/switch_hud - name = "Switch HUD" - -/datum/action/item_action/toggle_human_head - name = "Toggle Human Head" - -/datum/action/item_action/toggle_helmet - name = "Toggle Helmet" - -/datum/action/item_action/toggle_seclight - name = "Toggle Seclight" - -/datum/action/item_action/toggle_jetpack - name = "Toggle Jetpack" - -/datum/action/item_action/jetpack_stabilization - name = "Toggle Jetpack Stabilization" - -/datum/action/item_action/jetpack_stabilization/IsAvailable() - var/obj/item/tank/jetpack/J = target - if(!istype(J) || !J.on) - return 0 - return ..() - -/datum/action/item_action/hands_free - check_flags = AB_CHECK_CONSCIOUS - -/datum/action/item_action/hands_free/activate - name = "Activate" - -/datum/action/item_action/hands_free/shift_nerves - name = "Shift Nerves" - -/datum/action/item_action/explosive_implant - check_flags = NONE - name = "Activate Explosive Implant" - -/datum/action/item_action/toggle_research_scanner - name = "Toggle Research Scanner" - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "scan_mode" - var/active = FALSE - -/datum/action/item_action/toggle_research_scanner/Trigger() - if(IsAvailable()) - active = !active - if(active) - owner.research_scanner++ - else - owner.research_scanner-- - to_chat(owner, "[target] research scanner has been [active ? "activated" : "deactivated"].") - return 1 - -/datum/action/item_action/toggle_research_scanner/Remove(mob/M) - if(owner && active) - owner.research_scanner-- - active = FALSE - ..() - -/datum/action/item_action/instrument - name = "Use Instrument" - desc = "Use the instrument specified" - -/datum/action/item_action/instrument/Trigger() - if(istype(target, /obj/item/instrument)) - var/obj/item/instrument/I = target - I.interact(usr) - return - return ..() - -/datum/action/item_action/activate_remote_view - name = "Activate Remote View" - desc = "Activates the Remote View of your spy sunglasses." - -/datum/action/item_action/organ_action - check_flags = AB_CHECK_CONSCIOUS - -/datum/action/item_action/organ_action/IsAvailable() - var/obj/item/organ/I = target - if(!I.owner) - return 0 - return ..() - -/datum/action/item_action/organ_action/toggle/New(Target) - ..() - name = "Toggle [target.name]" - button.name = name - -/datum/action/item_action/organ_action/use/New(Target) - ..() - name = "Use [target.name]" - button.name = name - -/datum/action/item_action/cult_dagger - name = "Draw Blood Rune" - desc = "Use the ritual dagger to create a powerful blood rune" - icon_icon = 'icons/mob/actions/actions_cult.dmi' - button_icon_state = "draw" - buttontooltipstyle = "cult" - background_icon_state = "bg_demon" - -/datum/action/item_action/cult_dagger/Grant(mob/M) - if(!IS_CULTIST(M)) - Remove(owner) - return - . = ..() - button.screen_loc = "6:157,4:-2" - button.moved = "6:157,4:-2" - -/datum/action/item_action/cult_dagger/Trigger() - for(var/obj/item/melee/cultblade/dagger/held_item in owner.held_items) // In case we were already holding a dagger - held_item.attack_self(owner) - return - var/obj/item/target_item = target - if(owner.can_equip(target_item, ITEM_SLOT_HANDS)) - owner.temporarilyRemoveItemFromInventory(target_item) - owner.put_in_hands(target_item) - target_item.attack_self(owner) - return - if(!isliving(owner)) - to_chat(owner, "You lack the necessary living force for this action.") - return - var/mob/living/living_owner = owner - if (living_owner.usable_hands <= 0) - to_chat(living_owner, "You dont have any usable hands!") - else - to_chat(living_owner, "Your hands are full!") - - -///MGS BOX! -/datum/action/item_action/agent_box - name = "Deploy Box" - desc = "Find inner peace, here, in the box." - check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_INCAPACITATED|AB_CHECK_CONSCIOUS - background_icon_state = "bg_agent" - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "deploy_box" - ///The type of closet this action spawns. - var/boxtype = /obj/structure/closet/cardboard/agent - COOLDOWN_DECLARE(box_cooldown) - -///Handles opening and closing the box. -/datum/action/item_action/agent_box/Trigger() - . = ..() - if(!.) - return FALSE - if(istype(owner.loc, /obj/structure/closet/cardboard/agent)) - var/obj/structure/closet/cardboard/agent/box = owner.loc - owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE) - box.open() - return - //Box closing from here on out. - if(!isturf(owner.loc)) //Don't let the player use this to escape mechs/welded closets. - to_chat(owner, "You need more space to activate this implant.") - return - if(!COOLDOWN_FINISHED(src, box_cooldown)) - return - COOLDOWN_START(src, box_cooldown, 10 SECONDS) - var/box = new boxtype(owner.drop_location()) - owner.forceMove(box) - owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE) - -/datum/action/item_action/portaseeder_dissolve - name = "Activate Seed Extractor" - -/datum/action/item_action/portaseeder_dissolve/Trigger() - var/obj/item/storage/bag/plants/portaseeder/H = target - H.dissolve_contents() - -//Preset for spells -/datum/action/spell_action - check_flags = NONE - background_icon_state = "bg_spell" - -/datum/action/spell_action/New(Target) - ..() - var/obj/effect/proc_holder/S = target - S.action = src - name = S.name - desc = S.desc - icon_icon = S.action_icon - button_icon_state = S.action_icon_state - background_icon_state = S.action_background_icon_state - button.name = name - -/datum/action/spell_action/Destroy() - var/obj/effect/proc_holder/S = target - S?.action = null - return ..() - -/datum/action/spell_action/Trigger() - if(!..()) - return FALSE - if(target) - var/obj/effect/proc_holder/S = target - S.Click() - return TRUE - -/datum/action/spell_action/IsAvailable() - if(!target) - return FALSE - return TRUE - -/datum/action/spell_action/spell - -/datum/action/spell_action/spell/IsAvailable() - if(!target) - return FALSE - var/obj/effect/proc_holder/spell/S = target - if(owner) - return S.can_cast(owner) - return FALSE - -/datum/action/spell_action/alien - -/datum/action/spell_action/alien/IsAvailable() - if(!target) - return FALSE - var/obj/effect/proc_holder/alien/ab = target - if(owner) - return ab.cost_check(ab.check_turf,owner,1) - return FALSE - - - -//Preset for general and toggled actions -/datum/action/innate - check_flags = NONE - var/active = 0 - -/datum/action/innate/Trigger() - if(!..()) - return 0 - if(!active) - Activate() - else - Deactivate() - return 1 - -/datum/action/innate/proc/Activate() - return - -/datum/action/innate/proc/Deactivate() - return - -//Preset for an action with a cooldown - -/datum/action/cooldown - check_flags = NONE - transparent_when_unavailable = FALSE - var/cooldown_time = 0 - var/next_use_time = 0 - -/datum/action/cooldown/New() - ..() - button.maptext = "" - button.maptext_x = 8 - button.maptext_y = 0 - button.maptext_width = 24 - button.maptext_height = 12 - -/datum/action/cooldown/IsAvailable() - return next_use_time <= world.time - -/datum/action/cooldown/proc/StartCooldown() - next_use_time = world.time + cooldown_time - button.maptext = MAPTEXT("[round(cooldown_time/10, 0.1)]") - UpdateButtonIcon() - START_PROCESSING(SSfastprocess, src) - -/datum/action/cooldown/process() - if(!owner) - button.maptext = "" - return PROCESS_KILL - var/timeleft = max(next_use_time - world.time, 0) - if(timeleft == 0) - button.maptext = "" - UpdateButtonIcon() - return PROCESS_KILL - else - button.maptext = MAPTEXT("[round(timeleft/10, 0.1)]") - -/datum/action/cooldown/Grant(mob/M) - ..() - if(owner) - UpdateButtonIcon() - if(next_use_time > world.time) - START_PROCESSING(SSfastprocess, src) - - -//Stickmemes -/datum/action/item_action/stickmen - name = "Summon Stick Minions" - desc = "Allows you to summon faithful stickmen allies to aide you in battle." - icon_icon = 'icons/mob/actions/actions_minor_antag.dmi' - button_icon_state = "art_summon" - -//surf_ss13 -/datum/action/item_action/bhop - name = "Activate Jump Boots" - desc = "Activates the jump boot's internal propulsion system, allowing the user to dash over 4-wide gaps." - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "jetboot" - -/datum/action/language_menu - name = "Language Menu" - desc = "Open the language menu to review your languages, their keys, and select your default language." - button_icon_state = "language_menu" - check_flags = NONE - -/datum/action/language_menu/Trigger() - if(!..()) - return FALSE - if(ismob(owner)) - var/mob/M = owner - var/datum/language_holder/H = M.get_language_holder() - H.open_language_menu(usr) - -/datum/action/item_action/wheelys - name = "Toggle Wheely-Heel's Wheels" - desc = "Pops out or in your wheely-heel's wheels." - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "wheelys" - -/datum/action/item_action/kindleKicks - name = "Activate Kindle Kicks" - desc = "Kick you feet together, activating the lights in your Kindle Kicks." - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "kindleKicks" - -//Small sprites -/datum/action/small_sprite - name = "Toggle Giant Sprite" - desc = "Others will always see you as giant." - icon_icon = 'icons/mob/actions/actions_xeno.dmi' - button_icon_state = "smallqueen" - background_icon_state = "bg_alien" - var/small = FALSE - var/small_icon - var/small_icon_state - -/datum/action/small_sprite/queen - small_icon = 'icons/mob/alien.dmi' - small_icon_state = "alienq" - -/datum/action/small_sprite/megafauna - icon_icon = 'icons/mob/actions/actions_xeno.dmi' - small_icon = 'icons/mob/lavaland/lavaland_monsters.dmi' - -/datum/action/small_sprite/megafauna/drake - small_icon_state = "ash_whelp" - -/datum/action/small_sprite/megafauna/colossus - small_icon_state = "Basilisk" - -/datum/action/small_sprite/megafauna/bubblegum - small_icon_state = "goliath2" - -/datum/action/small_sprite/megafauna/legion - small_icon_state = "dwarf_legion" - -/datum/action/small_sprite/space_dragon - small_icon = 'icons/mob/carp.dmi' - small_icon_state = "carp" - icon_icon = 'icons/mob/carp.dmi' - button_icon_state = "carp" - -/datum/action/small_sprite/space_dragon/Trigger() - ..() - if(small) // parent call already reversed this. Effectively !small - owner.cut_overlays() // remove the overlays. can't be done with signals, unfortunately - else if(istype(owner, /mob/living/simple_animal/hostile/space_dragon)) - var/mob/living/simple_animal/hostile/space_dragon/D = owner - D.update_dragon_overlay() // restore overlays - -/datum/action/small_sprite/Trigger() - ..() - if(!small) - var/image/I = image(icon = small_icon, icon_state = small_icon_state, loc = owner) - I.override = TRUE - I.pixel_x -= owner.pixel_x - I.pixel_y -= owner.pixel_y - owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic, "smallsprite", I, AA_TARGET_SEE_APPEARANCE | AA_MATCH_TARGET_OVERLAYS) - small = TRUE - else - owner.remove_alt_appearance("smallsprite") - small = FALSE - -/datum/action/item_action/storage_gather_mode - name = "Switch gathering mode" - desc = "Switches the gathering mode of a storage object." - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "storage_gather_switch" - -/datum/action/item_action/storage_gather_mode/ApplyIcon(atom/movable/screen/movable/action_button/current_button) - . = ..() - var/old_layer = target.layer - var/old_plane = target.plane - target.layer = FLOAT_LAYER //AAAH - target.plane = FLOAT_PLANE //^ what that guy said - current_button.cut_overlays() - current_button.add_overlay(target) - target.layer = old_layer - target.plane = old_plane - current_button.appearance_cache = target.appearance diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm new file mode 100644 index 0000000000000..c20b4dbe9435c --- /dev/null +++ b/code/datums/actions/action.dm @@ -0,0 +1,256 @@ +/** + * # Action system + * + * A simple base for an modular behavior attached to atom or datum. + */ +/datum/action + /// The name of the action + var/name = "Generic Action" + /// The description of what the action does + var/desc + /// The target the action is attached to. If the target datum is deleted, the action is as well. + /// Set in New() via the proc link_to(). PLEASE set a target if you're making an action + var/datum/target + /// Where any buttons we create should be by default. Accepts screen_loc and location defines + var/default_button_position = SCRN_OBJ_IN_LIST + /// This is who currently owns the action, and most often, this is who is using the action if it is triggered + /// This can be the same as "target" but is not ALWAYS the same - this is set and unset with Grant() and Remove() + var/mob/owner + /// Flags that will determine of the owner / user of the action can... use the action + var/check_flags = NONE + /// The style the button's tooltips appear to be + var/buttontooltipstyle = "" + /// Whether the button becomes transparent when it can't be used or just reddened + var/transparent_when_unavailable = TRUE + /// This is the file for the BACKGROUND icon of the button + var/button_icon = 'icons/mob/actions/backgrounds.dmi' + /// This is the icon state state for the BACKGROUND icon of the button + var/background_icon_state = ACTION_BUTTON_DEFAULT_BACKGROUND + /// This is the file for the icon that appears OVER the button background + var/icon_icon = 'icons/hud/actions.dmi' + /// This is the icon state for the icon that appears OVER the button background + var/button_icon_state = "default" + ///List of all mobs that are viewing our action button -> A unique movable for them to view. + var/list/viewers = list() + +/datum/action/New(Target) + link_to(Target) + +/// Links the passed target to our action, registering any relevant signals +/datum/action/proc/link_to(Target) + target = Target + RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_ref, override = TRUE) + + if(isatom(target)) + RegisterSignal(target, COMSIG_ATOM_UPDATED_ICON, .proc/update_icon_on_signal) + + if(istype(target, /datum/mind)) + RegisterSignal(target, COMSIG_MIND_TRANSFERRED, .proc/on_target_mind_swapped) + +/datum/action/Destroy() + if(owner) + Remove(owner) + target = null + QDEL_LIST_ASSOC_VAL(viewers) // Qdel the buttons in the viewers list **NOT THE HUDS** + return ..() + +/// Signal proc that clears any references based on the owner or target deleting +/// If the owner's deleted, we will simply remove from them, but if the target's deleted, we will self-delete +/datum/action/proc/clear_ref(datum/ref) + SIGNAL_HANDLER + if(ref == owner) + Remove(owner) + if(ref == target) + qdel(src) + +/// Grants the action to the passed mob, making it the owner +/datum/action/proc/Grant(mob/grant_to) + if(!grant_to) + Remove(owner) + return + if(owner) + if(owner == grant_to) + return + Remove(owner) + SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, grant_to) + owner = grant_to + RegisterSignal(owner, COMSIG_PARENT_QDELETING, .proc/clear_ref, override = TRUE) + + // Register some signals based on our check_flags + // so that our button icon updates when relevant + if(check_flags & AB_CHECK_CONSCIOUS) + RegisterSignal(owner, COMSIG_MOB_STATCHANGE, .proc/update_icon_on_signal) + if(check_flags & AB_CHECK_IMMOBILE) + RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), .proc/update_icon_on_signal) + if(check_flags & AB_CHECK_HANDS_BLOCKED) + RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), .proc/update_icon_on_signal) + if(check_flags & AB_CHECK_LYING) + RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, .proc/update_icon_on_signal) + + GiveAction(grant_to) + +/// Remove the passed mob from being owner of our action +/datum/action/proc/Remove(mob/remove_from) + SHOULD_CALL_PARENT(TRUE) + + for(var/datum/hud/hud in viewers) + if(!hud.mymob) + continue + HideFrom(hud.mymob) + LAZYREMOVE(remove_from.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared + viewers = list() + + if(owner) + SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner) + UnregisterSignal(owner, COMSIG_PARENT_QDELETING) + + // Clean up our check_flag signals + UnregisterSignal(owner, list( + COMSIG_LIVING_SET_BODY_POSITION, + COMSIG_MOB_STATCHANGE, + SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), + SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), + )) + + if(target == owner) + RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_ref) + owner = null + +/// Actually triggers the effects of the action. +/// Called when the on-screen button is clicked, for example. +/datum/action/proc/Trigger(trigger_flags) + if(!IsAvailable()) + return FALSE + if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER) + return FALSE + return TRUE + +/// Whether our action is currently available to use or not +/datum/action/proc/IsAvailable() + if(!owner) + return FALSE + if((check_flags & AB_CHECK_HANDS_BLOCKED) && HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED)) + return FALSE + if((check_flags & AB_CHECK_IMMOBILE) && HAS_TRAIT(owner, TRAIT_IMMOBILIZED)) + return FALSE + if((check_flags & AB_CHECK_LYING) && isliving(owner)) + var/mob/living/action_user = owner + if(action_user.body_position == LYING_DOWN) + return FALSE + if((check_flags & AB_CHECK_CONSCIOUS) && owner.stat != CONSCIOUS) + return FALSE + return TRUE + +/datum/action/proc/UpdateButtons(status_only, force) + for(var/datum/hud/hud in viewers) + var/atom/movable/screen/movable/button = viewers[hud] + UpdateButton(button, status_only, force) + +/datum/action/proc/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE) + if(!button) + return + if(!status_only) + button.name = name + button.desc = desc + if(owner?.hud_used && background_icon_state == ACTION_BUTTON_DEFAULT_BACKGROUND) + var/list/settings = owner.hud_used.get_action_buttons_icons() + if(button.icon != settings["bg_icon"]) + button.icon = settings["bg_icon"] + if(button.icon_state != settings["bg_state"]) + button.icon_state = settings["bg_state"] + else + if(button.icon != button_icon) + button.icon = button_icon + if(button.icon_state != background_icon_state) + button.icon_state = background_icon_state + + ApplyIcon(button, force) + + var/available = IsAvailable() + if(available) + button.color = rgb(255,255,255,255) + else + button.color = transparent_when_unavailable ? rgb(128,0,0,128) : rgb(128,0,0) + return available + +/// Applies our button icon over top the background icon of the action +/datum/action/proc/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force = FALSE) + if(icon_icon && button_icon_state && ((current_button.button_icon_state != button_icon_state) || force)) + current_button.cut_overlays(TRUE) + current_button.add_overlay(mutable_appearance(icon_icon, button_icon_state)) + current_button.button_icon_state = button_icon_state + +/// Gives our action to the passed viewer. +/// Puts our action in their actions list and shows them the button. +/datum/action/proc/GiveAction(mob/viewer) + var/datum/hud/our_hud = viewer.hud_used + if(viewers[our_hud]) // Already have a copy of us? go away + return + + LAZYOR(viewer.actions, src) // Move this in + ShowTo(viewer) + +/// Adds our action button to the screen of the passed viewer. +/datum/action/proc/ShowTo(mob/viewer) + var/datum/hud/our_hud = viewer.hud_used + if(!our_hud || viewers[our_hud]) // There's no point in this if you have no hud in the first place + return + + var/atom/movable/screen/movable/action_button/button = CreateButton() + SetId(button, viewer) + + button.our_hud = our_hud + viewers[our_hud] = button + if(viewer.client) + viewer.client.screen += button + + button.load_position(viewer) + viewer.update_action_buttons() + +/// Removes our action from the passed viewer. +/datum/action/proc/HideFrom(mob/viewer) + var/datum/hud/our_hud = viewer.hud_used + var/atom/movable/screen/movable/action_button/button = viewers[our_hud] + LAZYREMOVE(viewer.actions, src) + if(button) + qdel(button) + +/// Creates an action button movable for the passed mob, and returns it. +/datum/action/proc/CreateButton() + var/atom/movable/screen/movable/action_button/button = new() + button.linked_action = src + button.name = name + button.actiontooltipstyle = buttontooltipstyle + if(desc) + button.desc = desc + return button + +/datum/action/proc/SetId(atom/movable/screen/movable/action_button/our_button, mob/owner) + //button id generation + var/bitfield = 0 + for(var/datum/action/action in owner.actions) + if(action == src) // This could be us, which is dumb + continue + var/atom/movable/screen/movable/action_button/button = action.viewers[owner.hud_used] + if(action.name == name && button.id) + bitfield |= button.id + + bitfield = ~bitfield // Flip our possible ids, so we can check if we've found a unique one + for(var/i in 0 to 23) // We get 24 possible bitflags in dm + var/bitflag = 1 << i // Shift us over one + if(bitfield & bitflag) + our_button.id = bitflag + return + +/// A general use signal proc that reacts to an event and updates our button icon in accordance +/datum/action/proc/update_icon_on_signal(datum/source) + SIGNAL_HANDLER + + UpdateButtons() + +/// Signal proc for COMSIG_MIND_TRANSFERRED - for minds, transfers our action to our new mob on mind transfer +/datum/action/proc/on_target_mind_swapped(datum/mind/source, mob/old_current) + SIGNAL_HANDLER + + // Grant() calls Remove() from the existing owner so we're covered on that + Grant(source.current) diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm new file mode 100644 index 0000000000000..f0a43fab1051c --- /dev/null +++ b/code/datums/actions/cooldown_action.dm @@ -0,0 +1,221 @@ +/// Preset for an action that has a cooldown. +/datum/action/cooldown + check_flags = NONE + transparent_when_unavailable = FALSE + + /// The actual next time this ability can be used + var/next_use_time = 0 + /// The stat panel this action shows up in the stat panel in. If null, will not show up. + var/panel + /// The default cooldown applied when StartCooldown() is called + var/cooldown_time = 0 + /// Whether or not you want the cooldown for the ability to display in text form + var/text_cooldown = TRUE + /// Setting for intercepting clicks before activating the ability + var/click_to_activate = FALSE + /// What icon to replace our mouse cursor with when active. Optional, Requires click_to_activate + var/ranged_mousepointer + /// The cooldown added onto the user's next click. Requires click_to_activate + var/click_cd_override = CLICK_CD_CLICK_ABILITY + /// If TRUE, we will unset after using our click intercept. Requires click_to_activate + var/unset_after_click = TRUE + /// Shares cooldowns with other cooldown abilities of the same value, not active if null + var/shared_cooldown + +/datum/action/cooldown/CreateButton() + var/atom/movable/screen/movable/action_button/button = ..() + button.maptext = "" + button.maptext_x = 8 + button.maptext_y = 0 + button.maptext_width = 24 + button.maptext_height = 12 + return button + +/datum/action/cooldown/IsAvailable() + return ..() && (next_use_time <= world.time) + +/datum/action/cooldown/Remove(mob/living/remove_from) + if(click_to_activate && remove_from.click_intercept == src) + unset_click_ability(remove_from, refund_cooldown = FALSE) + return ..() + +/// Starts a cooldown time to be shared with similar abilities +/// Will use default cooldown time if an override is not specified +/datum/action/cooldown/proc/StartCooldown(override_cooldown_time) + // "Shared cooldowns" covers actions which are not the same type, + // but have the same cooldown group and are on the same mob + if(shared_cooldown) + for(var/datum/action/cooldown/shared_ability in owner.actions - src) + if(shared_cooldown != shared_ability.shared_cooldown) + continue + shared_ability.StartCooldownSelf(override_cooldown_time) + + StartCooldownSelf(override_cooldown_time) + +/// Starts a cooldown time for this ability only +/// Will use default cooldown time if an override is not specified +/datum/action/cooldown/proc/StartCooldownSelf(override_cooldown_time) + if(isnum(override_cooldown_time)) + next_use_time = world.time + override_cooldown_time + else + next_use_time = world.time + cooldown_time + UpdateButtons() + START_PROCESSING(SSfastprocess, src) + +/datum/action/cooldown/Trigger(trigger_flags, atom/target) + . = ..() + if(!.) + return FALSE + if(!owner) + return FALSE + + var/mob/user = usr || owner + + // If our cooldown action is a click_to_activate action: + // The actual action is activated on whatever the user clicks on - + // the target is what the action is being used on + // In trigger, we handle setting the click intercept + if(click_to_activate) + if(target) + // For automatic / mob handling + return InterceptClickOn(user, null, target) + + var/datum/action/cooldown/already_set = user.click_intercept + if(already_set == src) + // if we clicked ourself and we're already set, unset and return + return unset_click_ability(user, refund_cooldown = TRUE) + + else if(istype(already_set)) + // if we have an active set already, unset it before we set our's + already_set.unset_click_ability(user, refund_cooldown = TRUE) + + return set_click_ability(user) + + // If our cooldown action is not a click_to_activate action: + // We can just continue on and use the action + // the target is the user of the action (often, the owner) + return PreActivate(user) + +/// Intercepts client owner clicks to activate the ability +/datum/action/cooldown/proc/InterceptClickOn(mob/living/caller, params, atom/target) + if(!IsAvailable()) + return FALSE + if(!target) + return FALSE + // The actual action begins here + if(!PreActivate(target)) + return FALSE + + // And if we reach here, the action was complete successfully + if(unset_after_click) + StartCooldown() + unset_click_ability(caller, refund_cooldown = FALSE) + caller.next_click = world.time + click_cd_override + + return TRUE + +/// For signal calling +/datum/action/cooldown/proc/PreActivate(atom/target) + if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src) & COMPONENT_BLOCK_ABILITY_START) + return + . = Activate(target) + // There is a possibility our action (or owner) is qdeleted in Activate(). + if(!QDELETED(src) && !QDELETED(owner)) + SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_FINISHED, src) + +/// To be implemented by subtypes +/datum/action/cooldown/proc/Activate(atom/target) + return + +/** + * Set our action as the click override on the passed mob. + */ +/datum/action/cooldown/proc/set_click_ability(mob/on_who) + SHOULD_CALL_PARENT(TRUE) + + on_who.click_intercept = src + if(ranged_mousepointer) + on_who.client?.mouse_override_icon = ranged_mousepointer + on_who.update_mouse_pointer() + UpdateButtons() + return TRUE + +/** + * Unset our action as the click override of the passed mob. + * + * if refund_cooldown is TRUE, we are being unset by the user clicking the action off + * if refund_cooldown is FALSE, we are being forcefully unset, likely by someone actually using the action + */ +/datum/action/cooldown/proc/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + SHOULD_CALL_PARENT(TRUE) + + on_who.click_intercept = null + if(ranged_mousepointer) + on_who.client?.mouse_override_icon = initial(on_who.client?.mouse_override_icon) + on_who.update_mouse_pointer() + UpdateButtons() + return TRUE + +/datum/action/cooldown/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE) + . = ..() + if(!button) + return + var/time_left = max(next_use_time - world.time, 0) + if(text_cooldown) + button.maptext = MAPTEXT("[round(time_left/10, 0.1)]") + if(!owner || time_left == 0) + button.maptext = "" + if(IsAvailable() && (button.our_hud.mymob.click_intercept == src)) + button.color = COLOR_GREEN + +/datum/action/cooldown/process() + if(!owner || (next_use_time - world.time) <= 0) + UpdateButtons() + STOP_PROCESSING(SSfastprocess, src) + return + + UpdateButtons() + +/datum/action/cooldown/Grant(mob/M) + ..() + if(!owner) + return + UpdateButtons() + if(next_use_time > world.time) + START_PROCESSING(SSfastprocess, src) + +/// Formats the action to be returned to the stat panel. +/datum/action/cooldown/proc/set_statpanel_format() + if(!panel) + return null + + var/time_remaining = max(next_use_time - world.time, 0) + var/time_remaining_in_seconds = round(time_remaining / 10, 0.1) + var/cooldown_time_in_seconds = round(cooldown_time / 10, 0.1) + + var/list/stat_panel_data = list() + + // Pass on what panel we should be displayed in. + stat_panel_data[PANEL_DISPLAY_PANEL] = panel + // Also pass on the name of the spell, with some spacing + stat_panel_data[PANEL_DISPLAY_NAME] = " - [name]" + + // No cooldown time at all, just show the ability + if(cooldown_time_in_seconds <= 0) + stat_panel_data[PANEL_DISPLAY_STATUS] = "" + + // It's a toggle-active ability, show if it's active + else if(click_to_activate && owner.click_intercept == src) + stat_panel_data[PANEL_DISPLAY_STATUS] = "ACTIVE" + + // It's on cooldown, show the cooldown + else if(time_remaining_in_seconds > 0) + stat_panel_data[PANEL_DISPLAY_STATUS] = "CD - [time_remaining_in_seconds]s / [cooldown_time_in_seconds]s" + + // It's not on cooldown, show that it is ready + else + stat_panel_data[PANEL_DISPLAY_STATUS] = "READY" + + SEND_SIGNAL(src, COMSIG_ACTION_SET_STATPANEL, stat_panel_data) + + return stat_panel_data diff --git a/code/datums/actions/innate_action.dm b/code/datums/actions/innate_action.dm new file mode 100644 index 0000000000000..933ed0561e494 --- /dev/null +++ b/code/datums/actions/innate_action.dm @@ -0,0 +1,84 @@ +//Preset for general and toggled actions +/datum/action/innate + check_flags = NONE + /// Whether we're active or not, if we're a innate - toggle action. + var/active = FALSE + /// Whether we're a click action or not, if we're a innate - click action. + var/click_action = FALSE + /// If we're a click action, the mouse pointer we use + var/ranged_mousepointer + /// If we're a click action, the text shown on enable + var/enable_text + /// If we're a click action, the text shown on disable + var/disable_text + +/datum/action/innate/Trigger(trigger_flags) + if(!..()) + return FALSE + // We're a click action, trigger just sets it as active or not + if(click_action) + if(owner.click_intercept == src) + unset_ranged_ability(owner, disable_text) + else + set_ranged_ability(owner, enable_text) + return TRUE + + // We're not a click action (we're a toggle or otherwise) + else + if(active) + Deactivate() + else + Activate() + + return TRUE + +/datum/action/innate/proc/Activate() + return + +/datum/action/innate/proc/Deactivate() + return + +/** + * This is gross, but a somewhat-required bit of copy+paste until action code becomes slightly more sane. + * Anything that uses these functions should eventually be moved to use cooldown actions. + * (Either that, or the click ability of cooldown actions should be moved down a type.) + * + * If you're adding something that uses these, rethink your choice in subtypes. + */ + +/// Sets this action as the active ability for the passed mob +/datum/action/innate/proc/set_ranged_ability(mob/living/on_who, text_to_show) + if(ranged_mousepointer) + on_who.client?.mouse_override_icon = ranged_mousepointer + on_who.update_mouse_pointer() + if(text_to_show) + to_chat(on_who, text_to_show) + on_who.click_intercept = src + +/// Removes this action as the active ability of the passed mob +/datum/action/innate/proc/unset_ranged_ability(mob/living/on_who, text_to_show) + if(ranged_mousepointer) + on_who.client?.mouse_override_icon = initial(owner.client?.mouse_pointer_icon) + on_who.update_mouse_pointer() + if(text_to_show) + to_chat(on_who, text_to_show) + on_who.click_intercept = null + +/// Handles whenever a mob clicks on something +/datum/action/innate/proc/InterceptClickOn(mob/living/caller, params, atom/clicked_on) + if(!IsAvailable()) + unset_ranged_ability(caller) + return FALSE + if(!clicked_on) + return FALSE + + return do_ability(caller, clicked_on) + +/// Actually goes through and does the click ability +/datum/action/innate/proc/do_ability(mob/living/caller, params, atom/clicked_on) + return FALSE + +/datum/action/innate/Remove(mob/removed_from) + if(removed_from.click_intercept == src) + unset_ranged_ability(removed_from) + return ..() diff --git a/code/datums/actions/item_action.dm b/code/datums/actions/item_action.dm new file mode 100644 index 0000000000000..9d93ef9e81a3a --- /dev/null +++ b/code/datums/actions/item_action.dm @@ -0,0 +1,33 @@ +//Presets for item actions +/datum/action/item_action + name = "Item Action" + check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_CONSCIOUS + button_icon_state = null + // If you want to override the normal icon being the item + // then change this to an icon state + +/datum/action/item_action/Trigger(trigger_flags) + . = ..() + if(!.) + return FALSE + if(target) + var/obj/item/I = target + I.ui_action_click(owner, src) + return TRUE + +/datum/action/item_action/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force) + var/obj/item/item_target = target + if(button_icon && button_icon_state) + // If set, use the custom icon that we set instead + // of the item appearence + ..() + else if((target && current_button.appearance_cache != item_target.appearance) || force) //replace with /ref comparison if this is not valid. + var/old_layer = item_target.layer + var/old_plane = item_target.plane + item_target.layer = FLOAT_LAYER //AAAH + item_target.plane = FLOAT_PLANE //^ what that guy said + current_button.cut_overlays() + current_button.add_overlay(item_target) + item_target.layer = old_layer + item_target.plane = old_plane + current_button.appearance_cache = item_target.appearance diff --git a/code/datums/actions/items/adjust.dm b/code/datums/actions/items/adjust.dm new file mode 100644 index 0000000000000..70d4966221984 --- /dev/null +++ b/code/datums/actions/items/adjust.dm @@ -0,0 +1,7 @@ +/datum/action/item_action/adjust + name = "Adjust Item" + +/datum/action/item_action/adjust/New(Target) + ..() + var/obj/item/item_target = target + name = "Adjust [item_target.name]" diff --git a/code/datums/actions/beam_rifle.dm b/code/datums/actions/items/beam_rifle.dm similarity index 100% rename from code/datums/actions/beam_rifle.dm rename to code/datums/actions/items/beam_rifle.dm diff --git a/code/datums/actions/items/berserk.dm b/code/datums/actions/items/berserk.dm new file mode 100644 index 0000000000000..9f8519906a001 --- /dev/null +++ b/code/datums/actions/items/berserk.dm @@ -0,0 +1,19 @@ +/datum/action/item_action/berserk_mode + name = "Berserk" + desc = "Increase your movement and melee speed while also increasing your melee armor for a short amount of time." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "berserk_mode" + background_icon_state = "bg_demon" + +/datum/action/item_action/berserk_mode/Trigger(trigger_flags) + if(istype(target, /obj/item/clothing/head/hooded/berserker)) + var/obj/item/clothing/head/hooded/berserker/berzerk = target + if(berzerk.berserk_active) + to_chat(owner, span_warning("You are already berserk!")) + return + if(berzerk.berserk_charge < 100) + to_chat(owner, span_warning("You don't have a full charge.")) + return + berzerk.berserk_mode(owner) + return + return ..() diff --git a/code/datums/actions/items/boot_dash.dm b/code/datums/actions/items/boot_dash.dm new file mode 100644 index 0000000000000..5768a79db63e0 --- /dev/null +++ b/code/datums/actions/items/boot_dash.dm @@ -0,0 +1,10 @@ +//surf_ss13 +/datum/action/item_action/bhop + name = "Activate Jump Boots" + desc = "Activates the jump boot's internal propulsion system, allowing the user to dash over 4-wide gaps." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "jetboot" + +/datum/action/item_action/bhop/brocket + name = "Activate Rocket Boots" + desc = "Activates the boot's rocket propulsion system, allowing the user to hurl themselves great distances." diff --git a/code/datums/actions/items/cult_dagger.dm b/code/datums/actions/items/cult_dagger.dm new file mode 100644 index 0000000000000..f2960ccb389be --- /dev/null +++ b/code/datums/actions/items/cult_dagger.dm @@ -0,0 +1,37 @@ +/datum/action/item_action/cult_dagger + name = "Draw Blood Rune" + desc = "Use the ritual dagger to create a powerful blood rune" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "draw" + buttontooltipstyle = "cult" + background_icon_state = "bg_demon" + default_button_position = "6:157,4:-2" + +/datum/action/item_action/cult_dagger/Grant(mob/grant_to) + if(!IS_CULTIST(grant_to)) + Remove(owner) + return + + return ..() + +/datum/action/item_action/cult_dagger/Trigger(trigger_flags) + for(var/obj/item/held_item as anything in owner.held_items) // In case we were already holding a dagger + if(istype(held_item, /obj/item/melee/cultblade/dagger)) + held_item.attack_self(owner) + return + var/obj/item/target_item = target + if(owner.can_equip(target_item, ITEM_SLOT_HANDS)) + owner.temporarilyRemoveItemFromInventory(target_item) + owner.put_in_hands(target_item) + target_item.attack_self(owner) + return + + if(!isliving(owner)) + to_chat(owner, span_warning("You lack the necessary living force for this action.")) + return + + var/mob/living/living_owner = owner + if (living_owner.usable_hands <= 0) + to_chat(living_owner, span_warning("You don't have any usable hands!")) + else + to_chat(living_owner, span_warning("Your hands are full!")) diff --git a/code/datums/actions/items/hands_free.dm b/code/datums/actions/items/hands_free.dm new file mode 100644 index 0000000000000..24fddb52942dc --- /dev/null +++ b/code/datums/actions/items/hands_free.dm @@ -0,0 +1,8 @@ +/datum/action/item_action/hands_free + check_flags = AB_CHECK_CONSCIOUS + +/datum/action/item_action/hands_free/activate + name = "Activate" + +/datum/action/item_action/hands_free/shift_nerves + name = "Shift Nerves" diff --git a/code/datums/actions/ninja.dm b/code/datums/actions/items/ninja.dm similarity index 100% rename from code/datums/actions/ninja.dm rename to code/datums/actions/items/ninja.dm diff --git a/code/datums/actions/items/organ_action.dm b/code/datums/actions/items/organ_action.dm new file mode 100644 index 0000000000000..19a8f700373df --- /dev/null +++ b/code/datums/actions/items/organ_action.dm @@ -0,0 +1,25 @@ +/datum/action/item_action/organ_action + name = "Organ Action" + check_flags = AB_CHECK_CONSCIOUS + +/datum/action/item_action/organ_action/IsAvailable() + var/obj/item/organ/attached_organ = target + if(!attached_organ.owner) + return FALSE + return ..() + +/datum/action/item_action/organ_action/toggle + name = "Toggle Organ" + +/datum/action/item_action/organ_action/toggle/New(Target) + ..() + var/obj/item/organ/organ_target = target + name = "Toggle [organ_target.name]" + +/datum/action/item_action/organ_action/use + name = "Use Organ" + +/datum/action/item_action/organ_action/use/New(Target) + ..() + var/obj/item/organ/organ_target = target + name = "Use [organ_target.name]" diff --git a/code/datums/actions/items/set_internals.dm b/code/datums/actions/items/set_internals.dm new file mode 100644 index 0000000000000..69262c108a77f --- /dev/null +++ b/code/datums/actions/items/set_internals.dm @@ -0,0 +1,12 @@ +/datum/action/item_action/set_internals + name = "Set Internals" + +/datum/action/item_action/set_internals/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force) + . = ..() + if(!. || !button) // no button available + return + if(!iscarbon(owner)) + return + var/mob/living/carbon/carbon_owner = owner + if(target == carbon_owner.internal) + button.icon_state = "template_active" diff --git a/code/datums/actions/items/stealth_box.dm b/code/datums/actions/items/stealth_box.dm new file mode 100644 index 0000000000000..b8aa7c989073d --- /dev/null +++ b/code/datums/actions/items/stealth_box.dm @@ -0,0 +1,55 @@ +///MGS BOX! +/datum/action/item_action/agent_box + name = "Deploy Box" + desc = "Find inner peace, here, in the box." + check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_IMMOBILE|AB_CHECK_CONSCIOUS + background_icon_state = "bg_agent" + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "deploy_box" + ///The type of closet this action spawns. + var/boxtype = /obj/structure/closet/cardboard/agent + COOLDOWN_DECLARE(box_cooldown) + +///Handles opening and closing the box. +/datum/action/item_action/agent_box/Trigger(trigger_flags) + . = ..() + if(!.) + return FALSE + if(istype(owner.loc, /obj/structure/closet/cardboard/agent)) + var/obj/structure/closet/cardboard/agent/box = owner.loc + if(box.open()) + owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE) + return + //Box closing from here on out. + if(!isturf(owner.loc)) //Don't let the player use this to escape mechs/welded closets. + to_chat(owner, span_warning("You need more space to activate this implant!")) + return + if(!COOLDOWN_FINISHED(src, box_cooldown)) + return + COOLDOWN_START(src, box_cooldown, 10 SECONDS) + var/box = new boxtype(owner.drop_location()) + owner.forceMove(box) + owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE) + +/datum/action/item_action/agent_box/Grant(mob/grant_to) + . = ..() + if(owner) + RegisterSignal(owner, COMSIG_HUMAN_SUICIDE_ACT, .proc/suicide_act) + +/datum/action/item_action/agent_box/Remove(mob/M) + if(owner) + UnregisterSignal(owner, COMSIG_HUMAN_SUICIDE_ACT) + return ..() + +/datum/action/item_action/agent_box/proc/suicide_act(datum/source) + SIGNAL_HANDLER + + if(!istype(owner.loc, /obj/structure/closet/cardboard/agent)) + return + + var/obj/structure/closet/cardboard/agent/box = owner.loc + owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE) + box.open() + owner.visible_message(span_suicide("[owner] falls out of [box]! It looks like [owner.p_they()] committed suicide!")) + owner.throw_at(get_turf(owner)) + return OXYLOSS diff --git a/code/datums/actions/items/summon_stickmen.dm b/code/datums/actions/items/summon_stickmen.dm new file mode 100644 index 0000000000000..c825c72dc515a --- /dev/null +++ b/code/datums/actions/items/summon_stickmen.dm @@ -0,0 +1,6 @@ +//Stickmemes +/datum/action/item_action/stickmen + name = "Summon Stick Minions" + desc = "Allows you to summon faithful stickmen allies to aide you in battle." + icon_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "art_summon" diff --git a/code/datums/actions/items/toggles.dm b/code/datums/actions/items/toggles.dm new file mode 100644 index 0000000000000..1af240a6b0264 --- /dev/null +++ b/code/datums/actions/items/toggles.dm @@ -0,0 +1,112 @@ +/datum/action/item_action/toggle + +/datum/action/item_action/toggle/New(Target) + ..() + var/obj/item/item_target = target + name = "Toggle [item_target.name]" + +/datum/action/item_action/toggle_light + name = "Toggle Light" + +/datum/action/item_action/toggle_computer_light + name = "Toggle Flashlight" + +/datum/action/item_action/toggle_hood + name = "Toggle Hood" + +/datum/action/item_action/toggle_firemode + name = "Toggle Firemode" + +/datum/action/item_action/toggle_gunlight + name = "Toggle Gunlight" + +/datum/action/item_action/toggle_mode + name = "Toggle Mode" + +/datum/action/item_action/toggle_barrier_spread + name = "Toggle Barrier Spread" + +/datum/action/item_action/toggle_paddles + name = "Toggle Paddles" + +/datum/action/item_action/toggle_mister + name = "Toggle Mister" + +/datum/action/item_action/toggle_helmet_light + name = "Toggle Helmet Light" + +/datum/action/item_action/toggle_welding_screen + name = "Toggle Welding Screen" + +/datum/action/item_action/toggle_spacesuit + name = "Toggle Suit Thermal Regulator" + icon_icon = 'icons/mob/actions/actions_spacesuit.dmi' + button_icon_state = "thermal_off" + +/datum/action/item_action/toggle_spacesuit/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force) + var/obj/item/clothing/suit/space/suit = target + if(istype(suit)) + button_icon_state = "thermal_[suit.thermal_on ? "on" : "off"]" + + return ..() + +/datum/action/item_action/toggle_helmet_flashlight + name = "Toggle Helmet Flashlight" + +/datum/action/item_action/toggle_helmet_mode + name = "Toggle Helmet Mode" + +/datum/action/item_action/toggle_voice_box + name = "Toggle Voice Box" + +/datum/action/item_action/toggle_human_head + name = "Toggle Human Head" + +/datum/action/item_action/toggle_helmet + name = "Toggle Helmet" + +/datum/action/item_action/toggle_seclight + name = "Toggle Seclight" + +/datum/action/item_action/toggle_jetpack + name = "Toggle Jetpack" + +/datum/action/item_action/jetpack_stabilization + name = "Toggle Jetpack Stabilization" + +/datum/action/item_action/jetpack_stabilization/IsAvailable() + var/obj/item/tank/jetpack/linked_jetpack = target + if(!istype(linked_jetpack) || !linked_jetpack.on) + return FALSE + return ..() + +/datum/action/item_action/wheelys + name = "Toggle Wheels" + desc = "Pops out or in your shoes' wheels." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "wheelys" + +/datum/action/item_action/kindle_kicks + name = "Activate Kindle Kicks" + desc = "Kick you feet together, activating the lights in your Kindle Kicks." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "kindleKicks" + +/datum/action/item_action/storage_gather_mode + name = "Switch gathering mode" + desc = "Switches the gathering mode of a storage object." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "storage_gather_switch" + +/datum/action/item_action/storage_gather_mode/ApplyIcon(atom/movable/screen/movable/action_button/current_button) + . = ..() + var/obj/item/item_target = target + var/old_layer = item_target.layer + var/old_plane = item_target.plane + item_target.layer = FLOAT_LAYER //AAAH + item_target.plane = FLOAT_PLANE //^ what that guy said + current_button.cut_overlays() + current_button.add_overlay(target) + item_target.layer = old_layer + item_target.plane = old_plane + current_button.appearance_cache = item_target.appearance diff --git a/code/datums/actions/items/vortex_recall.dm b/code/datums/actions/items/vortex_recall.dm new file mode 100644 index 0000000000000..943da403e7a5c --- /dev/null +++ b/code/datums/actions/items/vortex_recall.dm @@ -0,0 +1,15 @@ +/datum/action/item_action/vortex_recall + name = "Vortex Recall" + desc = "Recall yourself, and anyone nearby, to an attuned hierophant beacon at any time.
If the beacon is still attached, will detach it." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "vortex_recall" + +/datum/action/item_action/vortex_recall/IsAvailable() + var/area/current_area = get_area(target) + if(!current_area || current_area.area_flags & NOTELEPORT) + return FALSE + if(istype(target, /obj/item/hierophant_club)) + var/obj/item/hierophant_club/teleport_stick = target + if(teleport_stick.teleporting) + return FALSE + return ..() diff --git a/code/datums/actions/mobs/language_menu.dm b/code/datums/actions/mobs/language_menu.dm new file mode 100644 index 0000000000000..bcfcb5437a2fc --- /dev/null +++ b/code/datums/actions/mobs/language_menu.dm @@ -0,0 +1,13 @@ +/datum/action/language_menu + name = "Language Menu" + desc = "Open the language menu to review your languages, their keys, and select your default language." + button_icon_state = "language_menu" + check_flags = NONE + +/datum/action/language_menu/Trigger(trigger_flags) + . = ..() + if(!.) + return + + var/datum/language_holder/owner_holder = owner.get_language_holder() + owner_holder.open_language_menu(usr) diff --git a/code/datums/actions/mobs/small_sprite.dm b/code/datums/actions/mobs/small_sprite.dm new file mode 100644 index 0000000000000..46ffd26e499b1 --- /dev/null +++ b/code/datums/actions/mobs/small_sprite.dm @@ -0,0 +1,54 @@ +//Small sprites +/datum/action/small_sprite + name = "Toggle Giant Sprite" + desc = "Others will always see you as giant." + icon_icon = 'icons/mob/actions/actions_xeno.dmi' + button_icon_state = "smallqueen" + background_icon_state = "bg_alien" + var/small = FALSE + var/small_icon + var/small_icon_state + +/datum/action/small_sprite/queen + small_icon = 'icons/mob/alien.dmi' + small_icon_state = "alienq" + +/datum/action/small_sprite/megafauna + icon_icon = 'icons/mob/actions/actions_xeno.dmi' + small_icon = 'icons/mob/lavaland/lavaland_monsters.dmi' + +/datum/action/small_sprite/megafauna/drake + small_icon_state = "ash_whelp" + +/datum/action/small_sprite/megafauna/colossus + small_icon_state = "Basilisk" + +/datum/action/small_sprite/megafauna/bubblegum + small_icon_state = "goliath2" + +/datum/action/small_sprite/megafauna/legion + small_icon_state = "mega_legion" + +/datum/action/small_sprite/mega_arachnid + small_icon = 'icons/mob/jungle/arachnid.dmi' + small_icon_state = "arachnid_mini" + background_icon_state = "bg_demon" + +/datum/action/small_sprite/space_dragon + small_icon = 'icons/mob/carp.dmi' + small_icon_state = "carp" + icon_icon = 'icons/mob/carp.dmi' + button_icon_state = "carp" + +/datum/action/small_sprite/Trigger(trigger_flags) + ..() + if(!small) + var/image/I = image(icon = small_icon, icon_state = small_icon_state, loc = owner) + I.override = TRUE + I.pixel_x -= owner.pixel_x + I.pixel_y -= owner.pixel_y + owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic, "smallsprite", I, AA_TARGET_SEE_APPEARANCE | AA_MATCH_TARGET_OVERLAYS) + small = TRUE + else + owner.remove_alt_appearance("smallsprite") + small = FALSE diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm index b1c5d00fffd2d..a6500f074e888 100644 --- a/code/datums/brain_damage/imaginary_friend.dm +++ b/code/datums/brain_damage/imaginary_friend.dm @@ -286,7 +286,7 @@ name = "Hide" desc = "Hide yourself from your owner's sight." button_icon_state = "hide" - UpdateButtonIcon() + UpdateButtons() /datum/action/innate/imaginary_hide/Activate() var/mob/camera/imaginary_friend/I = owner diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm index 8b785485c6f11..dbe35f7fd60f8 100644 --- a/code/datums/components/cult_ritual_item.dm +++ b/code/datums/components/cult_ritual_item.dm @@ -15,8 +15,8 @@ var/list/turfs_that_boost_us /// A list of all shields surrounding us while drawing certain runes (Nar'sie). var/list/obj/structure/emergency_shield/sanguine/shields - /// An item action associated with our parent, to quick-draw runes. - var/datum/action/item_action/linked_action + /// Weakref to an action added to our parent item that allows for quick drawing runes + var/datum/weakref/linked_action_ref /datum/component/cult_ritual_item/Initialize( examine_message, @@ -35,12 +35,13 @@ src.turfs_that_boost_us = list(turfs_that_boost_us) if(ispath(action)) - linked_action = new action(parent) + var/obj/item/item_parent = parent + var/datum/action/added_action = item_parent.add_item_action(action) + linked_action_ref = WEAKREF(added_action) /datum/component/cult_ritual_item/Destroy(force, silent) cleanup_shields() - if(linked_action) - QDEL_NULL(linked_action) + QDEL_NULL(linked_action_ref) return ..() /datum/component/cult_ritual_item/RegisterWithParent() diff --git a/code/datums/components/manual_breathing.dm b/code/datums/components/manual_breathing.dm index 6d3d4ce64dc13..87f1a04a4b2be 100644 --- a/code/datums/components/manual_breathing.dm +++ b/code/datums/components/manual_breathing.dm @@ -30,7 +30,7 @@ else name = "Exhale" button_icon_state = "remove" - UpdateButtonIcon() + UpdateButtons() /datum/component/manual_breathing/Initialize() if(!iscarbon(parent)) diff --git a/code/datums/components/seclight_attachable.dm b/code/datums/components/seclight_attachable.dm index b9708ecbad9ce..77b592e451a2d 100644 --- a/code/datums/components/seclight_attachable.dm +++ b/code/datums/components/seclight_attachable.dm @@ -131,10 +131,8 @@ // Make a new toggle light item action for our parent var/obj/item/item_parent = parent - var/datum/action/item_action/toggle_seclight/toggle_action = new(item_parent) + var/datum/action/item_action/toggle_seclight/toggle_action = item_parent.add_item_action(/datum/action/item_action/toggle_seclight) toggle_action_ref = WEAKREF(toggle_action) - if(attacher && item_parent.loc == attacher) - toggle_action.Grant(attacher) update_light() diff --git a/code/datums/components/stationloving.dm b/code/datums/components/stationloving.dm index 81637fddd0e8c..78eccfdccf18b 100644 --- a/code/datums/components/stationloving.dm +++ b/code/datums/components/stationloving.dm @@ -55,12 +55,12 @@ if(inform_admins) message_admins("[parent] has been moved out of bounds in [ADMIN_VERBOSEJMP(currentturf)]. Moving it to [ADMIN_VERBOSEJMP(targetturf)].") -/datum/component/stationloving/proc/check_soul_imbue() +/datum/component/stationloving/proc/check_soul_imbue(datum/source) SIGNAL_HANDLER return disallow_soul_imbue -/datum/component/stationloving/proc/check_mark_retrieval() +/datum/component/stationloving/proc/check_mark_retrieval(datum/source) SIGNAL_HANDLER return COMPONENT_BLOCK_MARK_RETRIEVAL diff --git a/code/datums/components/storage/storage.dm b/code/datums/components/storage/storage.dm index 238c9f1460413..f2aa898ea351b 100644 --- a/code/datums/components/storage/storage.dm +++ b/code/datums/components/storage/storage.dm @@ -48,7 +48,7 @@ var/attack_hand_interact = TRUE //interact on attack hand. var/quickdraw = FALSE //altclick interact - var/datum/action/item_action/storage_gather_mode/modeswitch_action + var/datum/weakref/modeswitch_action_ref /// whether or not we should have those cute little animations var/animated = TRUE @@ -119,17 +119,17 @@ update_actions() /datum/component/storage/proc/update_actions() - QDEL_NULL(modeswitch_action) if(!isitem(parent) || !allow_quick_gather) + QDEL_NULL(modeswitch_action_ref) return - var/obj/item/I = parent - modeswitch_action = new(I) + var/datum/action/existing = modeswitch_action_ref?.resolve() + if(!QDELETED(existing)) + return + + var/obj/item/item_parent = parent + var/datum/action/modeswitch_action = item_parent.add_item_action(/datum/action/item_action/storage_gather_mode) RegisterSignal(modeswitch_action, COMSIG_ACTION_TRIGGER, PROC_REF(action_trigger)) - if(I.item_flags & PICKED_UP) - var/mob/M = I.loc - if(!istype(M)) - return - modeswitch_action.Grant(M) + modeswitch_action_ref = WEAKREF(modeswitch_action) /datum/component/storage/proc/change_master(datum/component/storage/concrete/new_master) if(new_master == src || (!isnull(new_master) && !istype(new_master))) diff --git a/code/datums/martial/plasma_fist.dm b/code/datums/martial/plasma_fist.dm index 095751cfe2f50..6809a7e08e589 100644 --- a/code/datums/martial/plasma_fist.dm +++ b/code/datums/martial/plasma_fist.dm @@ -35,8 +35,9 @@ /datum/martial_art/plasma_fist/proc/Tornado(mob/living/carbon/human/A, mob/living/carbon/human/D) A.say("TORNADO SWEEP!", forced="plasma fist") TornadoAnimate(A) - var/obj/effect/proc_holder/spell/aoe_turf/repulse/R = new(null) - R.cast(RANGE_TURFS(1,A)) + var/datum/action/cooldown/spell/aoe/repulse/tornado_spell = new(src) + tornado_spell.cast(A) + qdel(tornado_spell) log_combat(A, D, "tornado sweeped(Plasma Fist)", name) return diff --git a/code/datums/mind.dm b/code/datums/mind.dm index baac761495270..469d891a6bf6e 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -43,8 +43,6 @@ var/assigned_role var/special_role var/list/restricted_roles = list() - var/list/spell_list = list() // Wizard mode & "Give Spell" badmin button. - var/linglink var/datum/martial_art/martial_art var/static/default_martial_art = new/datum/martial_art @@ -152,7 +150,6 @@ var/mob/living/carbon/C = new_character C.last_mind = src transfer_antag_huds(hud_to_transfer) //inherit the antag HUD - transfer_actions(new_character) transfer_martial_arts(new_character) RegisterSignal(new_character, COMSIG_MOB_DEATH, PROC_REF(set_death_time)) if(active || force_key_move) @@ -704,35 +701,9 @@ add_antag_datum(head) special_role = ROLE_REV_HEAD -/datum/mind/proc/AddSpell(obj/effect/proc_holder/spell/S) - // HACK: Preferences menu creates one of every selectable species. - // Some species, like vampires, create spells when they're made. - // The "action" is created when those spells Initialize. - // Preferences menu can create these assets at *any* time, primarily before - // the atoms SS initializes. - // That means "action" won't exist. - if (isnull(S.action)) - return - spell_list += S - S.action.Grant(current) - /datum/mind/proc/owns_soul() return soulOwner == src -//To remove a specific spell from a mind -/datum/mind/proc/RemoveSpell(obj/effect/proc_holder/spell/spell) - if(!spell) - return - for(var/X in spell_list) - var/obj/effect/proc_holder/spell/S = X - if(istype(S, spell)) - spell_list -= S - qdel(S) - -/datum/mind/proc/RemoveAllSpells() - for(var/obj/effect/proc_holder/S in spell_list) - RemoveSpell(S) - /datum/mind/proc/transfer_martial_arts(mob/living/new_character) if(!ishuman(new_character)) return @@ -742,27 +713,6 @@ else martial_art.teach(new_character) -/datum/mind/proc/transfer_actions(mob/living/new_character) - if(current && current.actions) - for(var/datum/action/A in current.actions) - A.Grant(new_character) - transfer_mindbound_actions(new_character) - -/datum/mind/proc/transfer_mindbound_actions(mob/living/new_character) - for(var/X in spell_list) - var/obj/effect/proc_holder/spell/S = X - S.action.Grant(new_character) - -/datum/mind/proc/disrupt_spells(delay, list/exceptions = New()) - for(var/X in spell_list) - var/obj/effect/proc_holder/spell/S = X - for(var/type in exceptions) - if(istype(S, type)) - continue - S.charge_counter = delay - S.updateButtonIcon() - INVOKE_ASYNC(S, TYPE_PROC_REF(/obj/effect/proc_holder/spell, start_recharge)) - /datum/mind/proc/get_ghost(even_if_they_cant_reenter, ghosts_with_clients) for(var/mob/dead/observer/G in (ghosts_with_clients ? GLOB.player_list : GLOB.dead_mob_list)) if(G.mind == src) diff --git a/code/datums/mutations.dm b/code/datums/mutations/__mutations.dm similarity index 85% rename from code/datums/mutations.dm rename to code/datums/mutations/__mutations.dm index 4de7dbca16153..8305406b311ea 100644 --- a/code/datums/mutations.dm +++ b/code/datums/mutations/__mutations.dm @@ -4,10 +4,9 @@ var/locked var/quality var/static/list/visual_indicators = list() - var/obj/effect/proc_holder/spell/power - /// A list of traits to apply to the user whenever this mutation is active. var/list/traits var/layer_used = MUTATIONS_LAYER //which mutation layer to use + var/datum/action/cooldown/spell/power_path /// The path of action we grant to our user on mutation gain var/list/species_allowed = list() //to restrict mutation to only certain species var/list/mobtypes_allowed = list() //to restrict mutation to only certain mobs var/health_req //minimum health required to acquire the mutation @@ -80,7 +79,7 @@ owner.remove_overlay(layer_used) owner.overlays_standing[layer_used] = mut_overlay owner.apply_overlay(layer_used) - grant_spell() //we do checks here so nothing about hulk getting magic + grant_power() //we do checks here so nothing about hulk getting magic if(!modified && can_chromosome == CHROMOSOME_USED) addtimer(CALLBACK(src, PROC_REF(modify), 5)) //gonna want children calling ..() to run first RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) @@ -113,8 +112,10 @@ mut_overlay.Remove(get_visual_indicator()) owner.overlays_standing[layer_used] = mut_overlay owner.apply_overlay(layer_used) - if(power) - owner.RemoveSpell(power) + if(power_path) + // Any powers we made are linked to our mutation datum, + // so deleting ourself will also delete it and remove it + // ...Why don't all mutations delete on loss? Not sure. qdel(src) UnregisterSignal(owner, COMSIG_MOVABLE_MOVED) REMOVE_TRAITS_IN(owner, "[type]") @@ -144,12 +145,27 @@ overlays_standing[CM.layer_used] = mut_overlay apply_overlay(CM.layer_used) -/datum/mutation/proc/modify() //called when a genome is applied so we can properly update some stats without having to remove and reapply the mutation from someone +/** if(modified || !power || !owner) + * Called when a chromosome is applied so we can properly update some stats + * without having to remove and reapply the mutation from someone + * + * Returns `null` if no modification was done, and + * returns an instance of a power if modification was complete + */ + +/datum/mutation/proc/modify() //called when a genome is applied so we can properly update some stats without having to remove and reapply the mutation from someone + if(modified || !power_path || !owner) + return return power.charge_max *= GET_MUTATION_ENERGY(src) + var/datum/action/cooldown/spell/modified_power = locate(power_path) in owner.actions power.charge_counter *= GET_MUTATION_ENERGY(src) + if(!modified_power) modified = TRUE + CRASH("Genetic mutation [type] called modify(), but could not find a action to modify!") + modified_power.cooldown_time *= GET_MUTATION_ENERGY(src) // Doesn't do anything for mutations with energy_coeff unset + return modified_power /datum/mutation/proc/copy_mutation(datum/mutation/HM) if(!istype(HM)) @@ -178,18 +194,16 @@ else qdel(src) -/datum/mutation/proc/grant_spell() - if(!ispath(power) || !owner) +/datum/mutation/proc/grant_power() + if(!ispath(power_path) || !owner) return FALSE - if(ispath(power, /obj/effect/proc_holder/spell/targeted/touch/mutation)) - power = new power(null, src) - else - power = new power() - power.action_background_icon_state = "bg_tech_blue_on" - power.panel = "Genetic" - owner.AddSpell(power) - return TRUE + var/datum/action/cooldown/spell/new_power = new power_path(src) + new_power.background_icon_state = "bg_tech_blue_on" + new_power.panel = "Genetic" + new_power.Grant(owner) + + return new_power // Runs through all the coefficients and uses this to determine which chromosomes the // mutation can take. Stores these as text strings in a list. diff --git a/code/datums/mutations/actions.dm b/code/datums/mutations/actions.dm deleted file mode 100644 index fdf6d67e24a07..0000000000000 --- a/code/datums/mutations/actions.dm +++ /dev/null @@ -1,291 +0,0 @@ -/datum/mutation/telepathy - name = "Telepathy" - desc = "A rare mutation that allows the user to telepathically communicate to others." - quality = POSITIVE - difficulty = 12 - power = /obj/effect/proc_holder/spell/targeted/telepathy - instability = 10 - -/datum/mutation/olfaction - name = "Transcendent Olfaction" - desc = "Your sense of smell is comparable to that of a canine." - quality = POSITIVE - difficulty = 12 - power = /obj/effect/proc_holder/spell/targeted/olfaction - instability = 30 - energy_coeff = 1 - -/obj/effect/proc_holder/spell/targeted/olfaction - name = "Remember the Scent" - desc = "Get a scent off of the item you're currently holding to track it. With an empty hand, you'll track the scent you've remembered." - charge_max = 10 SECONDS - clothes_req = FALSE - range = -1 - include_user = TRUE - action_icon_state = "nose" - var/mob/living/carbon/tracking_target - var/list/mob/living/carbon/possible = list() - -/obj/effect/proc_holder/spell/targeted/olfaction/cast(list/targets, mob/living/user = usr) - var/atom/sniffed = user.get_active_held_item() - if(sniffed) - var/old_target = tracking_target - possible = list() - var/list/prints = sniffed.return_fingerprints() - for(var/mob/living/carbon/potential_target in GLOB.carbon_list) - if(prints[rustg_hash_string(RUSTG_HASH_MD5, potential_target.dna.uni_identity)]) - possible |= potential_target - if(!length(possible)) - to_chat(user, "Despite your best efforts, there are no scents to be found on [sniffed]...") - return - tracking_target = tgui_input_list(user, "Choose a scent to remember.", "Scent Tracking", sort_names(possible)) - if(!tracking_target) - if(!old_target) - to_chat(user,"You decide against remembering any scents. Instead, you notice your own nose in your peripheral vision. This goes on to remind you of that one time you started breathing manually and couldn't stop. What an awful day that was.") - return - tracking_target = old_target - on_the_trail(user) - return - to_chat(user,"You pick up the scent of [tracking_target]. The hunt begins.") - on_the_trail(user) - return - - if(!tracking_target) - to_chat(user,"You're not holding anything to smell, and you haven't smelled anything you can track. You smell your palm instead; it's kinda salty.") - return - - on_the_trail(user) - -/obj/effect/proc_holder/spell/targeted/olfaction/proc/on_the_trail(mob/living/user) - if(!tracking_target) - to_chat(user,"You're not tracking a scent, but the game thought you were. Something's gone wrong! Report this as a bug.") - return - if(tracking_target == user) - to_chat(user,"You smell out the trail to yourself. Yep, it's you.") - return - if(usr.get_virtual_z_level() < tracking_target.get_virtual_z_level()) - to_chat(user,"The trail leads... way up above you? Huh. They must be really, really far away.") - return - else if(usr.get_virtual_z_level() > tracking_target.get_virtual_z_level()) - to_chat(user,"The trail leads... way down below you? Huh. They must be really, really far away.") - return - var/direction_text = "[dir2text(get_dir(usr, tracking_target))]" - if(direction_text) - to_chat(user,"You consider [tracking_target]'s scent. The trail leads [direction_text].") - -/datum/mutation/firebreath - name = "Fire Breath" - desc = "An ancient mutation that gives lizards breath of fire." - quality = POSITIVE - difficulty = 12 - locked = TRUE - power = /obj/effect/proc_holder/spell/aimed/firebreath - instability = 30 - energy_coeff = 1 - power_coeff = 1 - species_allowed = list(SPECIES_LIZARD) - -/datum/mutation/firebreath/modify() - ..() - if(power) - var/obj/effect/proc_holder/spell/aimed/firebreath/firebreath = power - firebreath.strength = GET_MUTATION_POWER(src) - -/obj/effect/proc_holder/spell/aimed/firebreath - name = "Fire Breath" - desc = "You can breathe fire at a target." - school = "evocation" - invocation = "" - invocation_type = INVOCATION_NONE - charge_max = 1 MINUTES - clothes_req = FALSE - range = 20 - projectile_type = /obj/projectile/magic/fireball/firebreath - base_icon_state = "fireball" - action_icon_state = "fireball0" - sound = 'sound/magic/demon_dies.ogg' //horrifying lizard noises - active_msg = "You built up heat in your mouth." - deactive_msg = "You swallow the flame." - var/strength = 1 - -/obj/effect/proc_holder/spell/aimed/firebreath/before_cast(list/targets) - . = ..() - var/mob/living/carbon/user = usr - if(!istype(user)) - return - if(user.is_mouth_covered()) - user.adjust_fire_stacks(2) - user.IgniteMob() - to_chat(user, "Something in front of your mouth caught fire!") - return FALSE - -/obj/effect/proc_holder/spell/aimed/firebreath/ready_projectile(obj/projectile/magic/fireball/fireball, atom/target, mob/user, iteration) - if(!istype(fireball)) - return - fireball.exp_light = strength - 1 - fireball.exp_fire += strength - -/obj/projectile/magic/fireball/firebreath - name = "fire breath" - exp_heavy = 0 - exp_light = 0 - exp_flash = 0 - exp_fire = 4 - magic = FALSE - -/datum/mutation/void - name = "Void Magnet" - desc = "A rare genome that attracts odd forces not usually observed." - quality = MINOR_NEGATIVE //upsides and downsides - instability = 30 - power = /obj/effect/proc_holder/spell/self/void - energy_coeff = 1 - synchronizer_coeff = 1 - -/datum/mutation/void/on_life() - if(!isturf(owner.loc)) - return - if(prob((0.5 + ((100 - dna.stability) / 20))) * GET_MUTATION_SYNCHRONIZER(src)) //very rare, but enough to annoy you hopefully. +0.5 probability for every 10 points lost in stability - new /obj/effect/immortality_talisman/void(get_turf(owner), owner) - -/obj/effect/proc_holder/spell/self/void - name = "Convoke Void" //magic the gathering joke here - desc = "A rare genome that attracts odd forces not usually observed. May sometimes pull you in randomly." - school = "evocation" - clothes_req = FALSE - charge_max = 1 MINUTES - invocation = "DOOOOOOOOOOOOOOOOOOOOM!!!" - invocation_type = INVOCATION_SHOUT - action_icon_state = "void_magnet" - -/obj/effect/proc_holder/spell/self/void/can_cast(mob/user = usr) - if(!isturf(user.loc)) - return FALSE - return ..() - -/obj/effect/proc_holder/spell/self/void/cast(mob/user = usr) - . = ..() - new /obj/effect/immortality_talisman/void(get_turf(user), user) - -/datum/mutation/self_amputation - name = "Autotomy" - desc = "Allows a creature to voluntary discard a random appendage." - quality = POSITIVE - instability = 30 - power = /obj/effect/proc_holder/spell/self/self_amputation - energy_coeff = 1 - -/obj/effect/proc_holder/spell/self/self_amputation - name = "Drop a limb" - desc = "Concentrate to make a random limb pop right off your body." - clothes_req = FALSE - human_req = FALSE - charge_max = 10 SECONDS - action_icon_state = "autotomy" - -/obj/effect/proc_holder/spell/self/self_amputation/cast(mob/living/carbon/user = usr) - if(!istype(user) || HAS_TRAIT(user, TRAIT_NODISMEMBER)) - return - var/list/parts = list() - for(var/obj/item/bodypart/part as() in user.bodyparts) - if(part.body_part != HEAD && part.body_part != CHEST && part.dismemberable) - parts += part - if(!length(parts)) - to_chat(user, "You can't shed any more limbs!") - return - var/obj/item/bodypart/yeeted_limb = pick(parts) - yeeted_limb.dismember() - -/datum/mutation/overload - name = "Overload" - desc = "Allows an Ethereal to overload their skin to cause a bright flash." - quality = POSITIVE - locked = TRUE - instability = 30 - power = /obj/effect/proc_holder/spell/self/overload - species_allowed = list(SPECIES_ETHEREAL) - energy_coeff = 1 - power_coeff = 1 - -/datum/mutation/overload/modify() - ..() - if(power) - var/static/max_range = min(getviewsize(world.view)[1], getviewsize(world.view)[2]) - 2 - var/obj/effect/proc_holder/spell/self/overload/overload = power - overload.max_distance = min(max_range, initial(overload.max_distance) * GET_MUTATION_POWER(src)) - -/obj/effect/proc_holder/spell/self/overload - name = "Overload" - desc = "Concentrate to make your skin energize." - clothes_req = FALSE - human_req = FALSE - charge_max = 40 SECONDS - action_icon_state = "blind" - var/max_distance = 4 - -/obj/effect/proc_holder/spell/self/overload/cast(mob/living/carbon/human/user) - if(!isethereal(user)) - return - var/list/mob/targets = oviewers(max_distance, get_turf(user)) - visible_message("[user] emits a blinding light!") - for(var/mob/living/carbon/target in targets) - if(target.flash_act(1)) - target.Paralyze(10 + (5 * max_distance)) - - for(var/mob/living/carbon/C in targets) - if(C.flash_act(1)) - C.Paralyze(10 + (5*max_distance)) - -/datum/mutation/overload/modify() - if(power) - var/obj/effect/proc_holder/spell/self/overload/S = power - S.max_distance = 4 * GET_MUTATION_POWER(src) - -//Psyphoza species mutation -/datum/mutation/spores - name = "Agaricale Pores" //Pores, not spores - desc = "An ancient mutation that gives psyphoza the ability to produce spores." - quality = POSITIVE - difficulty = 12 - locked = TRUE - power = /obj/effect/proc_holder/spell/self/spores - instability = 30 - energy_coeff = 1 - power_coeff = 1 - -/obj/effect/proc_holder/spell/self/spores - name = "Release Spores" - desc = "A rare genome that forces the subject to evict spores from their pores." - school = "evocation" - invocation = "" - clothes_req = FALSE - charge_max = 600 - invocation_type = INVOCATION_NONE - base_icon_state = "smoke" - action_icon_state = "smoke" - -/obj/effect/proc_holder/spell/self/spores/cast(mob/user = usr) - . = ..() - //Setup reagents - var/datum/reagents/holder = new() - //If our user is a carbon, use their blood - var/mob/living/carbon/C = user - if(iscarbon(user) && C.blood_volume > 0) - C.blood_volume = max(0, C.blood_volume-15) - if(C.get_blood_id()) - holder.add_reagent(C.get_blood_id(), min(C.blood_volume, 15)) - else - holder.add_reagent(/datum/reagent/blood, min(C.blood_volume, 15)) - else - holder.add_reagent(/datum/reagent/drug/mushroomhallucinogen, 15) - - var/location = get_turf(user) - var/smoke_radius = round(sqrt(holder.total_volume / 2), 1) - var/datum/effect_system/smoke_spread/chem/S = new - S.attach(location) - playsound(location, 'sound/effects/smoke.ogg', 50, 1, -3) - if(S) - S.set_up(holder, smoke_radius, location, 0) - S.start() - if(holder?.my_atom) - holder.clear_reagents() diff --git a/code/datums/mutations/antenna.dm b/code/datums/mutations/antenna.dm index e58a85cb8d06b..bf6c5418125f5 100644 --- a/code/datums/mutations/antenna.dm +++ b/code/datums/mutations/antenna.dm @@ -38,3 +38,62 @@ /obj/item/implant/radio/antenna/Initialize() . = ..() radio.name = "internal antenna" + + +/datum/mutation/mindreader + name = "Mind Reader" + desc = "The affected person can look into the recent memories of others." + quality = POSITIVE + text_gain_indication = "You hear distant voices at the corners of your mind." + text_lose_indication = "The distant voices fade." + power_path = /datum/action/cooldown/spell/pointed/mindread + instability = 40 + difficulty = 8 + locked = TRUE + +/datum/action/cooldown/spell/pointed/mindread + name = "Mindread" + desc = "Read the target's mind." + button_icon_state = "mindread" + cooldown_time = 5 SECONDS + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE_MIND + + ranged_mousepointer = 'icons/effects/mouse_pointers/mindswap_target.dmi' + +/datum/action/cooldown/spell/pointed/mindread/is_valid_target(atom/cast_on) + if(!isliving(cast_on)) + return FALSE + var/mob/living/living_cast_on = cast_on + if(!living_cast_on.mind) + to_chat(owner, span_warning("[cast_on] has no mind to read!")) + return FALSE + if(living_cast_on.stat == DEAD) + to_chat(owner, span_warning("[cast_on] is dead!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/mindread/cast(mob/living/cast_on) + . = ..() + if(cast_on.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) + to_chat(owner, span_warning("As you reach into [cast_on]'s mind, \ + you are stopped by a mental blockage. It seems you've been foiled.")) + return + + if(cast_on == owner) + to_chat(owner, span_warning("You plunge into your mind... Yep, it's your mind.")) + return + + to_chat(owner, span_boldnotice("You plunge into [cast_on]'s mind...")) + if(prob(20)) + // chance to alert the read-ee + to_chat(cast_on, span_danger("You feel something foreign enter your mind.")) + + var/list/recent_speech = list() + var/list/say_log = list() + var/log_source = cast_on.logging + //this whole loop puts the read-ee's say logs into say_log in an easy to access way + for(var/log_type in log_source) + var/nlog_type = text2num(log_type) + if(nlog_type & LOG_SAY) diff --git a/code/datums/mutations/autonomy.dm b/code/datums/mutations/autonomy.dm new file mode 100644 index 0000000000000..be43875a78bff --- /dev/null +++ b/code/datums/mutations/autonomy.dm @@ -0,0 +1,43 @@ + +/datum/mutation/human/self_amputation + name = "Autotomy" + desc = "Allows a creature to voluntary discard a random appendage." + quality = POSITIVE + text_gain_indication = span_notice("Your joints feel loose.") + instability = 30 + power_path = /datum/action/cooldown/spell/self_amputation + + energy_coeff = 1 + synchronizer_coeff = 1 + +/datum/action/cooldown/spell/self_amputation + name = "Drop a limb" + desc = "Concentrate to make a random limb pop right off your body." + button_icon_state = "autotomy" + + cooldown_time = 10 SECONDS + spell_requirements = NONE + +/datum/action/cooldown/spell/self_amputation/is_valid_target(atom/cast_on) + return iscarbon(cast_on) + +/datum/action/cooldown/spell/self_amputation/cast(mob/living/carbon/cast_on) + . = ..() + if(HAS_TRAIT(cast_on, TRAIT_NODISMEMBER)) + to_chat(cast_on, span_notice("You concentrate really hard, but nothing happens.")) + return + + var/list/parts = list() + for(var/obj/item/bodypart/to_remove as anything in cast_on.bodyparts) + if(to_remove.body_zone == BODY_ZONE_HEAD || to_remove.body_zone == BODY_ZONE_CHEST) + continue + if(!to_remove.dismemberable) + continue + parts += to_remove + + if(!length(parts)) + to_chat(cast_on, span_notice("You can't shed any more limbs!")) + return + + var/obj/item/bodypart/to_remove = pick(parts) + to_remove.dismember() diff --git a/code/datums/mutations/cold.dm b/code/datums/mutations/cold.dm index be012a988cce0..48d16e22636aa 100644 --- a/code/datums/mutations/cold.dm +++ b/code/datums/mutations/cold.dm @@ -5,15 +5,18 @@ instability = 10 difficulty = 10 energy_coeff = 1 - power = /obj/effect/proc_holder/spell/targeted/conjure_item/snow + power_path = /datum/action/cooldown/spell/conjure_item/snow -/obj/effect/proc_holder/spell/targeted/conjure_item/snow +/datum/action/cooldown/spell/conjure_item/snow name = "Create Snow" desc = "Concentrates cryokinetic forces to create snow, useful for snow-like construction." - item_type = /obj/item/stack/sheet/snow - charge_max = 5 SECONDS + button_icon_state = "snow" + + cooldown_time = 5 SECONDS + spell_requirements = NONE + + item_type = /obj/item/stack/sheet/mineral/snow delete_old = FALSE - action_icon_state = "snow" /datum/mutation/wax_saliva name = "Waxy Saliva" @@ -41,30 +44,17 @@ difficulty = 12 energy_coeff = 1 power_coeff = 1 - power = /obj/effect/proc_holder/spell/aimed/cryo - -/datum/mutation/cryokinesis/modify() - ..() - if(power) - var/obj/effect/proc_holder/spell/aimed/cryo/cryobeam = power - cryobeam.power = GET_MUTATION_POWER(src) + power_path = /datum/action/cooldown/spell/pointed/projectile/cryo -/obj/effect/proc_holder/spell/aimed/cryo +/datum/action/cooldown/spell/pointed/projectile/cryo name = "Cryobeam" desc = "This power fires a frozen bolt at a target." - charge_max = 15 SECONDS - cooldown_min = 15 SECONDS - clothes_req = FALSE - range = 3 - projectile_type = /obj/projectile/temp/cryo + button_icon_state = "icebeam0" + cooldown_time = 15 SECONDS + spell_requirements = NONE + antimagic_flags = NONE + base_icon_state = "icebeam" - action_icon_state = "icebeam" active_msg = "You focus your cryokinesis!" deactive_msg = "You relax." - active = FALSE - var/power = 1 - -/obj/effect/proc_holder/spell/aimed/cryo/ready_projectile(obj/projectile/temp/cryo/cryobeam, atom/target, mob/user, iteration) - if(!istype(cryobeam)) - return - cryobeam.temperature *= power + projectile_type = /obj/projectile/temp/cryo diff --git a/code/datums/mutations/fire_breath.dm b/code/datums/mutations/fire_breath.dm new file mode 100644 index 0000000000000..9869d41283e0f --- /dev/null +++ b/code/datums/mutations/fire_breath.dm @@ -0,0 +1,96 @@ +/datum/mutation/human/firebreath + name = "Fire Breath" + desc = "An ancient mutation that gives lizards breath of fire." + quality = POSITIVE + difficulty = 12 + locked = TRUE + text_gain_indication = "Your throat is burning!" + text_lose_indication = "Your throat is cooling down." + power_path = /datum/action/cooldown/spell/cone/staggered/fire_breath + instability = 30 + energy_coeff = 1 + power_coeff = 1 + +/datum/mutation/human/firebreath/modify() + . = ..() + var/datum/action/cooldown/spell/cone/staggered/fire_breath/to_modify = . + if(!istype(to_modify)) // null or invalid + return + + if(GET_MUTATION_POWER(src) <= 1) // we only care about power from here on + return + + to_modify.cone_levels += 2 // Cone fwooshes further, and... + to_modify.self_throw_range += 1 // the breath throws the user back more + +/datum/action/cooldown/spell/cone/staggered/fire_breath + name = "Fire Breath" + desc = "You breathe a cone of fire directly in front of you." + button_icon_state = "fireball0" + sound = 'sound/magic/demon_dies.ogg' //horrifying lizard noises + + school = SCHOOL_EVOCATION + cooldown_time = 40 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = NONE + antimagic_flags = NONE + + cone_levels = 3 + respect_density = TRUE + /// The range our user is thrown backwards after casting the spell + var/self_throw_range = 1 + +/datum/action/cooldown/spell/cone/staggered/fire_breath/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + if(!iscarbon(cast_on)) + return + + var/mob/living/carbon/our_lizard = cast_on + if(!our_lizard.is_mouth_covered()) + return + + our_lizard.adjust_fire_stacks(cone_levels) + our_lizard.ignite_mob() + to_chat(our_lizard, span_warning("Something in front of your mouth catches fire!")) + +/datum/action/cooldown/spell/cone/staggered/fire_breath/after_cast(atom/cast_on) + . = ..() + if(!isliving(cast_on)) + return + + var/mob/living/living_cast_on = cast_on + // When casting, throw the caster backwards a few tiles. + var/original_dir = living_cast_on.dir + living_cast_on.throw_at( + get_edge_target_turf(living_cast_on, turn(living_cast_on.dir, 180)), + range = self_throw_range, + speed = 2, + gentle = TRUE, + ) + // Try to set us to our original direction after, so we don't end up backwards. + living_cast_on.setDir(original_dir) + +/datum/action/cooldown/spell/cone/staggered/fire_breath/calculate_cone_shape(current_level) + // This makes the cone shoot out into a 3 wide column of flames. + // You may be wondering, "that equation doesn't seem like it'd make a 3 wide column" + // well it does, and that's all that matters. + return (2 * current_level) - 1 + +/datum/action/cooldown/spell/cone/staggered/fire_breath/do_turf_cone_effect(turf/target_turf, atom/caster, level) + // Further turfs experience less exposed_temperature and exposed_volume + new /obj/effect/hotspot(target_turf) // for style + target_turf.hotspot_expose(max(500, 900 - (100 * level)), max(50, 200 - (50 * level)), 1) + +/datum/action/cooldown/spell/cone/staggered/fire_breath/do_mob_cone_effect(mob/living/target_mob, atom/caster, level) + // Further out targets take less immediate burn damage and get less fire stacks. + // The actual burn damage application is not blocked by fireproofing, like space dragons. + target_mob.apply_damage(max(10, 40 - (5 * level)), BURN, spread_damage = TRUE) + target_mob.adjust_fire_stacks(max(2, 5 - level)) + target_mob.ignite_mob() + +/datum/action/cooldown/spell/cone/staggered/firebreath/do_obj_cone_effect(obj/target_obj, atom/caster, level) + // Further out objects experience less exposed_temperature and exposed_volume + target_obj.fire_act(max(500, 900 - (100 * level)), max(50, 200 - (50 * level))) diff --git a/code/datums/mutations/olfaction.dm b/code/datums/mutations/olfaction.dm new file mode 100644 index 0000000000000..e014806233a7b --- /dev/null +++ b/code/datums/mutations/olfaction.dm @@ -0,0 +1,139 @@ +/datum/mutation/human/olfaction + name = "Transcendent Olfaction" + desc = "Your sense of smell is comparable to that of a canine." + quality = POSITIVE + difficulty = 12 + text_gain_indication = "Smells begin to make more sense..." + text_lose_indication = "Your sense of smell goes back to normal." + power_path = /datum/action/cooldown/spell/olfaction + instability = 30 + synchronizer_coeff = 1 + +/datum/mutation/human/olfaction/modify() + . = ..() + var/datum/action/cooldown/spell/olfaction/to_modify = . + if(!istype(to_modify)) // null or invalid + return + + to_modify.sensitivity = GET_MUTATION_SYNCHRONIZER(src) + +/datum/action/cooldown/spell/olfaction + name = "Remember the Scent" + desc = "Get a scent off of the item you're currently holding to track it. \ + With an empty hand, you'll track the scent you've remembered." + button_icon_state = "nose" + + cooldown_time = 10 SECONDS + spell_requirements = NONE + + /// Weakref to the mob we're tracking + var/datum/weakref/tracking_ref + /// Our nose's sensitivity + var/sensitivity = 1 + +/datum/action/cooldown/spell/olfaction/is_valid_target(atom/cast_on) + if(!isliving(cast_on)) + return FALSE + + var/mob/living/living_cast_on = cast_on + if(ishuman(living_cast_on) && !living_cast_on.get_bodypart(BODY_ZONE_HEAD)) + to_chat(owner, span_warning("You have no nose!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/olfaction/cast(mob/living/cast_on) + . = ..() + // Can we sniff? is there miasma in the air? + var/datum/gas_mixture/air = cast_on.loc.return_air() + var/list/cached_gases = air.gases + + if(cached_gases[/datum/gas/miasma]) + cast_on.adjust_disgust(sensitivity * 45) + to_chat(cast_on, span_warning("With your overly sensitive nose, \ + you get a whiff of stench and feel sick! Try moving to a cleaner area!")) + return + + var/atom/sniffed = cast_on.get_active_held_item() + if(sniffed) + pick_up_target(cast_on, sniffed) + else + follow_target(cast_on) + +/// Attempt to pick up a new target based on the fingerprints on [sniffed]. +/datum/action/cooldown/spell/olfaction/proc/pick_up_target(mob/living/caster, atom/sniffed) + var/mob/living/carbon/old_target = tracking_ref?.resolve() + var/list/possibles = list() + var/list/prints = GET_ATOM_FINGERPRINTS(sniffed) + if(prints) + for(var/mob/living/carbon/to_check as anything in GLOB.carbon_list) + if(prints[md5(to_check.dna?.unique_identity)]) + possibles |= to_check + + // There are no finger prints on the atom, so nothing to track + if(!length(possibles)) + to_chat(caster, span_warning("Despite your best efforts, there are no scents to be found on [sniffed]...")) + return + + var/mob/living/carbon/new_target = tgui_input_list(caster, "Scent to remember", "Scent Tracking", sort_names(possibles)) + if(QDELETED(src) || QDELETED(caster)) + return + + if(QDELETED(new_target)) + // We don't have a new target OR an old target + if(QDELETED(old_target)) + to_chat(caster, span_warning("You decide against remembering any scents. \ + Instead, you notice your own nose in your peripheral vision. \ + This goes on to remind you of that one time you started breathing manually and couldn't stop. \ + What an awful day that was.")) + tracking_ref = null + + // We don't have a new target, but we have an old target to fall back on + else + to_chat(caster, span_notice("You return to tracking [old_target]. The hunt continues.")) + on_the_trail(caster) + return + + // We have a new target to track + to_chat(caster, span_notice("You pick up the scent of [new_target]. The hunt begins.")) + tracking_ref = WEAKREF(new_target) + on_the_trail(caster) + +/// Attempt to follow our current tracking target. +/datum/action/cooldown/spell/olfaction/proc/follow_target(mob/living/caster) + var/mob/living/carbon/current_target = tracking_ref?.resolve() + // Either our weakref failed to resolve (our target's gone), + // or we never had a target in the first place + if(QDELETED(current_target)) + to_chat(caster, span_warning("You're not holding anything to smell, \ + and you haven't smelled anything you can track. You smell your skin instead; it's kinda salty.")) + tracking_ref = null + return + + on_the_trail(caster) + +/// Actually go through and give the user a hint of the direction our target is. +/datum/action/cooldown/spell/olfaction/proc/on_the_trail(mob/living/caster) + var/mob/living/carbon/current_target = tracking_ref?.resolve() + if(!current_target) + to_chat(caster, span_warning("You're not tracking a scent, but the game thought you were. \ + Something's gone wrong! Report this as a bug.")) + stack_trace("[type] - on_the_trail was called when no tracking target was set.") + tracking_ref = null + return + + if(current_target == caster) + to_chat(caster, span_warning("You smell out the trail to yourself. Yep, it's you.")) + return + + if(caster.z < current_target.z) + to_chat(caster, span_warning("The trail leads... way up above you? Huh. They must be really, really far away.")) + return + + else if(caster.z > current_target.z) + to_chat(caster, span_warning("The trail leads... way down below you? Huh. They must be really, really far away.")) + return + + var/direction_text = span_bold("[dir2text(get_dir(caster, current_target))]") + if(direction_text) + to_chat(caster, span_notice("You consider [current_target]'s scent. The trail leads [direction_text].")) diff --git a/code/datums/mutations/sight.dm b/code/datums/mutations/sight.dm index 0af88b668e29c..ee602c4e23c6c 100644 --- a/code/datums/mutations/sight.dm +++ b/code/datums/mutations/sight.dm @@ -40,16 +40,69 @@ instability = 25 locked = TRUE traits = TRAIT_THERMAL_VISION + power_path = /datum/action/cooldown/spell/thermal_vision -/datum/mutation/thermal/on_acquiring(mob/living/carbon/owner) +/datum/mutation/human/thermal/on_losing(mob/living/carbon/human/owner) if(..()) return - owner.update_sight() -/datum/mutation/thermal/on_losing(mob/living/carbon/owner) - if(..()) + // Something went wront and we still have the thermal vision from our power, no cheating. + if(HAS_TRAIT_FROM(owner, TRAIT_THERMAL_VISION, GENETIC_MUTATION)) + REMOVE_TRAIT(owner, TRAIT_THERMAL_VISION, GENETIC_MUTATION) + owner.update_sight() + + +/datum/mutation/human/thermal/modify() + . = ..() + var/datum/action/cooldown/spell/thermal_vision/to_modify = . + if(!istype(to_modify)) // null or invalid + return + + to_modify.eye_damage = 10 * GET_MUTATION_SYNCHRONIZER(src) + to_modify.thermal_duration = 10 * GET_MUTATION_POWER(src) + + +/datum/action/cooldown/spell/thermal_vision + name = "Activate Thermal Vision" + desc = "You can see thermal signatures, at the cost of your eyesight." + icon_icon = 'icons/mob/actions/actions_changeling.dmi' + button_icon_state = "augmented_eyesight" + + cooldown_time = 25 SECONDS + spell_requirements = NONE + + /// How much eye damage is given on cast + var/eye_damage = 10 + /// The duration of the thermal vision + var/thermal_duration = 10 SECONDS + +/datum/action/cooldown/spell/thermal_vision/Remove(mob/living/remove_from) + REMOVE_TRAIT(remove_from, TRAIT_THERMAL_VISION, GENETIC_MUTATION) + remove_from.update_sight() + return ..() + +/datum/action/cooldown/spell/thermal_vision/is_valid_target(atom/cast_on) + return isliving(cast_on) && !HAS_TRAIT(cast_on, TRAIT_THERMAL_VISION) + +/datum/action/cooldown/spell/thermal_vision/cast(mob/living/cast_on) + . = ..() + ADD_TRAIT(cast_on, TRAIT_THERMAL_VISION, GENETIC_MUTATION) + cast_on.update_sight() + to_chat(cast_on, span_info("You focus your eyes intensely, as your vision becomes filled with heat signatures.")) + addtimer(CALLBACK(src, .proc/deactivate, cast_on), thermal_duration) + +/datum/action/cooldown/spell/thermal_vision/proc/deactivate(mob/living/cast_on) + if(QDELETED(cast_on) || !HAS_TRAIT_FROM(cast_on, TRAIT_THERMAL_VISION, GENETIC_MUTATION)) return - owner.update_sight() + + REMOVE_TRAIT(cast_on, TRAIT_THERMAL_VISION, GENETIC_MUTATION) + cast_on.update_sight() + to_chat(cast_on, span_info("You blink a few times, your vision returning to normal as a dull pain settles in your eyes.")) + + if(iscarbon(cast_on)) + var/mob/living/carbon/carbon_cast_on = cast_on + carbon_cast_on.adjustOrganLoss(ORGAN_SLOT_EYES, eye_damage) + //X-ray Vision lets you see through walls. /datum/mutation/thermal/x_ray diff --git a/code/datums/mutations/telepathy.dm b/code/datums/mutations/telepathy.dm new file mode 100644 index 0000000000000..8619c2bddc476 --- /dev/null +++ b/code/datums/mutations/telepathy.dm @@ -0,0 +1,10 @@ +/datum/mutation/human/telepathy + name = "Telepathy" + desc = "A rare mutation that allows the user to telepathically communicate to others." + quality = POSITIVE + text_gain_indication = "You can hear your own voice echoing in your mind!" + text_lose_indication = "You don't hear your mind echo anymore." + difficulty = 12 + power_path = /datum/action/cooldown/spell/list_target/telepathy + instability = 10 + energy_coeff = 1 diff --git a/code/datums/mutations/tongue_spike.dm b/code/datums/mutations/tongue_spike.dm new file mode 100644 index 0000000000000..1bd02df0b3e2b --- /dev/null +++ b/code/datums/mutations/tongue_spike.dm @@ -0,0 +1,181 @@ +/datum/mutation/human/tongue_spike + name = "Tongue Spike" + desc = "Allows a creature to voluntary shoot their tongue out as a deadly weapon." + quality = POSITIVE + text_gain_indication = span_notice("Your feel like you can throw your voice.") + instability = 15 + power_path = /datum/action/cooldown/spell/tongue_spike + + energy_coeff = 1 + synchronizer_coeff = 1 + +/datum/action/cooldown/spell/tongue_spike + name = "Launch spike" + desc = "Shoot your tongue out in the direction you're facing, embedding it and dealing damage until they remove it." + icon_icon = 'icons/mob/actions/actions_genetic.dmi' + button_icon_state = "spike" + + cooldown_time = 10 SECONDS + spell_requirements = SPELL_REQUIRES_HUMAN + + /// The type-path to what projectile we spawn to throw at someone. + var/spike_path = /obj/item/hardened_spike + +/datum/action/cooldown/spell/tongue_spike/is_valid_target(atom/cast_on) + return iscarbon(cast_on) + +/datum/action/cooldown/spell/tongue_spike/cast(mob/living/carbon/cast_on) + . = ..() + if(HAS_TRAIT(cast_on, TRAIT_NODISMEMBER)) + to_chat(cast_on, span_notice("You concentrate really hard, but nothing happens.")) + return + + var/obj/item/organ/internal/tongue/to_fire = locate() in cast_on.internal_organs + if(!to_fire) + to_chat(cast_on, span_notice("You don't have a tongue to shoot!")) + return + + to_fire.Remove(cast_on, special = TRUE) + var/obj/item/hardened_spike/spike = new spike_path(get_turf(cast_on), cast_on) + to_fire.forceMove(spike) + spike.throw_at(get_edge_target_turf(cast_on, cast_on.dir), 14, 4, cast_on) + +/obj/item/hardened_spike + name = "biomass spike" + desc = "Hardened biomass, shaped into a spike. Very pointy!" + icon_state = "tonguespike" + force = 2 + throwforce = 15 //15 + 2 (WEIGHT_CLASS_SMALL) * 4 (EMBEDDED_IMPACT_PAIN_MULTIPLIER) = i didnt do the math + throw_speed = 4 + embedding = list( + "embedded_pain_multiplier" = 4, + "embed_chance" = 100, + "embedded_fall_chance" = 0, + "embedded_ignore_throwspeed_threshold" = TRUE, + ) + w_class = WEIGHT_CLASS_SMALL + sharpness = SHARP_POINTY + custom_materials = list(/datum/material/biomass = 500) + /// What mob "fired" our tongue + var/datum/weakref/fired_by_ref + /// if we missed our target + var/missed = TRUE + +/obj/item/hardened_spike/Initialize(mapload, mob/living/carbon/source) + . = ..() + src.fired_by_ref = WEAKREF(source) + addtimer(CALLBACK(src, .proc/check_embedded), 5 SECONDS) + +/obj/item/hardened_spike/proc/check_embedded() + if(missed) + unembedded() + +/obj/item/hardened_spike/embedded(atom/target) + if(isbodypart(target)) + missed = FALSE + +/obj/item/hardened_spike/unembedded() + visible_message(span_warning("[src] cracks and twists, changing shape!")) + for(var/obj/tongue as anything in contents) + tongue.forceMove(get_turf(src)) + + qdel(src) + +/datum/mutation/human/tongue_spike/chem + name = "Chem Spike" + desc = "Allows a creature to voluntary shoot their tongue out as biomass, allowing a long range transfer of chemicals." + quality = POSITIVE + text_gain_indication = span_notice("Your feel like you can really connect with people by throwing your voice.") + instability = 15 + locked = TRUE + power_path = /datum/action/cooldown/spell/tongue_spike/chem + energy_coeff = 1 + synchronizer_coeff = 1 + +/datum/action/cooldown/spell/tongue_spike/chem + name = "Launch chem spike" + desc = "Shoot your tongue out in the direction you're facing, \ + embedding it for a very small amount of damage. \ + While the other person has the spike embedded, \ + you can transfer your chemicals to them." + button_icon_state = "spikechem" + + spike_path = /obj/item/hardened_spike/chem + +/obj/item/hardened_spike/chem + name = "chem spike" + desc = "Hardened biomass, shaped into... something." + icon_state = "tonguespikechem" + throwforce = 2 //2 + 2 (WEIGHT_CLASS_SMALL) * 0 (EMBEDDED_IMPACT_PAIN_MULTIPLIER) = i didnt do the math again but very low or smthin + embedding = list( + "embedded_pain_multiplier" = 0, + "embed_chance" = 100, + "embedded_fall_chance" = 0, + "embedded_pain_chance" = 0, + "embedded_ignore_throwspeed_threshold" = TRUE, //never hurts once it's in you + ) + /// Whether the tongue's already embedded in a target once before + var/embedded_once_alread = FALSE + +/obj/item/hardened_spike/chem/embedded(mob/living/carbon/human/embedded_mob) + if(embedded_once_alread) + return + embedded_once_alread = TRUE + + var/mob/living/carbon/fired_by = fired_by_ref?.resolve() + if(!fired_by) + return + + var/datum/action/send_chems/chem_action = new(src) + chem_action.transfered_ref = WEAKREF(embedded_mob) + chem_action.Grant(fired_by) + + to_chat(fired_by, span_notice("Link established! Use the \"Transfer Chemicals\" ability \ + to send your chemicals to the linked target!")) + +/obj/item/hardened_spike/chem/unembedded() + var/mob/living/carbon/fired_by = fired_by_ref?.resolve() + if(fired_by) + to_chat(fired_by, span_warning("Link lost!")) + var/datum/action/send_chems/chem_action = locate() in fired_by.actions + QDEL_NULL(chem_action) + + return ..() + +/datum/action/send_chems + name = "Transfer Chemicals" + desc = "Send all of your reagents into whomever the chem spike is embedded in. One use." + background_icon_state = "bg_spell" + icon_icon = 'icons/mob/actions/actions_genetic.dmi' + button_icon_state = "spikechemswap" + check_flags = AB_CHECK_CONSCIOUS + + /// Weakref to the mob target that we transfer chemicals to on activation + var/datum/weakref/transfered_ref + +/datum/action/send_chems/New(Target) + . = ..() + if(!istype(target, /obj/item/hardened_spike/chem)) + qdel(src) + +/datum/action/send_chems/Trigger(trigger_flags) + . = ..() + if(!.) + return FALSE + if(!ishuman(owner) || !owner.reagents) + return FALSE + var/mob/living/carbon/human/transferer = owner + var/mob/living/carbon/human/transfered = transfered_ref?.resolve() + if(!ishuman(transfered)) + return FALSE + + to_chat(transfered, span_warning("You feel a tiny prick!")) + transferer.reagents.trans_to(transfered, transferer.reagents.total_volume, 1, 1, 0, transfered_by = transferer) + + var/obj/item/hardened_spike/chem/chem_spike = target + var/obj/item/bodypart/spike_location = chem_spike.check_embedded() + + //this is where it would deal damage, if it transfers chems it removes itself so no damage + chem_spike.forceMove(get_turf(spike_location)) + chem_spike.visible_message(span_notice("[chem_spike] falls out of [spike_location]!")) + return TRUE diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm index e0d6164274f40..f2513e2069e9b 100644 --- a/code/datums/mutations/touch.dm +++ b/code/datums/mutations/touch.dm @@ -4,50 +4,56 @@ quality = POSITIVE locked = TRUE difficulty = 16 - power = /obj/effect/proc_holder/spell/targeted/touch/mutation/shock + power_path = /datum/action/cooldown/spell/touch/shock instability = 30 locked = TRUE energy_coeff = 1 power_coeff = 1 -/obj/effect/proc_holder/spell/targeted/touch/mutation/shock +//datum/action/cooldown/spell/touch/shock name = "Shock Touch" desc = "Channel electricity to your hand to shock people with." - drawmessage = "You channel electricity into your hand." - dropmessage = "You let the electricity from your hand dissipate." - hand_path = /obj/item/melee/touch_attack/mutation/shock - charge_max = 10 SECONDS - action_icon_state = "zap" + button_icon_state = "zap" + sound = 'sound/weapons/zapbang.ogg' + cooldown_time = 10 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + hand_path = /obj/item/melee/touch_attack/shock + draw_message = span_notice("You channel electricity into your hand.") + drop_message = span_notice("You let the electricity from your hand dissipate.") + +/datum/action/cooldown/spell/touch/shock/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(iscarbon(victim)) + var/mob/living/carbon/carbon_victim = victim + if(carbon_victim.electrocute_act(15, caster, 1, SHOCK_NOGLOVES | SHOCK_NOSTUN))//doesnt stun. never let this stun + carbon_victim.dropItemToGround(carbon_victim.get_active_held_item()) + carbon_victim.dropItemToGround(carbon_victim.get_inactive_held_item()) + carbon_victim.adjust_timed_status_effect(15 SECONDS, /datum/status_effect/confusion) + carbon_victim.visible_message( + span_danger("[caster] electrocutes [victim]!"), + span_userdanger("[caster] electrocutes you!"), + ) + return TRUE + + else if(isliving(victim)) + var/mob/living/living_victim = victim + if(living_victim.electrocute_act(15, caster, 1, SHOCK_NOSTUN)) + living_victim.visible_message( + span_danger("[caster] electrocutes [victim]!"), + span_userdanger("[caster] electrocutes you!"), + ) + return TRUE + + to_chat(caster, span_warning("The electricity doesn't seem to affect [victim]...")) + return TRUE /obj/item/melee/touch_attack/mutation/shock name = "\improper shock touch" desc = "This is kind of like when you rub your feet on a shag rug so you can zap your friends, only a lot less safe." - on_use_sound = 'sound/weapons/zapbang.ogg' icon_state = "zapper" item_state = "zapper" -/obj/item/melee/touch_attack/mutation/shock/afterattack(atom/target, mob/living/carbon/user, proximity) - if(QDELETED(target) || isturf(target)) - return - user.Beam(target, icon_state = "lightning[rand(1, 12)]", time = 5, maxdistance = 32) - var/zap = 15 * GET_MUTATION_POWER(parent_mutation) - if(iscarbon(target)) - var/mob/living/carbon/ctarget = target - if(ctarget.electrocute_act(zap, user, flags = SHOCK_NOSTUN)) //doesnt stun. never let this stun - ctarget.drop_all_held_items() - ctarget.confused += zap - ctarget.visible_message("[user] electrocutes [target]!","[user] electrocutes you!") - else - user.visible_message("[user] fails to electrocute [target]!") - else if(isliving(target)) - var/mob/living/ltarget = target - ltarget.electrocute_act(zap, user, flags = SHOCK_NOSTUN) - ltarget.visible_message("[user] electrocutes [target]!","[user] electrocutes you!") - else - to_chat(user,"The electricity doesn't seem to affect [target]...") - use_charge(user) - return ..() - /datum/mutation/acidooze name = "Acidic Hands" desc = "Allows an Oozeling to metabolize some of their blood into acid, concentrated on their hands." diff --git a/code/datums/mutations/void_magnet.dm b/code/datums/mutations/void_magnet.dm new file mode 100644 index 0000000000000..7900b4c099f17 --- /dev/null +++ b/code/datums/mutations/void_magnet.dm @@ -0,0 +1,43 @@ +/datum/mutation/human/void + name = "Void Magnet" + desc = "A rare genome that attracts odd forces not usually observed." + quality = MINOR_NEGATIVE //upsides and downsides + text_gain_indication = "You feel a heavy, dull force just beyond the walls watching you." + instability = 30 + power_path = /datum/action/cooldown/spell/void + energy_coeff = 1 + synchronizer_coeff = 1 + +/datum/mutation/human/void/on_life(delta_time, times_fired) + // Move this onto the spell itself at some point? + var/datum/action/cooldown/spell/void/curse = locate(power_path) in owner + if(!curse) + remove() + return + + if(!curse.is_valid_target(owner)) + return + + //very rare, but enough to annoy you hopefully. + 0.5 probability for every 10 points lost in stability + if(DT_PROB((0.25 + ((100 - dna.stability) / 40)) * GET_MUTATION_SYNCHRONIZER(src), delta_time)) + curse.cast(owner) + +/datum/action/cooldown/spell/void + name = "Convoke Void" //magic the gathering joke here + desc = "A rare genome that attracts odd forces not usually observed. May sometimes pull you in randomly." + button_icon_state = "void_magnet" + + school = SCHOOL_EVOCATION + cooldown_time = 1 MINUTES + + invocation = "DOOOOOOOOOOOOOOOOOOOOM!!!" + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + antimagic_flags = NONE + +/datum/action/cooldown/spell/void/is_valid_target(atom/cast_on) + return isturf(cast_on.loc) + +/datum/action/cooldown/spell/void/cast(atom/cast_on) + . = ..() + new /obj/effect/immortality_talisman/void(get_turf(cast_on), cast_on) diff --git a/code/datums/mutations/webbing.dm b/code/datums/mutations/webbing.dm new file mode 100644 index 0000000000000..2d696938e6ca5 --- /dev/null +++ b/code/datums/mutations/webbing.dm @@ -0,0 +1,52 @@ +//spider webs +/datum/mutation/human/webbing + name = "Webbing Production" + desc = "Allows the user to lay webbing, and travel through it." + quality = POSITIVE + text_gain_indication = "Your skin feels webby." + instability = 15 + power_path = /datum/action/cooldown/spell/lay_genetic_web + +/datum/mutation/human/webbing/on_acquiring(mob/living/carbon/human/owner) + if(..()) + return + ADD_TRAIT(owner, TRAIT_WEB_WEAVER, GENETIC_MUTATION) + +/datum/mutation/human/webbing/on_losing(mob/living/carbon/human/owner) + if(..()) + return + REMOVE_TRAIT(owner, TRAIT_WEB_WEAVER, GENETIC_MUTATION) + +// In the future this could be unified with the spider's web action +/datum/action/cooldown/spell/lay_genetic_web + name = "Lay Web" + desc = "Drops a web. Only you will be able to traverse your web easily, making it pretty good for keeping you safe." + icon_icon = 'icons/mob/actions/actions_genetic.dmi' + button_icon_state = "lay_web" + + cooldown_time = 4 SECONDS //the same time to lay a web + spell_requirements = NONE + + /// How long it takes to lay a web + var/webbing_time = 4 SECONDS + /// The path of web that we create + var/web_path = /obj/structure/spider/stickyweb/genetic + +/datum/action/cooldown/spell/lay_genetic_web/cast(atom/cast_on) + var/turf/web_spot = cast_on.loc + if(!isturf(web_spot) || (locate(web_path) in web_spot)) + to_chat(cast_on, span_warning("You can't lay webs here!")) + reset_spell_cooldown() + return FALSE + + cast_on.visible_message( + span_notice("[cast_on] begins to secrete a sticky substance."), + span_notice("You begin to lay a web."), + ) + + if(!do_after(cast_on, webbing_time, target = web_spot)) + to_chat(cast_on, span_warning("Your web spinning was interrupted!")) + return + + new web_path(web_spot, cast_on) + return ..() diff --git a/code/datums/proximity_monitor/timestop.dm b/code/datums/proximity_monitor/timestop.dm new file mode 100644 index 0000000000000..161132c7558b1 --- /dev/null +++ b/code/datums/proximity_monitor/timestop.dm @@ -0,0 +1,207 @@ +/obj/effect/timestop + anchored = TRUE + name = "chronofield" + desc = "ZA WARUDO" + icon = 'icons/effects/160x160.dmi' + icon_state = "time" + layer = FLY_LAYER + plane = ABOVE_GAME_PLANE + pixel_x = -64 + pixel_y = -64 + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + var/list/immune = list() // the one who creates the timestop is immune, which includes wizards and the dead slime you murdered to make this chronofield + var/turf/target + var/freezerange = 2 + var/duration = 140 + var/datum/proximity_monitor/advanced/timestop/chronofield + alpha = 125 + var/antimagic_flags = NONE + ///if true, immune atoms moving ends the timestop instead of duration. + var/channelled = FALSE +/obj/effect/timestop/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE) //Immune atoms assoc list atom = TRUE + . = ..() + if(!isnull(time)) + duration = time + if(!isnull(radius)) + freezerange = radius + for(var/A in immune_atoms) + immune[A] = TRUE + for(var/mob/living/to_check in GLOB.player_list) + if(HAS_TRAIT(to_check, TRAIT_TIME_STOP_IMMUNE)) + immune[to_check] = TRUE + for(var/mob/living/simple_animal/hostile/guardian/stand in GLOB.parasites) + if(stand.summoner && HAS_TRAIT(stand.summoner, TRAIT_TIME_STOP_IMMUNE)) //It would only make sense that a person's stand would also be immune. + immune[stand] = TRUE + if(start) + INVOKE_ASYNC(src, .proc/timestop) + + + + + + + + + Expand Down + + + +/obj/effect/timestop/Destroy() + QDEL_NULL(chronofield) + playsound(src, 'sound/magic/timeparadox2.ogg', 75, TRUE, frequency = -1) //reverse! + return ..() +/obj/effect/timestop/proc/timestop() + target = get_turf(src) + playsound(src, 'sound/magic/timeparadox2.ogg', 75, TRUE, -1) + chronofield = new (src, freezerange, TRUE, immune, antimagic_flags, channelled) + if(!channelled) + QDEL_IN(src, duration) +/obj/effect/timestop/magic + antimagic_flags = MAGIC_RESISTANCE +///indefinite version, but only if no immune atoms move. +/obj/effect/timestop/channelled + channelled = TRUE +/datum/proximity_monitor/advanced/timestop + var/list/immune = list() + var/list/frozen_things = list() + var/list/frozen_mobs = list() //cached separately for processing + var/list/frozen_structures = list() //Also machinery, and only frozen aestethically + var/list/frozen_turfs = list() //Only aesthetically + var/antimagic_flags = NONE + ///if true, this doesn't time out after a duration but rather when an immune atom inside moves. + var/channelled = FALSE + var/static/list/global_frozen_atoms = list() +/datum/proximity_monitor/advanced/timestop/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled) + ..() + src.immune = immune + src.antimagic_flags = antimagic_flags + src.channelled = channelled + recalculate_field() + START_PROCESSING(SSfastprocess, src) +/datum/proximity_monitor/advanced/timestop/Destroy() + unfreeze_all() + if(channelled) + for(var/atom in immune) + UnregisterSignal(atom, COMSIG_MOVABLE_MOVED) + STOP_PROCESSING(SSfastprocess, src) + return ..() +/datum/proximity_monitor/advanced/timestop/field_turf_crossed(atom/movable/movable, turf/location) + freeze_atom(movable) +/datum/proximity_monitor/advanced/timestop/proc/freeze_atom(atom/movable/A) + if(global_frozen_atoms[A] || !istype(A)) + return FALSE + if(immune[A]) //a little special logic but yes immune things don't freeze + if(channelled) + RegisterSignal(A, COMSIG_MOVABLE_MOVED, .proc/atom_broke_channel, override = TRUE) + return FALSE + if(ismob(A)) + var/mob/M = A + if(M.can_block_magic(antimagic_flags)) + immune[A] = TRUE + return + var/frozen = TRUE + if(isliving(A)) + freeze_mob(A) + else if(istype(A, /obj/projectile)) + freeze_projectile(A) + else if(istype(A, /obj/vehicle/sealed/mecha)) + freeze_mecha(A) + else if((ismachinery(A) && !istype(A, /obj/machinery/light)) || isstructure(A)) //Special exception for light fixtures since recoloring causes them to change light + freeze_structure(A) + else + frozen = FALSE + if(A.throwing) + freeze_throwing(A) + frozen = TRUE + if(!frozen) + return + frozen_things[A] = A.move_resist + A.move_resist = INFINITY + global_frozen_atoms[A] = src + into_the_negative_zone(A) + RegisterSignal(A, COMSIG_MOVABLE_PRE_MOVE, .proc/unfreeze_atom) + RegisterSignal(A, COMSIG_ITEM_PICKUP, .proc/unfreeze_atom) + return TRUE +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_all() + for(var/i in frozen_things) + unfreeze_atom(i) + for(var/T in frozen_turfs) + unfreeze_turf(T) +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_atom(atom/movable/A) + SIGNAL_HANDLER + if(A.throwing) + unfreeze_throwing(A) + if(isliving(A)) + unfreeze_mob(A) + else if(istype(A, /obj/projectile)) + unfreeze_projectile(A) + else if(istype(A, /obj/vehicle/sealed/mecha)) + unfreeze_mecha(A) + UnregisterSignal(A, COMSIG_MOVABLE_PRE_MOVE) + UnregisterSignal(A, COMSIG_ITEM_PICKUP) + escape_the_negative_zone(A) + A.move_resist = frozen_things[A] + frozen_things -= A + global_frozen_atoms -= A +/datum/proximity_monitor/advanced/timestop/proc/freeze_mecha(obj/vehicle/sealed/mecha/M) + M.completely_disabled = TRUE +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_mecha(obj/vehicle/sealed/mecha/M) + M.completely_disabled = FALSE +/datum/proximity_monitor/advanced/timestop/proc/freeze_throwing(atom/movable/AM) + var/datum/thrownthing/T = AM.throwing + T.paused = TRUE +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_throwing(atom/movable/AM) + var/datum/thrownthing/T = AM.throwing + if(T) + T.paused = FALSE +/datum/proximity_monitor/advanced/timestop/proc/freeze_turf(turf/T) + into_the_negative_zone(T) + frozen_turfs += T +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_turf(turf/T) + escape_the_negative_zone(T) +/datum/proximity_monitor/advanced/timestop/proc/freeze_structure(obj/O) + into_the_negative_zone(O) + frozen_structures += O +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_structure(obj/O) + escape_the_negative_zone(O) +/datum/proximity_monitor/advanced/timestop/process() + for(var/i in frozen_mobs) + var/mob/living/m = i + m.Stun(20, ignore_canstun = TRUE) +/datum/proximity_monitor/advanced/timestop/setup_field_turf(turf/T) + . = ..() + for(var/i in T.contents) + freeze_atom(i) + freeze_turf(T) +/datum/proximity_monitor/advanced/timestop/proc/freeze_projectile(obj/projectile/P) + P.paused = TRUE +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_projectile(obj/projectile/P) + P.paused = FALSE +/datum/proximity_monitor/advanced/timestop/proc/freeze_mob(mob/living/L) + frozen_mobs += L + L.Stun(20, ignore_canstun = TRUE) + ADD_TRAIT(L, TRAIT_MUTE, TIMESTOP_TRAIT) + SSmove_manager.stop_looping(src) //stops them mid pathing even if they're stunimmune //This is really dumb + if(isanimal(L)) + var/mob/living/simple_animal/S = L + S.toggle_ai(AI_OFF) + if(ishostile(L)) + var/mob/living/simple_animal/hostile/H = L + H.LoseTarget() +/datum/proximity_monitor/advanced/timestop/proc/unfreeze_mob(mob/living/L) + L.AdjustStun(-20, ignore_canstun = TRUE) + REMOVE_TRAIT(L, TRAIT_MUTE, TIMESTOP_TRAIT) + frozen_mobs -= L + if(isanimal(L)) + var/mob/living/simple_animal/S = L + S.toggle_ai(initial(S.AIStatus)) +//you don't look quite right, is something the matter? +/datum/proximity_monitor/advanced/timestop/proc/into_the_negative_zone(atom/A) + A.add_atom_colour(list(-1,0,0,0, 0,-1,0,0, 0,0,-1,0, 0,0,0,1, 1,1,1,0), TEMPORARY_COLOUR_PRIORITY) +//let's put some colour back into your cheeks +/datum/proximity_monitor/advanced/timestop/proc/escape_the_negative_zone(atom/A) + A.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY) +//signal fired when an immune atom moves in the time freeze zone +/datum/proximity_monitor/advanced/timestop/proc/atom_broke_channel(datum/source) + SIGNAL_HANDLER +qdel(host) diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm index 9fcb61c6f3f9d..936a849d604f9 100644 --- a/code/datums/status_effects/neutral.dm +++ b/code/datums/status_effects/neutral.dm @@ -99,7 +99,7 @@ rewarded = caster /datum/status_effect/bounty/on_apply() - to_chat(owner, "You hear something behind you talking... You have been marked for death by [rewarded]. If you die, they will be rewarded.") + to_chat(owner, span_boldnotice("You hear something behind you talking... \"You have been marked for death by [rewarded]. If you die, they will be rewarded.\"")) playsound(owner, 'sound/weapons/shotgunpump.ogg', 75, 0) return ..() @@ -113,10 +113,8 @@ to_chat(owner, "You hear something behind you talking... Bounty claimed.") playsound(owner, 'sound/weapons/shotgunshot.ogg', 75, 0) to_chat(rewarded, "You feel a surge of mana flow into you!") - for(var/obj/effect/proc_holder/spell/spell in rewarded.mind.spell_list) - spell.charge_counter = spell.charge_max - spell.recharging = FALSE - spell.update_icon() + for(var/datum/action/cooldown/spell/spell in rewarded.actions) + spell.reset_spell_cooldown() rewarded.adjustBruteLoss(-25) rewarded.adjustFireLoss(-25) rewarded.adjustToxLoss(-25, FALSE, TRUE) diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm index 5d1e841ae5026..b9de68e7cb26d 100644 --- a/code/game/objects/effects/decals/cleanable.dm +++ b/code/game/objects/effects/decals/cleanable.dm @@ -95,3 +95,17 @@ return bloodiness else return FALSE + +/** + * Gets the color associated with the any blood present on this decal. If there is no blood, returns null. + */ +/obj/effect/decal/cleanable/proc/get_blood_color() + switch(blood_state) + if(BLOOD_STATE_HUMAN) + return rgb(149, 10, 10) + if(BLOOD_STATE_XENO) + return rgb(43, 186, 0) + if(BLOOD_STATE_OIL) + return rgb(22, 22, 22) + + return null diff --git a/code/game/objects/effects/forcefields.dm b/code/game/objects/effects/forcefields.dm index bf7ae1fb93d37..e6eb6acfac4a7 100644 --- a/code/game/objects/effects/forcefields.dm +++ b/code/game/objects/effects/forcefields.dm @@ -7,25 +7,47 @@ density = TRUE CanAtmosPass = ATMOS_PASS_DENSITY z_flags = Z_BLOCK_IN_DOWN | Z_BLOCK_IN_UP - var/timeleft = 300 //Set to 0 for permanent forcefields (ugh) + /// If set, how long the force field lasts after it's created. Set to 0 to have infinite duration forcefields. + var/initial_duration = 30 SECONDS /obj/effect/forcefield/Initialize(mapload, ntimeleft) . = ..() - if(isnum_safe(ntimeleft)) - timeleft = ntimeleft - if(timeleft) - QDEL_IN(src, timeleft) + if(initial_duration > 0 SECONDS) + QDEL_IN(src, initial_duration) /obj/effect/forcefield/singularity_pull() return +/// The wizard's forcefield, summoned by forcewall +/obj/effect/forcefield/wizard + /// Flags for what antimagic can just ignore our forcefields + var/antimagic_flags = MAGIC_RESISTANCE + /// A weakref to whoever casted our forcefield. + var/datum/weakref/caster_weakref + +/obj/effect/forcefield/wizard/Initialize(mapload, mob/caster, flags = MAGIC_RESISTANCE) + . = ..() + if(caster) + caster_weakref = WEAKREF(caster) + antimagic_flags = flags + +/obj/effect/forcefield/wizard/CanAllowThrough(atom/movable/mover, border_dir) + if(IS_WEAKREF_OF(mover, caster_weakref)) + return TRUE + if(isliving(mover)) + var/mob/living/living_mover = mover + if(living_mover.can_block_magic(antimagic_flags, charge_cost = 0)) + return TRUE + + return ..() + /obj/effect/forcefield/cult desc = "An unholy shield that blocks all attacks." name = "glowing wall" icon = 'icons/effects/cult_effects.dmi' icon_state = "cultshield" CanAtmosPass = ATMOS_PASS_NO - timeleft = 200 + initial_duration = 20 SECONDS ///////////Mimewalls/////////// @@ -37,7 +59,7 @@ /obj/effect/forcefield/mime/advanced name = "invisible blockade" desc = "You're gonna be here awhile." - timeleft = 600 + initial_duration = 1 MINUTES /obj/effect/forcefield/mime/Initialize(mapload, ntimeleft) . = ..() diff --git a/code/game/objects/effects/phased_mob.dm b/code/game/objects/effects/phased_mob.dm new file mode 100644 index 0000000000000..a915ebdd05e2e --- /dev/null +++ b/code/game/objects/effects/phased_mob.dm @@ -0,0 +1,88 @@ +/obj/effect/dummy/phased_mob + name = "water" + anchored = TRUE + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 + resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + invisibility = INVISIBILITY_OBSERVER + movement_type = FLOATING + /// The movable which's jaunting in this dummy + var/atom/movable/jaunter + /// The delay between moves while jaunted + var/movedelay = 0 + /// The speed of movement while jaunted + var/movespeed = 0 + +/obj/effect/dummy/phased_mob/Initialize(mapload, atom/movable/jaunter) + . = ..() + if(jaunter) + set_jaunter(jaunter) + +/// Sets [new_jaunter] as our jaunter, forcemoves them into our contents +/obj/effect/dummy/phased_mob/proc/set_jaunter(atom/movable/new_jaunter) + jaunter = new_jaunter + jaunter.forceMove(src) + if(ismob(jaunter)) + var/mob/mob_jaunter = jaunter + mob_jaunter.reset_perspective(src) + +/obj/effect/dummy/phased_mob/Destroy() + jaunter = null // If a mob was left in the jaunter on qdel, they'll be dumped into nullspace + return ..() + +/// Removes [jaunter] from our phased mob +/obj/effect/dummy/phased_mob/proc/eject_jaunter() + if(!jaunter) + CRASH("Phased mob ([type]) attempted to eject null jaunter.") + var/turf/eject_spot = get_turf(src) + if(!eject_spot) //You're in nullspace you clown! + return + + var/area/destination_area = get_area(eject_spot) + if(destination_area.area_flags & NOTELEPORT) + // this ONLY happens if someone uses a phasing effect + // to try to land in a NOTELEPORT zone after it is created, AKA trying to exploit. + if(isliving(jaunter)) + var/mob/living/living_cheaterson = jaunter + to_chat(living_cheaterson, span_userdanger("This area has a heavy universal force occupying it, and you are scattered to the cosmos!")) + if(ishuman(living_cheaterson)) + shake_camera(living_cheaterson, 20, 1) + addtimer(CALLBACK(living_cheaterson, /mob/living/carbon.proc/vomit), 2 SECONDS) + jaunter.forceMove(find_safe_turf(z)) + + else + jaunter.forceMove(eject_spot) + qdel(src) + +/obj/effect/dummy/phased_mob/Exited(atom/movable/gone, direction) + . = ..() + if(gone == jaunter) + jaunter = null + +/obj/effect/dummy/phased_mob/ex_act() + return FALSE + +/obj/effect/dummy/phased_mob/bullet_act(blah) + return BULLET_ACT_FORCE_PIERCE +/obj/effect/dummy/phased_mob/relaymove(mob/living/user, direction) + var/turf/newloc = phased_check(user, direction) + if(!newloc) + return + setDir(direction) + forceMove(newloc) +/// Checks if the conditions are valid to be able to phase. Returns a turf destination if positive. +/obj/effect/dummy/phased_mob/proc/phased_check(mob/living/user, direction) + RETURN_TYPE(/turf) + if (movedelay > world.time || !direction) + return + var/turf/newloc = get_step(src,direction) + if(!newloc) + return + var/area/destination_area = newloc.loc + movedelay = world.time + movespeed + if(newloc.flags_1 & NOJAUNT) + to_chat(user, span_warning("Some strange aura is blocking the way.")) + return + if(destination_area.area_flags & NOTELEPORT || SSmapping.level_trait(newloc.z, ZTRAIT_NOPHASE)) + to_chat(user, span_danger("Some dull, universal force is blocking the way. It's overwhelmingly oppressive force feels dangerous.")) + return + return newloc diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 396a55212e290..c326cdc3cef8e 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -237,7 +237,7 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) . = ..() for(var/path in actions_types) - new path(src) + add_item_action(path) actions_types = null if(force_string) @@ -265,11 +265,55 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) if(ismob(loc)) var/mob/m = loc m.temporarilyRemoveItemFromInventory(src, TRUE) - for(var/X in actions) - qdel(X) + + // Handle cleaning up our actions list + for(var/datum/action/action as anything in actions) + remove_item_action(action) QDEL_NULL(rpg_loot) return ..() +/// Called when an action associated with our item is deleted +/obj/item/proc/on_action_deleted(datum/source) + SIGNAL_HANDLER + + if(!(source in actions)) + CRASH("An action ([source.type]) was deleted that was associated with an item ([src]), but was not found in the item's actions list.") + + LAZYREMOVE(actions, source) + +/// Adds an item action to our list of item actions. +/// Item actions are actions linked to our item, that are granted to mobs who equip us. +/// This also ensures that the actions are properly tracked in the actions list and removed if they're deleted. +/// Can be be passed a typepath of an action or an instance of an action. +/obj/item/proc/add_item_action(action_or_action_type) + + var/datum/action/action + if(ispath(action_or_action_type, /datum/action)) + action = new action_or_action_type(src) + else if(istype(action_or_action_type, /datum/action)) + action = action_or_action_type + else + CRASH("item add_item_action got a type or instance of something that wasn't an action.") + + LAZYADD(actions, action) + RegisterSignal(action, COMSIG_PARENT_QDELETING, .proc/on_action_deleted) + if(ismob(loc)) + // We're being held or are equipped by someone while adding an action? + // Then they should also probably be granted the action, given it's in a correct slot + var/mob/holder = loc + give_item_action(action, holder, holder.get_slot_by_item(src)) + + return action + +/// Removes an instance of an action from our list of item actions. +/obj/item/proc/remove_item_action(datum/action/action) + if(!action) + return + + UnregisterSignal(action, COMSIG_PARENT_QDELETING) + LAZYREMOVE(actions, action) + qdel(action) + /obj/item/proc/check_allowed_items(atom/target, not_inside, target_self) if(((src in target) && !target_self) || (!isturf(target.loc) && !isturf(target) && not_inside)) return 0 @@ -675,9 +719,9 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) /obj/item/proc/dropped(mob/user, silent = FALSE) SHOULD_CALL_PARENT(TRUE) - for(var/X in actions) - var/datum/action/A = X - A.Remove(user) + // Remove any item actions we temporary gave out. + for(var/datum/action/action_item_has as anything in actions) + action_item_has.Remove(user) if(item_flags & DROPDEL) qdel(src) item_flags &= ~BEING_REMOVED @@ -718,10 +762,9 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) SHOULD_CALL_PARENT(TRUE) SEND_SIGNAL(src, COMSIG_ITEM_EQUIPPED, user, slot) SEND_SIGNAL(user, COMSIG_MOB_EQUIPPED_ITEM, src, slot) - for(var/X in actions) - var/datum/action/A = X - if(item_action_slot_check(slot, user)) //some items only give their actions buttons when in a specific slot. - A.Grant(user) + // Give out actions our item has to people who equip it. + for(var/datum/action/action as anything in actions) + give_item_action(action, user, slot) if(item_flags & SLOWS_WHILE_IN_HAND || slowdown) user.update_equipment_speed_mods() if(ismonkey(user)) //Only generate icons if we have to @@ -735,6 +778,18 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) user.update_equipment_speed_mods() +/// Gives one of our item actions to a mob, when equipped to a certain slot +/obj/item/proc/give_item_action(datum/action/action, mob/to_who, slot) + // Some items only give their actions buttons when in a specific slot. + if(!item_action_slot_check(slot, to_who)) + // There is a chance we still have our item action currently, + // and are moving it from a "valid slot" to an "invalid slot". + // So call Remove() here regardless, even if excessive. + action.Remove(to_who) + return + + action.Grant(to_who) + //sometimes we only want to grant the item's action if it's equipped in a specific slot. /obj/item/proc/item_action_slot_check(slot, mob/user) if(slot == ITEM_SLOT_BACKPACK || slot == ITEM_SLOT_LEGCUFFED) //these aren't true slots, so avoid granting actions there @@ -1372,7 +1427,6 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) /obj/item/proc/update_action_buttons(status_only = FALSE, force = FALSE) for(var/datum/action/current_action as anything in actions) current_action.UpdateButtonIcon(status_only, force) - /** * * An interrupt for offering an item to other people, called mainly from [/mob/living/carbon/proc/give], in case you want to run your own offer behavior instead. * diff --git a/code/game/objects/items/RCD.dm b/code/game/objects/items/RCD.dm index d899c38641ee2..bdec118e6a451 100644 --- a/code/game/objects/items/RCD.dm +++ b/code/game/objects/items/RCD.dm @@ -1015,6 +1015,10 @@ RLD desc = "It contains the design for chairs, stools, tables, and glass tables." upgrade = RCD_UPGRADE_FURNISHING +/datum/action/item_action/pick_color + name = "Choose A Color" + + #undef GLOW_MODE #undef LIGHT_MODE #undef REMOVE_MODE diff --git a/code/game/objects/items/RCL.dm b/code/game/objects/items/RCL.dm index 3e56adab98fa7..13b08eda9bed5 100644 --- a/code/game/objects/items/RCL.dm +++ b/code/game/objects/items/RCL.dm @@ -346,3 +346,13 @@ icon_state = "rclg-1" item_state = "rclg-1" return ..() + +/datum/action/item_action/rcl_col + name = "Change Cable Color" + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "rcl_rainbow" + +/datum/action/item_action/rcl_gui + name = "Toggle Fast Wiring Gui" + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "rcl_gui" diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm index a743677e5141e..ac899e8c648e5 100644 --- a/code/game/objects/items/cards_ids.dm +++ b/code/game/objects/items/cards_ids.dm @@ -519,6 +519,7 @@ update_label("John Doe", "Clowny") /obj/item/card/id/golem, /obj/item/card/id/pass), only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_card_action) /obj/item/card/id/syndicate/afterattack(obj/item/O, mob/user, proximity) if(!proximity) diff --git a/code/game/objects/items/chainsaw.dm b/code/game/objects/items/chainsaw.dm index cc09f01d685c7..e3c3a03795a17 100644 --- a/code/game/objects/items/chainsaw.dm +++ b/code/game/objects/items/chainsaw.dm @@ -143,3 +143,6 @@ /obj/item/chainsaw/energy/doom/attack(mob/living/target) ..() target.Knockdown(4) + +/datum/action/item_action/startchainsaw + name = "Pull The Starting Cord" diff --git a/code/game/objects/items/chromosome.dm b/code/game/objects/items/chromosome.dm index 994e394468adf..a58c78045a720 100644 --- a/code/game/objects/items/chromosome.dm +++ b/code/game/objects/items/chromosome.dm @@ -34,9 +34,11 @@ HM.power_coeff = power_coeff if(HM.energy_coeff != -1) HM.energy_coeff = energy_coeff - HM.can_chromosome = 2 + HM.can_chromosome = CHROMOSOME_USED HM.chromosome_name = name - HM.modify() + // Do the actual modification + if(HM.modify()) + HM.modified = TRUE qdel(src) /proc/generate_chromosome() diff --git a/code/game/objects/items/devices/multitool.dm b/code/game/objects/items/devices/multitool.dm index 25e2e1663a094..3bb1601b8ed44 100644 --- a/code/game/objects/items/devices/multitool.dm +++ b/code/game/objects/items/devices/multitool.dm @@ -41,25 +41,23 @@ // Syndicate device disguised as a multitool; it will turn red when an AI camera is nearby. /obj/item/multitool/ai_detect + actions_types = list(/datum/action/item_action/toggle_multitool) var/detect_state = PROXIMITY_NONE var/rangealert = 8 //Glows red when inside var/rangewarning = 20 //Glows yellow when inside var/hud_type = DATA_HUD_AI_DETECT var/hud_on = FALSE var/mob/camera/ai_eye/remote/ai_detector/eye - var/datum/action/item_action/toggle_multitool/toggle_action /obj/item/multitool/ai_detect/Initialize(mapload) . = ..() START_PROCESSING(SSfastprocess, src) eye = new /mob/camera/ai_eye/remote/ai_detector() - toggle_action = new /datum/action/item_action/toggle_multitool(src) /obj/item/multitool/ai_detect/Destroy() STOP_PROCESSING(SSfastprocess, src) if(hud_on && ismob(loc)) remove_hud(loc) - QDEL_NULL(toggle_action) QDEL_NULL(eye) return ..() diff --git a/code/game/objects/items/devices/spyglasses.dm b/code/game/objects/items/devices/spyglasses.dm index 27f30310894f5..8e94fd7b8e12a 100644 --- a/code/game/objects/items/devices/spyglasses.dm +++ b/code/game/objects/items/devices/spyglasses.dm @@ -36,6 +36,9 @@ linked_bug.linked_glasses = null return ..() +/datum/action/item_action/activate_remote_view + name = "Activate Remote View" + desc = "Activates the Remote View of your spy sunglasses." /obj/item/clothing/accessory/spy_bug name = "pocket protector" diff --git a/code/game/objects/items/granters.dm b/code/game/objects/items/granters.dm deleted file mode 100644 index b9e0a8ab618fd..0000000000000 --- a/code/game/objects/items/granters.dm +++ /dev/null @@ -1,470 +0,0 @@ - -///books that teach things (intrinsic actions like bar flinging, spells like fireball or smoke, or martial arts)/// - -/obj/item/book/granter - due_date = 0 // Game time in deciseconds - unique = 1 // 0 Normal book, 1 Should not be treated as normal book, unable to be copied, unable to be modified - var/list/remarks = list() //things to read about while learning. - var/pages_to_mastery = 3 //Essentially controls how long a mob must keep the book in his hand to actually successfully learn - var/reading = FALSE //sanity - var/oneuse = TRUE //default this is true, but admins can var this to 0 if we wanna all have a pass around of the rod form book - var/used = FALSE //only really matters if oneuse but it might be nice to know if someone's used it for admin investigations perhaps - -/obj/item/book/granter/proc/turn_page(mob/user) - playsound(user, pick('sound/effects/pageturn1.ogg','sound/effects/pageturn2.ogg','sound/effects/pageturn3.ogg'), 30, 1) - if(do_after(user,50, user)) - if(remarks.len) - to_chat(user, "[pick(remarks)]") - else - to_chat(user, "You keep reading...") - return TRUE - return FALSE - -/obj/item/book/granter/proc/recoil(mob/user) //nothing so some books can just return - -/obj/item/book/granter/proc/already_known(mob/user) - return FALSE - -/obj/item/book/granter/proc/on_reading_start(mob/user) - to_chat(user, "You start reading [name]...") - -/obj/item/book/granter/proc/on_reading_stopped(mob/user) - to_chat(user, "You stop reading...") - -/obj/item/book/granter/proc/on_reading_finished(mob/user) - to_chat(user, "You finish reading [name]!") - -/obj/item/book/granter/proc/onlearned(mob/user) - used = TRUE - - -/obj/item/book/granter/attack_self(mob/user) - if(reading) - to_chat(user, "You're already reading this!") - return FALSE - if(!user.can_read(src)) - return FALSE - if(already_known(user)) - return FALSE - if(used) - if(oneuse) - recoil(user) - return FALSE - on_reading_start(user) - reading = TRUE - for(var/i in 1 to pages_to_mastery) - if(!turn_page(user)) - on_reading_stopped() - reading = FALSE - return - if(do_after(user,50, user)) - on_reading_finished(user) - reading = FALSE - return TRUE - -///ACTION BUTTONS/// - -/obj/item/book/granter/action - var/granted_action - var/actionname = "catching bugs" //might not seem needed but this makes it so you can safely name action buttons toggle this or that without it fucking up the granter, also caps - -/obj/item/book/granter/action/already_known(mob/user) - if(!granted_action) - return TRUE - for(var/datum/action/A in user.actions) - if(A.type == granted_action) - to_chat(user, "You already know all about [actionname].") - return TRUE - return FALSE - -/obj/item/book/granter/action/on_reading_start(mob/user) - to_chat(user, "You start reading about [actionname]...") - -/obj/item/book/granter/action/on_reading_finished(mob/user) - to_chat(user, "You feel like you've got a good handle on [actionname]!") - var/datum/action/G = new granted_action - G.Grant(user) - onlearned(user) - -/obj/item/book/granter/action/origami - granted_action = /datum/action/innate/origami - name = "The Art of Origami" - desc = "A meticulously in-depth manual explaining the art of paper folding." - icon_state = "origamibook" - actionname = "origami" - oneuse = TRUE - remarks = list("Dead-stick stability...", "Symmetry seems to play a rather large factor...", "Accounting for crosswinds... really?", "Drag coefficients of various paper types...", "Thrust to weight ratios?", "Positive dihedral angle?", "Center of gravity forward of the center of lift...") - -/datum/action/innate/origami - name = "Origami Folding" - desc = "Toggles your ability to catch robust paper airplanes." - button_icon_state = "origami_off" - check_flags = NONE - -/datum/action/innate/origami/Activate() - to_chat(owner, "You will now catch origami planes.") - button_icon_state = "origami_on" - active = TRUE - UpdateButtonIcon() - -/datum/action/innate/origami/Deactivate() - to_chat(owner, "You will no longer catch origami planes.") - button_icon_state = "origami_off" - active = FALSE - UpdateButtonIcon() - -///SPELLS/// - -/obj/item/book/granter/spell - var/spell - var/spellname = "conjure bugs" - -/obj/item/book/granter/spell/already_known(mob/user) - if(!spell) - return TRUE - for(var/obj/effect/proc_holder/spell/knownspell in user.mind.spell_list) - if(knownspell.type == spell) - if(user.mind) - if(iswizard(user)) - to_chat(user,"You're already far more versed in this spell than this flimsy how-to book can provide.") - else - to_chat(user,"You've already read this one.") - return TRUE - return FALSE - -/obj/item/book/granter/spell/on_reading_start(mob/user) - to_chat(user, "You start reading about casting [spellname]...") - -/obj/item/book/granter/spell/on_reading_finished(mob/user) - to_chat(user, "You feel like you've experienced enough to cast [spellname]!") - var/obj/effect/proc_holder/spell/S = new spell - user.mind.AddSpell(S) - user.log_message("learned the spell [spellname] ([S])", LOG_ATTACK, color="orange") - onlearned(user) - -/obj/item/book/granter/spell/recoil(mob/user) - user.visible_message("[src] glows in a black light!") - -/obj/item/book/granter/spell/onlearned(mob/user) - ..() - if(oneuse) - user.visible_message("[src] glows dark for a second!") - -/obj/item/book/granter/spell/fireball - spell = /obj/effect/proc_holder/spell/aimed/fireball - spellname = "fireball" - icon_state ="bookfireball" - desc = "This book feels warm to the touch." - remarks = list("Aim...AIM, FOOL!", "Just catching them on fire won't do...", "Accounting for crosswinds... really?", "I think I just burned my hand...", "Why the dumb stance? It's just a flick of the hand...", "OMEE... ONI... Ugh...", "What's the difference between a fireball and a pyroblast...") - -/obj/item/book/granter/spell/fireball/recoil(mob/user) - ..() - explosion(user.loc, 1, 0, 2, 3, FALSE, FALSE, 2, magic = TRUE) - qdel(src) - -/obj/item/book/granter/spell/sacredflame - spell = /obj/effect/proc_holder/spell/targeted/sacred_flame - spellname = "sacred flame" - icon_state ="booksacredflame" - desc = "Become one with the flames that burn within... and invite others to do so as well." - remarks = list("Well, it's one way to stop an attacker...", "I'm gonna need some good gear to stop myself from burning to death...", "Keep a fire extinguisher handy, got it...", "I think I just burned my hand...", "Apply flame directly to chest for proper ignition...", "No pain, no gain...", "One with the flame...") - -/obj/item/book/granter/spell/smoke - spell = /obj/effect/proc_holder/spell/targeted/smoke - spellname = "smoke" - icon_state ="booksmoke" - desc = "This book is overflowing with the dank arts." - remarks = list("Smoke Bomb! Heh...", "Smoke bomb would do just fine too...", "Wait, there's a machine that does the same thing in chemistry?", "This book smells awful...", "Why all these weed jokes? Just tell me how to cast it...", "Wind will ruin the whole spell, good thing we're in space... Right?", "So this is how the spider clan does it...") - -/obj/item/book/granter/spell/smoke/lesser //Chaplain smoke book - spell = /obj/effect/proc_holder/spell/targeted/smoke/lesser - -/obj/item/book/granter/spell/smoke/recoil(mob/user) - ..() - to_chat(user,"Your stomach rumbles...") - if(user.nutrition) - user.set_nutrition(200) - if(user.nutrition <= 0) - user.set_nutrition(0) - -/obj/item/book/granter/spell/blind - spell = /obj/effect/proc_holder/spell/targeted/blind - spellname = "blind" - icon_state ="bookblind" - desc = "This book looks blurry, no matter how you look at it." - remarks = list("Well I can't learn anything if I can't read the damn thing!", "Why would you use a dark font on a dark background...", "Ah, I can't see an- Oh, I'm fine...", "I can't see my hand...!", "I'm manually blinking, damn you book...", "I can't read this page, but somehow I feel like I learned something from it...", "Hey, who turned off the lights?") - -/obj/item/book/granter/spell/blind/recoil(mob/user) - ..() - to_chat(user,"You go blind!") - user.adjust_blindness(10) - -/obj/item/book/granter/spell/mindswap - spell = /obj/effect/proc_holder/spell/targeted/mind_transfer - spellname = "mindswap" - icon_state ="bookmindswap" - desc = "This book's cover is pristine, though its pages look ragged and torn." - var/mob/stored_swap //Used in used book recoils to store an identity for mindswaps - remarks = list("If you mindswap from a mouse, they will be helpless when you recover...", "Wait, where am I...?", "This book is giving me a horrible headache...", "This page is blank, but I feel words popping into my head...", "GYNU... GYRO... Ugh...", "The voices in my head need to stop, I'm trying to read here...", "I don't think anyone will be happy when I cast this spell...") - -/obj/item/book/granter/spell/mindswap/onlearned() - spellname = pick("fireball","smoke","blind","forcewall","knock","barnyard","charge") - icon_state = "book[spellname]" - name = "spellbook of [spellname]" //Note, desc doesn't change by design - ..() - -/obj/item/book/granter/spell/mindswap/recoil(mob/user) - ..() - if(stored_swap in GLOB.dead_mob_list) - stored_swap = null - if(!stored_swap) - stored_swap = user - to_chat(user,"For a moment you feel like you don't even know who you are anymore.") - return - if(stored_swap == user) - to_chat(user,"You stare at the book some more, but there doesn't seem to be anything else to learn...") - return - var/obj/effect/proc_holder/spell/targeted/mind_transfer/swapper = new - if(swapper.cast(list(stored_swap), user, TRUE, TRUE)) - to_chat(user,"You're suddenly somewhere else... and someone else?!") - to_chat(stored_swap,"Suddenly you're staring at [src] again... where are you, who are you?!") - else - user.visible_message("[src] fizzles slightly as it stops glowing!") //if the mind_transfer failed to transfer mobs, likely due to the target being catatonic. - - stored_swap = null - -/obj/item/book/granter/spell/forcewall - spell = /obj/effect/proc_holder/spell/targeted/forcewall - spellname = "forcewall" - icon_state ="bookforcewall" - desc = "This book has a dedication to mimes everywhere inside the front cover." - remarks = list("I can go through the wall! Neat.", "Why are there so many mime references...?", "This would cause much grief in a hallway...", "This is some surprisingly strong magic to create a wall nobody can pass through...", "Why the dumb stance? It's just a flick of the hand...", "Why are the pages so hard to turn, is this even paper?", "I can't mo Oh, i'm fine...") - -/obj/item/book/granter/spell/forcewall/recoil(mob/living/user) - ..() - to_chat(user,"You suddenly feel very solid!") - user.Stun(40, ignore_canstun = TRUE) - user.petrify(60) - -/obj/item/book/granter/spell/knock - spell = /obj/effect/proc_holder/spell/aoe_turf/knock - spellname = "knock" - icon_state ="bookknock" - desc = "This book is hard to hold closed properly." - remarks = list("Open Sesame!", "So THAT'S the magic password!", "Slow down, book. I still haven't finished this page...", "The book won't stop moving!", "I think this is hurting the spine of the book...", "I can't get to the next page, it's stuck t- I'm good, it just turned to the next page on it's own.", "Yeah, staff of doors does the same thing. Go figure...") - -/obj/item/book/granter/spell/knock/recoil(mob/living/user) - ..() - to_chat(user,"You're knocked down!") - user.Paralyze(40) - -/obj/item/book/granter/spell/barnyard - spell = /obj/effect/proc_holder/spell/targeted/barnyardcurse - spellname = "barnyard" - icon_state ="bookhorses" - desc = "This book is more horse than your mind has room for." - remarks = list("Moooooooo!","Moo!","Moooo!", "NEEIIGGGHHHH!", "NEEEIIIIGHH!", "NEIIIGGHH!", "HAAWWWWW!", "HAAAWWW!", "Oink!", "Squeeeeeeee!", "Oink Oink!", "Ree!!", "Reee!!", "REEE!!", "REEEEE!!") - -/obj/item/book/granter/spell/barnyard/recoil(mob/living/carbon/user) - if(ishuman(user)) - to_chat(user,"HORSIE HAS RISEN") - var/obj/item/clothing/magichead = new /obj/item/clothing/mask/horsehead/cursed(user.drop_location()) - if(!user.dropItemToGround(user.wear_mask)) - qdel(user.wear_mask) - user.equip_to_slot_if_possible(magichead, ITEM_SLOT_MASK, TRUE, TRUE) - qdel(src) - else - to_chat(user,"I say thee neigh") //It still lives here - -/obj/item/book/granter/spell/charge - spell = /obj/effect/proc_holder/spell/targeted/charge - spellname = "charge" - icon_state ="bookcharge" - desc = "This book is made of 100% postconsumer wizard." - remarks = list("I feel ALIVE!", "I CAN TASTE THE MANA!", "What a RUSH!", "I'm FLYING through these pages!", "THIS GENIUS IS MAKING IT!", "This book is ACTION PAcKED!", "HE'S DONE IT", "LETS GOOOOOOOOOOOO") - -/obj/item/book/granter/spell/charge/recoil(mob/user) - ..() - to_chat(user,"[src] suddenly feels very warm!") - empulse(src, 1, 1, magic=TRUE) - -/obj/item/book/granter/spell/summonitem - spell = /obj/effect/proc_holder/spell/targeted/summonitem - spellname = "instant summons" - icon_state ="booksummons" - desc = "This book is bright and garish, very hard to miss." - remarks = list("I can't look away from the book!", "The words seem to pop around the page...", "I just need to focus on one item...", "Make sure to have a good grip on it when casting...", "Slow down, book. I still haven't finished this page...", "Sounds pretty great with some other magical artifacts...", "Magicians must love this one.") - -/obj/item/book/granter/spell/summonitem/recoil(mob/user) - ..() - to_chat(user,"[src] suddenly vanishes!") - qdel(src) - -/obj/item/book/granter/spell/random - icon_state = "random_book" - -/obj/item/book/granter/spell/random/Initialize(mapload) - . = ..() - var/static/banned_spells = list(/obj/item/book/granter/spell/mimery_blockade, /obj/item/book/granter/spell/mimery_guns) - var/real_type = pick(subtypesof(/obj/item/book/granter/spell) - banned_spells) - new real_type(loc) - return INITIALIZE_HINT_QDEL - -///MARTIAL ARTS/// - -/obj/item/book/granter/martial - var/martial - var/martialname = "bug jitsu" - var/greet = "You feel like you have mastered the art in breaking code. Nice work, jackass." - - -/obj/item/book/granter/martial/already_known(mob/user) - if(!martial) - return TRUE - var/datum/martial_art/MA = martial - if(user.mind.has_martialart(initial(MA.id))) - to_chat(user,"You already know [martialname]!") - return TRUE - return FALSE - -/obj/item/book/granter/martial/on_reading_start(mob/user) - to_chat(user, "You start reading about [martialname]...") - -/obj/item/book/granter/martial/on_reading_finished(mob/user) - to_chat(user, "[greet]") - var/datum/martial_art/MA = new martial - MA.teach(user) - user.log_message("learned the martial art [martialname] ([MA])", LOG_ATTACK, color="orange") - onlearned(user) - -/obj/item/book/granter/martial/cqc - martial = /datum/martial_art/cqc - name = "old manual" - martialname = "close quarters combat" - desc = "A small, black manual. There are drawn instructions of tactical hand-to-hand combat." - greet = "You've mastered the basics of CQC." - icon_state = "cqcmanual" - remarks = list("Kick... Slam...", "Lock... Kick...", "Strike their abdomen, neck and back for critical damage...", "Slam... Lock...", "I could probably combine this with some other martial arts!", "Words that kill...", "The last and final moment is yours...") - -/obj/item/book/granter/martial/cqc/onlearned(mob/living/carbon/user) - ..() - if(oneuse == TRUE) - to_chat(user, "[src] beeps ominously...") - -/obj/item/book/granter/martial/cqc/recoil(mob/living/carbon/user) - to_chat(user, "[src] explodes!") - playsound(src,'sound/effects/explosion1.ogg',40,1) - user.flash_act(1, 1) - user.adjustBruteLoss(6) - user.adjustFireLoss(6) - qdel(src) - -/obj/item/book/granter/martial/carp - martial = /datum/martial_art/the_sleeping_carp - name = "mysterious scroll" - martialname = "sleeping carp" - desc = "A scroll filled with strange markings. It seems to be drawings of some sort of martial art." - greet = "You have learned the ancient martial art of the Sleeping Carp! Your hand-to-hand combat has become much more effective, and you are now able to deflect any projectiles \ - directed toward you. However, you are also unable to use any ranged weaponry. You can learn more about your newfound art by using the Recall Teachings verb in the Sleeping Carp tab." - icon = 'icons/obj/wizard.dmi' - icon_state = "scroll2" - remarks = list("I must prove myself worthy to the masters of the sleeping carp...", "Stance means everything...", "Focus... And you'll be able to incapacitate any foe in seconds...", "I must pierce armor for maximum damage...", "I don't think this would combine with other martial arts...", "Grab them first so they don't retaliate...", "I must prove myself worthy of this power...") - -/obj/item/book/granter/martial/carp/onlearned(mob/living/carbon/user) - ..() - if(oneuse == TRUE) - desc = "It's completely blank." - name = "empty scroll" - icon_state = "blankscroll" - -/obj/item/book/granter/martial/tribal_claw - martial = /datum/martial_art/tribal_claw - name = "old scroll" - martialname = "tribal claw" - desc = "A scroll filled with ancient draconic markings." - greet = "You have learned the ancient martial art of the Tribal Claw! You are now able to use your tail and claws in a fight much better than before. \ - Check the combos you are now able to perform using the Recall Teachings verb in the Tribal Claw tab" - icon = 'icons/obj/wizard.dmi' - icon_state = "scroll2" - remarks = list("I must prove myself worthy to the masters of the Knoises clan...", "Use your tail to surprise any enemy...", "Your sharp claws can disorient them...", "I don't think this would combine with other martial arts...", "Ooga Booga...") - -/obj/item/book/granter/martial/tribal_claw/onlearned(mob/living/carbon/user) - ..() - if(!oneuse) - return - desc = "It's completely blank." - name = "empty scroll" - icon_state = "blankscroll" - -/obj/item/book/granter/martial/tribal_claw/already_known(mob/user) - if(islizard(user)) - return FALSE - else - to_chat(user, "You try to read the scroll but can't comprehend any of it.") - return TRUE - -/obj/item/book/granter/martial/plasma_fist - martial = /datum/martial_art/plasma_fist - name = "frayed scroll" - martialname = "plasma fist" - desc = "An aged and frayed scrap of paper written in shifting runes. There are hand-drawn illustrations of pugilism." - greet = "You have learned the ancient martial art of Plasma Fist. Your combos are extremely hard to pull off, but include some of the most deadly moves ever seen including \ - the plasma fist, which when pulled off will make someone violently explode." - icon = 'icons/obj/wizard.dmi' - icon_state ="scroll2" - remarks = list("Balance...", "Power...", "Control...", "Mastery...", "Vigilance...", "Skill...") - -/obj/item/book/granter/martial/plasma_fist/onlearned(mob/living/carbon/user) - ..() - if(oneuse == TRUE) - desc = "It's completely blank." - name = "empty scroll" - icon_state = "blankscroll" - -/obj/item/book/granter/martial/karate - martial = /datum/martial_art/karate - name = "dusty scroll" - martialname = "karate" - desc = "A dusty scroll filled with martial lessons. There seems to be drawings of some sort of martial art." - greet = "You have learned the ancient martial art of Karate! Your hand-to-hand combat has become more effective but require skill to combo effectively.\ - You can learn more about your newfound art by using the Recall Teachings verb in the Karate tab." - icon = 'icons/obj/wizard.dmi' - icon_state = "scroll2" - remarks = list("I must prove myself worthy to the masters of Karate...", "Disable their legs so they can't escape...", "Strike at pressure points to daze my foes...", "Stomp their head for maximum damage...", "I don't think this would combine with other martial arts...", "Wind them with a flying knee...", "I must practice to fully grasp these teachings...") - -/obj/item/book/granter/martial/karate/onlearned(mob/living/carbon/user) - ..() - if(oneuse == TRUE) - desc = "It's completely blank." - name = "empty scroll" - icon_state = "blankscroll" - -// I did not include mushpunch's grant, it is not a book and the item does it just fine. - -//Crafting Recipe books - -/obj/item/book/granter/crafting_recipe - var/list/crafting_recipe_types = list() - -/obj/item/book/granter/crafting_recipe/on_reading_finished(mob/user) - . = ..() - if(!user.mind) - return - for(var/crafting_recipe_type in crafting_recipe_types) - var/datum/crafting_recipe/R = crafting_recipe_type - user.mind.teach_crafting_recipe(crafting_recipe_type) - to_chat(user,"You learned how to make [initial(R.name)].") - -/obj/item/book/granter/crafting_recipe/cooking_sweets_101 - name = "Cooking Desserts 101" - desc = "A cook book that teaches you some more of the newest desserts. AI approved, and a best seller on Honkplanet." - crafting_recipe_types = list( - /datum/crafting_recipe/food/mimetart, - /datum/crafting_recipe/food/berrytart, - /datum/crafting_recipe/food/cocoalavatart, - /datum/crafting_recipe/food/clowncake, - /datum/crafting_recipe/food/vanillacake - ) - icon_state = "cooking_learing_sweets" - oneuse = FALSE - remarks = list("So that is how icing is made!", "Placing fruit on top? How simple...", "Huh layering cake seems harder then this...", "This book smells like candy", "A clown must have made this page, or they forgot to spell check it before printing...", "Wait, a way to cook slime to be safe?") diff --git a/code/game/objects/items/granters/_granters.dm b/code/game/objects/items/granters/_granters.dm new file mode 100644 index 0000000000000..fed49582d4f66 --- /dev/null +++ b/code/game/objects/items/granters/_granters.dm @@ -0,0 +1,106 @@ +/** + * Books that teach things. + * + * (Intrinsic actions like bar flinging, spells like fireball or smoke, or martial arts) + */ +/obj/item/book/granter + due_date = 0 + unique = 1 + /// Flavor messages displayed to mobs reading the granter + var/list/remarks = list() + /// Controls how long a mob must keep the book in his hand to actually successfully learn + var/pages_to_mastery = 3 + /// Sanity, whether it's currently being read + var/reading = FALSE + /// The amount of uses on the granter. + var/uses = 1 + /// The sounds played as the user's reading the book. + var/list/book_sounds = list( + 'sound/effects/pageturn1.ogg', + 'sound/effects/pageturn2.ogg', + 'sound/effects/pageturn3.ogg', + ) + +/obj/item/book/granter/attack_self(mob/living/user) + if(reading) + to_chat(user, span_warning("You're already reading this!")) + return FALSE + if(user.is_blind()) + to_chat(user, span_warning("You are blind and can't read anything!")) + return FALSE + if(!isliving(user) || !user.can_read(src)) + return FALSE + if(!can_learn(user)) + return FALSE + + if(uses <= 0) + recoil(user) + return FALSE + + on_reading_start(user) + reading = TRUE + for(var/i in 1 to pages_to_mastery) + if(!turn_page(user)) + on_reading_stopped() + reading = FALSE + return + if(do_after(user, 5 SECONDS, src)) + uses-- + on_reading_finished(user) + reading = FALSE + + return TRUE + +/// Called when the user starts to read the granter. +/obj/item/book/granter/proc/on_reading_start(mob/living/user) + to_chat(user, span_notice("You start reading [name]...")) + +/// Called when the reading is interrupted without finishing. +/obj/item/book/granter/proc/on_reading_stopped(mob/living/user) + to_chat(user, span_notice("You stop reading...")) + +/// Called when the reading is completely finished. This is where the actual granting should happen. +/obj/item/book/granter/proc/on_reading_finished(mob/living/user) + to_chat(user, span_notice("You finish reading [name]!")) + +/// The actual "turning over of the page" flavor bit that happens while someone is reading the granter. +/obj/item/book/granter/proc/turn_page(mob/living/user) + playsound(user, pick(book_sounds), 30, TRUE) + + if(!do_after(user, 5 SECONDS, src)) + return FALSE + + to_chat(user, span_notice("[length(remarks) ? pick(remarks) : "You keep reading..."]")) + return TRUE + +/// Effects that occur whenever the book is read when it has no uses left. +/obj/item/book/granter/proc/recoil(mob/living/user) + +/// Checks if the user can learn whatever this granter... grants +/obj/item/book/granter/proc/can_learn(mob/living/user) + return TRUE + +// Generic action giver +/obj/item/book/granter/action + /// The typepath of action that is given + var/datum/action/granted_action + /// The name of the action, formatted in a more text-friendly way. + var/action_name = "" + +/obj/item/book/granter/action/can_learn(mob/living/user) + if(!granted_action) + CRASH("Someone attempted to learn [type], which did not have an action set.") + if(locate(granted_action) in user.actions) + to_chat(user, span_warning("You already know all about [action_name]!")) + return FALSE + return TRUE + +/obj/item/book/granter/action/on_reading_start(mob/living/user) + to_chat(user, span_notice("You start reading about [action_name]...")) + +/obj/item/book/granter/action/on_reading_finished(mob/living/user) + to_chat(user, span_notice("You feel like you've got a good handle on [action_name]!")) + // Action goes on the mind as the user actually learns the thing in your brain + var/datum/action/new_action = new granted_action(user.mind || user) + new_action.Grant(user) + new_action.UpdateButtons() diff --git a/code/game/objects/items/granters/crafting/_crafting_granter.dm b/code/game/objects/items/granters/crafting/_crafting_granter.dm new file mode 100644 index 0000000000000..a4d2b46877a62 --- /dev/null +++ b/code/game/objects/items/granters/crafting/_crafting_granter.dm @@ -0,0 +1,11 @@ +/obj/item/book/granter/crafting_recipe + /// A list of all recipe types we grant on learn + var/list/crafting_recipe_types = list() + +/obj/item/book/granter/crafting_recipe/on_reading_finished(mob/user) + . = ..() + if(!user.mind) + return + for(var/datum/crafting_recipe/crafting_recipe_type as anything in crafting_recipe_types) + user.mind.teach_crafting_recipe(crafting_recipe_type) + to_chat(user, span_notice("You learned how to make [initial(crafting_recipe_type.name)].")) diff --git a/code/game/objects/items/granters/crafting/bone_notes.dm b/code/game/objects/items/granters/crafting/bone_notes.dm new file mode 100644 index 0000000000000..120e47a64d386 --- /dev/null +++ b/code/game/objects/items/granters/crafting/bone_notes.dm @@ -0,0 +1,20 @@ +/obj/item/book/granter/crafting_recipe/boneyard_notes + name = "The Complete Works of Lavaland Bone Architecture" + desc = "Pried from the lead Archaeologist's cold, dead hands, this seems to explain how ancient bone architecture was erected long ago." + crafting_recipe_types = list( + /datum/crafting_recipe/rib, + /datum/crafting_recipe/boneshovel, + /datum/crafting_recipe/halfskull, + /datum/crafting_recipe/skull, + ) + icon = 'icons/obj/library.dmi' + icon_state = "boneworking_learing" + uses = INFINITY + remarks = list( + "Who knew you could bend bones that far back?", + "I guess that was much easier before the planet heated up...", + "So that's how they made those ruins survive the ashstorms. Neat!", + "The page is just filled with insane ramblings about some 'legion' thing.", + "But why would they need vinegar to polish the bones? And rags too?", + "You spend a few moments cleaning dirt and blood off of the page, yeesh.", + ) diff --git a/code/game/objects/items/granters/crafting/cannon.dm b/code/game/objects/items/granters/crafting/cannon.dm new file mode 100644 index 0000000000000..7bf276642b63b --- /dev/null +++ b/code/game/objects/items/granters/crafting/cannon.dm @@ -0,0 +1,19 @@ +/obj/item/book/granter/crafting_recipe/trash_cannon + name = "diary of a demoted engineer" + desc = "A lost journal. The engineer seems very deranged about their demotion." + crafting_recipe_types = list( + /datum/crafting_recipe/trash_cannon, + /datum/crafting_recipe/trashball, + ) + icon_state = "book1" + remarks = list( + "\"I'll show them! I'll build a CANNON!\"", + "\"Gunpowder is ideal, but i'll have to improvise...\"", + "\"I savor the look on the CE's face when I BLOW down the walls to engineering!\"", + "\"If the supermatter gets loose from my rampage, so be it!\"", + "\"I'VE GONE COMPLETELY MENTAL!\"", + ) + +/obj/item/book/granter/crafting_recipe/trash_cannon/recoil(mob/living/user) + to_chat(user, span_warning("The book turns to dust in your hands.")) + qdel(src) diff --git a/code/game/objects/items/granters/crafting/desserts.dm b/code/game/objects/items/granters/crafting/desserts.dm new file mode 100644 index 0000000000000..1943375f28bd4 --- /dev/null +++ b/code/game/objects/items/granters/crafting/desserts.dm @@ -0,0 +1,20 @@ +/obj/item/book/granter/crafting_recipe/cooking_sweets_101 + name = "Cooking Desserts 101" + desc = "A cook book that teaches you some more of the newest desserts. AI approved, and a best seller on Honkplanet." + crafting_recipe_types = list( + /datum/crafting_recipe/food/mimetart, + /datum/crafting_recipe/food/berrytart, + /datum/crafting_recipe/food/cocolavatart, + /datum/crafting_recipe/food/clowncake, + /datum/crafting_recipe/food/vanillacake + ) + icon_state = "cooking_learing_sweets" + uses = INFINITY + remarks = list( + "So that is how icing is made!", + "Placing fruit on top? How simple...", + "Huh layering cake seems harder then this...", + "This book smells like candy", + "A clown must have made this page, or they forgot to spell check it before printing...", + "Wait, a way to cook slime to be safe?", + ) diff --git a/code/game/objects/items/granters/crafting/pipegun.dm b/code/game/objects/items/granters/crafting/pipegun.dm new file mode 100644 index 0000000000000..73e171846211b --- /dev/null +++ b/code/game/objects/items/granters/crafting/pipegun.dm @@ -0,0 +1,19 @@ +/obj/item/book/granter/crafting_recipe/pipegun_prime + name = "diary of a dead assistant" + desc = "A battered journal. Looks like he had a pretty rough life." + crafting_recipe_types = list( + /datum/crafting_recipe/pipegun_prime + ) + icon_state = "book1" + remarks = list( + "He apparently mastered some lost guncrafting technique.", + "Why do I have to go through so many hoops to get this shitty gun?", + "That much Grey Bull cannot be healthy...", + "Did he drop this into a moisture trap? Yuck.", + "Toolboxing techniques, huh? I kinda just want to know how to make the gun.", + "What the hell does he mean by 'ancient warrior tradition'?", + ) + +/obj/item/book/granter/crafting_recipe/pipegun_prime/recoil(mob/living/user) + to_chat(user, span_warning("The book turns to dust in your hands.")) + qdel(src) diff --git a/code/game/objects/items/granters/magic/_spell_granter.dm b/code/game/objects/items/granters/magic/_spell_granter.dm new file mode 100644 index 0000000000000..4f695e4d3af18 --- /dev/null +++ b/code/game/objects/items/granters/magic/_spell_granter.dm @@ -0,0 +1,93 @@ +/obj/item/book/granter/action/spell + +/obj/item/book/granter/action/spell/Initialize(mapload) + . = ..() + RegisterSignal(src, COMSIG_ITEM_MAGICALLY_CHARGED, .proc/on_magic_charge) + +/** + * Signal proc for [COMSIG_ITEM_MAGICALLY_CHARGED] + * + * Refreshes uses on our spell granter, or make it quicker to read if it's already infinite use + */ +/obj/item/book/granter/action/spell/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/spell, mob/living/caster) + SIGNAL_HANDLER + + // What're the odds someone uses 2000 uses of an infinite use book? + if(uses >= INFINITY - 2000) + to_chat(caster, span_notice("This book is infinite use and can't be recharged, \ + yet the magic has improved it somehow...")) + pages_to_mastery = max(pages_to_mastery - 1, 1) + return COMPONENT_ITEM_CHARGED|COMPONENT_ITEM_BURNT_OUT + + if(prob(80)) + caster.dropItemToGround(src, TRUE) + visible_message(span_warning("[src] catches fire and burns to ash!")) + new /obj/effect/decal/cleanable/ash(drop_location()) + qdel(src) + return COMPONENT_ITEM_BURNT_OUT + + uses++ + return COMPONENT_ITEM_CHARGED + +/obj/item/book/granter/action/spell/can_learn(mob/living/user) + if(!granted_action) + CRASH("Someone attempted to learn [type], which did not have an spell set.") + if(locate(granted_action) in user.actions) + if(IS_WIZARD(user)) + to_chat(user, span_warning("You're already far more versed in the spell [action_name] \ + than this flimsy how-to book can provide!")) + else + to_chat(user, span_warning("You've already know the spell [action_name]!")) + return FALSE + return TRUE + +/obj/item/book/granter/action/spell/on_reading_start(mob/living/user) + to_chat(user, span_notice("You start reading about casting [action_name]...")) + +/obj/item/book/granter/action/spell/on_reading_finished(mob/living/user) + to_chat(user, span_notice("You feel like you've experienced enough to cast [action_name]!")) + var/datum/action/cooldown/spell/new_spell = new granted_action(user.mind || user) + new_spell.Grant(user) + user.log_message("learned the spell [action_name] ([new_spell])", LOG_ATTACK, color = "orange") + if(uses <= 0) + user.visible_message(span_warning("[src] glows dark for a second!")) + +/obj/item/book/granter/action/spell/recoil(mob/living/user) + user.visible_message(span_warning("[src] glows in a black light!")) + +/// Simple granter that's replaced with a random spell granter on Initialize. +/obj/item/book/granter/action/spell/random + icon_state = "random_book" + +/obj/item/book/granter/action/spell/random/Initialize(mapload) + . = ..() + var/static/list/banned_spells = list( + /obj/item/book/granter/action/spell/true_random, + ) + typesof(/obj/item/book/granter/action/spell/mime) + + var/real_type = pick(subtypesof(/obj/item/book/granter/action/spell) - banned_spells) + new real_type(loc) + + return INITIALIZE_HINT_QDEL + +/// A more volatile granter that can potentially have any spell within. Use wisely. +/obj/item/book/granter/action/spell/true_random + icon_state = "random_book" + desc = "You feel as if anything could be gained from this book." + /// A list of schools we probably shouldn't grab, for various reasons + var/static/list/blacklisted_schools = list(SCHOOL_UNSET, SCHOOL_HOLY, SCHOOL_MIME) + +/obj/item/book/granter/action/spell/true_random/Initialize(mapload) + . = ..() + + var/static/list/spell_options + if(!spell_options) + spell_options = subtypesof(/datum/action/cooldown/spell) + for(var/datum/action/cooldown/spell/spell as anything in spell_options) + if(initial(spell.school) in blacklisted_schools) + spell_options -= spell + if(initial(spell.name) == "Spell") // Abstract types + spell_options -= spell + + granted_action = pick(spell_options) + action_name = lowertext(initial(granted_action.name)) diff --git a/code/game/objects/items/granters/magic/barnyard.dm b/code/game/objects/items/granters/magic/barnyard.dm new file mode 100644 index 0000000000000..1f512f96d2f91 --- /dev/null +++ b/code/game/objects/items/granters/magic/barnyard.dm @@ -0,0 +1,34 @@ +/obj/item/book/granter/action/spell/barnyard + granted_action = /datum/action/cooldown/spell/pointed/barnyardcurse + action_name = "barnyard" + icon_state ="bookhorses" + desc = "This book is more horse than your mind has room for." + remarks = list( + "Moooooooo!", + "Moo!", + "Moooo!", + "NEEIIGGGHHHH!", + "NEEEIIIIGHH!", + "NEIIIGGHH!", + "HAAWWWWW!", + "HAAAWWW!", + "Oink!", + "Squeeeeeeee!", + "Oink Oink!", + "Ree!!", + "Reee!!", + "REEE!!", + "REEEEE!!", + ) + +/obj/item/book/granter/action/spell/barnyard/recoil(mob/living/user) + if(ishuman(user)) + to_chat(user, "HORSIE HAS RISEN") + var/obj/item/clothing/magic_mask = new /obj/item/clothing/mask/animal/horsehead/cursed(user.drop_location()) + var/mob/living/carbon/human/human_user = user + if(!user.dropItemToGround(human_user.wear_mask)) + qdel(human_user.wear_mask) + user.equip_to_slot_if_possible(magic_mask, ITEM_SLOT_MASK, TRUE, TRUE) + qdel(src) + else + to_chat(user,span_notice("I say thee neigh")) //It still lives here diff --git a/code/game/objects/items/granters/magic/blind.dm b/code/game/objects/items/granters/magic/blind.dm new file mode 100644 index 0000000000000..2107af802c726 --- /dev/null +++ b/code/game/objects/items/granters/magic/blind.dm @@ -0,0 +1,19 @@ +/obj/item/book/granter/action/spell/blind + granted_action = /datum/action/cooldown/spell/pointed/blind + action_name = "blind" + icon_state = "bookblind" + desc = "This book looks blurry, no matter how you look at it." + remarks = list( + "Well I can't learn anything if I can't read the damn thing!", + "Why would you use a dark font on a dark background...", + "Ah, I can't see an Oh, I'm fine...", + "I can't see my hand...!", + "I'm manually blinking, damn you book...", + "I can't read this page, but somehow I feel like I learned something from it...", + "Hey, who turned off the lights?", + ) + +/obj/item/book/granter/action/spell/blind/recoil(mob/living/user) + . = ..() + to_chat(user, span_warning("You go blind!")) + user.blind_eyes(10) diff --git a/code/game/objects/items/granters/magic/charge.dm b/code/game/objects/items/granters/magic/charge.dm new file mode 100644 index 0000000000000..988d17aa13ba3 --- /dev/null +++ b/code/game/objects/items/granters/magic/charge.dm @@ -0,0 +1,20 @@ +/obj/item/book/granter/action/spell/charge + granted_action = /datum/action/cooldown/spell/charge + action_name = "charge" + icon_state ="bookcharge" + desc = "This book is made of 100% postconsumer wizard." + remarks = list( + "I feel ALIVE!", + "I CAN TASTE THE MANA!", + "What a RUSH!", + "I'm FLYING through these pages!", + "THIS GENIUS IS MAKING IT!", + "This book is ACTION PAcKED!", + "HE'S DONE IT", + "LETS GOOOOOOOOOOOO", + ) + +/obj/item/book/granter/action/spell/charge/recoil(mob/living/user) + . = ..() + to_chat(user,span_warning("[src] suddenly feels very warm!")) + empulse(src, 1, 1) diff --git a/code/game/objects/items/granters/magic/fireball.dm b/code/game/objects/items/granters/magic/fireball.dm new file mode 100644 index 0000000000000..b8b97e6502f6e --- /dev/null +++ b/code/game/objects/items/granters/magic/fireball.dm @@ -0,0 +1,27 @@ +/obj/item/book/granter/action/spell/fireball + granted_action = /datum/action/cooldown/spell/pointed/projectile/fireball + action_name = "fireball" + icon_state ="bookfireball" + desc = "This book feels warm to the touch." + remarks = list( + "Aim...AIM, FOOL!", + "Just catching them on fire won't do...", + "Accounting for crosswinds... really?", + "I think I just burned my hand...", + "Why the dumb stance? It's just a flick of the hand...", + "OMEE... ONI... Ugh...", + "What's the difference between a fireball and a pyroblast...", + ) + +/obj/item/book/granter/action/spell/fireball/recoil(mob/living/user) + . = ..() + explosion( + user, + devastation_range = 1, + light_impact_range = 2, + flame_range = 2, + flash_range = 3, + adminlog = FALSE, + explosion_cause = src, + ) + qdel(src) diff --git a/code/game/objects/items/granters/magic/forcewall.dm b/code/game/objects/items/granters/magic/forcewall.dm new file mode 100644 index 0000000000000..7df82dccd2431 --- /dev/null +++ b/code/game/objects/items/granters/magic/forcewall.dm @@ -0,0 +1,20 @@ +/obj/item/book/granter/action/spell/forcewall + granted_action = /datum/action/cooldown/spell/forcewall + action_name = "forcewall" + icon_state ="bookforcewall" + desc = "This book has a dedication to mimes everywhere inside the front cover." + remarks = list( + "I can go through the wall! Neat.", + "Why are there so many mime references...?", + "This would cause much grief in a hallway...", + "This is some surprisingly strong magic to create a wall nobody can pass through...", + "Why the dumb stance? It's just a flick of the hand...", + "Why are the pages so hard to turn, is this even paper?", + "I can't mo Oh, i'm fine...", + ) + +/obj/item/book/granter/action/spell/forcewall/recoil(mob/living/user) + . = ..() + to_chat(user, span_warning("You suddenly feel very solid!")) + user.Stun(4 SECONDS, ignore_canstun = TRUE) + user.petrify(6 SECONDS) diff --git a/code/game/objects/items/granters/magic/knock.dm b/code/game/objects/items/granters/magic/knock.dm new file mode 100644 index 0000000000000..11bdfeeadbfa2 --- /dev/null +++ b/code/game/objects/items/granters/magic/knock.dm @@ -0,0 +1,19 @@ +/obj/item/book/granter/action/spell/knock + granted_action = /datum/action/cooldown/spell/aoe/knock + action_name = "knock" + icon_state ="bookknock" + desc = "This book is hard to hold closed properly." + remarks = list( + "Open Sesame!", + "So THAT'S the magic password!", + "Slow down, book. I still haven't finished this page...", + "The book won't stop moving!", + "I think this is hurting the spine of the book...", + "I can't get to the next page, it's stuck t- I'm good, it just turned to the next page on it's own.", + "Yeah, staff of doors does the same thing. Go figure...", + ) + +/obj/item/book/granter/action/spell/knock/recoil(mob/living/user) + . = ..() + to_chat(user, span_warning("You're knocked down!")) + user.Paralyze(4 SECONDS) diff --git a/code/game/objects/items/granters/magic/mime.dm b/code/game/objects/items/granters/magic/mime.dm new file mode 100644 index 0000000000000..6e6bc03dc9854 --- /dev/null +++ b/code/game/objects/items/granters/magic/mime.dm @@ -0,0 +1,28 @@ +/obj/item/book/granter/action/spell/mime + name = "Guide to Mimery Vol 0" + desc = "The missing entry into the legendary saga. Unfortunately it doesn't teach you anything." + icon_state ="bookmime" + remarks = list("...") + +/obj/item/book/granter/action/spell/mime/attack_self(mob/user) + . = ..() + if(!.) + return + + // Gives the user a vow ability if they don't have one + var/datum/action/cooldown/spell/vow_of_silence/vow = locate() in user.actions + if(!vow && user.mind) + vow = new(user.mind) + vow.Grant(user) + +/obj/item/book/granter/action/spell/mime/mimery_blockade + granted_action = /datum/action/cooldown/spell/forcewall/mime + action_name = "Invisible Blockade" + name = "Guide to Advanced Mimery Vol 1" + desc = "The pages don't make any sound when turned." + +/obj/item/book/granter/action/spell/mime/mimery_guns + granted_action = /datum/action/cooldown/spell/pointed/projectile/finger_guns + action_name = "Finger Guns" + name = "Guide to Advanced Mimery Vol 2" + desc = "There aren't any words written..." diff --git a/code/game/objects/items/granters/magic/mindswap.dm b/code/game/objects/items/granters/magic/mindswap.dm new file mode 100644 index 0000000000000..6396b62136aa8 --- /dev/null +++ b/code/game/objects/items/granters/magic/mindswap.dm @@ -0,0 +1,57 @@ +/obj/item/book/granter/action/spell/mindswap + granted_action = /datum/action/cooldown/spell/pointed/mind_transfer + action_name = "mindswap" + icon_state ="bookmindswap" + desc = "This book's cover is pristine, though its pages look ragged and torn." + remarks = list( + "If you mindswap from a mouse, they will be helpless when you recover...", + "Wait, where am I...?", + "This book is giving me a horrible headache...", + "This page is blank, but I feel words popping into my head...", + "GYNU... GYRO... Ugh...", + "The voices in my head need to stop, I'm trying to read here...", + "I don't think anyone will be happy when I cast this spell...", + ) + /// Mob used in book recoils to store an identity for mindswaps + var/datum/weakref/stored_swap_ref + +/obj/item/book/granter/action/spell/mindswap/on_reading_finished() + . = ..() + visible_message(span_notice("[src] begins to shake and shift.")) + action_name = pick( + "fireball", + "smoke", + "blind", + "forcewall", + "knock", + "barnyard", + "charge", + ) + icon_state = "book[action_name]" + name = "spellbook of [action_name]" + +/obj/item/book/granter/action/spell/mindswap/recoil(mob/living/user) + . = ..() + var/mob/living/real_stored_swap = stored_swap_ref?.resolve() + if(QDELETED(real_stored_swap)) + stored_swap_ref = WEAKREF(user) + to_chat(user, span_warning("For a moment you feel like you don't even know who you are anymore.")) + return + if(real_stored_swap.stat == DEAD) + stored_swap_ref = null + return + if(real_stored_swap == user) + to_chat(user, span_notice("You stare at the book some more, but there doesn't seem to be anything else to learn...")) + return + + var/datum/action/cooldown/spell/pointed/mind_transfer/swapper = new(src) + + if(swapper.swap_minds(user, real_stored_swap)) + to_chat(user, span_warning("You're suddenly somewhere else... and someone else?!")) + to_chat(real_stored_swap, span_warning("Suddenly you're staring at [src] again... where are you, who are you?!")) + + else + // if the mind_transfer failed to transfer mobs (likely due to the target being catatonic). + user.visible_message(span_warning("[src] fizzles slightly as it stops glowing!")) + + stored_swap_ref = null diff --git a/code/game/objects/items/granters/magic/sacredflame.dm b/code/game/objects/items/granters/magic/sacredflame.dm new file mode 100644 index 0000000000000..1e044e8e03941 --- /dev/null +++ b/code/game/objects/items/granters/magic/sacredflame.dm @@ -0,0 +1,14 @@ +/obj/item/book/granter/action/spell/sacredflame + granted_action = /datum/action/cooldown/spell/aoe/sacred_flame + action_name = "sacred flame" + icon_state ="booksacredflame" + desc = "Become one with the flames that burn within... and invite others to do so as well." + remarks = list( + "Well, it's one way to stop an attacker...", + "I'm gonna need some good gear to stop myself from burning to death...", + "Keep a fire extinguisher handy, got it...", + "I think I just burned my hand...", + "Apply flame directly to chest for proper ignition...", + "No pain, no gain...", + "One with the flame...", + ) diff --git a/code/game/objects/items/granters/magic/smoke.dm b/code/game/objects/items/granters/magic/smoke.dm new file mode 100644 index 0000000000000..a83811f1e1947 --- /dev/null +++ b/code/game/objects/items/granters/magic/smoke.dm @@ -0,0 +1,26 @@ +/obj/item/book/granter/action/spell/smoke + granted_action = /datum/action/cooldown/spell/smoke + action_name = "smoke" + icon_state ="booksmoke" + desc = "This book is overflowing with the dank arts." + remarks = list( + "Smoke Bomb! Heh...", + "Smoke bomb would do just fine too...", + "Wait, there's a machine that does the same thing in chemistry?", + "This book smells awful...", + "Why all these weed jokes? Just tell me how to cast it...", + "Wind will ruin the whole spell, good thing we're in space... Right?", + "So this is how the spider clan does it...", + ) + +/obj/item/book/granter/action/spell/smoke/recoil(mob/living/user) + . = ..() + to_chat(user,span_warning("Your stomach rumbles...")) + if(user.nutrition) + user.set_nutrition(200) + if(user.nutrition <= 0) + user.set_nutrition(0) + +// Chaplain's smoke book +/obj/item/book/granter/action/spell/smoke/lesser + granted_action = /datum/action/cooldown/spell/smoke/lesser diff --git a/code/game/objects/items/granters/magic/summon_item.dm b/code/game/objects/items/granters/magic/summon_item.dm new file mode 100644 index 0000000000000..58fbdaf24d080 --- /dev/null +++ b/code/game/objects/items/granters/magic/summon_item.dm @@ -0,0 +1,19 @@ +/obj/item/book/granter/action/spell/summonitem + granted_action = /datum/action/cooldown/spell/summonitem + action_name = "instant summons" + icon_state ="booksummons" + desc = "This book is bright and garish, very hard to miss." + remarks = list( + "I can't look away from the book!", + "The words seem to pop around the page...", + "I just need to focus on one item...", + "Make sure to have a good grip on it when casting...", + "Slow down, book. I still haven't finished this page...", + "Sounds pretty great with some other magical artifacts...", + "Magicians must love this one.", + ) + +/obj/item/book/granter/action/spell/summonitem/recoil(mob/living/user) + . = ..() + to_chat(user,span_warning("[src] suddenly vanishes!")) + qdel(src) diff --git a/code/game/objects/items/granters/martial_arts/_martial_arts.dm b/code/game/objects/items/granters/martial_arts/_martial_arts.dm new file mode 100644 index 0000000000000..08f615a991e22 --- /dev/null +++ b/code/game/objects/items/granters/martial_arts/_martial_arts.dm @@ -0,0 +1,24 @@ +/obj/item/book/granter/martial + /// The martial arts type we give + var/datum/martial_art/martial + /// The name of the martial arts, formatted in a more text-friendly way. + var/martial_name = "" + /// The text given to the user when they learn the martial arts + var/greet = "" + +/obj/item/book/granter/martial/can_learn(mob/user) + if(!martial) + CRASH("Someone attempted to learn [type], which did not have a martial arts set.") + if(user.mind.has_martialart(initial(martial.id))) + to_chat(user, span_warning("You already know [martial_name]!")) + return FALSE + return TRUE + +/obj/item/book/granter/martial/on_reading_start(mob/user) + to_chat(user, span_notice("You start reading about [martial_name]...")) + +/obj/item/book/granter/martial/on_reading_finished(mob/user) + to_chat(user, "[greet]") + var/datum/martial_art/martial_to_learn = new martial() + martial_to_learn.teach(user) + user.log_message("learned the martial art [martial_name] ([martial_to_learn])", LOG_ATTACK, color = "orange") diff --git a/code/game/objects/items/granters/martial_arts/cqc.dm b/code/game/objects/items/granters/martial_arts/cqc.dm new file mode 100644 index 0000000000000..697541e021622 --- /dev/null +++ b/code/game/objects/items/granters/martial_arts/cqc.dm @@ -0,0 +1,29 @@ +/obj/item/book/granter/martial/cqc + martial = /datum/martial_art/cqc + name = "old manual" + martial_name = "close quarters combat" + desc = "A small, black manual. There are drawn instructions of tactical hand-to-hand combat." + greet = "You've mastered the basics of CQC." + icon_state = "cqcmanual" + remarks = list( + "Kick... Slam...", + "Lock... Kick...", + "Strike their abdomen, neck and back for critical damage...", + "Slam... Lock...", + "I could probably combine this with some other martial arts!", + "Words that kill...", + "The last and final moment is yours...", + ) + +/obj/item/book/granter/martial/cqc/on_reading_finished(mob/living/carbon/user) + . = ..() + if(uses <= 0) + to_chat(user, span_warning("[src] beeps ominously...")) + +/obj/item/book/granter/martial/cqc/recoil(mob/living/user) + to_chat(user, span_warning("[src] explodes!")) + playsound(src,'sound/effects/explosion1.ogg',40,TRUE) + user.flash_act(1, 1) + user.adjustBruteLoss(6) + user.adjustFireLoss(6) + qdel(src) diff --git a/code/game/objects/items/granters/martial_arts/plasma_fist.dm b/code/game/objects/items/granters/martial_arts/plasma_fist.dm new file mode 100644 index 0000000000000..d33fdf6eaae75 --- /dev/null +++ b/code/game/objects/items/granters/martial_arts/plasma_fist.dm @@ -0,0 +1,35 @@ +/obj/item/book/granter/martial/plasma_fist + martial = /datum/martial_art/plasma_fist + name = "frayed scroll" + martial_name = "plasma fist" + desc = "An aged and frayed scrap of paper written in shifting runes. There are hand-drawn illustrations of pugilism." + greet = "You have learned the ancient martial art of Plasma Fist. Your combos are extremely hard to pull off, but include some of the most deadly moves ever seen including \ + the plasma fist, which when pulled off will make someone violently explode." + icon = 'icons/obj/wizard.dmi' + icon_state ="scroll2" + remarks = list( + "Balance...", + "Power...", + "Control...", + "Mastery...", + "Vigilance...", + "Skill...", + ) + +/obj/item/book/granter/martial/plasma_fist/on_reading_finished(mob/living/carbon/user) + . = ..() + update_appearance() + +/obj/item/book/granter/martial/plasma_fist/update_appearance(updates) + . = ..() + if(uses <= 0) + name = "empty scroll" + desc = "It's completely blank." + icon_state = "blankscroll" + else + name = initial(name) + desc = initial(desc) + icon_state = initial(icon_state) + +/obj/item/book/granter/martial/plasma_fist/nobomb + martial = /datum/martial_art/plasma_fist/nobomb diff --git a/code/game/objects/items/granters/martial_arts/sleeping_carp.dm b/code/game/objects/items/granters/martial_arts/sleeping_carp.dm new file mode 100644 index 0000000000000..c50a062eae5d8 --- /dev/null +++ b/code/game/objects/items/granters/martial_arts/sleeping_carp.dm @@ -0,0 +1,35 @@ +/obj/item/book/granter/martial/carp + martial = /datum/martial_art/the_sleeping_carp + name = "mysterious scroll" + martial_name = "sleeping carp" + desc = "A scroll filled with strange markings. It seems to be drawings of some sort of martial art." + greet = "You have learned the ancient martial art of the Sleeping Carp! Your hand-to-hand combat has become much more effective, and you are now able to deflect any projectiles \ + directed toward you while in Throw Mode. Your body has also hardened itself, granting extra protection against lasting wounds that would otherwise mount during extended combat. \ + However, you are also unable to use any ranged weaponry. You can learn more about your newfound art by using the Recall Teachings verb in the Sleeping Carp tab." + icon = 'icons/obj/wizard.dmi' + icon_state = "scroll2" + worn_icon_state = "scroll" + remarks = list( + "Wait, a high protein diet is really all it takes to become stabproof...?", + "Overwhelming force, immovable object...", + "Focus... And you'll be able to incapacitate any foe in seconds...", + "I must pierce armor for maximum damage...", + "I don't think this would combine with other martial arts...", + "Become one with the carp...", + "Glub...", + ) + +/obj/item/book/granter/martial/carp/on_reading_finished(mob/living/carbon/user) + . = ..() + update_appearance() + +/obj/item/book/granter/martial/carp/update_appearance(updates) + . = ..() + if(uses <= 0) + name = "empty scroll" + desc = "It's completely blank." + icon_state = "blankscroll" + else + name = initial(name) + desc = initial(desc) + icon_state = initial(icon_state) diff --git a/code/game/objects/items/granters/oragami.dm b/code/game/objects/items/granters/oragami.dm new file mode 100644 index 0000000000000..b048c67ed722d --- /dev/null +++ b/code/game/objects/items/granters/oragami.dm @@ -0,0 +1,33 @@ +/obj/item/book/granter/action/origami + granted_action = /datum/action/innate/origami + name = "The Art of Origami" + desc = "A meticulously in-depth manual explaining the art of paper folding." + icon_state = "origamibook" + action_name = "origami" + remarks = list( + "Dead-stick stability...", + "Symmetry seems to play a rather large factor...", + "Accounting for crosswinds... really?", + "Drag coefficients of various paper types...", + "Thrust to weight ratios?", + "Positive dihedral angle?", + "Center of gravity forward of the center of lift...", + ) + +/datum/action/innate/origami + name = "Origami Folding" + desc = "Toggles your ability to fold and catch robust paper airplanes." + button_icon_state = "origami_off" + check_flags = NONE + +/datum/action/innate/origami/Activate() + to_chat(owner, span_notice("You will now fold origami planes.")) + button_icon_state = "origami_on" + active = TRUE + UpdateButtons() + +/datum/action/innate/origami/Deactivate() + to_chat(owner, span_notice("You will no longer fold origami planes.")) + button_icon_state = "origami_off" + active = FALSE + UpdateButtons() diff --git a/code/game/objects/items/implants/implant.dm b/code/game/objects/items/implants/implant.dm index 968b0d4e55443..ca5fcf8e96e75 100644 --- a/code/game/objects/items/implants/implant.dm +++ b/code/game/objects/items/implants/implant.dm @@ -7,7 +7,9 @@ icon_state = "generic" //Shows up as the action button icon item_flags = ABSTRACT | DROPDEL actions_types = list(/datum/action/item_action/hands_free/activate) - var/activated = TRUE //1 for implant types that can be activated, 0 for ones that are "always on" like mindshield implants + // This gives the user an action button that allows them to activate the implant. + // If the implant needs no action button, then null this out. + // Or, if you want to add a unique action button, then replace this. var/mob/living/imp_in = null var/implant_color = "b" var/allow_multiple = FALSE @@ -26,6 +28,11 @@ /obj/item/implant/ui_action_click() activate("action_button") +/obj/item/implant/item_action_slot_check(slot, mob/user) + return user == imp_in + + + /obj/item/implant/proc/can_be_implanted_in(mob/living/target) // for human-only and other special requirements return TRUE @@ -78,10 +85,8 @@ forceMove(target) imp_in = target target.implants += src - if(activated) - for(var/X in actions) - var/datum/action/A = X - A.Grant(target) + for(var/datum/action/implant_action as anything in actions) + implant_action.Grant(target) if(ishuman(target)) var/mob/living/carbon/human/H = target H.sec_hud_set_implants() @@ -104,10 +109,8 @@ user.implants -= src imp_in = target target.implants += src - if(activated) - for(var/X in actions) - var/datum/action/A = X - A.Grant(target) + for(var/datum/action/implant_action as anything in actions) + implant_action.Grant(target) if(ishuman(target)) var/mob/living/carbon/human/H = target H.sec_hud_set_implants() @@ -119,9 +122,8 @@ moveToNullspace() imp_in = null source.implants -= src - for(var/X in actions) - var/datum/action/A = X - A.Grant(source) + for(var/datum/action/implant_action as anything in actions) + implant_action.Remove(source) if(ishuman(source)) var/mob/living/carbon/human/H = source H.sec_hud_set_implants() diff --git a/code/game/objects/items/implants/implant_abductor.dm b/code/game/objects/items/implants/implant_abductor.dm index b551e06e56d69..f3d53268d8c1b 100644 --- a/code/game/objects/items/implants/implant_abductor.dm +++ b/code/game/objects/items/implants/implant_abductor.dm @@ -3,7 +3,6 @@ desc = "Returns you to the mothership." icon = 'icons/obj/abductor.dmi' icon_state = "implant" - activated = 1 var/obj/machinery/abductor/pad/home COOLDOWN_DECLARE(abductor_implant_cooldown) diff --git a/code/game/objects/items/implants/implant_camera.dm b/code/game/objects/items/implants/implant_camera.dm index 503ccee06baf6..95909d90432a5 100644 --- a/code/game/objects/items/implants/implant_camera.dm +++ b/code/game/objects/items/implants/implant_camera.dm @@ -1,7 +1,7 @@ /obj/item/implant/camera name = "camera implant" desc = "Watchful eye inside you." - activated = FALSE + actions_types = null var/obj/machinery/camera/camera /obj/item/implant/camera/get_data() diff --git a/code/game/objects/items/implants/implant_chem.dm b/code/game/objects/items/implants/implant_chem.dm index fbbbf51a79aac..48afcf0d7888f 100644 --- a/code/game/objects/items/implants/implant_chem.dm +++ b/code/game/objects/items/implants/implant_chem.dm @@ -2,7 +2,7 @@ name = "chem implant" desc = "Injects things." icon_state = "reagents" - activated = FALSE + actions_types = null /obj/item/implant/chem/get_data() var/dat = {"Implant Specifications:
diff --git a/code/game/objects/items/implants/implant_clown.dm b/code/game/objects/items/implants/implant_clown.dm index ae1fef109dd9e..c91014f6824f3 100644 --- a/code/game/objects/items/implants/implant_clown.dm +++ b/code/game/objects/items/implants/implant_clown.dm @@ -1,6 +1,6 @@ /obj/item/implant/sad_trombone name = "sad trombone implant" - activated = 0 + actions_types = null /obj/item/implant/sad_trombone/get_data() var/dat = {"Implant Specifications:
diff --git a/code/game/objects/items/implants/implant_deathrattle.dm b/code/game/objects/items/implants/implant_deathrattle.dm index 539e076e09bc2..ee6ee295a9f3f 100644 --- a/code/game/objects/items/implants/implant_deathrattle.dm +++ b/code/game/objects/items/implants/implant_deathrattle.dm @@ -67,7 +67,7 @@ name = "deathrattle implant" desc = "Hope no one else dies, prepare for when they do." - activated = FALSE + actions_types = null /obj/item/implant/deathrattle/can_be_implanted_in(mob/living/target) // Can be implanted in anything that's a mob. Syndicate cyborgs, talking fish, humans... diff --git a/code/game/objects/items/implants/implant_exile.dm b/code/game/objects/items/implants/implant_exile.dm index 6d5e29292905f..7ecea9c8a8513 100644 --- a/code/game/objects/items/implants/implant_exile.dm +++ b/code/game/objects/items/implants/implant_exile.dm @@ -4,7 +4,7 @@ /obj/item/implant/exile name = "exile implant" desc = "Prevents you from returning from away missions." - activated = 0 + actions_types = null /obj/item/implant/exile/get_data() var/dat = {"Implant Specifications:
diff --git a/code/game/objects/items/implants/implant_explosive.dm b/code/game/objects/items/implants/implant_explosive.dm index ee76e644aa2b7..55613e30df7f8 100644 --- a/code/game/objects/items/implants/implant_explosive.dm +++ b/code/game/objects/items/implants/implant_explosive.dm @@ -124,3 +124,7 @@ /obj/item/implanter/explosive_macro name = "implanter (macrobomb)" imp_type = /obj/item/implant/explosive/macro + +/datum/action/item_action/explosive_implant + check_flags = NONE + name = "Activate Explosive Implant" diff --git a/code/game/objects/items/implants/implant_krav_maga.dm b/code/game/objects/items/implants/implant_krav_maga.dm index 373658b386461..ec4b185ee1335 100644 --- a/code/game/objects/items/implants/implant_krav_maga.dm +++ b/code/game/objects/items/implants/implant_krav_maga.dm @@ -3,7 +3,6 @@ desc = "Teaches you the arts of Krav Maga in 5 short instructional videos beamed directly into your eyeballs." icon = 'icons/obj/wizard.dmi' icon_state ="scroll2" - activated = 1 var/datum/martial_art/krav_maga/style = new /obj/item/implant/krav_maga/get_data() diff --git a/code/game/objects/items/implants/implant_mindshield.dm b/code/game/objects/items/implants/implant_mindshield.dm index 113fc640ab485..8623deb21eb4b 100644 --- a/code/game/objects/items/implants/implant_mindshield.dm +++ b/code/game/objects/items/implants/implant_mindshield.dm @@ -1,7 +1,7 @@ /obj/item/implant/mindshield name = "mindshield implant" desc = "Protects against brainwashing." - activated = 0 + actions_types = null /obj/item/implant/mindshield/get_data() var/dat = {"Implant Specifications:
diff --git a/code/game/objects/items/implants/implant_misc.dm b/code/game/objects/items/implants/implant_misc.dm index 1cc169de68e8f..cad80ebce74ed 100644 --- a/code/game/objects/items/implants/implant_misc.dm +++ b/code/game/objects/items/implants/implant_misc.dm @@ -2,7 +2,7 @@ name = "firearms authentication implant" desc = "Lets you shoot your guns." icon_state = "auth" - activated = 0 + actions_types = null /obj/item/implant/weapons_auth/get_data() var/dat = {"Implant Specifications:
@@ -75,7 +75,7 @@ /obj/item/implant/health name = "health implant" - activated = 0 + actions_types = null var/healthstring = "" var/list/raw_data = list() @@ -102,7 +102,7 @@ /obj/item/implant/radio name = "internal radio implant" - activated = TRUE + actions_types = null var/obj/item/radio/radio var/radio_key var/subspace_transmission = FALSE diff --git a/code/game/objects/items/implants/implant_spell.dm b/code/game/objects/items/implants/implant_spell.dm index e925a17b2936b..491ea039131dc 100644 --- a/code/game/objects/items/implants/implant_spell.dm +++ b/code/game/objects/items/implants/implant_spell.dm @@ -1,36 +1,58 @@ /obj/item/implant/spell name = "spell implant" desc = "Allows you to cast a spell as if you were a wizard." - activated = FALSE + actions_types = null - var/autorobeless = TRUE // Whether to automagically make the spell robeless on implant - var/obj/effect/proc_holder/spell/spell +/// Whether to make the spell robeless + var/make_robeless = TRUE + /// The typepath of the spell we give to people. Instantiated in Initialize + var/datum/action/cooldown/spell/spell_type + /// The actual spell we give to the person on implant + var/datum/action/cooldown/spell/spell_to_give +/obj/item/implant/spell/Initialize(mapload) + . = ..() + if(!spell_type) + return + + spell_to_give = new spell_type(src) + + if(make_robeless && (spell_to_give.spell_requirements & SPELL_REQUIRES_WIZARD_GARB)) + spell_to_give.spell_requirements &= ~SPELL_REQUIRES_WIZARD_GARB + +/obj/item/implant/spell/Destroy() + QDEL_NULL(spell_to_give) + return ..() /obj/item/implant/spell/get_data() var/dat = {"Implant Specifications:
Name: Spell Implant
Life: 4 hours after death of host
Implant Details:
- Function: [spell ? "Allows a non-wizard to cast [spell] as if they were a wizard." : "None"]"} + Function: [spell_to_give ? "Allows a non-wizard to cast [spell_to_give] as if they were a wizard." : "None."]"} return dat /obj/item/implant/spell/implant(mob/living/target, mob/user, silent = FALSE, force = FALSE) . = ..() - if (.) - if (!spell) - return FALSE - if (autorobeless && spell.clothes_req) - spell.clothes_req = FALSE - target.AddSpell(spell) - return TRUE - -/obj/item/implant/spell/removed(mob/target, silent = FALSE, special = 0) + if (!.) + return + + if (!spell_to_give) + return FALSE + + spell_to_give.Grant(target) + return TRUE + +/obj/item/implant/spell/removed(mob/living/source, silent = FALSE, special = 0) . = ..() - if (.) - target.RemoveSpell(spell) - if(target.stat != DEAD && !silent) - to_chat(target, "The knowledge of how to cast [spell] slips out from your mind.") + if (!.) + return FALSE + + if(spell_to_give) + spell_to_give.Remove(source) + if(source.stat != DEAD && !silent) + to_chat(source, span_boldnotice("The knowledge of how to cast [spell_to_give] slips out from your mind.")) + return TRUE /obj/item/implanter/spell name = "implanter (spell)" diff --git a/code/game/objects/items/implants/implant_track.dm b/code/game/objects/items/implants/implant_track.dm index 1a222cfcc960b..e1e2975129c60 100644 --- a/code/game/objects/items/implants/implant_track.dm +++ b/code/game/objects/items/implants/implant_track.dm @@ -1,7 +1,7 @@ /obj/item/implant/tracking name = "tracking implant" desc = "Track with this." - activated = FALSE + actions_types = null ///for how many deciseconds after user death will the implant work? var/lifespan_postmortem = 6000 ///will people implanted with this act as teleporter beacons? diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm index c8a6796c9d501..567213d863195 100644 --- a/code/game/objects/items/robot/robot_upgrades.dm +++ b/code/game/objects/items/robot/robot_upgrades.dm @@ -661,6 +661,8 @@ crew_monitor.Grant(R) icon_state = "scanner" +/datum/action/item_action/crew_monitor + name = "Interface With Crew Monitor" /obj/item/borg/upgrade/pinpointer/deactivate(mob/living/silicon/robot/R, user = usr) . = ..() diff --git a/code/game/objects/items/scrolls.dm b/code/game/objects/items/scrolls.dm index 98a642cb88746..3cce579b62160 100644 --- a/code/game/objects/items/scrolls.dm +++ b/code/game/objects/items/scrolls.dm @@ -3,13 +3,26 @@ desc = "A scroll for moving around." icon = 'icons/obj/wizard.dmi' icon_state = "scroll" - var/uses = 4 + var/uses = 4 /// Number of uses the scroll gets. + actions_types = list(/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll) w_class = WEIGHT_CLASS_SMALL item_state = "paper" throw_speed = 3 throw_range = 7 resistance_flags = FLAMMABLE +/obj/item/teleportation_scroll/Initialize(mapload) + . = ..() + // In the future, this can be generalized into just "magic scrolls that give you a specific spell". + var/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll/teleport = locate() in actions + if(teleport) + teleport.name = name + teleport.icon_icon = icon + teleport.button_icon_state = icon_state + +/obj/item/teleportation_scroll/item_action_slot_check(slot, mob/user) + return (slot == ITEM_SLOT_HANDS) + /obj/item/teleportation_scroll/apprentice name = "lesser scroll of teleportation" uses = 1 @@ -17,55 +30,25 @@ /obj/item/teleportation_scroll/attack_self(mob/user) - user.set_machine(src) - var/dat = "Teleportation Scroll:
" - dat += "Number of uses: [src.uses]
" - dat += "
" - dat += "Four uses, use them wisely:
" - dat += "Teleport
" - dat += "Kind regards,
Wizards Federation

P.S. Don't forget to bring your gear, you'll need it to cast most spells.
" - user << browse(dat, "window=scroll") - onclose(user, "scroll") - return - -/obj/item/teleportation_scroll/Topic(href, href_list) - ..() - if (usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED) || src.loc != usr) + . = ..() + if(.) return - if (!ishuman(usr)) - return 1 - var/mob/living/carbon/human/H = usr - if(H.is_holding(src)) - H.set_machine(src) - if (href_list["spell_teleport"]) - if(uses) - teleportscroll(H) - if(H) - attack_self(H) - return -/obj/item/teleportation_scroll/proc/teleportscroll(mob/user) - - var/A = tgui_input_list(user, "Area to jump to", "BOOYEA", items = GLOB.teleportlocs) - if(!src || QDELETED(src) || !user || !user.is_holding(src) || user.incapacitated() || !A || !uses) + if(!uses) return - var/area/thearea = GLOB.teleportlocs[A] - - var/datum/effect_system/smoke_spread/smoke = new - smoke.set_up(2, user.loc) - smoke.attach(user) - smoke.start() - var/list/L = list() - for(var/turf/T in get_area_turfs(thearea.type)) - if(!T.is_blocked_turf()) - L += T - - if(!L.len) - to_chat(user, "The spell matrix was unable to locate a suitable teleport destination for an unknown reason. Sorry.") + if(!ishuman(user)) + return + var/mob/living/carbon/human/human_user = user + if(human_user.incapacitated() || !human_user.is_holding(src)) + return + var/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll/teleport = locate() in actions + if(!teleport) + to_chat(user, span_warning("[src] seems to be a faulty teleportation scroll, and has no magic associated.")) + return + if(!teleport.Activate(user)) return + if(--uses <= 0) + to_chat(user, span_warning("[src] runs out of uses and crumbles to dust!")) + qdel(src) + return TRUE - if(do_teleport(user, pick(L), channel = TELEPORT_CHANNEL_MAGIC, forced = TRUE)) - smoke.start() - uses-- - else - to_chat(user, "The spell matrix was disrupted by something near the destination.") diff --git a/code/game/objects/items/signs.dm b/code/game/objects/items/signs.dm index 99e9ac8c494ad..09b903e3fd646 100644 --- a/code/game/objects/items/signs.dm +++ b/code/game/objects/items/signs.dm @@ -51,3 +51,12 @@ /obj/item/stack/sheet/cardboard = 2) time = 80 category = CAT_MISC + +/datum/action/item_action/nano_picket_sign + name = "Retext Nano Picket Sign" + +/datum/action/item_action/nano_picket_sign/Trigger(trigger_flags) + if(!istype(target, /obj/item/picket_sign)) + return + var/obj/item/picket_sign/sign = target + sign.retext(owner) diff --git a/code/game/objects/items/storage/holsters.dm b/code/game/objects/items/storage/holsters.dm new file mode 100644 index 0000000000000..c5d1672c90cdf --- /dev/null +++ b/code/game/objects/items/storage/holsters.dm @@ -0,0 +1 @@ +///There's probably this file somewhere else in the codebase, treat this like a stickynote diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index feb0e77a45678..b00a9d1ef16a7 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -155,7 +155,7 @@ new /obj/item/clothing/suit/hooded/chaplain_hoodie(src) new /obj/item/card/id/syndicate(src) new /obj/item/clothing/shoes/chameleon/noslip(src) //because slipping while being a dark lord sucks - new /obj/item/book/granter/spell/summonitem(src) + new /obj/item/book/granter/action/spell/summonitem(src) if("white_whale_holy_grail") //Unique items that don't appear anywhere else new /obj/item/pneumatic_cannon/speargun(src) @@ -514,8 +514,8 @@ new /obj/item/gun/ballistic/revolver/reverse(src) /obj/item/storage/box/syndie_kit/mimery/PopulateContents() - new /obj/item/book/granter/spell/mimery_blockade(src) - new /obj/item/book/granter/spell/mimery_guns(src) + new /obj/item/book/granter/action/spell/mime/mimery_blockade(src) + new /obj/item/book/granter/action/spell/mime/mimery_guns(src) /obj/item/storage/box/syndie_kit/centcom_costume/PopulateContents() new /obj/item/clothing/under/rank/centcom/official(src) diff --git a/code/game/objects/items/tanks/watertank.dm b/code/game/objects/items/tanks/watertank.dm index 7c7c944d047eb..b5548f330e1b8 100644 --- a/code/game/objects/items/tanks/watertank.dm +++ b/code/game/objects/items/tanks/watertank.dm @@ -721,6 +721,9 @@ update_icon() user.update_inv_back() //for overlays update +/datum/action/item_action/activate_injector + name = "Activate Injector" + //Operator backpack spray /obj/item/watertank/op name = "backpack water tank" diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index f6d0983296766..60294a0e0a814 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -643,3 +643,9 @@ var/custom_data = item.on_object_saved(depth++) dat += "[custom_data ? ",\n[custom_data]" : ""]" return dat + +/obj/structure/closet/proc/on_magic_unlock(datum/source, datum/action/cooldown/spell/aoe/knock/spell, mob/living/caster) + SIGNAL_HANDLER + + locked = FALSE + INVOKE_ASYNC(src, .proc/open) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 9ff0afaa39ec6..3e8fd9c676ab0 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -109,7 +109,8 @@ GLOBAL_LIST_INIT(admin_verbs_fun, list( /client/proc/load_circuit, /client/proc/healall, /client/proc/spawn_floor_cluwne, - /client/proc/spawnhuman + /client/proc/spawnhuman, + /client/proc/debug_spell_requirements, )) GLOBAL_PROTECT(admin_verbs_fun) GLOBAL_LIST_INIT(admin_verbs_spawn, list( @@ -632,38 +633,73 @@ GLOBAL_PROTECT(admin_verbs_hideable) set category = "Fun" set name = "Give Spell" set desc = "Gives a spell to a mob." - + var/which = tgui_alert(usr, "Chose by name or by type path?", "Chose option", list("Name", "Typepath")) + if(!which) + return + if(QDELETED(spell_recipient)) + to_chat(usr, span_warning("The intended spell recipient no longer exists.")) + return var/list/spell_list = list() - var/type_length = length_char("/obj/effect/proc_holder/spell") + 2 - for(var/A in GLOB.spells) - spell_list[copytext_char("[A]", type_length)] = A - var/obj/effect/proc_holder/spell/S = input("Choose the spell to give to that guy", "ABRAKADABRA") as null|anything in sort_list(spell_list) - if(!S) + for(var/datum/action/cooldown/spell/to_add as anything in subtypesof(/datum/action/cooldown/spell)) + var/spell_name = initial(to_add.name) + if(spell_name == "Spell") // abstract or un-named spells should be skipped. + continue + + if(which == "Name") + spell_list[spell_name] = to_add + else + spell_list += to_add + + var/chosen_spell = tgui_input_list(usr, "Choose the spell to give to [spell_recipient]", "ABRAKADABRA", sort_list(spell_list)) + if(isnull(chosen_spell)) + return + var/datum/action/cooldown/spell/spell_path = which == "Typepath" ? chosen_spell : spell_list[chosen_spell] + if(!ispath(spell_path)) + return + + var/robeless = (tgui_alert(usr, "Would you like to force this spell to be robeless?", "Robeless Casting?", list("Force Robeless", "Use Spell Setting")) == "Force Robeless") + if(QDELETED(spell_recipient)) + to_chat(usr, span_warning("The intended spell recipient no longer exists.")) return SSblackbox.record_feedback("tally", "admin_verb", 1, "Give Spell") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - log_admin("[key_name(usr)] gave [key_name(T)] the spell [S].") - message_admins("[key_name_admin(usr)] gave [key_name_admin(T)] the spell [S].") + log_admin("[key_name(usr)] gave [key_name(spell_recipient)] the spell [chosen_spell][robeless ? " (Forced robeless)" : ""].") + message_admins("[key_name_admin(usr)] gave [key_name_admin(spell_recipient)] the spell [chosen_spell][robeless ? " (Forced robeless)" : ""].") - S = spell_list[S] - if(T.mind) - T.mind.AddSpell(new S) - else - T.AddSpell(new S) - message_admins("Spells given to mindless mobs will not be transferred in mindswap or cloning!") + var/datum/action/cooldown/spell/new_spell = new spell_path(spell_recipient.mind || spell_recipient) + + if(robeless) + new_spell.spell_requirements &= ~SPELL_REQUIRES_WIZARD_GARB + + new_spell.Grant(spell_recipient) + + if(!spell_recipient.mind) + to_chat(usr, span_userdanger("Spells given to mindless mobs will belong to the mob and not their mind, \ + and as such will not be transferred if their mind changes body (Such as from Mindswap).")) /client/proc/remove_spell(mob/T in GLOB.mob_list) set category = "Fun" set name = "Remove Spell" set desc = "Remove a spell from the selected mob." - if(T?.mind) - var/obj/effect/proc_holder/spell/S = input("Choose the spell to remove", "NO ABRAKADABRA") as null|anything in sort_list(T.mind.spell_list) - if(S) - T.mind.RemoveSpell(S) - log_admin("[key_name(usr)] removed the spell [S] from [key_name(T)].") - message_admins("[key_name_admin(usr)] removed the spell [S] from [key_name_admin(T)].") - SSblackbox.record_feedback("tally", "admin_verb", 1, "Remove Spell") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + var/list/target_spell_list = list() + for(var/datum/action/cooldown/spell/spell in removal_target.actions) + target_spell_list[spell.name] = spell + + if(!length(target_spell_list)) + return + + var/chosen_spell = tgui_input_list(usr, "Choose the spell to remove from [removal_target]", "ABRAKADABRA", sort_list(target_spell_list)) + if(isnull(chosen_spell)) + return + var/datum/action/cooldown/spell/to_remove = target_spell_list[chosen_spell] + if(!istype(to_remove)) + return + + qdel(to_remove) + log_admin("[key_name(usr)] removed the spell [chosen_spell] from [key_name(removal_target)].") + message_admins("[key_name_admin(usr)] removed the spell [chosen_spell] from [key_name_admin(removal_target)].") + SSblackbox.record_feedback("tally", "admin_verb", 1, "Remove Spell") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/give_disease(mob/living/T in GLOB.mob_living_list) set category = "Fun" @@ -856,6 +892,45 @@ GLOBAL_PROTECT(admin_verbs_hideable) qdel(query_burn_book) qdel(query_library_print) +/// Debug verb for seeing at a glance what all spells have as set requirements +/client/proc/debug_spell_requirements() + set name = "Show Spell Requirements" + set category = "Debug" + + var/header = "Name Requirements" + var/all_requirements = list() + for(var/datum/action/cooldown/spell/spell as anything in typesof(/datum/action/cooldown/spell)) + if(initial(spell.name) == "Spell") + continue + + var/list/real_reqs = list() + var/reqs = initial(spell.spell_requirements) + if(reqs & SPELL_CASTABLE_AS_BRAIN) + real_reqs += "Castable as brain" + if(reqs & SPELL_CASTABLE_WHILE_PHASED) + real_reqs += "Castable phased" + if(reqs & SPELL_REQUIRES_HUMAN) + real_reqs += "Must be human" + if(reqs & SPELL_REQUIRES_MIME_VOW) + real_reqs += "Must be miming" + if(reqs & SPELL_REQUIRES_MIND) + real_reqs += "Must have a mind" + if(reqs & SPELL_REQUIRES_NO_ANTIMAGIC) + real_reqs += "Must have no antimagic" + if(reqs & SPELL_REQUIRES_OFF_CENTCOM) + real_reqs += "Must be off central command z-level" + if(reqs & SPELL_REQUIRES_WIZARD_GARB) + real_reqs += "Must have wizard clothes" + + all_requirements += "[initial(spell.name)] [english_list(real_reqs, "No requirements")]" + + var/page_style = "" + var/page_contents = "[page_style][header][jointext(all_requirements, "")]
" + var/datum/browser/popup = new(mob, "spellreqs", "Spell Requirements", 600, 400) + popup.set_content(page_contents) + popup.open() + + #ifdef SENDMAPS_PROFILE /client/proc/display_sendmaps() set name = "Send Maps Profile" @@ -863,3 +938,5 @@ GLOBAL_PROTECT(admin_verbs_hideable) src << link("?debug=profile&type=sendmaps&window=test") #endif + + diff --git a/code/modules/admin/battle_royale.dm b/code/modules/admin/battle_royale.dm index 8b3548d0a79f0..f7acc7565de76 100644 --- a/code/modules/admin/battle_royale.dm +++ b/code/modules/admin/battle_royale.dm @@ -34,7 +34,7 @@ GLOBAL_LIST_INIT(battle_royale_basic_loot, list( /obj/item/gun/energy/disabler, /obj/item/construction/rcd, /obj/item/clothing/glasses/chameleon/flashproof, - /obj/item/book/granter/spell/knock, + /obj/item/book/granter/action/spell/knock, /obj/item/clothing/glasses/sunglasses/advanced, /obj/item/clothing/glasses/thermal/eyepatch, /obj/item/clothing/glasses/thermal/syndi, diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm index 62aa3ac36160e..0b252a7f4df73 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -256,16 +256,16 @@ /obj/item/antag_spawner/slaughter_demon/spawn_antag(client/C, turf/T, kind = "", datum/mind/user) - var/obj/effect/dummy/phased_mob/slaughter/holder = new /obj/effect/dummy/phased_mob/slaughter(T) - var/mob/living/simple_animal/slaughter/S = new demon_type(holder) + var/mob/living/simple_animal/hostile/imp/slaughter/S = new demon_type(T) + new /obj/effect/dummy/phased_mob(T, S) S.holder = holder S.key = C.key S.mind.assigned_role = S.name S.mind.special_role = S.name S.mind.add_antag_datum(antag_type) to_chat(S, S.playstyle_string) - to_chat(S, "You are currently not currently in the same plane of existence as the station. \ - Ctrl+Click a blood pool to manifest.") + to_chat(S, span_bold("You are currently not currently in the same plane of existence as the station. \ + Use your Blood Crawl ability near a pool of blood to manifest and wreak havoc.")) /obj/item/antag_spawner/slaughter_demon/laughter name = "vial of tickles" diff --git a/code/modules/antagonists/cult/blood_magic.dm b/code/modules/antagonists/cult/blood_magic.dm index 42da7a651f33e..7d69de0b38adf 100644 --- a/code/modules/antagonists/cult/blood_magic.dm +++ b/code/modules/antagonists/cult/blood_magic.dm @@ -215,68 +215,46 @@ name = "Hallucinations" desc = "Gives hallucinations to a target at range. A silent and invisible spell." button_icon_state = "horror" - var/obj/effect/proc_holder/horror/PH charges = 4 check_flags = AB_CHECK_CONSCIOUS + click_action = TRUE + enable_text = span_cult("You prepare to horrify a target...") + disable_text = span_cult("You dispel the magic...") -/datum/action/innate/cult/blood_spell/horror/New() - PH = new() - PH.attached_action = src - ..() +/datum/action/innate/cult/blood_spell/horror/InterceptClickOn(mob/living/caller, params, atom/clicked_on) + var/turf/caller_turf = get_turf(caller) + if(!isturf(caller_turf)) + return FALSE -/datum/action/innate/cult/blood_spell/horror/Destroy() - var/obj/effect/proc_holder/horror/destroy = PH - . = ..() - if(destroy && !QDELETED(destroy)) - QDEL_NULL(destroy) + if(!ishuman(clicked_on) || get_dist(caller, clicked_on) > 7) + return FALSE -/datum/action/innate/cult/blood_spell/horror/Activate() - PH.toggle(owner) //the important bit - return TRUE + var/mob/living/carbon/human/human_clicked = clicked_on + if(IS_CULTIST(human_clicked)) + return FALSE -/obj/effect/proc_holder/horror - active = FALSE - ranged_mousepointer = 'icons/effects/cult_target.dmi' - var/datum/action/innate/cult/blood_spell/attached_action + return ..() -/obj/effect/proc_holder/horror/Destroy() - var/datum/action/innate/cult/blood_spell/AA = attached_action - . = ..() - if(AA && !QDELETED(AA)) - QDEL_NULL(AA) +/datum/action/innate/cult/blood_spell/horror/do_ability(mob/living/caller, params, mob/living/carbon/human/clicked_on) -/obj/effect/proc_holder/horror/proc/toggle(mob/user) - if(active) - remove_ranged_ability("You dispel the magic...") - else - add_ranged_ability(user, "You prepare to horrify a target...") + clicked_on.hallucination = max(clicked_on.hallucination, 120) + SEND_SOUND(caller, sound('sound/effects/ghost.ogg', FALSE, TRUE, 50)) -/obj/effect/proc_holder/horror/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - if(ranged_ability_user.incapacitated() || !iscultist(caller)) - remove_ranged_ability() - return - var/turf/T = get_turf(ranged_ability_user) - if(!isturf(T)) - return FALSE - if(ranged_ability_user in viewers(7, get_turf(target))) - if(!ishuman(target) || iscultist(target)) - return - var/mob/living/carbon/human/H = target - H.hallucination = max(H.hallucination, 120) - SEND_SOUND(ranged_ability_user, sound('sound/effects/ghost.ogg',0,1,50)) - var/image/C = image('icons/effects/cult_effects.dmi',H,"bloodsparkles", ABOVE_MOB_LAYER) - add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/cult, "cult_apoc", C, NONE) - addtimer(CALLBACK(H,TYPE_PROC_REF(/atom, remove_alt_appearance),"cult_apoc",TRUE), 2400, TIMER_OVERRIDE|TIMER_UNIQUE) - to_chat(ranged_ability_user,"[H] has been cursed with living nightmares!") - attached_action.charges-- - attached_action.desc = attached_action.base_desc - attached_action.desc += "
Has [attached_action.charges] use\s remaining." - attached_action.UpdateButtonIcon() - if(attached_action.charges <= 0) - remove_ranged_ability("You have exhausted the spell's power!") - qdel(src) + var/image/sparkle_image = image('icons/effects/cult/effects.dmi', clicked_on, "bloodsparkles", ABOVE_MOB_LAYER) + clicked_on.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/cult, "cult_apoc", sparkle_image, NONE) + + addtimer(CALLBACK(clicked_on, /atom/.proc/remove_alt_appearance, "cult_apoc", TRUE), 4 MINUTES, TIMER_OVERRIDE|TIMER_UNIQUE) + to_chat(caller, span_cultbold("[clicked_on] has been cursed with living nightmares!")) + + charges-- + desc = base_desc + desc += "
Has [charges] use\s remaining." + UpdateButtons() + if(charges <= 0) + to_chat(caller, span_cult("You have exhausted the spell's power!")) + qdel(src) + + return TRUE /datum/action/innate/cult/blood_spell/veiling name = "Conceal Presence" @@ -326,7 +304,7 @@ qdel(src) desc = base_desc desc += "
Has [charges] use\s remaining." - UpdateButtonIcon() + UpdateButtons() /datum/action/innate/cult/blood_spell/manipulation name = "Blood Rites" diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index 692dfd9c2cdd8..f66d3fc41d45e 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -236,7 +236,7 @@ /datum/team/cult name = "Bloodcult" - var/blood_target + var/atom/blood_target var/image/blood_target_image var/blood_target_reset_timer @@ -246,6 +246,64 @@ var/cult_risen = FALSE var/cult_ascendent = FALSE +/// Sets a blood target for the cult. +/datum/team/cult/proc/set_blood_target(atom/new_target, mob/marker, duration = 90 SECONDS) + if(QDELETED(new_target)) + CRASH("A null or invalid target was passed to set_blood_target.") + + if(blood_target_reset_timer) + return FALSE + + blood_target = new_target + RegisterSignal(blood_target, COMSIG_PARENT_QDELETING, .proc/unset_blood_target_and_timer) + var/area/target_area = get_area(new_target) + + blood_target_image = image('icons/effects/mouse_pointers/cult_target.dmi', new_target, "glow", ABOVE_MOB_LAYER) + blood_target_image.appearance_flags = RESET_COLOR + blood_target_image.pixel_x = -new_target.pixel_x + blood_target_image.pixel_y = -new_target.pixel_y + + for(var/datum/mind/cultist as anything in members) + if(!cultist.current) + continue + if(cultist.current.stat == DEAD || !cultist.current.client) + continue + + to_chat(cultist.current, span_bold(span_cultlarge("[marker] has marked [blood_target] in the [target_area.name] as the cult's top priority, get there immediately!"))) + SEND_SOUND(cultist.current, sound(pick('sound/hallucinations/over_here2.ogg','sound/hallucinations/over_here3.ogg'), 0, 1, 75)) + cultist.current.client.images += blood_target_image + + blood_target_reset_timer = addtimer(CALLBACK(src, .proc/unset_blood_target), duration, TIMER_STOPPABLE) + return TRUE + +/// Unsets out blood target, clearing the images from all the cultists. +/datum/team/cult/proc/unset_blood_target() + blood_target_reset_timer = null + + for(var/datum/mind/cultist as anything in members) + if(!cultist.current) + continue + if(cultist.current.stat == DEAD || !cultist.current.client) + continue + + if(QDELETED(blood_target)) + to_chat(cultist.current, span_bold(span_cultlarge("The blood mark's target is lost!"))) + else + to_chat(cultist.current, span_bold(span_cultlarge("The blood mark has expired!"))) + cultist.current.client.images -= blood_target_image + + UnregisterSignal(blood_target, COMSIG_PARENT_QDELETING) + blood_target = null + + QDEL_NULL(blood_target_image) + +/// Unsets our blood target when they get deleted. +/datum/team/cult/proc/unset_blood_target_and_timer(datum/source) + SIGNAL_HANDLER + + deltimer(blood_target_reset_timer) + unset_blood_target() + /datum/team/cult/proc/check_size() if(cult_ascendent) return diff --git a/code/modules/antagonists/cult/cult_comms.dm b/code/modules/antagonists/cult/cult_comms.dm index d5d23dd0d4c4a..5edef16be986c 100644 --- a/code/modules/antagonists/cult/cult_comms.dm +++ b/code/modules/antagonists/cult/cult_comms.dm @@ -5,6 +5,7 @@ background_icon_state = "bg_demon" buttontooltipstyle = "cult" check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_INCAPACITATED|AB_CHECK_CONSCIOUS + ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' /datum/action/innate/cult/IsAvailable() if(!iscultist(owner)) @@ -219,9 +220,15 @@ name = "Mark Target" desc = "Marks a target for the cult." button_icon_state = "cult_mark" - var/obj/effect/proc_holder/cultmark/CM - var/cooldown = 0 - var/base_cooldown = 1200 + click_action = TRUE + enable_text = span_cult("You prepare to mark a target for your cult. Click a target to mark them!") + disable_text = span_cult("You cease the marking ritual.") + /// The duration of the mark itself + var/cult_mark_duration = 90 SECONDS + /// The duration of the cooldown for cult marks + var/cult_mark_cooldown_duration = 2 MINUTES + /// The actual cooldown tracked of the action + COOLDOWN_DECLARE(cult_mark_cooldown) /datum/action/innate/cult/master/cultmark/New(Target) CM = new() @@ -229,153 +236,112 @@ ..() /datum/action/innate/cult/master/cultmark/IsAvailable() - if(cooldown > world.time) - if(!CM.active) - to_chat(owner, "You need to wait [DisplayTimeText(cooldown - world.time)] before you can mark another target!") + return ..() && COOLDOWN_FINISHED(src, cult_mark_cooldown) + +/datum/action/innate/cult/master/cultmark/InterceptClickOn(mob/caller, params, atom/clicked_on) + var/turf/caller_turf = get_turf(caller) + if(!isturf(caller_turf)) return FALSE - return ..() -/datum/action/innate/cult/master/cultmark/Destroy() - QDEL_NULL(CM) + if(!(clicked_on in view(7, caller_turf))) + return FALSE return ..() -/datum/action/innate/cult/master/cultmark/Activate() - CM.toggle(owner) //the important bit +/datum/action/innate/cult/master/cultmark/do_ability(mob/living/caller, params, atom/clicked_on) + var/datum/antagonist/cult/cultist = caller.mind.has_antag_datum(/datum/antagonist/cult, TRUE) + if(!cultist) + CRASH("[type] was casted by someone without a cult antag datum.") + + var/datum/team/cult/cult_team = cultist.get_team() + if(!cult_team) + CRASH("[type] was casted by a cultist without a cult team datum.") + if(cult_team.blood_target) + to_chat(caller, span_cult("The cult has already designated a target!")) + return FALSE + + if(cult_team.set_blood_target(clicked_on, caller, cult_mark_duration)) + unset_ranged_ability(caller, span_cult("The marking rite is complete! It will last for [DisplayTimeText(cult_mark_duration)] seconds.")) + COOLDOWN_START(src, cult_mark_cooldown, cult_mark_cooldown_duration) + UpdateButtons() + addtimer(CALLBACK(src, .proc/UpdateButtons), cult_mark_cooldown_duration + 1) + return TRUE + unset_ranged_ability(caller, span_cult("The marking rite failed!")) return TRUE -/obj/effect/proc_holder/cultmark - active = FALSE - ranged_mousepointer = 'icons/effects/cult_target.dmi' - var/datum/action/innate/cult/master/cultmark/attached_action -/obj/effect/proc_holder/cultmark/Destroy() - attached_action = null - return ..() -/obj/effect/proc_holder/cultmark/proc/toggle(mob/user) - if(active) - remove_ranged_ability("You cease the marking ritual.") - else - add_ranged_ability(user, "You prepare to mark a target for your cult...") -/obj/effect/proc_holder/cultmark/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - if(ranged_ability_user.incapacitated()) - remove_ranged_ability() - return - var/turf/T = get_turf(ranged_ability_user) - if(!isturf(T)) - return FALSE +/datum/action/innate/cult/ghostmark //Ghost version + name = "Mark a Blood Target for the Cult" + desc = "Marks whatever you are orbiting for the entire cult to track." + button_icon_state = "cult_mark" + /// The duration of the mark on the target + var/cult_mark_duration = 60 SECONDS + /// The cooldown between marks - the ability can be used in between cooldowns, but can't mark (only clear) + var/cult_mark_cooldown_duration = 60 SECONDS + /// The actual cooldown tracked of the action + COOLDOWN_DECLARE(cult_mark_cooldown) - var/datum/antagonist/cult/C = caller.mind.has_antag_datum(/datum/antagonist/cult,TRUE) +/datum/action/innate/cult/ghostmark/IsAvailable() + return ..() && istype(owner, /mob/dead/observer) - if(ranged_ability_user in viewers(7, get_turf(target))) - if(C.cult_team.blood_target) - to_chat(ranged_ability_user, "The cult has already designated a target!") - return FALSE - C.cult_team.blood_target = target - var/area/A = get_area(target) - attached_action.cooldown = world.time + attached_action.base_cooldown - addtimer(CALLBACK(attached_action.owner, TYPE_PROC_REF(/mob, update_action_buttons_icon)), attached_action.base_cooldown) - C.cult_team.blood_target_image = image('icons/effects/cult_target.dmi', target, "glow", ABOVE_MOB_LAYER) - C.cult_team.blood_target_image.appearance_flags = RESET_COLOR - C.cult_team.blood_target_image.pixel_x = -target.pixel_x - C.cult_team.blood_target_image.pixel_y = -target.pixel_y - for(var/datum/mind/B in SSticker.mode.cult) - if(B.current && B.current.stat != DEAD && B.current.client) - to_chat(B.current, "[ranged_ability_user] has marked [C.cult_team.blood_target] in the [A.name] as the cult's top priority, get there immediately!") - SEND_SOUND(B.current, sound(pick('sound/hallucinations/over_here2.ogg','sound/hallucinations/over_here3.ogg'),0,1,75)) - B.current.client.images += C.cult_team.blood_target_image - attached_action.owner.update_action_buttons_icon() - remove_ranged_ability("The marking rite is complete! It will last for 90 seconds.") - C.cult_team.blood_target_reset_timer = addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(reset_blood_target),C.cult_team), 900, TIMER_STOPPABLE) - return TRUE - return FALSE +/datum/action/innate/cult/ghostmark/Activate() + var/datum/antagonist/cult/cultist = owner.mind?.has_antag_datum(/datum/antagonist/cult, TRUE) + if(!cultist) + CRASH("[type] was casted by someone without a cult antag datum.") -/proc/reset_blood_target(datum/team/cult/team) - for(var/datum/mind/B in team.members) - if(B.current && B.current.stat != DEAD && B.current.client) - if(team.blood_target) - to_chat(B.current,"The blood mark has expired!") - B.current.client.images -= team.blood_target_image - QDEL_NULL(team.blood_target_image) - team.blood_target = null + var/datum/team/cult/cult_team = cultist.get_team() + if(!cult_team) + CRASH("[type] was casted by a cultist without a cult team datum.") + if(cult_team.blood_target) + if(!COOLDOWN_FINISHED(src, cult_mark_cooldown)) + cult_team.unset_blood_target_and_timer() + to_chat(owner, span_cultbold("You have cleared the cult's blood target!")) + return TRUE -/datum/action/innate/cult/master/cultmark/ghost - name = "Mark a Blood Target for the Cult" - desc = "Marks a target for the entire cult to track." + to_chat(owner, span_cultbold("The cult has already designated a target!")) + return FALSE -/datum/action/innate/cult/master/cultmark/ghost/IsAvailable() - if(istype(owner, /mob/dead/observer) && iscultist(owner.mind.current)) - return TRUE - else - qdel(src) + if(!COOLDOWN_FINISHED(src, cult_mark_cooldown)) + to_chat(owner, span_cultbold("You aren't ready to place another blood mark yet!")) + return FALSE -/datum/action/innate/cult/ghostmark //Ghost version - name = "Blood Mark your Target" - desc = "Marks whatever you are orbitting - for the entire cult to track." - button_icon_state = "cult_mark" - var/tracking = FALSE - var/cooldown = 0 - var/base_cooldown = 600 + var/atom/mark_target = owner.orbiting?.parent || get_turf(owner) + if(!mark_target) + return FALSE -/datum/action/innate/cult/ghostmark/IsAvailable() - if(istype(owner, /mob/dead/observer) && iscultist(owner.mind.current)) + if(cult_team.set_blood_target(mark_target, owner, 60 SECONDS)) + to_chat(owner, span_cultbold("You have marked [mark_target] for the cult! It will last for [DisplayTimeText(cult_mark_duration)].")) + COOLDOWN_START(src, cult_mark_cooldown, cult_mark_cooldown_duration) + update_button_status() + addtimer(CALLBACK(src, .proc/reset_button), cult_mark_cooldown_duration + 1) return TRUE - else - qdel(src) -/datum/action/innate/cult/ghostmark/proc/reset_button() - if(owner) - name = "Blood Mark your Target" - desc = "Marks whatever you are orbitting - for the entire cult to track." - button_icon_state = "cult_mark" - owner.update_action_buttons_icon() - SEND_SOUND(owner, 'sound/magic/enter_blood.ogg') - to_chat(owner,"Your previous mark is gone - you are now ready to create a new blood mark.") + to_chat(owner, span_cult("The marking failed!")) + return FALSE -/datum/action/innate/cult/ghostmark/Activate() - var/datum/antagonist/cult/C = owner.mind.has_antag_datum(/datum/antagonist/cult,TRUE) - if(C.cult_team.blood_target) - if(cooldown>world.time) - reset_blood_target(C.cult_team) - to_chat(owner, "You have cleared the cult's blood target!") - deltimer(C.cult_team.blood_target_reset_timer) - return - else - to_chat(owner, "The cult has already designated a target!") - return - if(cooldown>world.time) - to_chat(owner, "You aren't ready to place another blood mark yet!") +/datum/action/innate/cult/ghostmark/proc/update_button_status() + if(!owner) return - target = owner.orbiting?.parent || get_turf(owner) - if(!target) + if(COOLDOWN_FINISHED(src, cult_mark_duration)) + name = initial(name) + desc = initial(desc) + button_icon_state = initial(button_icon_state) + else + name = "Clear the Blood Mark" + desc = "Remove the Blood Mark you previously set." + button_icon_state = "emp" + + UpdateButtons() + +/datum/action/innate/cult/ghostmark/proc/reset_button() + if(QDELETED(owner) || QDELETED(src)) return - C.cult_team.blood_target = target - var/area/A = get_area(target) - cooldown = world.time + base_cooldown - addtimer(CALLBACK(owner, TYPE_PROC_REF(/mob, update_action_buttons_icon)), base_cooldown) - C.cult_team.blood_target_image = image('icons/effects/cult_target.dmi', target, "glow", ABOVE_MOB_LAYER) - C.cult_team.blood_target_image.appearance_flags = RESET_COLOR - C.cult_team.blood_target_image.pixel_x = -target.pixel_x - C.cult_team.blood_target_image.pixel_y = -target.pixel_y - SEND_SOUND(owner, sound(pick('sound/hallucinations/over_here2.ogg','sound/hallucinations/over_here3.ogg'),0,1,75)) - owner.client.images += C.cult_team.blood_target_image - for(var/datum/mind/B in SSticker.mode.cult) - if(B.current && B.current.stat != DEAD && B.current.client) - to_chat(B.current, "[owner] has marked [C.cult_team.blood_target] in the [A.name] as the cult's top priority, get there immediately!") - SEND_SOUND(B.current, sound(pick('sound/hallucinations/over_here2.ogg','sound/hallucinations/over_here3.ogg'),0,1,75)) - B.current.client.images += C.cult_team.blood_target_image - to_chat(owner,"You have marked the [target] for the cult! It will last for [DisplayTimeText(base_cooldown)].") - name = "Clear the Blood Mark" - desc = "Remove the Blood Mark you previously set." - button_icon_state = "emp" - owner.update_action_buttons_icon() - C.cult_team.blood_target_reset_timer = addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(reset_blood_target),C.cult_team), base_cooldown, TIMER_STOPPABLE) - addtimer(CALLBACK(src, PROC_REF(reset_button)), base_cooldown) + SEND_SOUND(owner, 'sound/magic/enter_blood.ogg') + to_chat(owner, span_cultbold("Your previous mark is gone - you are now ready to create a new blood mark.")) + update_button_status() //////// ELDRITCH PULSE ///////// @@ -386,82 +352,90 @@ desc = "Seize upon a fellow cultist or cult structure and teleport it to a nearby location." icon_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "arcane_barrage" - var/obj/effect/proc_holder/pulse/PM - var/cooldown = 0 - var/base_cooldown = 150 - var/throwing = FALSE - var/mob/living/throwee - -/datum/action/innate/cult/master/pulse/New() - PM = new() - PM.attached_action = src - ..() + click_action = TRUE + enable_text = span_cult("You prepare to tear through the fabric of reality... Click a target to sieze them!") + disable_text = span_cult("You cease your preparations.") + /// Weakref to whoever we're currently about to toss + var/datum/weakref/throwee_ref + /// Cooldown of the ability + var/pulse_cooldown_duration = 15 SECONDS + /// The actual cooldown tracked of the action + COOLDOWN_DECLARE(pulse_cooldown) /datum/action/innate/cult/master/pulse/IsAvailable() - if(!owner.mind || !owner.mind.has_antag_datum(/datum/antagonist/cult/master)) + return ..() && COOLDOWN_FINISHED(src, pulse_cooldown) + +/datum/action/innate/cult/master/pulse/InterceptClickOn(mob/living/caller, params, atom/clicked_on) + var/turf/caller_turf = get_turf(caller) + if(!isturf(caller_turf)) return FALSE - if(cooldown > world.time) - if(!PM.active) - to_chat(owner, "You need to wait [DisplayTimeText(cooldown - world.time)] before you can pulse again!") + + if(!(clicked_on in view(7, caller_turf))) return FALSE - return ..() -/datum/action/innate/cult/master/pulse/Destroy() - PM.attached_action = null //What the fuck is even going on here. - QDEL_NULL(PM) + if(clicked_on == caller) + return FALSE return ..() +/datum/action/innate/cult/master/pulse/do_ability(mob/living/caller, params, atom/clicked_on) + var/atom/throwee = throwee_ref?.resolve() -/datum/action/innate/cult/master/pulse/Activate() - PM.toggle(owner) //the important bit - return TRUE + if(QDELETED(throwee)) + to_chat(caller, span_cult("You lost your target!")) + throwee = null + throwee_ref = null + return FALSE + + if(throwee) + if(get_dist(throwee, clicked_on) >= 16) + to_chat(caller, span_cult("You can't teleport [clicked_on.p_them()] that far!")) + return FALSE -/obj/effect/proc_holder/pulse - active = FALSE - ranged_mousepointer = 'icons/effects/throw_target.dmi' - var/datum/action/innate/cult/master/pulse/attached_action + var/turf/throwee_turf = get_turf(throwee) + + playsound(throwee_turf, 'sound/magic/exit_blood.ogg') + new /obj/effect/temp_visual/cult/sparks(throwee_turf, caller.dir) + throwee.visible_message( + span_warning("A pulse of magic whisks [throwee] away!"), + span_cult("A pulse of blood magic whisks you away..."), + ) + + if(!do_teleport(throwee, clicked_on, channel = TELEPORT_CHANNEL_CULT)) + to_chat(caller, span_cult("The teleport fails!")) + throwee.visible_message( + span_warning("...Except they don't go very far"), + span_cult("...Except you don't appear to have moved very far."), + ) + return FALSE -/obj/effect/proc_holder/pulse/Destroy() - attached_action = null - return ..() + throwee_turf.Beam(clicked_on, icon_state = "sendbeam", time = 0.4 SECONDS) + new /obj/effect/temp_visual/cult/sparks(get_turf(clicked_on), caller.dir) + throwee.visible_message( + span_warning("[throwee] appears suddenly in a pulse of magic!"), + span_cult("...And you appear elsewhere."), + ) + COOLDOWN_START(src, pulse_cooldown, pulse_cooldown_duration) + to_chat(caller, span_cult("A pulse of blood magic surges through you as you shift [throwee] through time and space.")) + caller.click_intercept = null + throwee_ref = null + UpdateButtons() + addtimer(CALLBACK(src, .proc/UpdateButtons), pulse_cooldown_duration + 1) -/obj/effect/proc_holder/pulse/proc/toggle(mob/user) - if(active) - remove_ranged_ability("You cease your preparations...") - attached_action.throwing = FALSE + return TRUE else - add_ranged_ability(user, "You prepare to tear through the fabric of reality...") + if(isliving(clicked_on)) + var/mob/living/living_clicked = clicked_on + if(!IS_CULTIST(living_clicked)) + return FALSE + SEND_SOUND(caller, sound('sound/weapons/thudswoosh.ogg')) + to_chat(caller, span_cultbold("You reach through the veil with your mind's eye and seize [clicked_on]! Click anywhere nearby to teleport [clicked_on.p_them()]!")) + throwee_ref = WEAKREF(clicked_on) + return TRUE + + if(istype(clicked_on, /obj/structure/destructible/cult)) + to_chat(caller, span_cultbold("You reach through the veil with your mind's eye and lift [clicked_on]! Click anywhere nearby to teleport it!")) + throwee_ref = WEAKREF(clicked_on) + return TRUE -/obj/effect/proc_holder/pulse/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - if(ranged_ability_user.incapacitated()) - remove_ranged_ability() - return - var/turf/T = get_turf(ranged_ability_user) - if(!isturf(T)) - return FALSE - if(ranged_ability_user in viewers(7, get_turf(target))) - if((!(iscultist(target) || istype(target, /obj/structure/destructible/cult)) || target == caller) && !(attached_action.throwing)) - return - if(!attached_action.throwing) - attached_action.throwing = TRUE - attached_action.throwee = target - SEND_SOUND(ranged_ability_user, sound('sound/weapons/thudswoosh.ogg')) - to_chat(ranged_ability_user,"You reach through the veil with your mind's eye and seize [target]!") - return - else - new /obj/effect/temp_visual/cult/sparks(get_turf(attached_action.throwee), ranged_ability_user.dir) - var/distance = get_dist(attached_action.throwee, target) - if(distance >= 16) - return - playsound(target,'sound/magic/exit_blood.ogg') - attached_action.throwee.Beam(target,icon_state="sendbeam", time = 4) - attached_action.throwee.forceMove(get_turf(target)) - new /obj/effect/temp_visual/cult/sparks(get_turf(target), ranged_ability_user.dir) - attached_action.throwing = FALSE - attached_action.cooldown = world.time + attached_action.base_cooldown - remove_ranged_ability("A pulse of blood magic surges through you as you shift [attached_action.throwee] through time and space.") - caller.update_action_buttons_icon() - addtimer(CALLBACK(caller, TYPE_PROC_REF(/mob, update_action_buttons_icon)), attached_action.base_cooldown) + return FALSE diff --git a/code/modules/antagonists/fugitive/fugitive_outfits.dm b/code/modules/antagonists/fugitive/fugitive_outfits.dm index 87acb285969da..eaeac0ed87517 100644 --- a/code/modules/antagonists/fugitive/fugitive_outfits.dm +++ b/code/modules/antagonists/fugitive/fugitive_outfits.dm @@ -38,8 +38,7 @@ H.hair_color = "000" H.facial_hair_color = H.hair_color H.update_body() - if(H.mind) - H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null)) + var/list/no_drops = list() no_drops += H.get_item_by_slot(ITEM_SLOT_FEET) no_drops += H.get_item_by_slot(ITEM_SLOT_ICLOTHING) @@ -49,6 +48,8 @@ for(var/i in no_drops) var/obj/item/I = i ADD_TRAIT(I, TRAIT_NODROP, CURSED_ITEM_TRAIT) + var/datum/action/cooldown/spell/aoe/knock/waldos_key = new(equipped_on.mind || equipped_on) + waldos_key.Grant(equipped_on) /datum/outfit/synthetic name = "Factory Error Synth" diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index 65e1aa44d4c56..7ee1e12653fbb 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -186,7 +186,7 @@ var/mob/living/our_mob = mob_override || owner.current handle_clown_mutation(our_mob, "Ancient knowledge described to you has allowed you to overcome your clownish nature, allowing you to wield weapons without harming yourself.") our_mob.faction |= FACTION_HERETIC - RegisterSignal(our_mob, COMSIG_MOB_PRE_CAST_SPELL, PROC_REF(on_spell_cast)) + RegisterSignal(our_mob, list(COMSIG_MOB_BEFORE_SPELL_CAST, COMSIG_MOB_SPELL_ACTIVATED), .proc/on_spell_cast) RegisterSignal(our_mob, COMSIG_MOB_ITEM_AFTERATTACK, PROC_REF(on_item_afterattack)) RegisterSignal(our_mob, COMSIG_MOB_LOGIN, PROC_REF(fix_influence_network)) update_heretic_icons_added() @@ -195,7 +195,7 @@ var/mob/living/our_mob = mob_override || owner.current handle_clown_mutation(our_mob, removing = FALSE) our_mob.faction -= FACTION_HERETIC - UnregisterSignal(our_mob, list(COMSIG_MOB_PRE_CAST_SPELL, COMSIG_MOB_ITEM_AFTERATTACK, COMSIG_MOB_LOGIN)) + UnregisterSignal(our_mob, list(COMSIG_MOB_BEFORE_SPELL_CAST, COMSIG_MOB_SPELL_ACTIVATED, COMSIG_MOB_ITEM_AFTERATTACK, COMSIG_MOB_LOGIN)) update_heretic_icons_removed() /datum/antagonist/heretic/proc/update_heretic_icons_added() @@ -216,19 +216,18 @@ knowledge.on_gain(new_body) /* - * Signal proc for [COMSIG_MOB_PRE_CAST_SPELL]. + * Signal proc for [COMSIG_MOB_BEFORE_SPELL_CAST] and [COMSIG_MOB_SPELL_ACTIVATED]. * - * Checks if our heretic has TRAIT_ALLOW_HERETIC_CASTING. + * Checks if our heretic has [TRAIT_ALLOW_HERETIC_CASTING] or is ascended. * If so, allow them to cast like normal. - * If not, cancel the cast. + * If not, cancel the cast, and returns [SPELL_CANCEL_CAST]. */ -/datum/antagonist/heretic/proc/on_spell_cast(mob/living/source, obj/effect/proc_holder/spell/spell) +/datum/antagonist/heretic/proc/on_spell_cast(mob/living/source, datum/action/cooldown/spell/spell) SIGNAL_HANDLER - // Non-Heretic spells, we don't care - if(!spell.requires_heretic_focus) + // Heretic spells are of the forbidden school, otherwise we don't care + if(spell.school != SCHOOL_FORBIDDEN) return - // If we've got the trait, we don't care if(HAS_TRAIT(source, TRAIT_ALLOW_HERETIC_CASTING)) return @@ -237,8 +236,8 @@ return // We shouldn't be able to cast this! Cancel it. - source.balloon_alert(source, "You need a focus") - return COMPONENT_CANCEL_SPELL + source.balloon_alert(source, "you need a focus!") + return SPELL_CANCEL_CAST /* * Signal proc for [COMSIG_MOB_ITEM_AFTERATTACK]. diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm index bc5e62eeeeb99..1421aa53568ca 100644 --- a/code/modules/antagonists/heretic/heretic_knowledge.dm +++ b/code/modules/antagonists/heretic/heretic_knowledge.dm @@ -55,7 +55,7 @@ * * user - the heretic which we're applying things to */ /datum/heretic_knowledge/proc/on_gain(mob/user) - + return /** * Called when the knowledge is removed from a mob, * either due to a heretic being de-heretic'd or bodyswap memery. @@ -64,7 +64,7 @@ * * user - the heretic which we're removing things from */ /datum/heretic_knowledge/proc/on_lose(mob/user) - + return /** * Determines if a heretic can actually attempt to invoke the knowledge as a ritual. * By default, we can only invoke knowledge with rituals associated. @@ -154,23 +154,27 @@ * A knowledge subtype that grants the heretic a certain spell. */ /datum/heretic_knowledge/spell - /// The proc holder spell we add to the heretic. Type-path, becomes an instance via on_research(). - var/obj/effect/proc_holder/spell/spell_to_add - -/datum/heretic_knowledge/spell/Destroy(force, ...) - if(istype(spell_to_add)) - QDEL_NULL(spell_to_add) - return ..() - -/datum/heretic_knowledge/spell/on_research(mob/user) - spell_to_add = new spell_to_add() + abstract_parent_type = /datum/heretic_knowledge/spell + /// Spell path we add to the heretic. Type-path. + var/datum/action/cooldown/spell/spell_to_add + /// The spell we actually created. + var/datum/weakref/created_spell_ref + +/datum/heretic_knowledge/spell/Destroy() + QDEL_NULL(created_spell_ref) return ..() /datum/heretic_knowledge/spell/on_gain(mob/user) - user.mind.AddSpell(spell_to_add) + // Added spells are tracked on the body, and not the mind, + // because we handle heretic mind transfers + // via the antag datum (on_gain and on_lose). + var/datum/action/cooldown/spell/created_spell = created_spell_ref?.resolve() || new spell_to_add(user) + created_spell.Grant(user) + created_spell_ref = WEAKREF(created_spell) /datum/heretic_knowledge/spell/on_lose(mob/user) - user.mind.RemoveSpell(spell_to_add) + var/datum/action/cooldown/spell/created_spell = created_spell_ref?.resolve() + created_spell?.Remove(user) /* * A knowledge subtype for knowledge that can only diff --git a/code/modules/antagonists/heretic/knowledge/ash_lore.dm b/code/modules/antagonists/heretic/knowledge/ash_lore.dm index 8f800db062533..541bc86047bd5 100644 --- a/code/modules/antagonists/heretic/knowledge/ash_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/ash_lore.dm @@ -93,7 +93,7 @@ /datum/heretic_knowledge/essence, /datum/heretic_knowledge/medallion, ) - spell_to_add = /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash + spell_to_add = /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash cost = 1 route = HERETIC_PATH_ASH @@ -136,8 +136,10 @@ mark.on_effect() // Also refunds 75% of charge! - for(var/obj/effect/proc_holder/spell/targeted/touch/mansus_grasp/grasp in user.mind.spell_list) - grasp.charge_counter = min(round(grasp.charge_counter + grasp.charge_max * 0.75), grasp.charge_max) + var/datum/action/cooldown/spell/touch/mansus_grasp/grasp = locate() in source.actions + if(grasp) + grasp.next_use_time = min(round(grasp.next_use_time - grasp.cooldown_time * 0.75, 0), 0) + grasp.UpdateButtons() /datum/heretic_knowledge/knowledge_ritual/ash next_knowledge = list(/datum/heretic_knowledge/mad_mask) @@ -213,7 +215,7 @@ /datum/heretic_knowledge/summon/ashy, /datum/heretic_knowledge/spell/cleave, ) - spell_to_add = /obj/effect/proc_holder/spell/targeted/fiery_rebirth + spell_to_add = /datum/action/cooldown/spell/aoe/fiery_rebirth cost = 1 route = HERETIC_PATH_ASH @@ -253,7 +255,9 @@ /datum/heretic_knowledge/final/ash_final/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) . = ..() priority_announce("[generate_heretic_text()] Fear the blaze, for the Ashlord, [user.real_name] has ascended! The flames shall consume all! [generate_heretic_text()]","[generate_heretic_text()]", ANNOUNCER_SPANOMALIES) - user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/fire_cascade/big) - user.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/fire_sworn) + var/datum/action/cooldown/spell/fire_sworn/circle_spell = new(user.mind) + circle_spell.Grant(user) + var/datum/action/cooldown/spell/fire_cascade/big/screen_wide_fire_spell = new(user.mind) + screen_wide_fire_spell.Grant(user) for(var/trait in traits_to_apply) ADD_TRAIT(user, trait, MAGIC_TRAIT) diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index 32c5ebb716b1a..d2d8e18b57e51 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -92,19 +92,19 @@ // Skeletons can't become husks, and monkeys are monkeys. if(!ishuman(target) || isskeleton(target) || ismonkey(target)) target.balloon_alert(source, "Invalid body") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE var/mob/living/carbon/human/human_target = target human_target.grab_ghost() if(!human_target.mind || !human_target.client) target.balloon_alert(source, "No soul") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE if(HAS_TRAIT(human_target, TRAIT_HUSK)) target.balloon_alert(source, "Husked") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE if(LAZYLEN(created_items) >= limit) target.balloon_alert(source, "At ghoul limit") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE LAZYADD(created_items, WEAKREF(human_target)) log_game("[key_name(source)] created a ghoul, controlled by [key_name(human_target)].") @@ -349,7 +349,8 @@ /datum/heretic_knowledge/final/flesh_final/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) . = ..() priority_announce("[generate_heretic_text()] Ever-coiling vortex. Reality unfolded. ARMS OUTREACHED, THE LORD OF THE NIGHT, [user.real_name] has ascended! Fear the ever-twisting hand! [generate_heretic_text()]", "[generate_heretic_text()]", ANNOUNCER_SPANOMALIES) - user.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shed_human_form) + var/datum/action/cooldown/spell/shed_human_form/worm_spell = new(user.mind) + worm_spell.Grant(user) var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) var/datum/heretic_knowledge/limited_amount/flesh_grasp/grasp_ghoul = heretic_datum.get_knowledge(/datum/heretic_knowledge/limited_amount/flesh_grasp) diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index 43ee73e2b6ca2..f7e93dff26d68 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -196,7 +196,7 @@ /datum/heretic_knowledge/curse/corrosion, /datum/heretic_knowledge/crucible, ) - spell_to_add = /obj/effect/proc_holder/spell/aoe_turf/rust_conversion + spell_to_add = /datum/action/cooldown/spell/aoe/rust_conversion cost = 1 route = HERETIC_PATH_RUST @@ -238,7 +238,7 @@ /datum/heretic_knowledge/final/rust_final, /datum/heretic_knowledge/summon/rusty, ) - spell_to_add = /obj/effect/proc_holder/spell/cone/staggered/entropic_plume + spell_to_add = /datum/action/cooldown/spell/cone/staggered/entropic_plume cost = 1 route = HERETIC_PATH_RUST diff --git a/code/modules/antagonists/heretic/knowledge/side_flesh_void.dm b/code/modules/antagonists/heretic/knowledge/side_flesh_void.dm index bb0df2ef29368..8977de554b731 100644 --- a/code/modules/antagonists/heretic/knowledge/side_flesh_void.dm +++ b/code/modules/antagonists/heretic/knowledge/side_flesh_void.dm @@ -48,6 +48,6 @@ /datum/heretic_knowledge/summon/stalker, /datum/heretic_knowledge/spell/voidpull, ) - spell_to_add = /obj/effect/proc_holder/spell/pointed/blood_siphon + spell_to_add = /datum/action/cooldown/spell/pointed/blood_siphon cost = 1 route = HERETIC_PATH_SIDE diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm index ea1c00d41a830..c3e70c7de7e78 100644 --- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm @@ -27,7 +27,7 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) /datum/heretic_knowledge/limited_amount/base_flesh, /datum/heretic_knowledge/limited_amount/base_void, ) - spell_to_add = /obj/effect/proc_holder/spell/targeted/touch/mansus_grasp + spell_to_add = /datum/action/cooldown/spell/touch/mansus_grasp cost = 0 route = HERETIC_PATH_START diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm index e3ae1b7644442..261b40d256a2e 100644 --- a/code/modules/antagonists/heretic/knowledge/void_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm @@ -173,7 +173,7 @@ /datum/heretic_knowledge/rune_carver, /datum/heretic_knowledge/crucible, ) - spell_to_add = /obj/effect/proc_holder/spell/pointed/void_phase + spell_to_add = /datum/action/cooldown/spell/pointed/void_phase cost = 1 route = HERETIC_PATH_VOID @@ -222,7 +222,7 @@ /datum/heretic_knowledge/spell/blood_siphon, /datum/heretic_knowledge/summon/rusty ) - spell_to_add = /obj/effect/proc_holder/spell/targeted/void_pull + spell_to_add = /datum/action/cooldown/spell/aoe/void_pull cost = 1 route = HERETIC_PATH_VOID diff --git a/code/modules/antagonists/heretic/magic/aggressive_spread.dm b/code/modules/antagonists/heretic/magic/aggressive_spread.dm index bcd8f65ab3138..01eca69edbd97 100644 --- a/code/modules/antagonists/heretic/magic/aggressive_spread.dm +++ b/code/modules/antagonists/heretic/magic/aggressive_spread.dm @@ -1,27 +1,38 @@ - -/obj/effect/proc_holder/spell/aoe_turf/rust_conversion +/datum/action/cooldown/spell/aoe/rust_conversion name = "Aggressive Spread" desc = "Spreads rust onto nearby surfaces." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "corrode" - action_background_icon_state = "bg_ecult" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "corrode" + sound = 'sound/items/welder.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS + invocation = "A'GRSV SPR'D" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - charge_max = 300 //twice as long as mansus grasp - clothes_req = FALSE - range = 3 - -/obj/effect/proc_holder/spell/aoe_turf/rust_conversion/cast(list/targets, mob/user = usr) - playsound(user, 'sound/items/welder.ogg', 75, TRUE) - for(var/turf/T in targets) - ///What we want is the 3 tiles around the user and the tile under him to be rusted, so min(dist,1)-1 causes us to get 0 for these tiles, rest of the tiles are based on chance - var/chance = 100 - (max(get_dist(T,user),1)-1)*100/(range+1) - if(!prob(chance)) - continue - T.rust_heretic_act() - -/obj/effect/proc_holder/spell/aoe_turf/rust_conversion/small + spell_requirements = NONE + + aoe_radius = 3 + +/datum/action/cooldown/spell/aoe/rust_conversion/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/turf/nearby_turf in range(aoe_radius, center)) + things += nearby_turf + + return things + +/datum/action/cooldown/spell/aoe/rust_conversion/cast_on_thing_in_aoe(turf/victim, atom/caster) + // We have less chance of rusting stuff that's further + var/distance_to_caster = get_dist(victim, caster) + var/chance_of_not_rusting = (max(distance_to_caster, 1) - 1) * 100 / (aoe_radius + 1) + + if(prob(chance_of_not_rusting)) + return + + victim.rust_heretic_act() + +/datum/action/cooldown/spell/aoe/rust_conversion/small name = "Rust Conversion" desc = "Spreads rust onto nearby surfaces." - range = 2 + aoe_radius = 2 diff --git a/code/modules/antagonists/heretic/magic/ash_ascension.dm b/code/modules/antagonists/heretic/magic/ash_ascension.dm index 4b4d2ef267ed8..c0ad6cea3ec8d 100644 --- a/code/modules/antagonists/heretic/magic/ash_ascension.dm +++ b/code/modules/antagonists/heretic/magic/ash_ascension.dm @@ -1,111 +1,129 @@ -/obj/effect/proc_holder/spell/targeted/fire_sworn +/// Creates a constant Ring of Fire around the caster for a set duration of time, which follows them. +/datum/action/cooldown/spell/fire_sworn name = "Oath of Flame" desc = "For a minute, you will passively create a ring of fire around you." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "fire_ring" - action_background_icon_state = "bg_ecult" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "fire_ring" + + school = SCHOOL_FORBIDDEN + cooldown_time = 70 SECONDS + invocation = "FL'MS" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - clothes_req = FALSE - range = -1 - include_user = TRUE - charge_max = 700 - ///how long it lasts + spell_requirements = NONE + + /// The radius of the fire ring + var/fire_radius = 1 + /// How long it the ring lasts var/duration = 1 MINUTES - ///who casted it right now - var/mob/current_user - ///Determines if you get the fire ring effect - var/has_fire_ring = FALSE -/obj/effect/proc_holder/spell/targeted/fire_sworn/cast(list/targets, mob/user) - . = ..() - current_user = user - has_fire_ring = TRUE - addtimer(CALLBACK(src, PROC_REF(remove), user), duration, TIMER_OVERRIDE|TIMER_UNIQUE) +/datum/action/cooldown/spell/fire_sworn/Remove(mob/living/remove_from) + remove_from.remove_status_effect(/datum/status_effect/fire_ring) + return ..() -/obj/effect/proc_holder/spell/targeted/fire_sworn/proc/remove() - has_fire_ring = FALSE - current_user = null +/datum/action/cooldown/spell/fire_sworn/is_valid_target(atom/cast_on) + return isliving(cast_on) -/obj/effect/proc_holder/spell/targeted/fire_sworn/process(delta_time) +/datum/action/cooldown/spell/fire_sworn/cast(mob/living/cast_on) . = ..() - if(!has_fire_ring) - return - if(current_user.stat == DEAD) - remove() + cast_on.apply_status_effect(/datum/status_effect/fire_ring, duration, fire_radius) + +/// Simple status effect for adding a ring of fire around a mob. +/datum/status_effect/fire_ring + id = "fire_ring" + tick_interval = 0.1 SECONDS + status_type = STATUS_EFFECT_REFRESH + alert_type = null + /// The radius of the ring around us. + var/ring_radius = 1 + +/datum/status_effect/fire_ring/on_creation(mob/living/new_owner, duration = 1 MINUTES, radius = 1) + src.duration = duration + src.ring_radius = radius + return ..() + +/datum/status_effect/fire_ring/tick(delta_time, times_fired) + if(QDELETED(owner) || owner.stat == DEAD) + qdel(src) return - if(!isturf(current_user.loc)) + + if(!isturf(owner.loc)) return - for(var/turf/nearby_turf as anything in RANGE_TURFS(1, current_user)) + for(var/turf/nearby_turf as anything in RANGE_TURFS(1, owner)) new /obj/effect/hotspot(nearby_turf) nearby_turf.hotspot_expose(750, 25 * delta_time, 1) - for(var/mob/living/fried_living in nearby_turf.contents - current_user) - fried_living.adjustFireLoss(2.5 * delta_time) + for(var/mob/living/fried_living in nearby_turf.contents - owner) + fried_living.apply_damage(2.5 * delta_time, BURN) -/obj/effect/proc_holder/spell/aoe_turf/fire_cascade - name = "Fire Cascade" +/// Creates one, large, expanding ring of fire around the caster, which does not follow them. +/datum/action/cooldown/spell/fire_cascade + name = "Lesser Fire Cascade" desc = "Heats the air around you." - requires_heretic_focus = TRUE - charge_max = 300 //twice as long as mansus grasp - clothes_req = FALSE + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "fire_ring" + sound = 'sound/items/welder.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS + invocation = "C'SC'DE" invocation_type = INVOCATION_WHISPER - range = 4 - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "fire_ring" - action_background_icon_state = "bg_ecult" - -/obj/effect/proc_holder/spell/aoe_turf/fire_cascade/cast(list/targets, mob/user = usr) - INVOKE_ASYNC(src, PROC_REF(fire_cascade), user, range) - -/obj/effect/proc_holder/spell/aoe_turf/fire_cascade/proc/fire_cascade(atom/centre, max_range) - playsound(get_turf(centre), 'sound/items/welder.ogg', 75, TRUE) - var/current_range = 1 - for(var/i in 0 to max_range) - for(var/turf/nearby_turf as anything in spiral_range_turfs(current_range, centre)) + spell_requirements = NONE + + /// The radius the flames will go around the caster. + var/flame_radius = 4 + +/datum/action/cooldown/spell/fire_cascade/cast(atom/cast_on) + . = ..() + INVOKE_ASYNC(src, .proc/fire_cascade, get_turf(cast_on), flame_radius) + +/// Spreads a huge wave of fire in a radius around us, staggered between levels +/datum/action/cooldown/spell/fire_cascade/proc/fire_cascade(atom/centre, flame_radius = 1) + for(var/i in 0 to flame_radius) + for(var/turf/nearby_turf as anything in spiral_range_turfs(i + 1, centre)) new /obj/effect/hotspot(nearby_turf) nearby_turf.hotspot_expose(750, 50, 1) for(var/mob/living/fried_living in nearby_turf.contents - centre) - fried_living.adjustFireLoss(5) + fried_living.apply_damage(5, BURN) - current_range++ stoplag(0.3 SECONDS) -/obj/effect/proc_holder/spell/aoe_turf/fire_cascade/big - range = 6 +/datum/action/cooldown/spell/fire_cascade/big + name = "Greater Fire Cascade" + flame_radius = 6 -// Currently unused. -/obj/effect/proc_holder/spell/pointed/ash_final +// Currently unused - releases streams of fire around the caster. +/datum/action/cooldown/spell/pointed/ash_beams name = "Nightwatcher's Rite" - desc = "A powerful spell that releases 5 streams of fire away from you." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "flames" - action_background_icon_state = "bg_ecult" + desc = "A powerful spell that releases five streams of eldritch fire towards the target." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "flames" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' + + school = SCHOOL_FORBIDDEN + cooldown_time = 300 + invocation = "F'RE" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - charge_max = 300 - range = 15 - clothes_req = FALSE - -/obj/effect/proc_holder/spell/pointed/ash_final/cast(list/targets, mob/user) - for(var/X in targets) - var/T - T = line_target(-25, range, X, user) - INVOKE_ASYNC(src, PROC_REF(fire_line), user, T) - T = line_target(10, range, X, user) - INVOKE_ASYNC(src, PROC_REF(fire_line), user, T) - T = line_target(0, range, X, user) - INVOKE_ASYNC(src, PROC_REF(fire_line), user, T) - T = line_target(-10, range, X, user) - INVOKE_ASYNC(src, PROC_REF(fire_line), user, T) - T = line_target(25, range, X, user) - INVOKE_ASYNC(src, PROC_REF(fire_line), user, T) - return ..() + spell_requirements = NONE -/obj/effect/proc_holder/spell/pointed/ash_final/proc/line_target(offset, range, atom/at , atom/user) + /// The length of the flame line spit out. + var/flame_line_length = 15 + +/datum/action/cooldown/spell/pointed/ash_beams/is_valid_target(atom/cast_on) + return TRUE + +/datum/action/cooldown/spell/pointed/ash_beams/cast(atom/target) + . = ..() + var/static/list/offsets = list(-25, -10, 0, 10, 25) + for(var/offset in offsets) + INVOKE_ASYNC(src, .proc/fire_line, owner, line_target(offset, flame_line_length, target, owner)) + +/datum/action/cooldown/spell/pointed/ash_beams/proc/line_target(offset, range, atom/at, atom/user) if(!at) return var/angle = ATAN2(at.x - user.x, at.y - user.y) + offset @@ -115,30 +133,9 @@ if(!check) break T = check - return (getline(user, T) - get_turf(user)) + return (get_line(user, T) - get_turf(user)) -/obj/effect/proc_holder/spell/pointed/ash_final/proc/fire_line(atom/source, list/turfs) +/datum/action/cooldown/spell/pointed/ash_beams/proc/fire_line(atom/source, list/turfs) var/list/hit_list = list() for(var/turf/T in turfs) if(istype(T, /turf/closed)) - break - - for(var/mob/living/L in T.contents) - if(L.anti_magic_check()) - L.visible_message("The spell bounces off of [L]!","The spell bounces off of you!") - continue - if(L in hit_list || L == source) - continue - hit_list += L - L.adjustFireLoss(20) - to_chat(L, "You're hit by [source]'s eldritch flames!") - - new /obj/effect/hotspot(T) - T.hotspot_expose(700,50,1) - // deals damage to mechs - for(var/obj/vehicle/sealed/mecha/M in T.contents) - if(M in hit_list) - continue - hit_list += M - M.take_damage(45, BURN, MELEE, 1) - sleep(1.5) diff --git a/code/modules/antagonists/heretic/magic/ash_jaunt.dm b/code/modules/antagonists/heretic/magic/ash_jaunt.dm index e775c1f8d24dc..9c0d403fb23e8 100644 --- a/code/modules/antagonists/heretic/magic/ash_jaunt.dm +++ b/code/modules/antagonists/heretic/magic/ash_jaunt.dm @@ -1,30 +1,38 @@ -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash name = "Ashen Passage" desc = "A short range spell that allows you to pass unimpeded through walls." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "ash_shift" - action_background_icon_state = "bg_ecult" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "ash_shift" + sound = null + + school = SCHOOL_FORBIDDEN + cooldown_time = 15 SECONDS + invocation = "ASH'N P'SSG'" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - charge_max = 150 - range = -1 - jaunt_in_time = 13 - jaunt_duration = 10 + spell_requirements = NONE + + exit_jaunt_sound = null + jaunt_duration = 1.1 SECONDS + jaunt_in_time = 1.3 SECONDS + jaunt_out_time = 0.6 SECONDS jaunt_in_type = /obj/effect/temp_visual/dir_setting/ash_shift jaunt_out_type = /obj/effect/temp_visual/dir_setting/ash_shift/out -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash/long - jaunt_duration = 50 - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash/play_sound() +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/do_steam_effects() return +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/long + name = "Ashen Walk" + desc = "A long range spell that allows you pass unimpeded through multiple walls." + jaunt_duration = 5 SECONDS + /obj/effect/temp_visual/dir_setting/ash_shift name = "ash_shift" icon = 'icons/mob/mob.dmi' icon_state = "ash_shift2" - duration = 13 + duration = 1.3 SECONDS /obj/effect/temp_visual/dir_setting/ash_shift/out icon_state = "ash_shift" diff --git a/code/modules/antagonists/heretic/magic/blood_cleave.dm b/code/modules/antagonists/heretic/magic/blood_cleave.dm index c9597c5841c05..c31174a1ac51c 100644 --- a/code/modules/antagonists/heretic/magic/blood_cleave.dm +++ b/code/modules/antagonists/heretic/magic/blood_cleave.dm @@ -1,61 +1,58 @@ -/obj/effect/proc_holder/spell/pointed/cleave +/datum/action/cooldown/spell/pointed/cleave name = "Cleave" desc = "Causes severe bleeding on a target and several targets around them." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "cleave" - action_background_icon_state = "bg_ecult" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "cleave" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' + + school = SCHOOL_FORBIDDEN + cooldown_time = 35 SECONDS + invocation = "CL'VE" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - charge_max = 350 - clothes_req = FALSE - range = 9 - -/obj/effect/proc_holder/spell/pointed/cleave/cast(list/targets, mob/user) - if(!length(targets)) - revert_cast() - user.balloon_alert(user, "No targets") - return FALSE - if(!can_target(targets[1], user)) - revert_cast() - return FALSE - - for(var/mob/living/carbon/human/nearby_human in range(1, targets[1])) - targets |= nearby_human - - for(var/mob/living/carbon/human/victim as anything in targets) - if(victim == user) + spell_requirements = NONE + + cast_range = 9 + /// The radius of the cleave effect + var/cleave_radius = 1 + +/datum/action/cooldown/spell/pointed/cleave/is_valid_target(atom/cast_on) + return ..() && ishuman(cast_on) + +/datum/action/cooldown/spell/pointed/cleave/cast(mob/living/carbon/human/cast_on) + . = ..() + var/list/mob/living/carbon/human/nearby = list(cast_on) + for(var/mob/living/carbon/human/nearby_human in range(cleave_radius, cast_on)) + nearby += nearby_human + + for(var/mob/living/carbon/human/victim as anything in nearby) + if(victim == owner) continue - if(victim.anti_magic_check()) + if(victim.can_block_magic()) victim.visible_message( - "[victim]'s body flashes in a fiery glow, but repels the blaze!", - "Your body begins to flash in a fiery glow, but you are protected!" + span_danger("[victim]'s flashes in a firey glow, but repels the blaze!"), + span_danger("Your body begins to flash a firey glow, but you are protected!!") ) continue - if(!victim.blood_volume) continue - victim.visible_message( - "[victim]'s veins are shredded from within as an unholy blaze erupts from [victim.p_their()] blood!", - "Your veins burst from within and unholy flame erupts from your blood!" + span_danger("[victim]'s veins are shredded from within as an unholy blaze erupts from [victim.p_their()] blood!"), + span_danger("Your veins burst from within and unholy flame erupts from your blood!") ) + var/obj/item/bodypart/bodypart = pick(victim.bodyparts) + var/datum/wound/slash/critical/crit_wound = new() + crit_wound.apply_wound(bodypart) + victim.apply_damage(20, BURN, wound_bonus = CANT_WOUND) - victim.add_bleeding(BLEED_DEEP_WOUND) - victim.adjustFireLoss(20) new /obj/effect/temp_visual/cleave(victim.drop_location()) -/obj/effect/proc_holder/spell/pointed/cleave/can_target(atom/target, mob/user, silent) - if(!ishuman(target)) - if(!silent) - target.balloon_alert(user, "Invalid target") - return FALSE return TRUE -/obj/effect/proc_holder/spell/pointed/cleave/long - charge_max = 650 +/datum/action/cooldown/spell/pointed/cleave/long + name = "Lesser Cleave" + cooldown_time = 65 SECONDS /obj/effect/temp_visual/cleave - icon = 'icons/effects/heretic.dmi' - icon_state = "cleave" - duration = 6 + icon = 'icons/effects/eldritch.dmi' diff --git a/code/modules/antagonists/heretic/magic/blood_siphon.dm b/code/modules/antagonists/heretic/magic/blood_siphon.dm index 919253807b9e7..406d3123d7823 100644 --- a/code/modules/antagonists/heretic/magic/blood_siphon.dm +++ b/code/modules/antagonists/heretic/magic/blood_siphon.dm @@ -1,50 +1,67 @@ -/obj/effect/proc_holder/spell/pointed/blood_siphon +/datum/action/cooldown/spell/pointed/blood_siphon name = "Blood Siphon" - desc = "A spell that heals your wounds while damaging the enemy." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "blood_siphon" - action_background_icon_state = "bg_ecult" + desc = "A touch spell that heals your wounds while damaging the enemy. \ + It has a chance to transfer wounds between you and your enemy." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "blood_siphon" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' + + school = SCHOOL_FORBIDDEN + cooldown_time = 15 SECONDS + invocation = "FL'MS O'ET'RN'ITY" invocation_type = INVOCATION_WHISPER - charge_max = 150 - clothes_req = FALSE - range = 9 - -/obj/effect/proc_holder/spell/pointed/blood_siphon/cast(list/targets, mob/living/user) - if(!istype(user)) - revert_cast() - return - var/mob/living/target = targets[1] - if(!istype(target)) - user.balloon_alert(user, "Invalid target") - return - playsound(user, 'sound/magic/demon_attack1.ogg', vol = 75, vary = TRUE) - if(target.anti_magic_check()) - user.balloon_alert(user, "Spell blocked") - target.visible_message( - "The spell bounces off of [target]!", - "The spell bounces off of you!", + spell_requirements = NONE + + cast_range = 9 + +/datum/action/cooldown/spell/pointed/blood_siphon/can_cast_spell(feedback = TRUE) + return ..() && isliving(owner) + +/datum/action/cooldown/spell/pointed/blood_siphon/is_valid_target(atom/cast_on) + return ..() && isliving(cast_on) + +/datum/action/cooldown/spell/pointed/blood_siphon/cast(mob/living/cast_on) + . = ..() + playsound(owner, 'sound/magic/demon_attack1.ogg', 75, TRUE) + if(cast_on.can_block_magic()) + owner.balloon_alert(owner, "spell blocked!") + cast_on.visible_message( + span_danger("The spell bounces off of [cast_on]!"), + span_danger("The spell bounces off of you!"), ) - return + return FALSE - target.visible_message( - "[target] turns pale as a red glow envelops [target.p_them()]!", - "You turn pale as a red glow enevelops you!", + cast_on.visible_message( + span_danger("[cast_on] turns pale as a red glow envelops [cast_on.p_them()]!"), + span_danger("You pale as a red glow enevelops you!"), ) - target.take_overall_damage(brute = 20) - user.heal_overall_damage(brute = 20) + var/mob/living/living_owner = owner + cast_on.adjustBruteLoss(20) + living_owner.adjustBruteLoss(-20) - if(!user.blood_volume) - return + if(!cast_on.blood_volume || !living_owner.blood_volume) + return TRUE - target.blood_volume -= 20 - if(user.blood_volume < BLOOD_VOLUME_MAXIMUM) // we dont want to explode from casting - user.blood_volume += 20 + cast_on.blood_volume -= 20 + if(living_owner.blood_volume < BLOOD_VOLUME_MAXIMUM) // we dont want to explode from casting + living_owner.blood_volume += 20 + + if(!iscarbon(cast_on) || !iscarbon(owner)) + return TRUE + + var/mob/living/carbon/carbon_target = cast_on + var/mob/living/carbon/carbon_user = owner + for(var/obj/item/bodypart/bodypart as anything in carbon_user.bodyparts) + for(var/datum/wound/iter_wound as anything in bodypart.wounds) + if(prob(50)) + continue + var/obj/item/bodypart/target_bodypart = locate(bodypart.type) in carbon_target.bodyparts + if(!target_bodypart) + continue + iter_wound.remove_wound() + iter_wound.apply_wound(target_bodypart) -/obj/effect/proc_holder/spell/pointed/blood_siphon/can_target(atom/target, mob/user, silent) - if(!isliving(target)) - if(!silent) - target.balloon_alert(user, "Invalid target") - return FALSE return TRUE diff --git a/code/modules/antagonists/heretic/magic/eldritch_blind.dm b/code/modules/antagonists/heretic/magic/eldritch_blind.dm index d7c32c297b3d6..1901c86aef05f 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_blind.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_blind.dm @@ -1,5 +1,7 @@ // Given to heretic monsters. -/obj/effect/proc_holder/spell/targeted/blind/eldritch - action_background_icon_state = "bg_ecult" +/datum/action/cooldown/spell/pointed/blind/eldritch + name = "Eldritch Blind" + background_icon_state = "bg_ecult" + school = SCHOOL_FORBIDDEN invocation = "E'E'S" - range = 10 + cast_range = 10 diff --git a/code/modules/antagonists/heretic/magic/eldritch_emplosion.dm b/code/modules/antagonists/heretic/magic/eldritch_emplosion.dm index e5f168252db8e..bd1daeb46711b 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_emplosion.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_emplosion.dm @@ -1,13 +1,11 @@ // Given to heretic monsters. -/obj/effect/proc_holder/spell/targeted/emplosion/eldritch +/datum/action/cooldown/spell/emp/eldritch name = "Energetic Pulse" - action_background_icon_state = "bg_ecult" + background_icon_state = "bg_ecult" + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS invocation = "E'P" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - clothes_req = FALSE - range = -1 - include_user = TRUE - charge_max = 300 + spell_requirements = NONE emp_heavy = 6 emp_light = 10 diff --git a/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm b/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm index 31dbc0f174823..954f279e9eee4 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm @@ -1,9 +1,9 @@ // Given to heretic monsters. -/obj/effect/proc_holder/spell/targeted/shapeshift/eldritch - action_background_icon_state = "bg_ecult" +/datum/action/cooldown/spell/shapeshift/eldritch + school = SCHOOL_FORBIDDEN + background_icon_state = "bg_ecult" invocation = "SH'PE" invocation_type = INVOCATION_WHISPER - clothes_req = FALSE possible_shapes = list( /mob/living/simple_animal/mouse, /mob/living/simple_animal/pet/dog/corgi, @@ -11,4 +11,4 @@ /mob/living/simple_animal/bot/secbot, /mob/living/simple_animal/pet/fox, /mob/living/simple_animal/pet/cat, - ) +) diff --git a/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm b/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm index 19b2d63ab5dc0..d4a70d81cc001 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm @@ -1,6 +1,7 @@ // Given to heretic monsters. -/obj/effect/proc_holder/spell/targeted/telepathy/eldritch - action_background_icon_state = "bg_ecult" - invocation = "" - invocation_type = INVOCATION_WHISPER - clothes_req = FALSE +/datum/action/cooldown/spell/list_target/telepathy/eldritch + name = "Eldritch Telepathy" + school = SCHOOL_FORBIDDEN + background_icon_state = "bg_ecult" + invocation_type = INVOCATION_NONE + antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND diff --git a/code/modules/antagonists/heretic/magic/flesh_ascension.dm b/code/modules/antagonists/heretic/magic/flesh_ascension.dm index f865124c37c82..7cb5913c1c716 100644 --- a/code/modules/antagonists/heretic/magic/flesh_ascension.dm +++ b/code/modules/antagonists/heretic/magic/flesh_ascension.dm @@ -1,66 +1,72 @@ -/obj/effect/proc_holder/spell/targeted/shed_human_form +/datum/action/cooldown/spell/shed_human_form name = "Shed form" - desc = "Shed your fragile form, become one with the arms, become one with the emperor." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "worm_ascend" + desc = "Shed your fragile form, become one with the arms, become one with the emperor. \ + Causes heavy amounts of brain damage and sanity loss to nearby mortals." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "worm_ascend" + + school = SCHOOL_FORBIDDEN + cooldown_time = 10 SECONDS + invocation = "REALITY UNCOIL!" invocation_type = INVOCATION_SHOUT - requires_heretic_focus = TRUE - clothes_req = FALSE - action_background_icon_state = "bg_ecult" - range = -1 - include_user = TRUE - charge_max = 100 + spell_requirements = NONE + /// The length of our new wormy when we shed. var/segment_length = 10 + /// The radius around us that we cause brain damage / sanity damage to. + var/scare_radius = 9 + +/datum/action/cooldown/spell/shed_human_form/is_valid_target(atom/cast_on) + return isliving(cast_on) -/obj/effect/proc_holder/spell/targeted/shed_human_form/cast(list/targets, mob/user) +/datum/action/cooldown/spell/shed_human_form/cast(mob/living/cast_on) . = ..() - var/mob/living/target = user - var/mob/living/mob_inside = locate() in target.contents - target - - if(!mob_inside) - var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/outside = new(user.loc, TRUE, segment_length) - target.mind.transfer_to(outside, TRUE) - target.forceMove(outside) - target.apply_status_effect(STATUS_EFFECT_STASIS, STASIS_ASCENSION_EFFECT) - for(var/mob/living/carbon/human/nearby_human in view(9, outside) - target) + if(istype(cast_on, /mob/living/simple_animal/hostile/heretic_summon/armsy/prime)) + var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/old_armsy = cast_on + var/mob/living/our_heretic = locate() in old_armsy + + if(our_heretic.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_ASCENSION_EFFECT)) + our_heretic.forceMove(old_armsy.loc) + + old_armsy.mind.transfer_to(our_heretic, TRUE) + segment_length = old_armsy.get_length() + qdel(old_armsy) + + else + var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/new_armsy = new(cast_on.loc, TRUE, segment_length) + + cast_on.mind.transfer_to(new_armsy, TRUE) + cast_on.forceMove(new_armsy) + cast_on.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_ASCENSION_EFFECT) + + // They see the very reality uncoil before their eyes. + for(var/mob/living/carbon/human/nearby_human in view(scare_radius, new_armsy)) if(IS_HERETIC_OR_MONSTER(nearby_human)) continue SEND_SIGNAL(nearby_human, COMSIG_ADD_MOOD_EVENT, "gates_of_mansus", /datum/mood_event/gates_of_mansus) - ///They see the very reality uncoil before their eyes. + if(prob(25)) - var/trauma = pick(subtypesof(BRAIN_TRAUMA_MILD) + subtypesof(BRAIN_TRAUMA_SEVERE)) - nearby_human.gain_trauma(new trauma(), TRAUMA_RESILIENCE_LOBOTOMY) - return - - if(iscarbon(mob_inside)) - var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/armsy = target - if(mob_inside.remove_status_effect(STATUS_EFFECT_STASIS, STASIS_ASCENSION_EFFECT)) - mob_inside.forceMove(armsy.loc) - armsy.mind.transfer_to(mob_inside, TRUE) - segment_length = armsy.get_length() - qdel(armsy) - return - -/obj/effect/proc_holder/spell/targeted/worm_contract + var/datum/brain_trauma/trauma = pick(subtypesof(BRAIN_TRAUMA_MILD) + subtypesof(BRAIN_TRAUMA_SEVERE)) + nearby_human.gain_trauma(trauma, TRAUMA_RESILIENCE_LOBOTOMY) + +/datum/action/cooldown/spell/worm_contract name = "Force Contract" desc = "Forces your body to contract onto a single tile." - invocation_type = "none" - requires_heretic_focus = TRUE - clothes_req = FALSE - action_background_icon_state = "bg_ecult" - range = -1 - include_user = TRUE - charge_max = 300 - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "worm_contract" - -/obj/effect/proc_holder/spell/targeted/worm_contract/cast(list/targets, mob/user) - . = ..() - if(!istype(user, /mob/living/simple_animal/hostile/heretic_summon/armsy)) - to_chat(user, "You try to contract your muscles, but nothing happens...") - return + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "worm_contract" - var/mob/living/simple_animal/hostile/heretic_summon/armsy/lord_of_night = user - lord_of_night.contract_next_chain_into_single_tile() + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS + + invocation_type = INVOCATION_NONE + spell_requirements = NONE + +/datum/action/cooldown/spell/worm_contract/is_valid_target(atom/cast_on) + return istype(cast_on, /mob/living/simple_animal/hostile/heretic_summon/armsy) + +/datum/action/cooldown/spell/worm_contract/cast(mob/living/simple_animal/hostile/heretic_summon/armsy/cast_on) + . = ..() + cast_on.contract_next_chain_into_single_tile() diff --git a/code/modules/antagonists/heretic/magic/madness_touch.dm b/code/modules/antagonists/heretic/magic/madness_touch.dm index 90da70fcdd0fd..ac9e2b3b87cd4 100644 --- a/code/modules/antagonists/heretic/magic/madness_touch.dm +++ b/code/modules/antagonists/heretic/magic/madness_touch.dm @@ -1,32 +1,32 @@ - -// Currently unused -/obj/effect/proc_holder/spell/pointed/touch/mad_touch +// Currently unused. +/datum/action/cooldown/spell/touch/mad_touch name = "Touch of Madness" desc = "A touch spell that drains your enemy's sanity." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "mad_touch" - action_background_icon_state = "bg_ecult" - requires_heretic_focus = TRUE - charge_max = 150 - clothes_req = FALSE - invocation_type = "none" - range = 2 + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "mad_touch" + + school = SCHOOL_FORBIDDEN + cooldown_time = 15 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = NONE + antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND -/obj/effect/proc_holder/spell/pointed/touch/mad_touch/can_target(atom/target, mob/user, silent) - if(!ishuman(target)) - if(!silent) - target.balloon_alert(user, "Invalid target") +/datum/action/cooldown/spell/touch/mad_touch/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!ishuman(victim)) + return FALSE + + var/mob/living/carbon/human/human_victim = victim + if(!human_victim.mind || IS_HERETIC(human_victim)) + return FALSE + + if(human_victim.can_block_magic(antimagic_flags)) + victim.visible_message( + span_danger("The spell bounces off of [victim]!"), + span_danger("The spell bounces off of you!"), + ) return FALSE - return TRUE -/obj/effect/proc_holder/spell/pointed/touch/mad_touch/cast(list/targets, mob/user) - . = ..() - for(var/mob/living/carbon/target in targets) - if(ishuman(targets)) - var/mob/living/carbon/human/tar = target - if(tar.anti_magic_check()) - tar.visible_message("The spell bounces off of [target]!","The spell bounces off of you!") - return - if(target.mind && !IS_HERETIC(target)) - to_chat(user, "[target.name] has been cursed!") - SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, "gates_of_mansus", /datum/mood_event/gates_of_mansus) + to_chat(caster, span_warning("[human_victim.name] has been cursed!")) + SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, "gates_of_mansus", /datum/mood_event/gates_of_mansus) + return TRUE diff --git a/code/modules/antagonists/heretic/magic/manse_link.dm b/code/modules/antagonists/heretic/magic/manse_link.dm index fcdd62467d944..1eb20455f72b7 100644 --- a/code/modules/antagonists/heretic/magic/manse_link.dm +++ b/code/modules/antagonists/heretic/magic/manse_link.dm @@ -1,73 +1,64 @@ -/obj/effect/proc_holder/spell/pointed/manse_link - name = "Mansus Link" - desc = "Piercing through reality, connecting minds. This spell allows you to add people to a Mansus Net, allowing them to communicate with each other from afar." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "mansus_link" - action_background_icon_state = "bg_ecult" - invocation = "PI'RC' TH' M'ND" - invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - charge_max = 300 - clothes_req = FALSE - range = 10 - -/obj/effect/proc_holder/spell/pointed/manse_link/can_target(atom/target, mob/user, silent) - if(!isliving(target)) - return FALSE - return TRUE - -/obj/effect/proc_holder/spell/pointed/manse_link/cast(list/targets, mob/user) - var/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/originator = user +/datum/action/cooldown/spell/pointed/manse_link + name = "Manse Link" + desc = "This spell allows you to pierce through reality and connect minds to one another \ + via your Mansus Link. All minds connected to your Mansus Link will be able to communicate discreetly across great distances." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "mansus_link" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' - var/mob/living/target = targets[1] + school = SCHOOL_FORBIDDEN + cooldown_time = 20 SECONDS - to_chat(originator, "You begin linking [target]'s mind to yours...") - to_chat(target, "You feel your mind being pulled... connected... intertwined with the very fabric of reality...") - if(!do_after(originator, 6 SECONDS, target = target)) - revert_cast() - return - if(!originator.link_mob(target)) - revert_cast() - to_chat(originator, "You can't seem to link [target]'s mind...") - to_chat(target, "The foreign presence leaves your mind.") - return - to_chat(originator, "You connect [target]'s mind to your mansus link!") + invocation = "PI'RC' TH' M'ND." + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + cast_range = 7 -/datum/action/innate/mansus_speech - name = "Mansus Link" - desc = "Send a psychic message to everyone connected to your Mansus Net." - button_icon_state = "link_speech" - icon_icon = 'icons/mob/actions/actions_slime.dmi' - background_icon_state = "bg_ecult" - /// The raw prophet that hosts our link. - var/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/originator + /// The time it takes to link to a mob. + var/link_time = 6 SECONDS -/datum/action/innate/mansus_speech/New(originator) +/datum/action/cooldown/spell/pointed/manse_link/New(Target) . = ..() - src.originator = originator + if(!istype(Target, /datum/component/mind_linker)) + stack_trace("[name] ([type]) was instantiated on a non-mind_linker target, this doesn't work.") + qdel(src) -/datum/action/innate/mansus_speech/Activate() - var/mob/living/living_owner = owner - if(!originator?.linked_mobs[living_owner]) - CRASH("Uh oh, a Mansus Link ([type]) got somehow called Activate() [isnull(originator) ? "without an originator Raw Prophet" : "without being in the originator's linked_mobs list"].") - - var/message = sanitize(input(living_owner, "Enter your message", "Telepathy from the Mansus")) - if(!message) - return +/datum/action/cooldown/spell/pointed/manse_link/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return FALSE - if(QDELETED(living_owner)) - return + return isliving(cast_on) - if(!originator?.linked_mobs[living_owner]) - to_chat(living_owner, "The link seems to have been severed...") - Remove(living_owner) +/datum/action/cooldown/spell/pointed/manse_link/before_cast(mob/living/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) return - var/msg = "\[Mansus Link\] [living_owner]: [message]" - log_directed_talk(living_owner, originator, msg, LOG_SAY, "Mansus Link") - to_chat(originator.linked_mobs, msg) + // If we fail to link, cancel the spell. + if(!do_linking(cast_on)) + return . | SPELL_CANCEL_CAST - for(var/dead_mob in GLOB.dead_mob_list) - var/link = FOLLOW_LINK(dead_mob, living_owner) - to_chat(dead_mob, "[link] [msg]") +/** + * The actual process of linking [linkee] to our network. + */ +/datum/action/cooldown/spell/pointed/manse_link/proc/do_linking(mob/living/linkee) + var/datum/component/mind_linker/linker = target + if(linkee.stat == DEAD) + to_chat(owner, span_warning("They're dead!")) + return FALSE + to_chat(owner, span_notice("You begin linking [linkee]'s mind to yours...")) + to_chat(linkee, span_warning("You feel your mind being pulled somewhere... connected... intertwined with the very fabric of reality...")) + if(!do_after(owner, link_time, linkee)) + to_chat(owner, span_warning("You fail to link to [linkee]'s mind.")) + to_chat(linkee, span_warning("The foreign presence leaves your mind.")) + return FALSE + if(QDELETED(src) || QDELETED(owner) || QDELETED(linkee)) + return FALSE + if(!linker.link_mob(linkee)) + to_chat(owner, span_warning("You can't seem to link to [linkee]'s mind.")) + to_chat(linkee, span_warning("The foreign presence leaves your mind.")) + return FALSE + return TRUE diff --git a/code/modules/antagonists/heretic/magic/mansus_grasp.dm b/code/modules/antagonists/heretic/magic/mansus_grasp.dm index 0fb467dce66db..75df42ac2cc20 100644 --- a/code/modules/antagonists/heretic/magic/mansus_grasp.dm +++ b/code/modules/antagonists/heretic/magic/mansus_grasp.dm @@ -1,94 +1,89 @@ -/obj/effect/proc_holder/spell/targeted/touch/mansus_grasp +/datum/action/cooldown/spell/touch/mansus_grasp name = "Mansus Grasp" desc = "A touch spell that lets you channel the power of the Old Gods through your grip." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "mansus_grasp" + sound = 'sound/items/welder.ogg' + + school = SCHOOL_EVOCATION + cooldown_time = 10 SECONDS + + invocation = "R'CH T'H TR'TH!" + invocation_type = INVOCATION_SHOUT + // Mimes can cast it. Chaplains can cast it. Anyone can cast it, so long as they have a hand. + spell_requirements = SPELL_CASTABLE_WITHOUT_INVOCATION + hand_path = /obj/item/melee/touch_attack/mansus_fist - charge_max = 100 - clothes_req = FALSE - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "mansus_grasp" - action_background_icon_state = "bg_ecult" + +/datum/action/cooldown/spell/touch/mansus_grasp/can_cast_spell(feedback = TRUE) + return ..() && !!IS_HERETIC(owner) + +/datum/action/cooldown/spell/touch/mansus_grasp/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!isliving(victim)) + return FALSE + + var/mob/living/living_hit = victim + if(living_hit.can_block_magic(antimagic_flags)) + victim.visible_message( + span_danger("The spell bounces off of [victim]!"), + span_danger("The spell bounces off of you!"), + ) + return FALSE + + if(SEND_SIGNAL(caster, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, victim) & COMPONENT_BLOCK_HAND_USE) + return FALSE + + living_hit.apply_damage(10, BRUTE, wound_bonus = CANT_WOUND) + if(iscarbon(victim)) + var/mob/living/carbon/carbon_hit = victim + carbon_hit.adjust_timed_status_effect(4 SECONDS, /datum/status_effect/speech/slurring/heretic) + carbon_hit.AdjustKnockdown(5 SECONDS) + carbon_hit.adjustStaminaLoss(80) + + return TRUE + +/datum/action/cooldown/spell/touch/mansus_grasp/cast_on_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(isliving(victim)) // if it's a living mob, go with our normal afterattack + return SECONDARY_ATTACK_CALL_NORMAL + + if(SEND_SIGNAL(caster, COMSIG_HERETIC_MANSUS_GRASP_ATTACK_SECONDARY, victim) & COMPONENT_USE_HAND) + return SECONDARY_ATTACK_CONTINUE_CHAIN + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN /obj/item/melee/touch_attack/mansus_fist name = "Mansus Grasp" - desc = "A sinister looking aura that distorts the flow of reality around it. Causes knockdown and major stamina damage in addition to some brute. It gains additional beneficial effects as you expand your knowledge of the Mansus." - icon_state = "mansus_grasp" - item_state = "mansus_grasp" - catchphrase = "R'CH T'H TR'TH!" - on_use_sound = 'sound/items/welder.ogg' + desc = "A sinister looking aura that distorts the flow of reality around it. \ + Causes knockdown, minor bruises, and major stamina damage. \ + It gains additional beneficial effects as you expand your knowledge of the Mansus." + icon_state = "mansus" + inhand_icon_state = "mansus" -/obj/item/melee/touch_attack/mansus_fist/Initialize(mapload, obj/effect/proc_holder/spell/targeted/touch/_spell) +/obj/item/melee/touch_attack/mansus_fist/Initialize(mapload) . = ..() AddComponent(/datum/component/effect_remover, \ success_feedback = "You remove %THEEFFECT.", \ - on_clear_callback = CALLBACK(src, PROC_REF(after_clear_rune)), \ + tip_text = "Clear rune", \ + on_clear_callback = CALLBACK(src, .proc/after_clear_rune), \ effects_we_clear = list(/obj/effect/heretic_rune)) - /* * Callback for effect_remover component. */ /obj/item/melee/touch_attack/mansus_fist/proc/after_clear_rune(obj/effect/target, mob/living/user) - use_charge(user, whisper = TRUE) - -/obj/item/melee/touch_attack/mansus_fist/ignition_effect(atom/to_light, mob/user) - . = "[user] effortlessly snaps [user.p_their()] fingers near [to_light], igniting it with eldritch energies. Fucking badass!" - use_charge(user) - -/obj/item/melee/touch_attack/mansus_fist/afterattack(atom/target, mob/user, proximity_flag, click_parameters) - if(!proximity_flag || !IS_HERETIC(user) || target == user) - return - if(ishuman(target) && antimagic_check(target, user)) - return ..() - - if(isliving(target)) - if(on_mob_hit(target, user)) - return - else - if(SEND_SIGNAL(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, target)) - use_charge(user) - return - - return ..() - -/** - * Checks if the [target] has some form of anti-magic. - * - * Returns TRUE If the attack was blocked. FALSE otherwise. - */ -/obj/item/melee/touch_attack/mansus_fist/proc/antimagic_check(mob/living/carbon/human/target, mob/living/carbon/user) - if(target.anti_magic_check()) - target.visible_message( - "The spell bounces off of [target]!", - "The spell bounces off of you!", - ) - return TRUE - return FALSE - -/** - * Called with [hit] is successfully hit by a mansus grasp by [heretic]. - * - * Sends signal COMSIG_HERETIC_MANSUS_GRASP_ATTACK. - * If it returns COMPONENT_BLOCK_CHARGE_USE, the proc returns FALSE. - * Otherwise, returns TRUE. - */ -/obj/item/melee/touch_attack/mansus_fist/proc/on_mob_hit(mob/living/hit, mob/living/heretic) - if(SEND_SIGNAL(heretic, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, hit) & COMPONENT_BLOCK_CHARGE_USE) - return FALSE + var/datum/action/cooldown/spell/touch/mansus_grasp/grasp = spell_which_made_us?.resolve() + grasp?.spell_feedback() - hit.adjustBruteLoss(10) - if(iscarbon(hit)) - var/mob/living/carbon/carbon_hit = hit - carbon_hit.AdjustKnockdown(5 SECONDS) - carbon_hit.adjustStaminaLoss(80) - carbon_hit.adjustBruteLoss(10) - carbon_hit.silent = 3 SECONDS + remove_hand_with_no_refund(user) - use_charge(heretic) - - return TRUE +/obj/item/melee/touch_attack/mansus_fist/ignition_effect(atom/to_light, mob/user) + . = span_notice("[user] effortlessly snaps [user.p_their()] fingers near [to_light], igniting it with eldritch energies. Fucking badass!") + remove_hand_with_no_refund(user) -/obj/item/melee/touch_attack/mansus_fist/suicide_act(mob/living/user) - user.visible_message("[user] covers [user.p_their()] face with [user.p_their()] sickly-looking hand! It looks like [user.p_theyre()] trying to commit suicide!") +/obj/item/melee/touch_attack/mansus_fist/suicide_act(mob/user) + user.visible_message(span_suicide("[user] covers [user.p_their()] face with [user.p_their()] sickly-looking hand! It looks like [user.p_theyre()] trying to commit suicide!")) var/mob/living/carbon/carbon_user = user //iscarbon already used in spell's parent + var/datum/action/cooldown/spell/touch/mansus_grasp/source = locate() in user.actions if(!IS_HERETIC(user)) return @@ -98,16 +93,15 @@ return SHAME if(escape_our_torment > 20) //Stops us from infinitely stunning ourselves if we're just not taking the damage return FIRELOSS - if(prob(70)) carbon_user.adjustFireLoss(20) - playsound(carbon_user, 'sound/items/welder.ogg', 70, vary = TRUE) + playsound(carbon_user, 'sound/effects/wounds/sizzle1.ogg', 70, vary = TRUE) if(prob(50)) carbon_user.emote("scream") - carbon_user.stuttering += 13 + carbon_user.adjust_timed_status_effect(26 SECONDS, /datum/status_effect/speech/stutter) - on_mob_hit(user, user) + source?.cast_on_hand_hit(src, user, user) escape_our_torment++ stoplag(0.4 SECONDS) - return FIRELOSS +return FIRELOSS diff --git a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm index 313c32721aa97..bbf6f2001f62e 100644 --- a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm +++ b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm @@ -1,44 +1,55 @@ -/obj/effect/proc_holder/spell/targeted/fiery_rebirth +/datum/action/cooldown/spell/aoe/fiery_rebirth name = "Nightwatcher's Rebirth" - desc = "A spell that extinguishes you and drains nearby heathens engulfed in flames of their life force, \ - healing you for each victim drained. Those in critical condition will have the last of their vitality drained, killing them." + desc = "A spell that extinguishes you drains nearby heathens engulfed in flames of their life force, \ + healing you for each victim drained. Those in critical condition \ + will have the last of their vitality drained, killing them." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "smoke" + + school = SCHOOL_FORBIDDEN + cooldown_time = 1 MINUTES + invocation = "GL'RY T' TH' N'GHT'W'TCH'ER" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - clothes_req = FALSE - action_background_icon_state = "bg_ecult" - range = -1 - include_user = TRUE - charge_max = 600 - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "smoke" - -/obj/effect/proc_holder/spell/targeted/fiery_rebirth/cast(list/targets, mob/living/carbon/human/user) - if(!istype(user)) - revert_cast() - return - var/did_something = user.on_fire // This might be a false negative if the user has items on fire but they themselves are not. - user.ExtinguishMob() - - for(var/mob/living/carbon/target in view(7, user)) - if(!target.mind || !target.client || target.stat == DEAD || !target.on_fire || IS_HERETIC_OR_MONSTER(target)) + spell_requirements = SPELL_REQUIRES_HUMAN + +/datum/action/cooldown/spell/aoe/fiery_rebirth/cast(mob/living/carbon/human/cast_on) + cast_on.extinguish_mob() + return ..() + +/datum/action/cooldown/spell/aoe/fiery_rebirth/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/mob/living/carbon/nearby_mob in range(aoe_radius, center)) + if(nearby_mob == owner || nearby_mob == center) + continue + if(!nearby_mob.mind || !nearby_mob.client) continue - //This is essentially a death mark, use this to finish your opponent quicker. - if(HAS_TRAIT(target, TRAIT_CRITICAL_CONDITION) && !HAS_TRAIT(target, TRAIT_NODEATH)) - target.investigate_log("has been killed by fiery rebirth.", INVESTIGATE_DEATHS) - target.death() + if(IS_HERETIC_OR_MONSTER(nearby_mob)) + continue + if(nearby_mob.stat == DEAD || !nearby_mob.on_fire) + continue + + things += nearby_mob + + return things + +/datum/action/cooldown/spell/aoe/fiery_rebirth/cast_on_thing_in_aoe(mob/living/carbon/victim, mob/living/carbon/human/caster) + new /obj/effect/temp_visual/eldritch_smoke(victim.drop_location()) - target.take_overall_damage(burn = 20) - new /obj/effect/temp_visual/eldritch_smoke(target.drop_location()) - user.heal_overall_damage(brute = 10, burn = 10, stamina = 10, updating_health = FALSE) - user.adjustToxLoss(-10, updating_health = FALSE, forced = TRUE) - user.adjustOxyLoss(-10) - did_something = TRUE + //This is essentially a death mark, use this to finish your opponent quicker. + if(HAS_TRAIT(victim, TRAIT_CRITICAL_CONDITION) && !HAS_TRAIT(victim, TRAIT_NODEATH)) + victim.death() + victim.apply_damage(20, BURN) - if(!did_something) - revert_cast() + // Heal the caster for every victim damaged + caster.adjustBruteLoss(-10, FALSE) + caster.adjustFireLoss(-10, FALSE) + caster.adjustToxLoss(-10, FALSE) + caster.adjustOxyLoss(-10, FALSE) + caster.adjustStaminaLoss(-10) /obj/effect/temp_visual/eldritch_smoke - icon = 'icons/effects/heretic.dmi' + icon = 'icons/effects/eldritch.dmi' icon_state = "smoke" - duration = 10 +duration = 10 diff --git a/code/modules/antagonists/heretic/magic/rust_wave.dm b/code/modules/antagonists/heretic/magic/rust_wave.dm index d4cb3641b22c9..673da51df0ce5 100644 --- a/code/modules/antagonists/heretic/magic/rust_wave.dm +++ b/code/modules/antagonists/heretic/magic/rust_wave.dm @@ -1,49 +1,48 @@ // Shoots out in a wave-like, what rust heretics themselves get -/obj/effect/proc_holder/spell/cone/staggered/entropic_plume +/datum/action/cooldown/spell/cone/staggered/entropic_plume name = "Entropic Plume" - desc = "Spews forth a disorienting plume that causes enemies to strike each other, briefly blinds them (increasing with range) and poisons them (decreasing with range). Also spreads rust in the path of the plume." - action_background_icon_state = "bg_ecult" - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "entropic_plume" + desc = "Spews forth a disorienting plume that causes enemies to strike each other, briefly blinds them(increasing with range) and poisons them(decreasing with range). Also spreads rust in the path of the plume." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "entropic_plume" + + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS + invocation = "'NTR'P'C PL'M'" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - clothes_req = FALSE - charge_max = 300 + spell_requirements = NONE + cone_levels = 5 respect_density = TRUE -/obj/effect/proc_holder/spell/cone/staggered/entropic_plume/cast(list/targets,mob/user = usr) +/datum/action/cooldown/spell/cone/staggered/entropic_plume/cast(atom/cast_on) . = ..() - new /obj/effect/temp_visual/dir_setting/entropic(get_step(user,user.dir), user.dir) + new /obj/effect/temp_visual/dir_setting/entropic(get_step(cast_on, cast_on.dir), cast_on.dir) -/obj/effect/proc_holder/spell/cone/staggered/entropic_plume/do_turf_cone_effect(turf/target_turf, level) - . = ..() +/datum/action/cooldown/spell/cone/staggered/entropic_plume/do_turf_cone_effect(turf/target_turf, atom/caster, level) target_turf.rust_heretic_act() -/obj/effect/proc_holder/spell/cone/staggered/entropic_plume/do_mob_cone_effect(mob/living/victim, level) - . = ..() - if(victim.anti_magic_check() || IS_HERETIC_OR_MONSTER(victim)) +/datum/action/cooldown/spell/cone/staggered/entropic_plume/do_mob_cone_effect(mob/living/victim, atom/caster, level) + if(victim.can_block_magic(antimagic_flags) || IS_HERETIC_OR_MONSTER(victim)) return victim.apply_status_effect(/datum/status_effect/amok) - victim.apply_status_effect(/datum/status_effect/cloudstruck, (level * 10)) + victim.apply_status_effect(/datum/status_effect/cloudstruck, (level * 1 SECONDS)) if(iscarbon(victim)) var/mob/living/carbon/carbon_victim = victim - carbon_victim.reagents.add_reagent(/datum/reagent/eldritch, min(1, 6 - level)) + carbon_victim.reagents?.add_reagent(/datum/reagent/eldritch, min(1, 6 - level)) -/obj/effect/proc_holder/spell/cone/staggered/entropic_plume/calculate_cone_shape(current_level) +/datum/action/cooldown/spell/cone/staggered/entropic_plume/calculate_cone_shape(current_level) if(current_level == cone_levels) return 5 - else if(current_level == cone_levels-1) + else if(current_level == cone_levels - 1) return 3 else return 2 - /obj/effect/temp_visual/dir_setting/entropic icon = 'icons/effects/160x160.dmi' icon_state = "entropic_plume" duration = 3 SECONDS - /obj/effect/temp_visual/dir_setting/entropic/setDir(dir) . = ..() switch(dir) @@ -59,20 +58,23 @@ pixel_x = -128 // Shoots a straight line of rusty stuff ahead of the caster, what rust monsters get -/obj/effect/proc_holder/spell/targeted/projectile/dumbfire/rust_wave +/datum/action/cooldown/spell/basic_projectile/rust_wave name = "Patron's Reach" desc = "Channels energy into your hands to release a wave of rust." - proj_type = /obj/projectile/magic/spell/rust_wave - requires_heretic_focus = TRUE - charge_max = 350 - clothes_req = FALSE - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "rust_wave" - action_background_icon_state = "bg_ecult" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "rust_wave" + + school = SCHOOL_FORBIDDEN + cooldown_time = 35 SECONDS + invocation = "SPR'D TH' WO'D" invocation_type = INVOCATION_WHISPER + spell_requirements = NONE + + projectile_type = /obj/projectile/magic/aoe/rust_wave -/obj/projectile/magic/spell/rust_wave +/obj/projectile/magic/aoe/rust_wave name = "Patron's Reach" icon_state = "eldritch_projectile" alpha = 180 @@ -84,7 +86,7 @@ range = 15 speed = 1 -/obj/projectile/magic/spell/rust_wave/Moved(atom/OldLoc, Dir) +/obj/projectile/magic/aoe/rust_wave/Moved(atom/OldLoc, Dir) . = ..() playsound(src, 'sound/items/welder.ogg', 75, TRUE) var/list/turflist = list() @@ -102,10 +104,10 @@ var/turf/T = X T.rust_heretic_act() -/obj/effect/proc_holder/spell/targeted/projectile/dumbfire/rust_wave/short - name = "Small Patron's Reach" - proj_type = /obj/projectile/magic/spell/rust_wave/short +/datum/action/cooldown/spell/basic_projectile/rust_wave/short + name = "Lesser Patron's Reach" + projectile_type = /obj/projectile/magic/aoe/rust_wave/short -/obj/projectile/magic/spell/rust_wave/short +/obj/projectile/magic/aoe/rust_wave/short range = 7 speed = 2 diff --git a/code/modules/antagonists/heretic/magic/void_phase.dm b/code/modules/antagonists/heretic/magic/void_phase.dm index 79ae103984ad0..44d322b70364c 100644 --- a/code/modules/antagonists/heretic/magic/void_phase.dm +++ b/code/modules/antagonists/heretic/magic/void_phase.dm @@ -1,45 +1,63 @@ -/obj/effect/proc_holder/spell/pointed/void_phase +/datum/action/cooldown/spell/pointed/void_phase name = "Void Phase" - desc = "Lets you blink to your pointed destination, causes 3x3 AoE damage bubble around your destination and your current location. It has a minimum range of 3 tiles and a maximum range of 9 tiles." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "voidblink" - action_background_icon_state = "bg_ecult" + desc = "Let's you blink to your pointed destination, causes 3x3 aoe damage bubble \ + around your pointed destination and your current location. \ + It has a minimum range of 3 tiles and a maximum range of 9 tiles." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "voidblink" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' + + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS + invocation = "RE'L'TY PH'S'E" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - selection_type = "range" - clothes_req = FALSE - range = 9 - charge_max = 300 + spell_requirements = NONE -/obj/effect/proc_holder/spell/pointed/void_phase/can_target(atom/target, mob/user, silent) - . = ..() - if(get_dist(get_turf(user), get_turf(target)) < 3 ) - user.balloon_alert(user, "Too close") + cast_range = 9 + /// The minimum range to cast the phase. + var/min_cast_range = 3 + /// The radius of damage around the void bubble + var/damage_radius = 1 + +/datum/action/cooldown/spell/pointed/void_phase/is_valid_target(atom/cast_on) + // We do the close range check first + if(get_dist(get_turf(owner), get_turf(cast_on)) < min_cast_range) + owner.balloon_alert(owner, "too close!") return FALSE -/obj/effect/proc_holder/spell/pointed/void_phase/cast(list/targets, mob/user) - . = ..() - var/target = targets[1] - var/turf/targeted_turf = get_turf(target) + return ..() - playsound(user,'sound/magic/voidblink.ogg',100) - playsound(targeted_turf,'sound/magic/voidblink.ogg',100) +/datum/action/cooldown/spell/pointed/void_phase/cast(atom/cast_on) + . = ..() + var/turf/source_turf = get_turf(owner) + var/turf/targeted_turf = get_turf(cast_on) - new /obj/effect/temp_visual/voidin(user.drop_location()) + new /obj/effect/temp_visual/voidin(source_turf) new /obj/effect/temp_visual/voidout(targeted_turf) - for(var/mob/living/living_mob in range(1, user) - user) - if(IS_HERETIC_OR_MONSTER(living_mob)) + // We handle sounds here so we can disable vary + playsound(source_turf, 'sound/magic/voidblink.ogg', 60, FALSE) + playsound(targeted_turf, 'sound/magic/voidblink.ogg', 60, FALSE) + + for(var/mob/living/living_mob in range(damage_radius, source_turf)) + if(IS_HERETIC_OR_MONSTER(living_mob) || living_mob == cast_on) continue - living_mob.adjustBruteLoss(40) + living_mob.apply_damage(40, BRUTE, wound_bonus = CANT_WOUND) - for(var/mob/living/living_mob in range(1, targeted_turf) - user) - if(IS_HERETIC_OR_MONSTER(living_mob)) + for(var/mob/living/living_mob in range(damage_radius, targeted_turf)) + if(IS_HERETIC_OR_MONSTER(living_mob) || living_mob == cast_on) continue - living_mob.adjustBruteLoss(40) + living_mob.apply_damage(40, BRUTE, wound_bonus = CANT_WOUND) - do_teleport(user,targeted_turf,TRUE,no_effects = TRUE,channel=TELEPORT_CHANNEL_MAGIC) + do_teleport( + owner, + targeted_turf, + precision = 1, + no_effects = TRUE, + channel = TELEPORT_CHANNEL_MAGIC, + ) /obj/effect/temp_visual/voidin icon = 'icons/effects/96x96.dmi' @@ -48,11 +66,10 @@ duration = 6 pixel_x = -32 pixel_y = -32 - /obj/effect/temp_visual/voidout icon = 'icons/effects/96x96.dmi' icon_state = "void_blink_out" alpha = 150 duration = 6 pixel_x = -32 - pixel_y = -32 +pixel_y = -32 diff --git a/code/modules/antagonists/heretic/magic/void_pull.dm b/code/modules/antagonists/heretic/magic/void_pull.dm index 3dafc153a3e80..f8063f8635fe9 100644 --- a/code/modules/antagonists/heretic/magic/void_pull.dm +++ b/code/modules/antagonists/heretic/magic/void_pull.dm @@ -1,31 +1,60 @@ -/obj/effect/proc_holder/spell/targeted/void_pull +/datum/action/cooldown/spell/aoe/void_pull name = "Void Pull" - desc = "Call the void, this pulls all nearby people closer to you and damages people already around you. If they are 4 tiles or closer they are also knocked down and a short stun is applied." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "voidpull" - action_background_icon_state = "bg_ecult" + desc = "Calls the void, damaging, knocking down, and stunning people nearby. \ + Distant foes are also pulled closer to you (but not damaged)." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "voidpull" + sound = 'sound/magic/voidblink.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 40 SECONDS + invocation = "BR'NG F'RTH TH'M T' M'" invocation_type = INVOCATION_WHISPER - requires_heretic_focus = TRUE - clothes_req = FALSE - range = -1 - include_user = TRUE - charge_max = 400 + spell_requirements = NONE + + aoe_radius = 7 + /// The radius of the actual damage circle done before cast + var/damage_radius = 1 + /// The radius of the stun applied to nearby people on cast + var/stun_radius = 4 -/obj/effect/proc_holder/spell/targeted/void_pull/cast(list/targets, mob/user) +// Before the cast, we do some small AOE damage around the caster +/datum/action/cooldown/spell/aoe/void_pull/before_cast(atom/cast_on) . = ..() - for(var/mob/living/living_mob in range(1, user) - user) - if(IS_HERETIC_OR_MONSTER(living_mob)) + if(. & SPELL_CANCEL_CAST) + return + + new /obj/effect/temp_visual/voidin(get_turf(cast_on)) + + // Before we cast the actual effects, deal AOE damage to anyone adjacent to us + var/list/mob/living/people_near_us = get_things_to_cast_on(cast_on, damage_radius) + for(var/mob/living/nearby_living as anything in people_near_us) + nearby_living.apply_damage(30, BRUTE, wound_bonus = CANT_WOUND) + +/datum/action/cooldown/spell/aoe/void_pull/get_things_to_cast_on(atom/center, radius_override = 0) + var/list/things = list() + for(var/mob/living/nearby_mob in view(radius_override || aoe_radius, center)) + if(nearby_mob == owner || nearby_mob == center) + continue + // Don't grab people who are tucked away or something + if(!isturf(nearby_mob.loc)) continue - living_mob.adjustBruteLoss(30) + if(IS_HERETIC_OR_MONSTER(nearby_mob)) + continue + + things += nearby_mob - playsound(user,'sound/magic/voidblink.ogg',100) - new /obj/effect/temp_visual/voidin(user.drop_location()) - for(var/mob/living/livies in view(7, user) - user) + return things - if(get_dist(user, livies) < 4) - livies.AdjustKnockdown(3 SECONDS) - livies.AdjustParalyzed(0.5 SECONDS) +// For the actual cast, we microstun people nearby and pull them in +/datum/action/cooldown/spell/aoe/void_pull/cast_on_thing_in_aoe(mob/living/victim, atom/caster) + // If the victim's within the stun radius, they're stunned / knocked down + if(get_dist(victim, caster) < stun_radius) + victim.AdjustKnockdown(3 SECONDS) + victim.AdjustParalyzed(0.5 SECONDS) - for(var/i in 1 to 3) - livies.forceMove(get_step_towards(livies,user)) + // Otherwise, they take a few steps closer + for(var/i in 1 to 3) + victim.forceMove(get_step_towards(victim, caster)) diff --git a/code/modules/antagonists/revenant/revenant.dm b/code/modules/antagonists/revenant/revenant.dm index 4dcd3d8d278b0..9d7cc777ec9ba 100644 --- a/code/modules/antagonists/revenant/revenant.dm +++ b/code/modules/antagonists/revenant/revenant.dm @@ -75,13 +75,20 @@ /mob/living/simple_animal/revenant/Initialize(mapload) . = ..() // more rev abilities are in 'revenant_abilities.dm' - AddSpell(new /obj/effect/proc_holder/spell/targeted/night_vision/revenant(null)) - AddSpell(new /obj/effect/proc_holder/spell/self/revenant_phase_shift(null)) - AddSpell(new /obj/effect/proc_holder/spell/targeted/telepathy/revenant(null)) - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/revenant/defile(null)) - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/revenant/overload(null)) - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/revenant/blight(null)) - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/revenant/malfunction(null)) + // Starting spells + var/datum/action/cooldown/spell/night_vision/revenant/vision = new(src) + vision.Grant(src) + var/datum/action/cooldown/spell/list_target/telepathy/revenant/telepathy = new(src) + telepathy.Grant(src) + // Starting spells that start locked + var/datum/action/cooldown/spell/aoe/revenant/overload/lights_go_zap = new(src) + lights_go_zap.Grant(src) + var/datum/action/cooldown/spell/aoe/revenant/defile/windows_go_smash = new(src) + windows_go_smash.Grant(src) + var/datum/action/cooldown/spell/aoe/revenant/blight/botany_go_mad = new(src) + botany_go_mad.Grant(src) + var/datum/action/cooldown/spell/aoe/revenant/malfunction/shuttle_go_emag = new(src) + shuttle_go_emag.Grant(src) check_rev_teleport() // they're spawned in non-station for some reason... random_revenant_name() AddComponent(/datum/component/tracking_beacon, "ghost", null, null, TRUE, "#9e4d91", TRUE, TRUE, "#490066") diff --git a/code/modules/antagonists/revenant/revenant_abilities.dm b/code/modules/antagonists/revenant/revenant_abilities.dm index 2ca00e5b20de5..2a23b6cd6afe9 100644 --- a/code/modules/antagonists/revenant/revenant_abilities.dm +++ b/code/modules/antagonists/revenant/revenant_abilities.dm @@ -150,361 +150,266 @@ // ------------------------------------------- //Toggle night vision: lets the revenant toggle its night vision -/obj/effect/proc_holder/spell/targeted/night_vision/revenant - charge_max = 0 +/datum/action/cooldown/spell/night_vision/revenant + name = "Toggle Darkvision" panel = "Revenant Abilities" - message = "You toggle your night vision." - action_icon = 'icons/mob/actions/actions_revenant.dmi' - action_icon_state = "r_nightvision" - action_background_icon_state = "bg_revenant" - -// Recall to Station: teleport & recall to the station -/obj/effect/proc_holder/spell/self/rev_teleport - name = "Recall to Station" - desc = "Teleport to the station." - charge_max = 0 - panel = "Revenant Abilities" - action_icon = 'icons/mob/actions/actions_revenant.dmi' - action_icon_state = "r_teleport" - action_background_icon_state = "bg_revenant" - clothes_req = FALSE - -/obj/effect/proc_holder/spell/self/rev_teleport/cast(mob/living/simple_animal/revenant/user = usr) - if(!isrevenant(user)) - to_chat(user, "You are not revenant.") - return - if(is_station_level(user.z)) - to_chat(user, "Recalling yourself to the station is only available when you're not in the station.") - return - else - if(user.revealed) - to_chat(user, "Recalling yourself to the station is only available when you're invisible.") - return - - to_chat(user, "You start to concentrate recalling yourself to the station.") - if(do_after(user, 30) && !user.revealed) - if(QDELETED(src)) // it's bad when someone spams this... - return - var/turf/targetturf = get_random_station_turf() - if(!do_teleport(user, targetturf, channel = TELEPORT_CHANNEL_CULT, forced=TRUE)) - to_chat(user, "You have failed to recall yourself to the station... You should try again.") - else - user.reveal(80) - user.stun(40) + background_icon_state = "bg_revenant" + icon_icon = 'icons/mob/actions/actions_revenant.dmi' + button_icon_state = "r_nightvision" + toggle_span = "revennotice" //Transmit: the revemant's only direct way to communicate. Sends a single message silently to a single mob -/obj/effect/proc_holder/spell/targeted/telepathy/revenant +/datum/action/cooldown/spell/list_target/telepathy/revenant name = "Revenant Transmit" panel = "Revenant Abilities" - action_icon = 'icons/mob/actions/actions_revenant.dmi' - action_icon_state = "r_transmit" - action_background_icon_state = "bg_revenant" - notice = "revennotice" - boldnotice = "revenboldnotice" - holy_check = TRUE - -/obj/effect/proc_holder/spell/targeted/telepathy/revenant/cast(list/targets, mob/living/simple_animal/revenant/user = usr) - for(var/mob/living/M in targets) - if(istype(M.get_item_by_slot(ITEM_SLOT_HEAD), /obj/item/clothing/head/costume/foilhat)) - to_chat(user, "It appears the target's mind is ironclad! No getting a message in there!") - return - if(M.anti_magic_check(magic_check, holy_check)) //hear no evil - to_chat(user, "Something is blocking your power into their mind!") - return - - - var/msg = stripped_input(usr, "What do you wish to tell [M]?", null, "") - if(!msg) - charge_counter = charge_max - return - if(CHAT_FILTER_CHECK(msg)) - to_chat(user, "Your message contains forbidden words.") - return - msg = user.treat_message_min(msg) - log_directed_talk(user, M, msg, LOG_SAY, "[name]") - - to_chat(user, "You transmit to [M]: [msg]") - to_chat(M, "You hear something haunting... [msg]") - user.create_private_chat_message(message="...[msg]", - message_language = /datum/language/metalanguage, - hearers=list(user, M)) - for(var/ded in GLOB.dead_mob_list) - if(!isobserver(ded)) - continue - var/follow_rev = FOLLOW_LINK(ded, user) - var/follow_whispee = FOLLOW_LINK(ded, M) - to_chat(ded, "[follow_rev] [user] [name]: \"[msg]\" to [follow_whispee] [M]") - - -/obj/effect/proc_holder/spell/self/revenant_phase_shift - name = "Phase Shift" - desc = "Shift in and out of your corporeal form" - panel = "Revenant Abilities" - action_icon = 'icons/mob/actions/actions_revenant.dmi' - action_icon_state = "r_phase" - action_background_icon_state = "bg_revenant" - clothes_req = FALSE - charge_max = 0 - -/obj/effect/proc_holder/spell/self/revenant_phase_shift/cast(mob/user = usr) - if(!isrevenant(user)) - return FALSE - var/mob/living/simple_animal/revenant/revenant = user - if(!revenant.castcheck(0)) - return FALSE - // if they're trapped in consecrated tiles, they can get out with this. but they can't hide back on these tiles. - if(revenant.incorporeal_move != INCORPOREAL_MOVE_JAUNT) - var/turf/open/floor/stepTurf = get_turf(user) - if(stepTurf) - var/obj/effect/decal/cleanable/food/salt/salt = locate() in stepTurf - if(salt) - to_chat(user, "[salt] blocks your way to spirit realm!") - // the purpose is just letting not them hide onto salt tiles incorporeally. no need to stun. - return - if(stepTurf.flags_1 & NOJAUNT_1) - to_chat(user, "Some strange aura blocks your way to spirit realm.") - return - if(stepTurf.is_holy()) - to_chat(user, "Holy energies block your way to spirit realm!") - return - revenant.phase_shift() - revenant.orbiting?.end_orbit(revenant) - -/obj/effect/proc_holder/spell/aoe_turf/revenant - clothes_req = 0 - action_icon = 'icons/mob/actions/actions_revenant.dmi' - action_background_icon_state = "bg_revenant" + background_icon_state = "bg_revenant" + + telepathy_span = "revennotice" + bold_telepathy_span = "revenboldnotice" + + antimagic_flags = MAGIC_RESISTANCE_HOLY|MAGIC_RESISTANCE_MIND + +/datum/action/cooldown/spell/aoe/revenant panel = "Revenant Abilities (Locked)" - name = "Report this to a coder" - var/reveal = 80 //How long it reveals the revenant in deciseconds - var/stun = 20 //How long it stuns the revenant in deciseconds - var/locked = TRUE //revenant needs to pay essence to learn their ability - var/unlock_amount = 100 //How much essence it costs to unlock - var/cast_amount = 50 //How much essence it costs to use - -/obj/effect/proc_holder/spell/aoe_turf/revenant/Initialize(mapload) + background_icon_state = "bg_revenant" + icon_icon = 'icons/mob/actions/actions_revenant.dmi' + + antimagic_flags = MAGIC_RESISTANCE_HOLY + spell_requirements = NONE + + /// If it's locked, and needs to be unlocked before use + var/locked = TRUE + /// How much essence it costs to unlock + var/unlock_amount = 100 + /// How much essence it costs to use + var/cast_amount = 50 + + /// How long it reveals the revenant + var/reveal_duration = 8 SECONDS + // How long it stuns the revenant + var/stun_duration = 2 SECONDS + +/datum/action/cooldown/spell/aoe/revenant/New(Target) . = ..() - update_button_info() + if(!istype(target, /mob/living/simple_animal/revenant)) + stack_trace("[type] was given to a non-revenant mob, please don't.") + qdel(src) + return -/obj/effect/proc_holder/spell/aoe_turf/revenant/proc/update_button_info() - if(!locked) - action.name = "[initial(name)][cast_amount ? " ([cast_amount]E to cast)" : ""]" + if(locked) + name = "[initial(name)] ([unlock_amount]SE)" else - action.name = "[initial(name)][unlock_amount ? " ([unlock_amount]SE to learn)" : ""]" - action.UpdateButtonIcon() + name = "[initial(name)] ([cast_amount]E)" -/obj/effect/proc_holder/spell/aoe_turf/revenant/can_cast(mob/living/simple_animal/revenant/user = usr) - if(charge_counter < charge_max) +/datum/action/cooldown/spell/aoe/revenant/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) return FALSE - if(!isrevenant(user)) // If you're not a revenant, it works anyway. - return TRUE - if(user.inhibited) + if(!istype(owner, /mob/living/simple_animal/revenant)) + stack_trace("[type] was owned by a non-revenant mob, please don't.") return FALSE - if(locked) - if(user.essence_excess <= unlock_amount) - return FALSE - if(user.essence <= cast_amount) + + var/mob/living/simple_animal/revenant/ghost = owner + if(ghost.inhibited) + return FALSE + if(locked && ghost.essence_excess <= unlock_amount) + return FALSE + if(ghost.essence <= cast_amount) return FALSE + return TRUE -/obj/effect/proc_holder/spell/aoe_turf/revenant/proc/attempt_cast(mob/living/simple_animal/revenant/user = usr) - // If you're not a revenant, it works anyway. - if(!isrevenant(user)) - if(locked) - locked = FALSE - panel = "Revenant Abilities" - action.name = "[initial(name)]" - action.UpdateButtonIcon() - return TRUE - - // actual revenant check +/datum/action/cooldown/spell/aoe/revenant/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/turf/nearby_turf in range(aoe_radius, center)) + things += nearby_turf + + return things + +/datum/action/cooldown/spell/aoe/revenant/before_cast(mob/living/simple_animal/revenant/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return FALSE + if(locked) - if (!user.unlock(unlock_amount)) - charge_counter = charge_max - return FALSE - to_chat(user, "You have unlocked [initial(name)]!") + if(!cast_on.unlock(unlock_amount)) + to_chat(cast_on, span_revenwarning("You don't have enough essence to unlock [initial(name)]!")) + reset_spell_cooldown() + return . | SPELL_CANCEL_CAST + + name = "[initial(name)] ([cast_amount]E)" + to_chat(cast_on, span_revennotice("You have unlocked [initial(name)]!")) panel = "Revenant Abilities" locked = FALSE - charge_counter = charge_max - update_button_info() - return FALSE - if(!user.castcheck(-cast_amount)) - charge_counter = charge_max - return FALSE - user.reveal(reveal) - user.stun(stun) - if(action) - action.UpdateButtonIcon() - return TRUE + reset_spell_cooldown() + return . | SPELL_CANCEL_CAST + + if(!cast_on.castcheck(-cast_amount)) + reset_spell_cooldown() + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/aoe/revenant/after_cast(mob/living/simple_animal/revenant/cast_on) + . = ..() + if(reveal_duration > 0 SECONDS) + cast_on.reveal(reveal_duration) + if(stun_duration > 0 SECONDS) + cast_on.stun(stun_duration) //Overload Light: Breaks a light that's online and sends out lightning bolts to all nearby people. -/obj/effect/proc_holder/spell/aoe_turf/revenant/overload +/datum/action/cooldown/spell/aoe/revenant/overload name = "Overload Lights" desc = "Directs a large amount of essence into nearby electrical lights, causing lights to shock those nearby." - charge_max = 200 - range = 5 - stun = 30 + button_icon_state = "overload_lights" + cooldown_time = 20 SECONDS + + aoe_radius = 5 unlock_amount = 25 cast_amount = 40 + stun_duration = 3 SECONDS + + /// The range the shocks from the lights go var/shock_range = 2 + /// The damage the shcoskf rom the lgihts do var/shock_damage = 15 - action_icon_state = "overload_lights" - -/obj/effect/proc_holder/spell/aoe_turf/revenant/overload/cast(list/targets, mob/living/simple_animal/revenant/user = usr) - if(attempt_cast(user)) - for(var/turf/T in targets) - INVOKE_ASYNC(src, PROC_REF(overload), T, user) - -/obj/effect/proc_holder/spell/aoe_turf/revenant/overload/proc/overload(turf/T, mob/user) - for(var/obj/machinery/light/L in T) - if(!L.on) - return - L.visible_message("\The [L] suddenly flares brightly and begins to spark!") - var/datum/effect_system/spark_spread/s = new /datum/effect_system/spark_spread - s.set_up(4, 0, L) - s.start() - new /obj/effect/temp_visual/revenant(get_turf(L)) - addtimer(CALLBACK(src, PROC_REF(overload_shock), L, user), 20) - -/obj/effect/proc_holder/spell/aoe_turf/revenant/overload/proc/overload_shock(obj/machinery/light/L, mob/user) - if(!L.on) //wait, wait, don't shock me - return - flick("[L.base_state]2", L) - for(var/mob/living/carbon/human/M in hearers(shock_range, L)) - if(M == user) + +/datum/action/cooldown/spell/aoe/revenant/overload/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster) + for(var/obj/machinery/light/light in victim) + if(!light.on) + continue + + light.visible_message(span_boldwarning("[light] suddenly flares brightly and begins to spark!")) + var/datum/effect_system/spark_spread/light_sparks = new /datum/effect_system/spark_spread() + light_sparks.set_up(4, 0, light) + light_sparks.start() + new /obj/effect/temp_visual/revenant(get_turf(light)) + addtimer(CALLBACK(src, .proc/overload_shock, light, caster), 20) + +/datum/action/cooldown/spell/aoe/revenant/overload/proc/overload_shock(obj/machinery/light/to_shock, mob/living/simple_animal/revenant/caster) + flick("[to_shock.base_state]2", to_shock) + for(var/mob/living/carbon/human/human_mob in view(shock_range, to_shock)) + if(human_mob == caster) continue - L.Beam(M,icon_state="purple_lightning", time = 5) - if(!M.anti_magic_check(FALSE, TRUE)) - M.electrocute_act(shock_damage, L, flags = SHOCK_NOGLOVES) - do_sparks(4, FALSE, M) - playsound(M, 'sound/machines/defib_zap.ogg', 50, 1, -1) + to_shock.Beam(human_mob, icon_state = "purple_lightning", time = 0.5 SECONDS) + if(!human_mob.can_block_magic(antimagic_flags)) + human_mob.electrocute_act(shock_damage, to_shock, flags = SHOCK_NOGLOVES) + + do_sparks(4, FALSE, human_mob) + playsound(human_mob, 'sound/machines/defib_zap.ogg', 50, TRUE, -1) //Defile: Corrupts nearby stuff, unblesses floor tiles. -/obj/effect/proc_holder/spell/aoe_turf/revenant/defile +/datum/action/cooldown/spell/aoe/revenant/defile name = "Defile" desc = "Twists and corrupts the nearby area as well as dispelling holy auras on floors." - charge_max = 150 - range = 4 - stun = 20 - reveal = 40 + button_icon_state = "defile" + cooldown_time = 15 SECONDS + + aoe_radius = 4 unlock_amount = 10 cast_amount = 30 - action_icon_state = "defile" + reveal_duration = 4 SECONDS + stun_duration = 2 SECONDS -/obj/effect/proc_holder/spell/aoe_turf/revenant/defile/cast(list/targets, mob/living/simple_animal/revenant/user = usr) - if(attempt_cast(user)) - for(var/turf/T in targets) - INVOKE_ASYNC(src, PROC_REF(defile), T) +/datum/action/cooldown/spell/aoe/revenant/defile/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster) + for(var/obj/effect/blessing/blessing in victim) + qdel(blessing) + new /obj/effect/temp_visual/revenant(victim) -/obj/effect/proc_holder/spell/aoe_turf/revenant/defile/proc/defile(turf/T) - for(var/obj/effect/blessing/B in T) - qdel(B) - new /obj/effect/temp_visual/revenant(T) - - if(!isplatingturf(T) && !istype(T, /turf/open/floor/engine/cult) && isfloorturf(T) && prob(15)) - var/turf/open/floor/floor = T + if(!isplatingturf(victim) && !istype(victim, /turf/open/floor/engine/cult) && isfloorturf(victim) && prob(15)) + var/turf/open/floor/floor = victim if(floor.overfloor_placed && floor.floor_tile) new floor.floor_tile(floor) floor.broken = 0 floor.burnt = 0 - floor.make_plating(1) - if(T.type == /turf/closed/wall && prob(15)) - new /obj/effect/temp_visual/revenant(T) - T.AddElement(/datum/element/rust) - if(T.type == /turf/closed/wall/r_wall && prob(10)) - new /obj/effect/temp_visual/revenant(T) - T.AddElement(/datum/element/rust) - for(var/obj/effect/decal/cleanable/food/salt/salt in T) - new /obj/effect/temp_visual/revenant(T) + floor.make_plating(TRUE) + + if(victim.type == /turf/closed/wall && prob(15) && !HAS_TRAIT(victim, TRAIT_RUSTY)) + new /obj/effect/temp_visual/revenant(victim) + victim.AddElement(/datum/element/rust) + if(victim.type == /turf/closed/wall/r_wall && prob(10) && !HAS_TRAIT(victim, TRAIT_RUSTY)) + new /obj/effect/temp_visual/revenant(victim) + victim.AddElement(/datum/element/rust) + for(var/obj/effect/decal/cleanable/food/salt/salt in victim) + new /obj/effect/temp_visual/revenant(victim) qdel(salt) - for(var/obj/structure/closet/closet in T.contents) + for(var/obj/structure/closet/closet in victim.contents) closet.open() - for(var/obj/structure/bodycontainer/corpseholder in T) + for(var/obj/structure/bodycontainer/corpseholder in victim) if(corpseholder.connected.loc == corpseholder) corpseholder.open() - for(var/obj/machinery/dna_scannernew/dna in T) + for(var/obj/machinery/dna_scannernew/dna in victim) dna.open_machine() - for(var/obj/structure/window/window in T) - window.take_damage(rand(30,80)) - if(window && window.fulltile) + for(var/obj/structure/window/window in victim) + window.take_damage(rand(30, 80)) + if(window?.fulltile) new /obj/effect/temp_visual/revenant/cracks(window.loc) - for(var/obj/machinery/light/light in T) + for(var/obj/machinery/light/light in victim) light.flicker(20) //spooky //Malfunction: Makes bad stuff happen to robots and machines. -/obj/effect/proc_holder/spell/aoe_turf/revenant/malfunction +/datum/action/cooldown/spell/aoe/revenant/malfunction name = "Malfunction" desc = "Corrupts and damages nearby machines and mechanical objects." - charge_max = 200 - range = 4 + button_icon_state = "malfunction" + cooldown_time = 20 SECONDS + + aoe_radius = 4 cast_amount = 60 unlock_amount = 125 - action_icon_state = "malfunction" - -//A note to future coders: do not replace this with an EMP because it will wreck malf AIs and everyone will hate you. -/obj/effect/proc_holder/spell/aoe_turf/revenant/malfunction/cast(list/targets, mob/living/simple_animal/revenant/user = usr) - if(attempt_cast(user)) - for(var/turf/T in targets) - INVOKE_ASYNC(src, PROC_REF(malfunction), T, user) -/obj/effect/proc_holder/spell/aoe_turf/revenant/malfunction/proc/malfunction(turf/T, mob/user) - for(var/mob/living/simple_animal/bot/bot in T) - if(!bot.emagged) +// A note to future coders: do not replace this with an EMP because it will wreck malf AIs and everyone will hate you. +/datum/action/cooldown/spell/aoe/revenant/malfunction/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster) + for(var/mob/living/simple_animal/bot/bot in victim) + if(!(bot.bot_cover_flags & BOT_COVER_EMAGGED)) new /obj/effect/temp_visual/revenant(bot.loc) - bot.locked = FALSE - bot.open = TRUE - bot.use_emag() - for(var/mob/living/carbon/human/human in T) - if(human == user) + bot.bot_cover_flags &= ~BOT_COVER_LOCKED + bot.bot_cover_flags |= BOT_COVER_OPEN + bot.emag_act(caster) + for(var/mob/living/carbon/human/human in victim) + if(human == caster) continue - if(human.anti_magic_check(FALSE, TRUE)) + if(human.can_block_magic(antimagic_flags)) continue - to_chat(human, "You feel [pick("your sense of direction flicker out", "a stabbing pain in your head", "your mind fill with static")].") + to_chat(human, span_revenwarning("You feel [pick("your sense of direction flicker out", "a stabbing pain in your head", "your mind fill with static")].")) new /obj/effect/temp_visual/revenant(human.loc) human.emp_act(EMP_HEAVY) - for(var/obj/thing in T) - if(istype(thing, /obj/machinery/power/apc) || istype(thing, /obj/machinery/power/smes)) //Doesn't work on SMES and APCs, to prevent kekkery + for(var/obj/thing in victim) + //Doesn't work on SMES and APCs, to prevent kekkery. + if(istype(thing, /obj/machinery/power/apc) || istype(thing, /obj/machinery/power/smes)) continue if(prob(20)) if(prob(50)) new /obj/effect/temp_visual/revenant(thing.loc) - thing.use_emag(null) - else - if(!istype(thing, /obj/machinery/clonepod)) //I hate everything but mostly the fact there's no better way to do this without just not affecting it at all - thing.emp_act(EMP_HEAVY) - for(var/mob/living/silicon/robot/S in T) //Only works on cyborgs, not AI - playsound(S, 'sound/machines/warning-buzzer.ogg', 50, 1) - new /obj/effect/temp_visual/revenant(S.loc) - S.spark_system.start() - S.emp_act(EMP_HEAVY) + thing.emag_act(caster) + // Only works on cyborgs, not AI! + for(var/mob/living/silicon/robot/cyborg in victim) + playsound(cyborg, 'sound/machines/warning-buzzer.ogg', 50, TRUE) + new /obj/effect/temp_visual/revenant(cyborg.loc) + cyborg.spark_system.start() + cyborg.emp_act(EMP_HEAVY) //Blight: Infects nearby humans and in general messes living stuff up. -/obj/effect/proc_holder/spell/aoe_turf/revenant/blight +/datum/action/cooldown/spell/aoe/revenant/blight name = "Blight" desc = "Causes nearby living things to waste away." - charge_max = 200 - range = 3 + button_icon_state = "blight" + cooldown_time = 20 SECONDS + + aoe_radius = 3 cast_amount = 50 unlock_amount = 75 - action_icon_state = "blight" - -/obj/effect/proc_holder/spell/aoe_turf/revenant/blight/cast(list/targets, mob/living/simple_animal/revenant/user = usr) - if(attempt_cast(user)) - for(var/turf/T in targets) - INVOKE_ASYNC(src, PROC_REF(blight), T, user) -/obj/effect/proc_holder/spell/aoe_turf/revenant/blight/proc/blight(turf/T, mob/user) - for(var/mob/living/mob in T) - if(mob == user) +/datum/action/cooldown/spell/aoe/revenant/blight/cast_on_thing_in_aoe(turf/victim, mob/living/simple_animal/revenant/caster) + for(var/mob/living/mob in victim) + if(mob == caster) continue - if(mob.anti_magic_check(FALSE, TRUE)) + if(mob.can_block_magic(antimagic_flags)) + to_chat(caster, span_warning("The spell had no effect on [mob]!")) continue new /obj/effect/temp_visual/revenant(mob.loc) if(iscarbon(mob)) if(ishuman(mob)) var/mob/living/carbon/human/H = mob - if(H.dna?.species) - H.dna.species.handle_hair(H,"#1d2953") //will be reset when blight is cured + H.set_haircolor("#1d2953", override = TRUE) //will be reset when blight is cured var/blightfound = FALSE for(var/datum/disease/revblight/blight in H.diseases) blightfound = TRUE @@ -512,22 +417,21 @@ blight.stage++ if(!blightfound) H.ForceContractDisease(new /datum/disease/revblight(), FALSE, TRUE) - to_chat(H, "You feel [pick("suddenly sick", "a surge of nausea", "like your skin is wrong")].") + to_chat(H, span_revenminor("You feel [pick("suddenly sick", "a surge of nausea", "like your skin is wrong")].")) else if(mob.reagents) mob.reagents.add_reagent(/datum/reagent/toxin/plasma, 5) else mob.adjustToxLoss(5) - for(var/obj/structure/spacevine/vine in T) //Fucking with botanists, the ability. + for(var/obj/structure/spacevine/vine in victim) //Fucking with botanists, the ability. vine.add_atom_colour("#823abb", TEMPORARY_COLOUR_PRIORITY) new /obj/effect/temp_visual/revenant(vine.loc) QDEL_IN(vine, 10) - for(var/obj/structure/glowshroom/shroom in T) + for(var/obj/structure/glowshroom/shroom in victim) shroom.add_atom_colour("#823abb", TEMPORARY_COLOUR_PRIORITY) new /obj/effect/temp_visual/revenant(shroom.loc) QDEL_IN(shroom, 10) - for(var/obj/machinery/hydroponics/tray in T) + for(var/obj/machinery/hydroponics/tray in victim) new /obj/effect/temp_visual/revenant(tray.loc) - tray.pestlevel = rand(8, 10) - tray.weedlevel = rand(8, 10) - tray.toxic = rand(45, 55) + tray.set_pestlevel(rand(8, 10)) + tray.set_weedlevel(rand(8, 10)) diff --git a/code/modules/antagonists/santa/santa.dm b/code/modules/antagonists/santa/santa.dm index 563e89059cb82..77b7928ea0ca5 100644 --- a/code/modules/antagonists/santa/santa.dm +++ b/code/modules/antagonists/santa/santa.dm @@ -23,7 +23,8 @@ H.equipOutfit(/datum/outfit/santa) H.dna.update_dna_identity() - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/area_teleport/teleport/santa) + var/datum/action/cooldown/spell/teleport/area_teleport/wizard/santa/teleport = new(owner) + teleport.Grant(H) /datum/antagonist/santa/proc/give_objective() if(!give_objectives) diff --git a/code/modules/antagonists/slaughter/slaughter.dm b/code/modules/antagonists/slaughter/slaughter.dm index 696dbe1d60cd9..205b4bcb98d93 100644 --- a/code/modules/antagonists/slaughter/slaughter.dm +++ b/code/modules/antagonists/slaughter/slaughter.dm @@ -50,15 +50,29 @@ /obj/effect/decal/cleanable/blood/innards, \ /obj/item/organ/heart/demon) del_on_death = TRUE + var/crawl_type = /datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon deathmessage = "screams in anger as it collapses into a puddle of viscera!" discovery_points = 3000 -/mob/living/simple_animal/slaughter/Initialize(mapload) +/mob/living/simple_animal/hostile/imp/slaughter/Initialize(mapload) . = ..() - var/obj/effect/proc_holder/spell/bloodcrawl/bloodspell = new - AddSpell(bloodspell) - if(istype(loc, /obj/effect/dummy/phased_mob/slaughter)) - bloodspell.phased = TRUE + var/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/crawl = new crawl_type(src) + crawl.Grant(src) + RegisterSignal(src, list(COMSIG_MOB_ENTER_JAUNT, COMSIG_MOB_AFTER_EXIT_JAUNT), .proc/on_crawl) + +/// Whenever we enter or exit blood crawl, reset our bonus and hitstreaks. +/mob/living/simple_animal/hostile/imp/slaughter/proc/on_crawl(datum/source) + SIGNAL_HANDLER + + // Grant us a speed boost if we're on the mortal plane + if(isturf(loc)) + add_movespeed_modifier(/datum/movespeed_modifier/slaughter) + addtimer(CALLBACK(src, .proc/remove_movespeed_modifier, /datum/movespeed_modifier/slaughter), 6 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE) + + // Reset our streaks + current_hitstreak = 0 + wound_bonus = initial(wound_bonus) + bare_wound_bonus = initial(bare_wound_bonus) /obj/effect/decal/cleanable/blood/innards name = "pile of viscera" @@ -68,12 +82,6 @@ icon_state = "innards" random_icon_states = null -/mob/living/simple_animal/slaughter/phasein() - . = ..() - add_movespeed_modifier(/datum/movespeed_modifier/slaughter) - addtimer(CALLBACK(src, PROC_REF(remove_movespeed_modifier), /datum/movespeed_modifier/slaughter, TRUE), 6 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE) - - //The loot from killing a slaughter demon - can be consumed to allow the user to blood crawl /obj/item/organ/heart/demon name = "demon heart" @@ -88,28 +96,34 @@ /obj/item/organ/heart/demon/attack(mob/M, mob/living/carbon/user, obj/target) if(M != user) return ..() - user.visible_message("[user] raises [src] to [user.p_their()] mouth and tears into it with [user.p_their()] teeth!", \ - "An unnatural hunger consumes you. You raise [src] your mouth and devour it!") - playsound(user, 'sound/magic/demon_consume.ogg', 50, 1) - for(var/obj/effect/proc_holder/spell/knownspell in user.mind.spell_list) - if(knownspell.type == /obj/effect/proc_holder/spell/bloodcrawl) - to_chat(user, "...and you don't feel any different.") - qdel(src) - return - user.visible_message("[user]'s eyes flare a deep crimson!", \ - "You feel a strange power seep into your body... you have absorbed the demon's blood-travelling powers!") + user.visible_message(span_warning( + "[user] raises [src] to [user.p_their()] mouth and tears into it with [user.p_their()] teeth!"), + span_danger("An unnatural hunger consumes you. You raise [src] your mouth and devour it!"), + ) + playsound(user, 'sound/magic/demon_consume.ogg', 50, TRUE) + + if(locate(/datum/action/cooldown/spell/jaunt/bloodcrawl) in user.actions) + to_chat(user, span_warning("...and you don't feel any different.")) + qdel(src) + return + + user.visible_message( + span_warning("[user]'s eyes flare a deep crimson!"), + span_userdanger("You feel a strange power seep into your body... you have absorbed the demon's blood-travelling powers!"), + ) user.temporarilyRemoveItemFromInventory(src, TRUE) src.Insert(user) //Consuming the heart literally replaces your heart with a demon heart. H A R D C O R E -/obj/item/organ/heart/demon/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) +/obj/item/organ/internal/heart/demon/Insert(mob/living/carbon/M, special = 0) ..() - if(M.mind) - M.mind.AddSpell(new /obj/effect/proc_holder/spell/bloodcrawl(null)) + // Gives a non-eat-people crawl to the new owner + var/datum/action/cooldown/spell/jaunt/bloodcrawl/crawl = new(M) + crawl.Grant(M) -/obj/item/organ/heart/demon/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) +/obj/item/organ/internal/heart/demon/Remove(mob/living/carbon/M, special = 0) ..() - if(M.mind) - M.mind.RemoveSpell(/obj/effect/proc_holder/spell/bloodcrawl) + var/datum/action/cooldown/spell/jaunt/bloodcrawl/crawl = locate() in M.actions + qdel(crawl) /obj/item/organ/heart/demon/Stop() return 0 // Always beating. @@ -135,6 +149,7 @@ deathmessage = "fades out, as all of its friends are released from its \ prison of hugs." loot = list(/mob/living/simple_animal/pet/cat/kitten{name = "Laughter"}) + crawl_type = /datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny // Keep the people we hug! var/list/consumed_mobs = list() @@ -153,10 +168,6 @@ released and fully healed, because in the end it's just a jape, \ sibling!" -/mob/living/simple_animal/slaughter/laughter/Destroy() - release_friends() - . = ..() - /mob/living/simple_animal/slaughter/laughter/ex_act(severity) switch(severity) if(EXPLODE_DEVASTATE) @@ -166,29 +177,3 @@ adjustBruteLoss(60) if(EXPLODE_LIGHT) adjustBruteLoss(30) - -/mob/living/simple_animal/slaughter/laughter/proc/release_friends() - if(!consumed_mobs) - return - - for(var/mob/living/M in consumed_mobs) - if(!M) - continue - var/turf/T = find_safe_turf() - if(!T) - T = get_turf(src) - M.forceMove(T) - if(M.revive(full_heal = TRUE, admin_revive = TRUE)) - M.grab_ghost(force = TRUE) - playsound(T, feast_sound, 50, 1, -1) - to_chat(M, "You leave [src]'s warm embrace, and feel ready to take on the world.") - -/mob/living/simple_animal/slaughter/laughter/bloodcrawl_swallow(var/mob/living/victim) - if(consumed_mobs) - // Keep their corpse so rescue is possible - consumed_mobs += victim - else - // Be safe and just eject the corpse - victim.forceMove(get_turf(victim)) - victim.exit_blood_effect() - victim.visible_message("[victim] falls out of the air, covered in blood, looking highly confused. And dead.") diff --git a/code/modules/antagonists/slaughter/slaughterevent.dm b/code/modules/antagonists/slaughter/slaughterevent.dm index 9c2fdaf3d134f..f79a9b6924e15 100644 --- a/code/modules/antagonists/slaughter/slaughterevent.dm +++ b/code/modules/antagonists/slaughter/slaughterevent.dm @@ -32,15 +32,17 @@ message_admins("No valid spawn locations found, aborting...") return MAP_ERROR - var/obj/effect/dummy/phased_mob/slaughter/holder = new /obj/effect/dummy/phased_mob/slaughter((pick(spawn_locs))) - var/mob/living/simple_animal/slaughter/S = new (holder) + var/turf/chosen = pick(spawn_locs) + var/mob/living/simple_animal/hostile/imp/slaughter/S = new(chosen) + new /obj/effect/dummy/phased_mob(chosen, S) + S.holder = holder player_mind.transfer_to(S) player_mind.assigned_role = "Slaughter Demon" player_mind.special_role = "Slaughter Demon" player_mind.add_antag_datum(/datum/antagonist/slaughter) - to_chat(S, S.playstyle_string) - to_chat(S, "You are currently not currently in the same plane of existence as the station. Blood Crawl near a blood pool to manifest.") + to_chat(S, span_bold("You are currently not currently in the same plane of existence as the station. \ + Use your Blood Crawl ability near a pool of blood to manifest and wreak havoc.")) SEND_SOUND(S, 'sound/magic/demon_dies.ogg') message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a slaughter demon by an event.") log_game("[key_name(S)] was spawned as a slaughter demon by an event.") diff --git a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm index c9a15ce60686e..8a6a1a215872c 100644 --- a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm +++ b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm @@ -74,17 +74,7 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list( /datum/action/innate/ai/ranged name = "Ranged AI Action" auto_use_uses = FALSE //This is so we can do the thing and disable/enable freely without having to constantly add uses - var/obj/effect/proc_holder/ranged_ai/linked_ability //The linked proc holder that contains the actual ability code - var/linked_ability_type //The path of our linked ability - -/datum/action/innate/ai/ranged/New() - if(!linked_ability_type) - WARNING("Ranged AI action [name] attempted to spawn without a linked ability!") - qdel(src) //uh oh! - return - linked_ability = new linked_ability_type() - linked_ability.attached_action = src - ..() + click_action = TRUE /datum/action/innate/ai/ranged/adjust_uses(amt, silent) uses += amt @@ -97,31 +87,6 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list( Remove(owner) QDEL_IN(src, 100) //let any active timers on us finish up -/datum/action/innate/ai/ranged/Destroy() - QDEL_NULL(linked_ability) - return ..() - -/datum/action/innate/ai/ranged/Activate() - linked_ability.toggle(owner) - return TRUE - -//The actual ranged proc holder. -/obj/effect/proc_holder/ranged_ai - var/enable_text = "Hello World!" //Appears when the user activates the ability - var/disable_text = "Goodbye Cruel World!" //Context clues! - var/datum/action/innate/ai/ranged/attached_action - -/obj/effect/proc_holder/ranged_ai/Destroy() - attached_action = null - return ..() - -/obj/effect/proc_holder/ranged_ai/proc/toggle(mob/user) - if(active) - remove_ranged_ability(disable_text) - else - add_ranged_ability(user, enable_text) - - //The datum and interface for the malf unlock menu, which lets them choose actions to unlock. /datum/module_picker var/temp @@ -221,7 +186,6 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list( var/engaged = 0 var/cost = 5 var/one_purchase = FALSE //If this module can only be purchased once. This always applies to upgrades, even if the variable is set to false. - var/power_type = /datum/action/innate/ai //If the module gives an active ability, use this. Mutually exclusive with upgrade. var/upgrade //If the module gives a passive upgrade, use this. Mutually exclusive with power_type. var/unlock_text = "Hello World!" //Text shown when an ability is unlocked @@ -583,41 +547,42 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list( desc = "Overheats a machine, causing a small explosion after a short time." button_icon_state = "overload_machine" uses = 2 - linked_ability_type = /obj/effect/proc_holder/ranged_ai/overload_machine - -/datum/action/innate/ai/ranged/overload_machine/proc/detonate_machine(obj/machinery/M) - if(M && !QDELETED(M)) - var/turf/T = get_turf(M) - message_admins("[ADMIN_LOOKUPFLW(usr)] overloaded [M.name] at [ADMIN_VERBOSEJMP(T)].") - log_game("[key_name(usr)] overloaded [M.name] at [AREACOORD(T)].") - explosion(get_turf(M), 0, 2, 3, 0) - if(M) //to check if the explosion killed it before we try to delete it - qdel(M) - -/obj/effect/proc_holder/ranged_ai/overload_machine - active = FALSE - ranged_mousepointer = 'icons/effects/overload_machine_target.dmi' + ranged_mousepointer = 'icons/effects/mouse_pointers/overload_machine_target.dmi' enable_text = "You tap into the station's powernet. Click on a machine to detonate it, or use the ability again to cancel." disable_text = "You release your hold on the powernet." -/obj/effect/proc_holder/ranged_ai/overload_machine/InterceptClickOn(mob/living/caller, params, obj/machinery/target) - if(..()) - return - if(ranged_ability_user.incapacitated()) - remove_ranged_ability() - return - if(!istype(target)) - to_chat(ranged_ability_user, "You can only overload machines!") +/datum/action/innate/ai/ranged/overload_machine/proc/detonate_machine(mob/living/caller, obj/machinery/to_explode) + if(QDELETED(to_explode)) return - if(is_type_in_typecache(target, GLOB.blacklisted_malf_machines)) - to_chat(ranged_ability_user, "You cannot overload that device!") - return - ranged_ability_user.playsound_local(ranged_ability_user, "sparks", 50, 0) - attached_action.adjust_uses(-1) - target.audible_message("You hear a loud electrical buzzing sound coming from [target]!") - caller.log_message("activated malf module [name]", LOG_GAME) - addtimer(CALLBACK(attached_action, TYPE_PROC_REF(/datum/action/innate/ai/ranged/overload_machine, detonate_machine), target), 50) //kaboom! - remove_ranged_ability("Overcharging machine...") + + var/turf/machine_turf = get_turf(to_explode) + message_admins("[ADMIN_LOOKUPFLW(caller)] overloaded [to_explode.name] ([to_explode.type]) at [ADMIN_VERBOSEJMP(machine_turf)].") + log_game("[key_name(caller)] overloaded [to_explode.name] ([to_explode.type]) at [AREACOORD(machine_turf)].") + explosion(to_explode, heavy_impact_range = 2, light_impact_range = 3) + if(!QDELETED(to_explode)) //to check if the explosion killed it before we try to delete it + qdel(to_explode) + +/datum/action/innate/ai/ranged/overload_machine/do_ability(mob/living/caller, atom/clicked_on) + if(caller.incapacitated()) + unset_ranged_ability(caller) + return FALSE + if(!istype(clicked_on, /obj/machinery)) + to_chat(caller, span_warning("You can only overload machines!")) + return FALSE + var/obj/machinery/clicked_machine = clicked_on + if(is_type_in_typecache(clicked_machine, GLOB.blacklisted_malf_machines)) + to_chat(caller, span_warning("You cannot overload that device!")) + return FALSE + + caller.playsound_local(caller, SFX_SPARKS, 50, 0) + adjust_uses(-1) + if(uses) + desc = "[initial(desc)] It has [uses] use\s remaining." + UpdateButtons() + + clicked_machine.audible_message(span_userdanger("You hear a loud electrical buzzing sound coming from [clicked_machine]!")) + addtimer(CALLBACK(src, .proc/detonate_machine, caller, clicked_machine), 5 SECONDS) //kaboom! + unset_ranged_ability(caller, span_danger("Overcharging machine...")) return TRUE @@ -636,41 +601,43 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list( desc = "Animates a targeted machine, causing it to attack anyone nearby." button_icon_state = "override_machine" uses = 4 - linked_ability_type = /obj/effect/proc_holder/ranged_ai/override_machine - -/datum/action/innate/ai/ranged/override_machine/proc/animate_machine(obj/machinery/M) - if(M && !QDELETED(M)) - var/turf/T = get_turf(M) - message_admins("[ADMIN_LOOKUPFLW(owner)] overrided (animated) [M.name] at [ADMIN_VERBOSEJMP(T)].") - log_game("[key_name(owner)] overrided (animated) [M.name] at [AREACOORD(T)].") - new/mob/living/simple_animal/hostile/mimic/copy/machine(get_turf(M), M, owner) - -/obj/effect/proc_holder/ranged_ai/override_machine - active = FALSE - ranged_mousepointer = 'icons/effects/override_machine_target.dmi' + ranged_mousepointer = 'icons/effects/mouse_pointers/override_machine_target.dmi' enable_text = "You tap into the station's powernet. Click on a machine to animate it, or use the ability again to cancel." disable_text = "You release your hold on the powernet." -/obj/effect/proc_holder/ranged_ai/override_machine/InterceptClickOn(mob/living/caller, params, obj/machinery/target) - if(..()) - return - if(ranged_ability_user.incapacitated()) - remove_ranged_ability() - return - if(!istype(target)) - to_chat(ranged_ability_user, "You can only animate machines!") - return - if(!target.can_be_overridden() || is_type_in_typecache(target, GLOB.blacklisted_malf_machines)) - to_chat(ranged_ability_user, "That machine can't be overridden!") - return - ranged_ability_user.playsound_local(ranged_ability_user, 'sound/misc/interference.ogg', 50, 0) - attached_action.adjust_uses(-1) - target.audible_message("You hear a loud electrical buzzing sound coming from [target]!") - caller.log_message("activated malf module [name]", LOG_GAME) - addtimer(CALLBACK(attached_action, TYPE_PROC_REF(/datum/action/innate/ai/ranged/override_machine, animate_machine), target), 50) //kabeep! - remove_ranged_ability("Sending override signal...") +/datum/action/innate/ai/ranged/override_machine/New() + . = ..() + desc = "[desc] It has [uses] use\s remaining." + +/datum/action/innate/ai/ranged/override_machine/do_ability(mob/living/caller, atom/clicked_on) + if(caller.incapacitated()) + unset_ranged_ability(caller) + return FALSE + if(!istype(clicked_on, /obj/machinery)) + to_chat(caller, span_warning("You can only animate machines!")) + return FALSE + var/obj/machinery/clicked_machine = clicked_on + if(!clicked_machine.can_be_overridden() || is_type_in_typecache(clicked_machine, GLOB.blacklisted_malf_machines)) + to_chat(caller, span_warning("That machine can't be overridden!")) + return FALSE + + caller.playsound_local(caller, 'sound/misc/interference.ogg', 50, FALSE, use_reverb = FALSE) + adjust_uses(-1) + + if(uses) + desc = "[initial(desc)] It has [uses] use\s remaining." + UpdateButtons() + + clicked_machine.audible_message(span_userdanger("You hear a loud electrical buzzing sound coming from [clicked_machine]!")) + addtimer(CALLBACK(src, .proc/animate_machine, caller, clicked_machine), 5 SECONDS) //kabeep! + unset_ranged_ability(caller, span_danger("Sending override signal...")) return TRUE +/datum/action/innate/ai/ranged/override_machine/proc/animate_machine(mob/living/caller, obj/machinery/to_animate) + if(QDELETED(to_animate)) + return + + new /mob/living/simple_animal/hostile/mimic/copy/machine(get_turf(to_animate), to_animate, caller, TRUE) //Robotic Factory: Places a large machine that converts humans that go through it into cyborgs. Unlocking this ability removes shunting. /datum/AI_Module/large/place_cyborg_transformer diff --git a/code/modules/antagonists/wizard/equipment/spellbook.dm b/code/modules/antagonists/wizard/equipment/spellbook.dm deleted file mode 100644 index 8b4cf484bf103..0000000000000 --- a/code/modules/antagonists/wizard/equipment/spellbook.dm +++ /dev/null @@ -1,821 +0,0 @@ -/datum/spellbook_entry - var/name = "Entry Name" - - var/spell_type = null - var/desc = "" - var/category = "Offensive" - var/cost = 2 - var/refundable = TRUE - var/surplus = -1 // -1 for infinite, not used by anything atm - var/obj/effect/proc_holder/spell/S = null //Since spellbooks can be used by only one person anyway we can track the actual spell - var/buy_word = "Learn" - var/limit //used to prevent a spellbook_entry from being bought more than X times with one wizard spellbook - var/list/no_coexistence_typecache //Used so you can't have specific spells together - var/no_random = FALSE // This is awful one to be a part of randomness - i.e.) soul tap - -/datum/spellbook_entry/New() - ..() - no_coexistence_typecache = typecacheof(no_coexistence_typecache) - -/datum/spellbook_entry/proc/IsAvailable(obj/item/spellbook/book) // For config prefs / gamemode restrictions - these are round applied - return TRUE - -/datum/spellbook_entry/proc/CanBuy(mob/living/carbon/human/user,obj/item/spellbook/book) // Specific circumstances - if(book.uses= aspell.level_max) - to_chat(user, "This spell cannot be improved further.") - return FALSE - else - aspell.name = initial(aspell.name) - aspell.spell_level++ - aspell.charge_max = round(initial(aspell.charge_max) - aspell.spell_level * (initial(aspell.charge_max) - aspell.cooldown_min)/ aspell.level_max) - if(aspell.charge_max < aspell.charge_counter) - aspell.charge_counter = aspell.charge_max - switch(aspell.spell_level) - if(1) - to_chat(user, "You have improved [aspell.name] into Efficient [aspell.name].") - aspell.name = "Efficient [aspell.name]" - if(2) - to_chat(user, "You have further improved [aspell.name] into Quickened [aspell.name].") - aspell.name = "Quickened [aspell.name]" - if(3) - to_chat(user, "You have further improved [aspell.name] into Free [aspell.name].") - aspell.name = "Free [aspell.name]" - if(4) - to_chat(user, "You have further improved [aspell.name] into Instant [aspell.name].") - aspell.name = "Instant [aspell.name]" - if(aspell.spell_level >= aspell.level_max) - to_chat(user, "This spell cannot be strengthened any further.") - SSblackbox.record_feedback("nested tally", "wizard_spell_improved", 1, list("[name]", "[aspell.spell_level]")) - return TRUE - //debug handling - if(book.everything_robeless) - SSblackbox.record_feedback("tally", "debug_wizard_spell_learned", 1, name) - S.clothes_req = FALSE // You'd want no cloth req if you learned spells from a debug spellbook - else - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - - //No same spell found - just learn it - user.mind.AddSpell(S) - to_chat(user, "You have learned [S.name].") - return TRUE - -/datum/spellbook_entry/proc/CanRefund(mob/living/carbon/human/user,obj/item/spellbook/book) - if(!refundable) - return FALSE - if(!S) - S = new spell_type() - for(var/obj/effect/proc_holder/spell/aspell in user.mind.spell_list) - if(initial(S.name) == initial(aspell.name)) - return TRUE - return FALSE - -/datum/spellbook_entry/proc/Refund(mob/living/carbon/human/user,obj/item/spellbook/book) //return point value or -1 for failure - var/area/wizard_station/A = GLOB.areas_by_type[/area/wizard_station] - if(!(user in A.contents)) - to_chat(user, "You can only refund spells at the wizard lair") - return -1 - if(!S) - S = new spell_type() - var/spell_levels = 0 - for(var/obj/effect/proc_holder/spell/aspell in user.mind.spell_list) - if(initial(S.name) == initial(aspell.name)) - spell_levels = aspell.spell_level - user.mind.spell_list.Remove(aspell) - qdel(S) - return cost * (spell_levels+1) - return -1 -/datum/spellbook_entry/proc/GetInfo() - if(!S) - S = new spell_type() - var/dat ="" - dat += "[initial(S.name)]" - if(S.charge_type == "recharge") - dat += " Cooldown:[S.charge_max/10]" - dat += " Cost:[cost]
" - dat += "[S.desc][desc]
" - dat += "[S.clothes_req?"Requires wizard garb.":"Can be cast without wizard garb."]
" - return dat - -/datum/spellbook_entry/fireball - name = "Fireball" - spell_type = /obj/effect/proc_holder/spell/aimed/fireball - -/datum/spellbook_entry/spell_cards - name = "Spell Cards" - spell_type = /obj/effect/proc_holder/spell/aimed/spell_cards - cost = 1 - -/datum/spellbook_entry/rod_form - name = "Rod Form" - spell_type = /obj/effect/proc_holder/spell/targeted/rod_form - -/datum/spellbook_entry/magicm - name = "Magic Missile" - spell_type = /obj/effect/proc_holder/spell/targeted/projectile/magic_missile - category = "Defensive" - -/datum/spellbook_entry/disintegrate - name = "Disintegrate" - spell_type = /obj/effect/proc_holder/spell/targeted/touch/disintegrate - -/datum/spellbook_entry/disabletech - name = "Disable Tech" - spell_type = /obj/effect/proc_holder/spell/targeted/emplosion/disable_tech - category = "Defensive" - cost = 1 - -/datum/spellbook_entry/repulse - name = "Repulse" - spell_type = /obj/effect/proc_holder/spell/aoe_turf/repulse - category = "Defensive" - -/datum/spellbook_entry/lightningPacket - name = "Lightning bolt! Lightning bolt!" - spell_type = /obj/effect/proc_holder/spell/targeted/conjure_item/spellpacket - category = "Defensive" - -/datum/spellbook_entry/timestop - name = "Time Stop" - spell_type = /obj/effect/proc_holder/spell/aoe_turf/conjure/timestop - category = "Defensive" - -/datum/spellbook_entry/smoke - name = "Smoke" - spell_type = /obj/effect/proc_holder/spell/targeted/smoke - category = "Defensive" - cost = 1 - -/datum/spellbook_entry/blind - name = "Blind" - spell_type = /obj/effect/proc_holder/spell/targeted/blind - cost = 1 - -/datum/spellbook_entry/mindswap - name = "Mindswap" - spell_type = /obj/effect/proc_holder/spell/targeted/mind_transfer - category = "Mobility" - -/datum/spellbook_entry/forcewall - name = "Force Wall" - spell_type = /obj/effect/proc_holder/spell/targeted/forcewall - category = "Defensive" - cost = 1 - -/datum/spellbook_entry/blink - name = "Blink" - spell_type = /obj/effect/proc_holder/spell/targeted/turf_teleport/blink - category = "Mobility" - -/datum/spellbook_entry/teleport - name = "Teleport" - spell_type = /obj/effect/proc_holder/spell/targeted/area_teleport/teleport - category = "Mobility" - -/datum/spellbook_entry/mutate - name = "Mutate" - spell_type = /obj/effect/proc_holder/spell/targeted/genetic/mutate - -/datum/spellbook_entry/jaunt - name = "Ethereal Jaunt" - spell_type = /obj/effect/proc_holder/spell/targeted/ethereal_jaunt - category = "Mobility" - -/datum/spellbook_entry/knock - name = "Knock" - spell_type = /obj/effect/proc_holder/spell/aoe_turf/knock - category = "Mobility" - cost = 1 - -/datum/spellbook_entry/fleshtostone - name = "Flesh to Stone" - spell_type = /obj/effect/proc_holder/spell/targeted/touch/flesh_to_stone - -/datum/spellbook_entry/summonitem - name = "Summon Item" - spell_type = /obj/effect/proc_holder/spell/targeted/summonitem - category = "Assistance" - cost = 1 - -/datum/spellbook_entry/lichdom - name = "Bind Soul" - spell_type = /obj/effect/proc_holder/spell/targeted/lichdom - category = "Defensive" - cost = 3 - no_random = WIZARD_NORANDOM_WILDAPPRENTICE - -/datum/spellbook_entry/teslablast - name = "Tesla Blast" - spell_type = /obj/effect/proc_holder/spell/targeted/tesla - -/datum/spellbook_entry/lightningbolt - name = "Lightning Bolt" - spell_type = /obj/effect/proc_holder/spell/aimed/lightningbolt - -/datum/spellbook_entry/lightningbolt/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) //return TRUE on success - . = ..() - user.flags_1 |= TESLA_IGNORE_1 - -/datum/spellbook_entry/infinite_guns - name = "Lesser Summon Guns" - spell_type = /obj/effect/proc_holder/spell/targeted/infinite_guns/gun - cost = 3 - no_coexistence_typecache = /obj/effect/proc_holder/spell/targeted/infinite_guns/arcane_barrage - -/datum/spellbook_entry/arcane_barrage - name = "Arcane Barrage" - spell_type = /obj/effect/proc_holder/spell/targeted/infinite_guns/arcane_barrage - no_coexistence_typecache = /obj/effect/proc_holder/spell/targeted/infinite_guns/gun - -/datum/spellbook_entry/barnyard - name = "Barnyard Curse" - spell_type = /obj/effect/proc_holder/spell/targeted/barnyardcurse - -/datum/spellbook_entry/charge - name = "Charge" - spell_type = /obj/effect/proc_holder/spell/targeted/charge - category = "Assistance" - cost = 1 - -/datum/spellbook_entry/shapeshift - name = "Wild Shapeshift" - spell_type = /obj/effect/proc_holder/spell/targeted/shapeshift - category = "Assistance" - cost = 1 - -/datum/spellbook_entry/tap - name = "Soul Tap" - spell_type = /obj/effect/proc_holder/spell/self/tap - category = "Assistance" - cost = 1 - no_random = WIZARD_NORANDOM_WILDAPPRENTICE - -/datum/spellbook_entry/spacetime_dist - name = "Spacetime Distortion" - spell_type = /obj/effect/proc_holder/spell/spacetime_dist - category = "Defensive" - cost = 1 - -/datum/spellbook_entry/the_traps - name = "The Traps!" - spell_type = /obj/effect/proc_holder/spell/aoe_turf/conjure/the_traps - category = "Defensive" - cost = 1 - -/datum/spellbook_entry/bees - name = "Lesser Summon Bees" - spell_type = /obj/effect/proc_holder/spell/aoe_turf/conjure/creature/bee - category = "Defensive" - -/datum/spellbook_entry/item - name = "Buy Item" - refundable = FALSE - buy_word = "Summon" - var/item_path= null - - -/datum/spellbook_entry/item/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) - new item_path(get_turf(user)) - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - return TRUE - -/datum/spellbook_entry/item/GetInfo() - var/dat ="" - dat += "[name]" - dat += " Cost:[cost]
" - dat += "[desc]
" - if(surplus>=0) - dat += "[surplus] left.
" - return dat - -/datum/spellbook_entry/item/staffchange - name = "Staff of Change" - desc = "An artefact that spits bolts of coruscating energy which cause the target's very form to reshape itself." - item_path = /obj/item/gun/magic/staff/change - -/datum/spellbook_entry/item/staffanimation - name = "Staff of Animation" - desc = "An arcane staff capable of shooting bolts of eldritch energy which cause inanimate objects to come to life. This magic doesn't affect machines." - item_path = /obj/item/gun/magic/staff/animate - category = "Assistance" - -/datum/spellbook_entry/item/staffchaos - name = "Staff of Chaos" - desc = "A caprious tool that can fire all sorts of magic without any rhyme or reason. Using it on people you care about is not recommended." - item_path = /obj/item/gun/magic/staff/chaos - -/datum/spellbook_entry/item/spellblade - name = "Spellblade" - desc = "A sword capable of firing blasts of energy which rip targets limb from limb." - item_path = /obj/item/gun/magic/staff/spellblade - -/datum/spellbook_entry/item/staffdoor - name = "Staff of Door Creation" - desc = "A particular staff that can mold solid walls into ornate doors. Useful for getting around in the absence of other transportation. Does not work on glass." - item_path = /obj/item/gun/magic/staff/door - cost = 1 - category = "Mobility" - -/datum/spellbook_entry/item/staffhealing - name = "Staff of Healing" - desc = "An altruistic staff that can heal the lame and raise the dead." - item_path = /obj/item/gun/magic/staff/healing - cost = 1 - category = "Defensive" - -/datum/spellbook_entry/item/lockerstaff - name = "Staff of the Locker" - desc = "A staff that shoots lockers. It eats anyone it hits on its way, leaving a welded locker with your victims behind." - item_path = /obj/item/gun/magic/staff/locker - category = "Defensive" - -/datum/spellbook_entry/item/scryingorb - name = "Scrying Orb" - desc = "An incandescent orb of crackling energy. Using it will allow you to release your ghost while alive, allowing you to spy upon the station and talk to the deceased. In addition, buying it will permanently grant you X-ray vision." - item_path = /obj/item/scrying - category = "Defensive" - -/datum/spellbook_entry/item/soulstones - name = "Six Soul Stone Shards and the spell Artificer" - desc = "Soul Stone Shards are ancient tools capable of capturing and harnessing the spirits of the dead and dying. The spell Artificer allows you to create arcane machines for the captured souls to pilot." - item_path = /obj/item/storage/belt/soulstone/full - category = "Assistance" - -/datum/spellbook_entry/item/soulstones/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) - . =..() - if(.) - user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/construct(null)) - return . - -/datum/spellbook_entry/item/necrostone - name = "A Necromantic Stone" - desc = "A Necromantic stone is able to resurrect three dead individuals as skeletal thralls for you to command." - item_path = /obj/item/necromantic_stone - category = "Assistance" - -/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." - item_path = /obj/item/storage/belt/wands/full - category = "Defensive" - -/datum/spellbook_entry/item/armor - name = "Mastercrafted Armor Set" - desc = "An artefact suit of armor that allows you to cast spells while providing more protection against attacks and the void of space." - item_path = /obj/item/clothing/suit/space/hardsuit/wizard - category = "Defensive" - -/datum/spellbook_entry/item/armor/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) - . = ..() - if(.) - new /obj/item/clothing/shoes/sandal/magic(get_turf(user)) //In case they've lost them. - new /obj/item/clothing/gloves/color/purple(get_turf(user))//To complete the outfit - new /obj/item/clothing/mask/breath(get_turf(user)) // so the air gets to your mouth. Just an average mask. - new /obj/item/tank/internals/emergency_oxygen/magic_oxygen(get_turf(user)) // so you have something to actually breathe. Near infinite. - -/datum/spellbook_entry/item/contract - name = "Contract of Apprenticeship" - desc = "A magical contract binding an apprentice wizard to your service, using it will summon them to your side." - item_path = /obj/item/antag_spawner/contract - category = "Assistance" - -/datum/spellbook_entry/item/guardian - name = "Guardian Deck" - desc = "A deck of guardian tarot cards, capable of binding a personal guardian to your body. There are multiple types of guardian available, but all of them will transfer some amount of damage to you. \ - It would be wise to avoid buying these with anything capable of causing you to swap bodies with others." - item_path = /obj/item/holoparasite_creator/wizard - category = "Assistance" - -/datum/spellbook_entry/item/bloodbottle - name = "Bottle of Blood" - desc = "A bottle of magically infused blood, the smell of which will attract extradimensional \ - beings when broken. Be careful though, the kinds of creatures summoned by blood magic are \ - indiscriminate in their killing, and you yourself may become a victim." - item_path = /obj/item/antag_spawner/slaughter_demon - limit = 1 - category = "Assistance" - -/datum/spellbook_entry/item/hugbottle - name = "Bottle of Tickles" - desc = "A bottle of magically infused fun, the smell of which will \ - attract adorable extradimensional beings when broken. These beings \ - are similar to slaughter demons, but they do not permamently kill \ - their victims, instead putting them in an extradimensional hugspace, \ - to be released on the demon's death. Chaotic, but not ultimately \ - damaging. The crew's reaction to the other hand could be very \ - destructive." - item_path = /obj/item/antag_spawner/slaughter_demon/laughter - cost = 1 //non-destructive; it's just a jape, sibling! - limit = 1 - category = "Assistance" - -/datum/spellbook_entry/item/mjolnir - name = "Mjolnir" - desc = "A mighty hammer on loan from Thor, God of Thunder. It crackles with barely contained power." - item_path = /obj/item/mjolnir - -/datum/spellbook_entry/item/singularity_hammer - name = "Singularity Hammer" - desc = "A hammer that creates an intensely powerful field of gravity where it strikes, pulling everything nearby to the point of impact." - item_path = /obj/item/singularityhammer - -/datum/spellbook_entry/item/battlemage - name = "Battlemage Armour" - desc = "An ensorceled suit of armour, protected by a powerful shield. The shield can completely negate sixteen attacks before being permanently depleted." - item_path = /obj/item/clothing/suit/space/hardsuit/shielded/wizard - limit = 1 - category = "Defensive" - -/datum/spellbook_entry/item/battlemage_charge - name = "Battlemage Armour Charges" - desc = "A powerful defensive rune, it will grant eight additional charges to a suit of battlemage armour." - item_path = /obj/item/wizard_armour_charge - category = "Defensive" - cost = 1 - -/datum/spellbook_entry/item/warpwhistle - name = "Warp Whistle" - desc = "A strange whistle that will transport you to a distant safe place on the station. There is a window of vulnerability at the beginning of every use." - item_path = /obj/item/warpwhistle - category = "Mobility" - cost = 1 - -/// How much threat we need to let these rituals happen on dynamic -#define MINIMUM_THREAT_FOR_RITUALS 85 - -/datum/spellbook_entry/summon - name = "Summon Stuff" - category = "Rituals" - refundable = FALSE - buy_word = "Cast" - var/active = FALSE - var/ritual_invocation // This does nothing. This is a flavor to ghosts observing a wizard. - -/datum/spellbook_entry/summon/CanBuy(mob/living/carbon/human/user,obj/item/spellbook/book) - return ..() && !active - -/datum/spellbook_entry/summon/GetInfo() - var/dat ="" - dat += "[name]" - if(cost>0) - dat += " Cost:[cost]
" - else - dat += " No Cost
" - dat += "[desc]
" - if(active) - dat += "Already cast!
" - return dat - -/datum/spellbook_entry/summon/proc/say_invocation(mob/living/carbon/human/user) - if(ritual_invocation) - user.say(ritual_invocation, forced = "spell") - -/datum/spellbook_entry/summon/ghosts - name = "Summon Ghosts" - desc = "Spook the crew out by making them see dead people. Be warned, ghosts are capricious and occasionally vindicative, and some will use their incredibly minor abilities to frustrate you." - cost = 0 - ritual_invocation = "ALADAL DESINARI ODORI'IN TUUR'IS OVOR'E POR" - -/datum/spellbook_entry/summon/ghosts/Buy(mob/living/carbon/human/user, obj/item/spellbook/book) - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - new /datum/round_event/wizard/ghost() - active = TRUE - to_chat(user, "You have cast summon ghosts!") - playsound(get_turf(user), 'sound/effects/ghost2.ogg', 50, 1) - say_invocation(user) - return TRUE - -/datum/spellbook_entry/summon/guns - name = "Summon Guns" - desc = "Nothing could possibly go wrong with arming a crew of lunatics just itching for an excuse to kill you. There is a good chance that they will shoot each other first." - ritual_invocation = "ALADAL DESINARI ODORI'IN DOL'G FLAM OVOR'E POR" - -/datum/spellbook_entry/summon/guns/IsAvailable(obj/item/spellbook/book) - if(!SSticker.mode) // In case spellbook is placed on map - return FALSE - if(book.bypass_lock) - return TRUE - if(istype(SSticker.mode, /datum/game_mode/dynamic)) // Disable events on dynamic - var/datum/game_mode/dynamic/mode = SSticker.mode - if(mode.threat_level < MINIMUM_THREAT_FOR_RITUALS) - return FALSE - return !CONFIG_GET(flag/no_summon_guns) - -/datum/spellbook_entry/summon/guns/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - rightandwrong(SUMMON_GUNS, user, 10) - active = TRUE - playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1) - to_chat(user, "You have cast summon guns!") - say_invocation(user) - return TRUE - -/datum/spellbook_entry/summon/magic - name = "Summon Magic" - desc = "Share the wonders of magic with the crew and show them why they aren't to be trusted with it at the same time." - ritual_invocation = "ALADAL DESINARI ODORI'IN IDO'LEX SPERMITA OVOR'E POR" - -/datum/spellbook_entry/summon/magic/IsAvailable(obj/item/spellbook/book) - if(!SSticker.mode) // In case spellbook is placed on map - return FALSE - if(book.bypass_lock) - return TRUE - if(istype(SSticker.mode, /datum/game_mode/dynamic)) // Disable events on dynamic - var/datum/game_mode/dynamic/mode = SSticker.mode - if(mode.threat_level < MINIMUM_THREAT_FOR_RITUALS) - return FALSE - return !CONFIG_GET(flag/no_summon_magic) - -/datum/spellbook_entry/summon/magic/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - rightandwrong(SUMMON_MAGIC, user, 10) - active = TRUE - playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1) - to_chat(user, "You have cast summon magic!") - say_invocation(user) - return TRUE - -/datum/spellbook_entry/summon/events - name = "Summon Events" - desc = "Give Murphy's law a little push and replace all events with special wizard ones that will confound and confuse everyone. Multiple castings increase the rate of these events." - var/times = 0 - ritual_invocation = "ALADAL DESINARI ODORI'IN IDO'LEX MANAG'ROKT OVOR'E POR" - -/datum/spellbook_entry/summon/events/IsAvailable(obj/item/spellbook/book) - if(!SSticker.mode) // In case spellbook is placed on map - return FALSE - if(book.bypass_lock) - return TRUE - if(istype(SSticker.mode, /datum/game_mode/dynamic)) // Disable events on dynamic - var/datum/game_mode/dynamic/mode = SSticker.mode - if(mode.threat_level < MINIMUM_THREAT_FOR_RITUALS) - return FALSE - return !CONFIG_GET(flag/no_summon_events) - -/datum/spellbook_entry/summon/events/Buy(mob/living/carbon/human/user,obj/item/spellbook/book) - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - summonevents() - times++ - playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, 1) - to_chat(user, "You have cast summon events.") - say_invocation(user) - return TRUE - -/datum/spellbook_entry/summon/events/GetInfo() - . = ..() - if(times>0) - . += "You cast it [times] times.
" - return . - -/datum/spellbook_entry/summon/curse_of_madness - name = "Curse of Madness" - desc = "Curses the station, warping the minds of everyone inside, causing lasting traumas. Warning: this spell can affect you if not cast from a safe distance." - cost = 4 - ritual_invocation = "ALADAL DESINARI ODORI'IN PORES ENHIDO'LEN MORI MAKA TU" - -/datum/spellbook_entry/summon/curse_of_madness/Buy(mob/living/carbon/human/user, obj/item/spellbook/book) - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - active = TRUE - var/message - while(!message) - message = stripped_input(user, "Whisper a secret truth to drive your victims to madness.", "Whispers of Madness") - curse_of_madness(user, message) - to_chat(user, "You have cast the curse of insanity!") - playsound(user, 'sound/magic/mandswap.ogg', 50, 1) - return TRUE - -/datum/spellbook_entry/summon/wild_magic - name = "Wild Magic Manipulation" - desc = "multiply your remaining spell points by 70%(round down) and expand all of them to Wild Magic Manipulation. \ - You purchase random spells and items upto the spell points you expanded. Spells from this ritual will no longer be refundable even if you learned it manually, but also the book will no longer accept items to refund." - cost = 0 - ritual_invocation = "ALADAL DESINARI ODORI'IN A'EN SPERMITEN G'ATUA H'UN OVORA DUN SPERMITUN" - -/datum/spellbook_entry/summon/wild_magic/Buy(mob/living/carbon/human/user, obj/item/spellbook/book) - if(!book.uses) - to_chat(user, "You have no spell points for this ritual.") // You can cast it again as long as you get more spell points somehow - return FALSE - SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) - book.uses = round(book.uses*WIZARD_WILDMAGIC_SPELLPOINT_MULTIPLIER) // more spell points - book.refuses_refund = TRUE - book.desc = "An unearthly tome that once had a great power." - while(book.uses) - var/datum/spellbook_entry/target = pick(book.entries) - if(istype(target, /datum/spellbook_entry/summon/wild_magic)) - continue // Too lucky to get more spell points, but no. - if(target.CanBuy(user,book)) - if(target.Buy(user,book)) - book.uses -= target.cost - target.refundable = FALSE - say_invocation(user) - return TRUE - - -#undef MINIMUM_THREAT_FOR_RITUALS - -/obj/item/spellbook - name = "spell book" - desc = "An unearthly tome that glows with power." - icon = 'icons/obj/library.dmi' - icon_state ="book" - throw_speed = 2 - throw_range = 5 - w_class = WEIGHT_CLASS_TINY - var/uses = 10 - var/temp = null - var/tab = null - var/refuses_refund = FALSE - var/mob/living/carbon/human/owner - var/list/datum/spellbook_entry/entries = list() - var/list/categories = list() - var/everything_robeless = FALSE //! if TRUE, all spells you learn become robeless. Ask admin. - var/bypass_lock = FALSE //! bypasses some locked ritual & spell combinations. Ask admin. - -/obj/item/spellbook/examine(mob/user) - . = ..() - if(owner) - . += {"There is a small signature on the front cover: "[owner]"."} - else - . += "It appears to have no author." - -/obj/item/spellbook/Initialize(mapload) - . = ..() - prepare_spells() - -/obj/item/spellbook/proc/prepare_spells() - var/entry_types = subtypesof(/datum/spellbook_entry) - /datum/spellbook_entry/item - /datum/spellbook_entry/summon - for(var/T in entry_types) - var/datum/spellbook_entry/E = new T - if(E.IsAvailable(src)) - entries |= E - categories |= E.category - else - qdel(E) - tab = categories[1] - -/obj/item/spellbook/attackby(obj/item/O, mob/user, params) - if(refuses_refund) - to_chat(user, "Your book is powerless because of Wild Magic Manipulation ritual. The book doesn't accept the item.") - return - if(istype(O, /obj/item/antag_spawner/contract)) - var/obj/item/antag_spawner/contract/contract = O - if(contract.used) - to_chat(user, "The contract has been used, you can't get your points back now!") - else - to_chat(user, "You feed the contract back into the spellbook, refunding your points.") - uses++ - for(var/datum/spellbook_entry/item/contract/CT in entries) - if(!isnull(CT.limit)) - CT.limit++ - qdel(O) - else if(istype(O, /obj/item/antag_spawner/slaughter_demon)) - to_chat(user, "On second thought, maybe summoning a demon is a bad idea. You refund your points.") - uses++ - for(var/datum/spellbook_entry/item/bloodbottle/BB in entries) - if(!isnull(BB.limit)) - BB.limit++ - qdel(O) - -/obj/item/spellbook/proc/GetCategoryHeader(category) - var/dat = "" - switch(category) - if("Offensive") - dat += "Spells and items geared towards debilitating and destroying.

" - dat += "Items are not bound to you and can be stolen. Additionally they cannot typically be returned once purchased.
" - dat += "For spells: the number after the spell name is the cooldown time.
" - dat += "You can reduce this number by spending more points on the spell.
" - if("Defensive") - dat += "Spells and items geared towards improving your survivability or reducing foes' ability to attack.

" - dat += "Items are not bound to you and can be stolen. Additionally they cannot typically be returned once purchased.
" - dat += "For spells: the number after the spell name is the cooldown time.
" - dat += "You can reduce this number by spending more points on the spell.
" - if("Mobility") - dat += "Spells and items geared towards improving your ability to move. It is a good idea to take at least one.

" - dat += "Items are not bound to you and can be stolen. Additionally they cannot typically be returned once purchased.
" - dat += "For spells: the number after the spell name is the cooldown time.
" - dat += "You can reduce this number by spending more points on the spell.
" - if("Assistance") - dat += "Spells and items geared towards bringing in outside forces to aid you or improving upon your other items and abilities.

" - dat += "Items are not bound to you and can be stolen. Additionally they cannot typically be returned once purchased.
" - dat += "For spells: the number after the spell name is the cooldown time.
" - dat += "You can reduce this number by spending more points on the spell.
" - if("Challenges") - dat += "The Wizard Federation typically has hard limits on the potency and number of spells brought to the station based on risk.
" - dat += "Arming the station against you will increases the risk, but will grant you one more charge for your spellbook.
" - if("Rituals") - dat += "These powerful spells change the very fabric of reality. Not always in your favour.
" - return dat - -/obj/item/spellbook/proc/wrap(content) - var/dat = "" - dat +="Spellbook" - dat += {" - - - - "} - dat += {"[content]"} - return dat - -/obj/item/spellbook/attack_self(mob/user) - if(!owner) - to_chat(user, "You bind the spellbook to yourself.") - owner = user - return - if(user != owner) - to_chat(user, "The [name] does not recognize you as its owner and refuses to open!") - return - user.set_machine(src) - var/dat = "" - - dat += "" - - var/datum/spellbook_entry/E - for(var/i=1,i<=entries.len,i++) - var/spell_info = "" - E = entries[i] - spell_info += E.GetInfo() - if(E.CanBuy(user,src)) - spell_info+= "[E.buy_word]
" - else - spell_info+= "Can't [E.buy_word]
" - if(E.CanRefund(user,src)) - spell_info+= "Refund
" - spell_info += "
" - if(cat_dat[E.category]) - cat_dat[E.category] += spell_info - - for(var/category in categories) - dat += "
" - dat += GetCategoryHeader(category) - dat += cat_dat[category] - dat += "
" - - user << browse(wrap(dat), "window=spellbook;size=700x500") - onclose(user, "spellbook") - return - -/obj/item/spellbook/Topic(href, href_list) - . = ..() - - if(usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED)) - return - if(!ishuman(usr)) - return TRUE - var/mob/living/carbon/human/H = usr - - if(H.mind.special_role == "apprentice") - temp = "If you got caught sneaking a peek from your teacher's spellbook, you'd likely be expelled from the Wizard Academy. Better not." - return - - var/datum/spellbook_entry/E = null - if(loc == H || (in_range(src, H) && isturf(loc))) - H.set_machine(src) - if(href_list["buy"]) - E = entries[text2num(href_list["buy"])] - if(E && E.CanBuy(H,src)) - if(E.Buy(H,src)) - if(E.limit) - E.limit-- - uses -= E.cost - log_game("[initial(E.name)] purchased by [H.ckey]/[H.name] the [H.job] for [E.cost] SP, [uses] SP remaining.") - else if(href_list["refund"]) - E = entries[text2num(href_list["refund"])] - if(E?.refundable) - var/result = E.Refund(H,src) - if(result > 0) - if(!isnull(E.limit)) - E.limit += result - uses += result - else if(href_list["page"]) - tab = sanitize(href_list["page"]) - attack_self(H) - return diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm new file mode 100644 index 0000000000000..0d5982d668919 --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/_entry.dm @@ -0,0 +1,232 @@ +/** + * ## Spellbook entries + * + * Wizard spellbooks are automatically populated with + * a list of every spellbook entry subtype when they're made. + * + * Wizards can then buy entries from the book to learn magic, + * invoke rituals, or summon items. + */ +/datum/spellbook_entry + /// The name of the entry + var/name + /// The description of the entry + var/desc + /// The type of spell that the entry grants (typepath) + var/datum/action/cooldown/spell/spell_type + /// What category the entry falls in + var/category + /// How many book charges does the spell take + var/cost = 2 + /// How many times has the spell been purchased. Compared against limit. + var/times = 0 + /// The limit on number of purchases from this entry in a given spellbook. If null, infinite are allowed. + var/limit + /// Is this refundable? + var/refundable = TRUE + /// Flavor. Verb used in saying how the spell is aquired. Ex "[Learn] Fireball" or "[Summon] Ghosts" + var/buy_word = "Learn" + /// The cooldown of the spell + var/cooldown + /// Whether the spell requires wizard garb or not + var/requires_wizard_garb = FALSE + /// Used so you can't have specific spells together + var/list/no_coexistance_typecache + +/datum/spellbook_entry/New() + no_coexistance_typecache = typecacheof(no_coexistance_typecache) + + if(ispath(spell_type)) + if(isnull(limit)) + limit = initial(spell_type.spell_max_level) + if(initial(spell_type.spell_requirements) & SPELL_REQUIRES_WIZARD_GARB) + requires_wizard_garb = TRUE + +/** + * Determines if this entry can be purchased from a spellbook + * Used for configs / round related restrictions. + * + * Return FALSE to prevent the entry from being added to wizard spellbooks, TRUE otherwise + */ +/datum/spellbook_entry/proc/can_be_purchased() + if(!name || !desc || !category) // Erroneously set or abstract + return FALSE + return TRUE + +/** + * Checks if the user, with the supplied spellbook, can purchase the given entry. + * + * Arguments + * * user - the mob who's buying the spell + * * book - what book they're buying the spell from + * + * Return TRUE if it can be bought, FALSE otherwise + */ +/datum/spellbook_entry/proc/can_buy(mob/living/carbon/human/user, obj/item/spellbook/book) + if(book.uses < cost) + return FALSE + if(!isnull(limit) && times >= limit) + return FALSE + for(var/spell in user.actions) + if(is_type_in_typecache(spell, no_coexistance_typecache)) + return FALSE + return TRUE + +/** + * Actually buy the entry for the user + * + * Arguments + * * user - the mob who's bought the spell + * * book - what book they've bought the spell from + * + * Return TRUE if the purchase was successful, FALSE otherwise + */ +/datum/spellbook_entry/proc/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + var/datum/action/cooldown/spell/existing = locate(spell_type) in user.actions + if(existing) + var/before_name = existing.name + if(!existing.level_spell()) + to_chat(user, span_warning("This spell cannot be improved further!")) + return FALSE + + to_chat(user, span_notice("You have improved [before_name] into [existing.name].")) + name = existing.name + + //we'll need to update the cooldowns for the spellbook + set_spell_info() + log_spellbook("[key_name(user)] improved their knowledge of [initial(existing.name)] to level [existing.spell_level] for [cost] points") + SSblackbox.record_feedback("nested tally", "wizard_spell_improved", 1, list("[name]", "[existing.spell_level]")) + log_purchase(user.key) + return TRUE + + //No same spell found - just learn it + var/datum/action/cooldown/spell/new_spell = new spell_type(user.mind || user) + new_spell.Grant(user) + to_chat(user, span_notice("You have learned [new_spell.name].")) + + log_spellbook("[key_name(user)] learned [new_spell] for [cost] points") + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + log_purchase(user.key) + return TRUE + +/datum/spellbook_entry/proc/log_purchase(key) + if(!islist(GLOB.wizard_spellbook_purchases_by_key[key])) + GLOB.wizard_spellbook_purchases_by_key[key] = list() + + for(var/list/log as anything in GLOB.wizard_spellbook_purchases_by_key[key]) + if(log[LOG_SPELL_TYPE] == type) + log[LOG_SPELL_AMOUNT]++ + return + + var/list/to_log = list( + LOG_SPELL_TYPE = type, + LOG_SPELL_AMOUNT = 1, + ) + GLOB.wizard_spellbook_purchases_by_key[key] += list(to_log) + +/** + * Checks if the user, with the supplied spellbook, can refund the entry + * + * Arguments + * * user - the mob who's refunding the spell + * * book - what book they're refunding the spell from + * + * Return TRUE if it can refunded, FALSE otherwise + */ +/datum/spellbook_entry/proc/can_refund(mob/living/carbon/human/user, obj/item/spellbook/book) + if(!refundable) + return FALSE + if(!book.refunds_allowed) + return FALSE + + for(var/datum/action/cooldown/spell/other_spell in user.actions) + if(initial(spell_type.name) == initial(other_spell.name)) + return TRUE + + return FALSE + +/** + * Actually refund the entry for the user + * + * Arguments + * * user - the mob who's refunded the spell + * * book - what book they're refunding the spell from + * + * Return -1 on failure, or return the point value of the refund on success + */ +/datum/spellbook_entry/proc/refund_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + var/area/centcom/wizard_station/wizard_home = GLOB.areas_by_type[/area/centcom/wizard_station] + if(get_area(user) != wizard_home) + to_chat(user, span_warning("You can only refund spells at the wizard lair!")) + return -1 + + for(var/datum/action/cooldown/spell/to_refund in user.actions) + if(initial(spell_type.name) != initial(to_refund.name)) + continue + + var/amount_to_refund = to_refund.spell_level * cost + if(amount_to_refund <= 0) + return -1 + + qdel(to_refund) + name = initial(name) + log_spellbook("[key_name(user)] refunded [src] for [amount_to_refund] points") + return amount_to_refund + + return -1 + +/** + * Set any of the spell info saved on our entry + * after something has occured + * + * For example, updating the cooldown after upgrading it + */ +/datum/spellbook_entry/proc/set_spell_info() + if(!spell_type) + return + + cooldown = (initial(spell_type.cooldown_time) / 10) + +/// Item summons, they give you an item. +/datum/spellbook_entry/item + refundable = FALSE + buy_word = "Summon" + /// Typepath of what item we create when purchased + var/obj/item/item_path + +/datum/spellbook_entry/item/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + var/atom/spawned_path = new item_path(get_turf(user)) + log_spellbook("[key_name(user)] bought [src] for [cost] points") + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + try_equip_item(user, spawned_path) + log_purchase(user.key) + return spawned_path + +/// Attempts to give the item to the buyer on purchase. +/datum/spellbook_entry/item/proc/try_equip_item(mob/living/carbon/human/user, obj/item/to_equip) + var/was_put_in_hands = user.put_in_hands(to_equip) + to_chat(user, span_notice("\A [to_equip.name] has been summoned [was_put_in_hands ? "in your hands" : "at your feet"].")) + +/// Ritual, these cause station wide effects and are (pretty much) a blank slate to implement stuff in +/datum/spellbook_entry/summon + category = "Rituals" + limit = 1 + refundable = FALSE + buy_word = "Cast" + +/datum/spellbook_entry/summon/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + log_spellbook("[key_name(user)] cast [src] for [cost] points") + SSblackbox.record_feedback("tally", "wizard_spell_learned", 1, name) + log_purchase(user.key) + return TRUE + +/// Non-purchasable flavor spells to populate the spell book with, for style. +/datum/spellbook_entry/challenge + name = "Take the Challenge" + category = "Challenges" + refundable = FALSE + buy_word = "Accept" + +// See, non-purchasable. +/datum/spellbook_entry/challenge/can_buy(mob/living/carbon/human/user, obj/item/spellbook/book) + return FALSE diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm new file mode 100644 index 0000000000000..beec27fc99da1 --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm @@ -0,0 +1,107 @@ +// Wizard spells that assist the caster in some way +/datum/spellbook_entry/summonitem + name = "Summon Item" + desc = "Recalls a previously marked item to your hand from anywhere in the universe." + spell_type = /datum/action/cooldown/spell/summonitem + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/charge + name = "Charge" + desc = "This spell can be used to recharge a variety of things in your hands, from magical artifacts to electrical components. A creative wizard can even use it to grant magical power to a fellow magic user." + spell_type = /datum/action/cooldown/spell/charge + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/shapeshift + name = "Wild Shapeshift" + desc = "Take on the shape of another for a time to use their natural abilities. Once you've made your choice it cannot be changed." + spell_type = /datum/action/cooldown/spell/shapeshift/wizard + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/tap + name = "Soul Tap" + desc = "Fuel your spells using your own soul!" + spell_type = /datum/action/cooldown/spell/tap + category = "Assistance" + cost = 1 + +/datum/spellbook_entry/item/staffanimation + name = "Staff of Animation" + desc = "An arcane staff capable of shooting bolts of eldritch energy which cause inanimate objects to come to life. This magic doesn't affect machines." + item_path = /obj/item/gun/magic/staff/animate + category = "Assistance" + +/datum/spellbook_entry/item/soulstones + name = "Soulstone Shard Kit" + desc = "Soul Stone Shards are ancient tools capable of capturing and harnessing the spirits of the dead and dying. \ + The spell Artificer allows you to create arcane machines for the captured souls to pilot." + item_path = /obj/item/storage/belt/soulstone/full + category = "Assistance" + +/datum/spellbook_entry/item/soulstones/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"].")) + +/datum/spellbook_entry/item/soulstones/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + . =..() + if(!.) + return + + var/datum/action/cooldown/spell/conjure/construct/bonus_spell = new(user.mind || user) + bonus_spell.Grant(user) + +/datum/spellbook_entry/item/necrostone + name = "A Necromantic Stone" + desc = "A Necromantic stone is able to resurrect three dead individuals as skeletal thralls for you to command." + item_path = /obj/item/necromantic_stone + category = "Assistance" + +/datum/spellbook_entry/item/contract + name = "Contract of Apprenticeship" + desc = "A magical contract binding an apprentice wizard to your service, using it will summon them to your side." + item_path = /obj/item/antag_spawner/contract + category = "Assistance" + refundable = TRUE + +/datum/spellbook_entry/item/guardian + name = "Guardian Deck" + desc = "A deck of guardian tarot cards, capable of binding a personal guardian to your body. There are multiple types of guardian available, but all of them will transfer some amount of damage to you. \ + It would be wise to avoid buying these with anything capable of causing you to swap bodies with others." + item_path = /obj/item/guardiancreator/choose/wizard + category = "Assistance" + +/datum/spellbook_entry/item/guardian/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + . = ..() + if(!.) + return + + new /obj/item/paper/guides/antag/guardian/wizard(get_turf(user)) + to_chat(user, span_notice("If you are not experienced in the ways of wizardly guardians, a guide has been summoned at your feet.")) + +/datum/spellbook_entry/item/bloodbottle + name = "Bottle of Blood" + desc = "A bottle of magically infused blood, the smell of which will \ + attract extradimensional beings when broken. Be careful though, \ + the kinds of creatures summoned by blood magic are indiscriminate \ + in their killing, and you yourself may become a victim." + item_path = /obj/item/antag_spawner/slaughter_demon + limit = 3 + category = "Assistance" + refundable = TRUE + +/datum/spellbook_entry/item/hugbottle + name = "Bottle of Tickles" + desc = "A bottle of magically infused fun, the smell of which will \ + attract adorable extradimensional beings when broken. These beings \ + are similar to slaughter demons, but they do not permanently kill \ + their victims, instead putting them in an extradimensional hugspace, \ + to be released on the demon's death. Chaotic, but not ultimately \ + damaging. The crew's reaction to the other hand could be very \ + destructive." + item_path = /obj/item/antag_spawner/slaughter_demon/laughter + cost = 1 //non-destructive; it's just a jape, sibling! + limit = 3 + category = "Assistance" + refundable = TRUE diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/challenges.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/challenges.dm new file mode 100644 index 0000000000000..d200b89758854 --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/challenges.dm @@ -0,0 +1,10 @@ +// THESE ARE NOT PURCHASABLE SPELLS! +// They're flavor references to old spells that got removed + +// shit that sounds stupid but fun so we can painfully lock behind a dimmer +/datum/spellbook_entry/challenge/multiverse + name = "Multiverse Sword" + desc = "The Station gets a multiverse sword to stop you. Can you withstand the hordes of multiverse realities?" + +/datum/spellbook_entry/challenge/antiwizard + name = "Friendly Wizard Scum" + desc = "A \"Friendly\" Wizard will protect the station, and try to kill you. They get a spellbook much like you, but will use it for \"GOOD\"." diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm new file mode 100644 index 0000000000000..4227927ae22a0 --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm @@ -0,0 +1,148 @@ +// Defensive wizard spells +/datum/spellbook_entry/magicm + name = "Magic Missile" + desc = "Fires several, slow moving, magic projectiles at nearby targets." + spell_type = /datum/action/cooldown/spell/aoe/magic_missile + category = "Defensive" + +/datum/spellbook_entry/disabletech + name = "Disable Tech" + desc = "Disables all weapons, cameras and most other technology in range." + spell_type = /datum/action/cooldown/spell/emp/disable_tech + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/repulse + name = "Repulse" + desc = "Throws everything around the user away." + spell_type = /datum/action/cooldown/spell/aoe/repulse/wizard + category = "Defensive" + +/datum/spellbook_entry/lightning_packet + name = "Thrown Lightning" + desc = "Forged from eldrich energies, a packet of pure power, \ + known as a spell packet will appear in your hand, that when thrown will stun the target." + spell_type = /datum/action/cooldown/spell/conjure_item/spellpacket + category = "Defensive" + +/datum/spellbook_entry/timestop + name = "Time Stop" + desc = "Stops time for everyone except for you, allowing you to move freely \ + while your enemies and even projectiles are frozen." + spell_type = /datum/action/cooldown/spell/timestop + category = "Defensive" + +/datum/spellbook_entry/smoke + name = "Smoke" + desc = "Spawns a cloud of choking smoke at your location." + spell_type = /datum/action/cooldown/spell/smoke + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/forcewall + name = "Force Wall" + desc = "Create a magical barrier that only you can pass through." + spell_type = /datum/action/cooldown/spell/forcewall + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/lichdom + name = "Bind Soul" + desc = "A dark necromantic pact that can forever bind your soul to an item of your choosing, \ + turning you into an immortal Lich. So long as the item remains intact, you will revive from death, \ + no matter the circumstances. Be wary - with each revival, your body will become weaker, and \ + it will become easier for others to find your item of power." + spell_type = /datum/action/cooldown/spell/lichdom + category = "Defensive" + +/datum/spellbook_entry/spacetime_dist + name = "Spacetime Distortion" + desc = "Entangle the strings of space-time in an area around you, \ + randomizing the layout and making proper movement impossible. The strings vibrate..." + spell_type = /datum/action/cooldown/spell/spacetime_dist + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/the_traps + name = "The Traps!" + desc = "Summon a number of traps around you. They will damage and enrage any enemies that step on them." + spell_type = /datum/action/cooldown/spell/conjure/the_traps + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/bees + name = "Lesser Summon Bees" + desc = "This spell magically kicks a transdimensional beehive, \ + instantly summoning a swarm of bees to your location. These bees are NOT friendly to anyone." + spell_type = /datum/action/cooldown/spell/conjure/bee + category = "Defensive" + +/datum/spellbook_entry/duffelbag + name = "Bestow Cursed Duffel Bag" + desc = "A curse that firmly attaches a demonic duffel bag to the target's back. \ + The duffel bag will make the person it's attached to take periodical damage \ + if it is not fed regularly, and regardless of whether or not it's been fed, \ + it will slow the person wearing it down significantly." + spell_type = /datum/action/cooldown/spell/touch/duffelbag + category = "Defensive" + cost = 1 + +/datum/spellbook_entry/item/staffhealing + name = "Staff of Healing" + desc = "An altruistic staff that can heal the lame and raise the dead." + item_path = /obj/item/gun/magic/staff/healing + cost = 1 + category = "Defensive" + +/datum/spellbook_entry/item/lockerstaff + name = "Staff of the Locker" + desc = "A staff that shoots lockers. It eats anyone it hits on its way, leaving a welded locker with your victims behind." + item_path = /obj/item/gun/magic/staff/locker + category = "Defensive" + +/datum/spellbook_entry/item/scryingorb + name = "Scrying Orb" + desc = "An incandescent orb of crackling energy. Using it will allow you to release your ghost while alive, allowing you to spy upon the station and talk to the deceased. In addition, buying it will permanently grant you X-ray vision." + item_path = /obj/item/scrying + category = "Defensive" + +/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." + item_path = /obj/item/storage/belt/wands/full + 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"].")) + +/datum/spellbook_entry/item/armor + name = "Mastercrafted Armor Set" + desc = "An artefact suit of armor that allows you to cast spells \ + while providing more protection against attacks and the void of space. \ + Also grants a battlemage shield." + item_path = /obj/item/mod/control/pre_equipped/enchanted + category = "Defensive" + +/datum/spellbook_entry/item/armor/try_equip_item(mob/living/carbon/human/user, obj/item/to_equip) + var/obj/item/mod/control/mod = to_equip + var/obj/item/mod/module/storage/storage = locate() in mod.modules + var/obj/item/back = user.back + if(back) + if(!user.dropItemToGround(back)) + return + for(var/obj/item/item as anything in back.contents) + item.forceMove(storage) + if(!user.equip_to_slot_if_possible(mod, mod.slot_flags, qdel_on_fail = FALSE, disable_warning = TRUE)) + return + if(!user.dropItemToGround(user.wear_suit) || !user.dropItemToGround(user.head)) + return + mod.quick_activation() + +/datum/spellbook_entry/item/battlemage_charge + name = "Battlemage Armour Charges" + desc = "A powerful defensive rune, it will grant eight additional charges to a battlemage shield." + item_path = /obj/item/wizard_armour_charge + category = "Defensive" + cost = 1 diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/mobility.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/mobility.dm new file mode 100644 index 0000000000000..6dbc92d4a26eb --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/mobility.dm @@ -0,0 +1,45 @@ +// Wizard spells that aid mobiilty(or stealth?) +/datum/spellbook_entry/mindswap + name = "Mindswap" + desc = "Allows you to switch bodies with a target next to you. You will both fall asleep when this happens, and it will be quite obvious that you are the target's body if someone watches you do it." + spell_type = /datum/action/cooldown/spell/pointed/mind_transfer + category = "Mobility" + +/datum/spellbook_entry/knock + name = "Knock" + desc = "Opens nearby doors and closets." + spell_type = /datum/action/cooldown/spell/aoe/knock + category = "Mobility" + cost = 1 + +/datum/spellbook_entry/blink + name = "Blink" + desc = "Randomly teleports you a short distance." + spell_type = /datum/action/cooldown/spell/teleport/radius_turf/blink + category = "Mobility" + +/datum/spellbook_entry/teleport + name = "Teleport" + desc = "Teleports you to an area of your selection." + spell_type = /datum/action/cooldown/spell/teleport/area_teleport/wizard + category = "Mobility" + +/datum/spellbook_entry/jaunt + name = "Ethereal Jaunt" + desc = "Turns your form ethereal, temporarily making you invisible and able to pass through walls." + spell_type = /datum/action/cooldown/spell/jaunt/ethereal_jaunt + category = "Mobility" + +/datum/spellbook_entry/item/warpwhistle + name = "Warp Whistle" + desc = "A strange whistle that will transport you to a distant safe place on the station. There is a window of vulnerability at the beginning of every use." + item_path = /obj/item/warp_whistle + category = "Mobility" + cost = 1 + +/datum/spellbook_entry/item/staffdoor + name = "Staff of Door Creation" + desc = "A particular staff that can mold solid walls into ornate doors. Useful for getting around in the absence of other transportation. Does not work on glass." + item_path = /obj/item/gun/magic/staff/door + cost = 1 + category = "Mobility" diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm new file mode 100644 index 0000000000000..37bdef69a646e --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/summons.dm @@ -0,0 +1,87 @@ +// Ritual spells which affect the station at large +/// How much threat we need to let these rituals happen on dynamic +#define MINIMUM_THREAT_FOR_RITUALS 100 + +/datum/spellbook_entry/summon/ghosts + name = "Summon Ghosts" + desc = "Spook the crew out by making them see dead people. \ + Be warned, ghosts are capricious and occasionally vindicative, \ + and some will use their incredibly minor abilities to frustrate you." + cost = 0 + +/datum/spellbook_entry/summon/ghosts/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + summon_ghosts(user) + playsound(get_turf(user), 'sound/effects/ghost2.ogg', 50, TRUE) + return ..() + +/datum/spellbook_entry/summon/guns + name = "Summon Guns" + desc = "Nothing could possibly go wrong with arming a crew of lunatics just itching for an excuse to kill you. \ + There is a good chance that they will shoot each other first." + +/datum/spellbook_entry/summon/guns/can_be_purchased() + // Summon Guns requires 100 threat. + var/datum/game_mode/dynamic/mode = SSticker.mode + if(mode.threat_level < MINIMUM_THREAT_FOR_RITUALS) + return FALSE + // Also must be config enabled + return !CONFIG_GET(flag/no_summon_guns) + +/datum/spellbook_entry/summon/guns/buy_spell(mob/living/carbon/human/user,obj/item/spellbook/book) + summon_guns(user, 10) + playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, TRUE) + return ..() + +/datum/spellbook_entry/summon/magic + name = "Summon Magic" + desc = "Share the wonders of magic with the crew and show them \ + why they aren't to be trusted with it at the same time." + +/datum/spellbook_entry/summon/magic/can_be_purchased() + // Summon Magic requires 100 threat. + var/datum/game_mode/dynamic/mode = SSticker.mode + if(mode.threat_level < MINIMUM_THREAT_FOR_RITUALS) + return FALSE + // Also must be config enabled + return !CONFIG_GET(flag/no_summon_magic) + +/datum/spellbook_entry/summon/magic/buy_spell(mob/living/carbon/human/user,obj/item/spellbook/book) + summon_magic(user, 10) + playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, TRUE) + return ..() + +/datum/spellbook_entry/summon/events + name = "Summon Events" + desc = "Give Murphy's law a little push and replace all events with \ + special wizard ones that will confound and confuse everyone. \ + Multiple castings increase the rate of these events." + cost = 2 + limit = 5 // Each purchase can intensify it. + +/datum/spellbook_entry/summon/events/can_be_purchased() + // Summon Events requires 100 threat. + var/datum/game_mode/dynamic/mode = SSticker.mode + if(mode.threat_level < MINIMUM_THREAT_FOR_RITUALS) + return FALSE + // Also, must be config enabled + return !CONFIG_GET(flag/no_summon_events) + +/datum/spellbook_entry/summon/events/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + summon_events(user) + playsound(get_turf(user), 'sound/magic/castsummon.ogg', 50, TRUE) + return ..() + +/datum/spellbook_entry/summon/curse_of_madness + name = "Curse of Madness" + desc = "Curses the station, warping the minds of everyone inside, causing lasting traumas. Warning: this spell can affect you if not cast from a safe distance." + cost = 4 + +/datum/spellbook_entry/summon/curse_of_madness/buy_spell(mob/living/carbon/human/user, obj/item/spellbook/book) + var/message = tgui_input_text(user, "Whisper a secret truth to drive your victims to madness", "Whispers of Madness") + if(!message) + return FALSE + curse_of_madness(user, message) + playsound(user, 'sound/magic/mandswap.ogg', 50, TRUE) + return ..() + +#undef MINIMUM_THREAT_FOR_RITUALS diff --git a/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm b/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm new file mode 100644 index 0000000000000..df83035fc7f53 --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/wizard_spellbook.dm @@ -0,0 +1,329 @@ +/obj/item/spellbook + name = "spell book" + desc = "An unearthly tome that glows with power." + icon = 'icons/obj/library.dmi' + icon_state ="book" + worn_icon_state = "book" + throw_speed = 2 + throw_range = 5 + w_class = WEIGHT_CLASS_TINY + + /// The number of book charges we have to buy spells + var/uses = 10 + /// The bonus that you get from going semi-random. + var/semi_random_bonus = 2 + /// The bonus that you get from going full random. + var/full_random_bonus = 5 + /// Determines if this spellbook can refund anything. + var/refunds_allowed = TRUE + /// The mind that first used the book. Automatically assigned when a wizard spawns. + var/datum/mind/owner + /// A list to all spellbook entries within + var/list/entries = list() + +/obj/item/spellbook/Initialize(mapload) + . = ..() + prepare_spells() + RegisterSignal(src, COMSIG_ITEM_MAGICALLY_CHARGED, .proc/on_magic_charge) + +/obj/item/spellbook/Destroy(force) + owner = null + entries.Cut() + return ..() + +/** + * Signal proc for [COMSIG_ITEM_MAGICALLY_CHARGED] + * + * Has no effect on charge, but gives a funny message to people who think they're clever. + */ +/obj/item/spellbook/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/spell, mob/living/caster) + SIGNAL_HANDLER + + var/static/list/clever_girl = list( + "NICE TRY BUT NO!", + "CLEVER BUT NOT CLEVER ENOUGH!", + "SUCH FLAGRANT CHEESING IS WHY WE ACCEPTED YOUR APPLICATION!", + "CUTE! VERY CUTE!", + "YOU DIDN'T THINK IT'D BE THAT EASY, DID YOU?", + ) + + to_chat(caster, span_warning("Glowing red letters appear on the front cover...")) + to_chat(caster, span_red(pick(clever_girl))) + + return COMPONENT_ITEM_BURNT_OUT + +/obj/item/spellbook/examine(mob/user) + . = ..() + if(owner) + . += {"There is a small signature on the front cover: "[owner]"."} + else + . += "It appears to have no author." + +/obj/item/spellbook/attack_self(mob/user) + if(!owner) + if(!user.mind) + return + to_chat(user, span_notice("You bind [src] to yourself.")) + owner = user.mind + return + + if(user.mind != owner) + if(user.mind?.special_role == ROLE_WIZARD_APPRENTICE) + to_chat(user, span_warning("If you got caught sneaking a peek from your teacher's spellbook, you'd likely be expelled from the Wizard Academy. Better not.")) + else + to_chat(user, span_warning("[src] does not recognize you as its owner and refuses to open!")) + return + + return ..() + +/obj/item/spellbook/attackby(obj/item/O, mob/user, params) + // This can be generalized in the future, but for now it stays + if(istype(O, /obj/item/antag_spawner/contract)) + var/datum/spellbook_entry/item/contract/contract_entry = locate() in entries + if(!istype(contract_entry)) + to_chat(user, span_warning("[src] doesn't seem to want to refund [O].")) + return + if(!contract_entry.can_refund(user, src)) + to_chat(user, span_warning("You can't refund [src].")) + return + var/obj/item/antag_spawner/contract/contract = O + if(contract.used) + to_chat(user, span_warning("The contract has been used, you can't get your points back now!")) + return + + to_chat(user, span_notice("You feed the contract back into the spellbook, refunding your points.")) + uses += contract_entry.cost + contract_entry.times-- + qdel(O) + + else if(istype(O, /obj/item/antag_spawner/slaughter_demon/laughter)) + var/datum/spellbook_entry/item/hugbottle/demon_entry = locate() in entries + if(!istype(demon_entry)) + to_chat(user, span_warning("[src] doesn't seem to want to refund [O].")) + return + if(!demon_entry.can_refund(user, src)) + to_chat(user, span_warning("You can't refund [O].")) + return + + to_chat(user, span_notice("On second thought, maybe summoning a demon isn't a funny idea. You refund your points.")) + uses += demon_entry.cost + demon_entry.times-- + qdel(O) + + else if(istype(O, /obj/item/antag_spawner/slaughter_demon)) + var/datum/spellbook_entry/item/bloodbottle/demon_entry = locate() in entries + if(!istype(demon_entry)) + to_chat(user, span_warning("[src] doesn't seem to want to refund [O].")) + return + if(!demon_entry.can_refund(user, src)) + to_chat(user, span_warning("You can't refund [O].")) + return + + to_chat(user, span_notice("On second thought, maybe summoning a demon is a bad idea. You refund your points.")) + uses += demon_entry.cost + demon_entry.times-- + qdel(O) + + return ..() + +/// Instantiates our list of spellbook entries. +/obj/item/spellbook/proc/prepare_spells() + var/entry_types = subtypesof(/datum/spellbook_entry) + for(var/type in entry_types) + var/datum/spellbook_entry/possible_entry = new type() + if(!possible_entry.can_be_purchased()) + qdel(possible_entry) + continue + + possible_entry.set_spell_info() //loads up things for the entry that require checking spell instance. + entries |= possible_entry + +/obj/item/spellbook/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Spellbook") + ui.open() + +/obj/item/spellbook/ui_data(mob/user) + var/list/data = list() + data["owner"] = owner + data["points"] = uses + data["semi_random_bonus"] = initial(uses) + semi_random_bonus + data["full_random_bonus"] = initial(uses) + full_random_bonus + return data + +//This is a MASSIVE amount of data, please be careful if you remove it from static. +/obj/item/spellbook/ui_static_data(mob/user) + var/list/data = list() + // Collect all info from each intry. + var/list/entry_data = list() + for(var/datum/spellbook_entry/entry as anything in entries) + var/list/individual_entry_data = list() + individual_entry_data["name"] = entry.name + individual_entry_data["desc"] = entry.desc + individual_entry_data["ref"] = REF(entry) + individual_entry_data["requires_wizard_garb"] = entry.requires_wizard_garb + individual_entry_data["cost"] = entry.cost + individual_entry_data["times"] = entry.times + individual_entry_data["cooldown"] = entry.cooldown + individual_entry_data["cat"] = entry.category + individual_entry_data["refundable"] = entry.refundable + individual_entry_data["buyword"] = entry.buy_word + entry_data += list(individual_entry_data) + + data["entries"] = entry_data + return data + +/obj/item/spellbook/ui_act(action, params) + . = ..() + if(.) + return + var/mob/living/carbon/human/wizard = usr + if(!istype(wizard)) + to_chat(wizard, span_warning("The book doesn't seem to listen to lower life forms.")) + return FALSE + + // Actions that are always available + switch(action) + if("purchase") + var/datum/spellbook_entry/entry = locate(params["spellref"]) in entries + return purchase_entry(entry, wizard) + + if("refund") + var/datum/spellbook_entry/entry = locate(params["spellref"]) in entries + if(!istype(entry)) + CRASH("[type] had an invalid ref to a spell passed in refund.") + if(!entry.can_refund(wizard, src)) + return FALSE + var/result = entry.refund_spell(wizard, src) + if(result <= 0) + return FALSE + + entry.times = 0 + uses += result + return TRUE + + if(uses < initial(uses)) + to_chat(wizard, span_warning("You need to have all your spell points to do this!")) + return FALSE + + // Actions that are only available if you have full spell points + switch(action) + if("semirandomize") + semirandomize(wizard, semi_random_bonus) + return TRUE + + if("randomize") + randomize(wizard, full_random_bonus) + return TRUE + + if("purchase_loadout") + wizard_loadout(wizard, locate(params["id"])) + return TRUE + +/// Attempts to purchased the passed entry [to_buy] for [user]. +/obj/item/spellbook/proc/purchase_entry(datum/spellbook_entry/to_buy, mob/living/carbon/human/user) + if(!istype(to_buy)) + CRASH("Spellbook attempted to buy an invalid entry. Got: [to_buy ? "[to_buy] ([to_buy.type])" : "null"]") + if(!to_buy.can_buy(user, src)) + return FALSE + if(!to_buy.buy_spell(user, src)) + return FALSE + + to_buy.times++ + uses -= to_buy.cost + return TRUE + +/// Purchases a wizard loadout [loadout] for [wizard]. +/obj/item/spellbook/proc/wizard_loadout(mob/living/carbon/human/wizard, loadout) + var/list/wanted_spells + switch(loadout) + if(WIZARD_LOADOUT_CLASSIC) //(Fireball>2, MM>2, Smite>2, Jauntx2>4) = 10 + wanted_spells = list( + /datum/spellbook_entry/fireball = 1, + /datum/spellbook_entry/magicm = 1, + /datum/spellbook_entry/disintegrate = 1, + /datum/spellbook_entry/jaunt = 2, + ) + if(WIZARD_LOADOUT_MJOLNIR) //(Mjolnir>2, Summon Itemx1>1, Mutate>2, Force Wall>1, Blink>2, tesla>2) = 10 + wanted_spells = list( + /datum/spellbook_entry/item/mjolnir = 1, + /datum/spellbook_entry/summonitem = 1, + /datum/spellbook_entry/mutate = 1, + /datum/spellbook_entry/forcewall = 1, + /datum/spellbook_entry/blink = 1, + /datum/spellbook_entry/teslablast = 1, + ) + if(WIZARD_LOADOUT_WIZARMY) //(Soulstones>2, Staff of Change>2, A Necromantic Stone>2, Teleport>2, Ethereal Jaunt>2) = 10 + wanted_spells = list( + /datum/spellbook_entry/item/soulstones = 1, + /datum/spellbook_entry/item/staffchange = 1, + /datum/spellbook_entry/item/necrostone = 1, + /datum/spellbook_entry/teleport = 1, + /datum/spellbook_entry/jaunt = 1, + ) + if(WIZARD_LOADOUT_SOULTAP) //(Soul Tap>1, Smite>2, Flesh to Stone>2, Mindswap>2, Knock>1, Teleport>2) = 10 + wanted_spells = list( + /datum/spellbook_entry/tap = 1, + /datum/spellbook_entry/disintegrate = 1, + /datum/spellbook_entry/fleshtostone = 1, + /datum/spellbook_entry/mindswap = 1, + /datum/spellbook_entry/knock = 1, + /datum/spellbook_entry/teleport = 1, + ) + + if(!length(wanted_spells)) + stack_trace("Wizard Loadout \"[loadout]\" did not find a loadout that existed.") + return + + for(var/entry in wanted_spells) + if(!ispath(entry, /datum/spellbook_entry)) + stack_trace("Wizard Loadout \"[loadout]\" had an non-spellbook_entry type in its wanted spells list. ([entry])") + continue + + var/datum/spellbook_entry/to_buy = locate(entry) in entries + if(!istype(to_buy)) + stack_trace("Wizard Loadout \"[loadout]\" had an invalid entry in its wanted spells list. ([entry])") + continue + + for(var/i in 1 to wanted_spells[entry]) + if(!purchase_entry(to_buy, wizard)) + stack_trace("Wizard Loadout \"[loadout]\" was unable to buy a spell for [wizard]. ([entry])") + message_admins("Wizard [wizard] purchased Loadout \"[loadout]\" but was unable to purchase one of the entries ([to_buy]) for some reason.") + break + + refunds_allowed = FALSE + + if(uses > 0) + stack_trace("Wizard Loadout \"[loadout]\" does not use 10 wizard spell slots (used: [initial(uses) - uses]). Stop scamming players out.") + +/// Purchases a semi-random wizard loadout for [wizard] +/// If passed a number [bonus_to_give], the wizard is given additional uses on their spellbook, used in randomization. +/obj/item/spellbook/proc/semirandomize(mob/living/carbon/human/wizard, bonus_to_give = 0) + var/list/needed_cats = list("Offensive", "Mobility") + var/list/shuffled_entries = shuffle(entries) + for(var/i in 1 to 2) + for(var/datum/spellbook_entry/entry as anything in shuffled_entries) + if(!(entry.category in needed_cats)) + continue + if(!purchase_entry(entry, wizard)) + continue + needed_cats -= entry.category //so the next loop doesn't find another offense spell + break + + refunds_allowed = FALSE + //we have given two specific category spells to the wizard. the rest are completely random! + randomize(wizard, bonus_to_give = bonus_to_give) + +/// Purchases a fully random wizard loadout for [wizard], with a point bonus [bonus_to_give]. +/// If passed a number [bonus_to_give], the wizard is given additional uses on their spellbook, used in randomization. +/obj/item/spellbook/proc/randomize(mob/living/carbon/human/wizard, bonus_to_give = 0) + var/list/entries_copy = entries.Copy() + uses += bonus_to_give + while(uses > 0 && length(entries_copy)) + var/datum/spellbook_entry/entry = pick(entries_copy) + if(!purchase_entry(entry, wizard)) + continue + entries_copy -= entry + + refunds_allowed = FALSE diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index 21bb1675add2a..f04e776f13567 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -1,3 +1,6 @@ +/// Global assoc list. [ckey] = [spellbook entry type] +GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) + /datum/antagonist/wizard name = "Space Wizard" roundend_category = "wizards/witches" @@ -130,8 +133,11 @@ log_objective(owner, hijack_objective.explanation_text) /datum/antagonist/wizard/on_removal() - unregister() - owner.RemoveAllSpells() // TODO keep track which spells are wizard spells which innate stuff + // Currently removes all spells regardless of innate or not. Could be improved. + for(var/datum/action/cooldown/spell/spell in owner.current.actions) + if(spell.target == owner) + qdel(spell) + owner.current.actions -= spell return ..() /datum/antagonist/wizard/proc/equip_wizard() @@ -216,53 +222,56 @@ . = ..() if(!owner) return - var/mob/living/carbon/human/H = owner.current - if(!istype(H)) + if(!ishuman(owner.current)) return + var/list/spells_to_grant = list() + var/list/items_to_grant = list() switch(school) if(APPRENTICE_DESTRUCTION) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/projectile/magic_missile(null)) - owner.AddSpell(new /obj/effect/proc_holder/spell/aimed/fireball(null)) - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned powerful, destructive spells. You are able to cast magic missile and fireball.") + spells_to_grant = list( + /datum/action/cooldown/spell/aoe/magic_missile, + /datum/action/cooldown/spell/pointed/projectile/fireball, + ) + to_chat(owner, span_bold("Your service has not gone unrewarded, however. \ + Studying under [master.current.real_name], you have learned powerful, \ + destructive spells. You are able to cast magic missile and fireball.")) + if(APPRENTICE_BLUESPACE) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/area_teleport/teleport(null)) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/ethereal_jaunt(null)) - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned reality bending mobility spells. You are able to cast teleport and ethereal jaunt.") + spells_to_grant = list( + /datum/action/cooldown/spell/teleport/area_teleport/wizard, + /datum/action/cooldown/spell/jaunt/ethereal_jaunt, + ) + to_chat(owner, span_bold("Your service has not gone unrewarded, however. \ + Studying under [master.current.real_name], you have learned reality-bending \ + mobility spells. You are able to cast teleport and ethereal jaunt.")) + if(APPRENTICE_HEALING) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/charge(null)) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/forcewall(null)) - H.put_in_hands(new /obj/item/gun/magic/staff/healing(H)) - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned livesaving survival spells. You are able to cast charge and forcewall.") + spells_to_grant = list( + /datum/action/cooldown/spell/charge, + /datum/action/cooldown/spell/forcewall, + ) + items_to_grant = list( + /obj/item/gun/magic/staff/healing, + ) + to_chat(owner, span_bold("Your service has not gone unrewarded, however. \ + Studying under [master.current.real_name], you have learned life-saving \ + survival spells. You are able to cast charge and forcewall, and have a staff of healing.")) if(APPRENTICE_ROBELESS) - owner.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null)) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/mind_transfer(null)) - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned stealthy, robeless spells. You are able to cast knock and mindswap.") - if(APPRENTICE_WILDMAGIC) - var/static/list/spell_entry - if(!spell_entry) - spell_entry = list() - for(var/datum/spellbook_entry/each_entry as() in subtypesof(/datum/spellbook_entry)-typesof(/datum/spellbook_entry/item)-typesof(/datum/spellbook_entry/summon)) - spell_entry += new each_entry - - var/spells_left = 2 - while(spells_left) - var/failsafe = FALSE - var/datum/spellbook_entry/chosen_spell = pick(spell_entry) - if(chosen_spell.no_random) - continue - for(var/obj/effect/proc_holder/spell/my_spell in owner.spell_list) - if(chosen_spell.spell_type == my_spell.type) // You don't learn the same spell - failsafe = TRUE - break - if(is_type_in_typecache(my_spell.type, chosen_spell.no_coexistence_typecache)) // You don't learn a spell that isn't compatible with another - failsafe = TRUE - break - if(failsafe) - continue - var/obj/effect/proc_holder/spell/new_spell = chosen_spell.spell_type - owner.AddSpell(new new_spell(null)) - spells_left-- - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned special spells that aren't available to standard apprentices.") + spells_to_grant = list( + /datum/action/cooldown/spell/aoe/knock, + /datum/action/cooldown/spell/pointed/mind_transfer, + ) + to_chat(owner, span_bold("Your service has not gone unrewarded, however. \ + Studying under [master.current.real_name], you have learned stealthy, \ + robeless spells. You are able to cast knock and mindswap.")) + + for(var/spell_type in spells_to_grant) + var/datum/action/cooldown/spell/new_spell = new spell_type(owner) + new_spell.Grant(owner.current) + + for(var/item_type in items_to_grant) + var/obj/item/new_item = new item_type(owner.current) + owner.current.put_in_hands(new_item) /datum/antagonist/wizard/apprentice/create_objectives() var/datum/objective/protect/new_objective = new /datum/objective/protect @@ -301,9 +310,12 @@ H.equip_to_slot_or_del(new master_mob.back.type, ITEM_SLOT_BACK) //Operation: Fuck off and scare people - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/area_teleport/teleport(null)) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/turf_teleport/blink(null)) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/ethereal_jaunt(null)) + var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/jaunt = new(owner) + jaunt.Grant(H) + var/datum/action/cooldown/spell/teleport/area_teleport/wizard/teleport = new(owner) + teleport.Grant(H) + var/datum/action/cooldown/spell/teleport/radius_turf/blink/blink = new(owner) + blink.Grant(H) /datum/antagonist/wizard/proc/update_wiz_icons_added(mob/living/wiz,join = TRUE) var/datum/atom_hud/antag/wizhud = GLOB.huds[ANTAG_HUD_WIZ] @@ -323,17 +335,19 @@ /datum/antagonist/wizard/academy/equip_wizard() . = ..() - - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/ethereal_jaunt) - owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/projectile/magic_missile) - owner.AddSpell(new /obj/effect/proc_holder/spell/aimed/fireball) - - var/mob/living/M = owner.current - if(!istype(M)) + if(!isliving(owner.current)) return + var/mob/living/living_current = owner.current + + var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/jaunt = new(owner) + jaunt.Grant(living_current) + var/datum/action/cooldown/spell/aoe/magic_missile/missile = new(owner) + missile.Grant(living_current) + var/datum/action/cooldown/spell/pointed/projectile/fireball/fireball = new(owner) + fireball.Grant(living_current) - var/obj/item/implant/exile/Implant = new/obj/item/implant/exile(M) - Implant.implant(M) + var/obj/item/implant/exile/exiled = new /obj/item/implant/exile(living_current) + exiled.implant(living_current) /datum/antagonist/wizard/academy/create_objectives() var/datum/objective/new_objective = new("Protect Wizard Academy from the intruders") @@ -362,12 +376,18 @@ else parts += "The wizard has failed!" - if(owner.spell_list.len>0) - parts += "[owner.name] used the following spells: " - var/list/spell_names = list() - for(var/obj/effect/proc_holder/spell/S in owner.spell_list) - spell_names += S.name - parts += spell_names.Join(", ") + var/list/purchases = list() + for(var/list/log as anything in GLOB.wizard_spellbook_purchases_by_key[owner.key]) + var/datum/spellbook_entry/bought = log[LOG_SPELL_TYPE] + var/amount = log[LOG_SPELL_AMOUNT] + + purchases += "[amount > 1 ? "[amount]x ":""][initial(bought.name)]" + + if(length(purchases)) + parts += span_bold("[owner.name] used the following spells:") + parts += purchases.Join(", ") + else + parts += span_bold("[owner.name] didn't buy any spells!") return parts.Join("
") diff --git a/code/modules/awaymissions/mission_code/Academy.dm b/code/modules/awaymissions/mission_code/Academy.dm index f9163f92e7d01..548355f0e5de3 100644 --- a/code/modules/awaymissions/mission_code/Academy.dm +++ b/code/modules/awaymissions/mission_code/Academy.dm @@ -301,7 +301,7 @@ //Random One-use spellbook T.visible_message("A magical looking book drops to the floor!") do_smoke(0, drop_location()) - new /obj/item/book/granter/spell/random(drop_location()) + new /obj/item/book/granter/action/spell/random(drop_location()) if(16) //Servant & Servant Summon T.visible_message("A Dice Servant appears in a cloud of smoke!") @@ -321,9 +321,8 @@ message_admins("[ADMIN_LOOKUPFLW(C)] was spawned as Dice Servant") H.key = C.key - var/obj/effect/proc_holder/spell/targeted/summonmob/S = new - S.target_mob = H - user.mind.AddSpell(S) + var/datum/action/cooldown/spell/summon_mob/summon_servant = new(user.mind || user, H) + summon_servant.Grant(user) if(17) //Tator Kit @@ -357,30 +356,44 @@ glasses = /obj/item/clothing/glasses/monocle gloves = /obj/item/clothing/gloves/color/white -/obj/effect/proc_holder/spell/targeted/summonmob +/datum/action/cooldown/spell/summon_mob name = "Summon Servant" desc = "This spell can be used to call your servant, whenever you need it." - charge_max = 100 - clothes_req = 0 + button_icon_state = "summons" + + school = SCHOOL_CONJURATION + cooldown_time = 10 SECONDS + invocation = "JE VES" invocation_type = INVOCATION_WHISPER - range = -1 - level_max = 0 //cannot be improved - cooldown_min = 100 - include_user = 1 + spell_requirements = NONE + spell_max_level = 0 //cannot be improved + + smoke_type = /datum/effect_system/fluid_spread/smoke + smoke_amt = 2 - var/mob/living/target_mob + var/datum/weakref/summon_weakref - action_icon_state = "summons" +/datum/action/cooldown/spell/summon_mob/New(Target, mob/living/summoned_mob) + . = ..() + if(summoned_mob) + summon_weakref = WEAKREF(summoned_mob) -/obj/effect/proc_holder/spell/targeted/summonmob/cast(list/targets,mob/user = usr) - if(!target_mob) +/datum/action/cooldown/spell/summon_mob/cast(atom/cast_on) + . = ..() + var/mob/living/to_summon = summon_weakref?.resolve() + if(QDELETED(to_summon)) + to_chat(cast_on, span_warning("You can't seem to summon your servant - it seems they've vanished from reality, or never existed in the first place...")) return - var/turf/Start = get_turf(user) - for(var/direction in GLOB.alldirs) - var/turf/T = get_step(Start,direction) - if(!T.density) - target_mob.Move(T) + + do_teleport( + to_summon, + get_turf(cast_on), + precision = 1, + asoundin = 'sound/magic/wand_teleport.ogg', + asoundout = 'sound/magic/wand_teleport.ogg', + channel = TELEPORT_CHANNEL_MAGIC, + ) /obj/structure/ladder/unbreakable/rune name = "\improper Teleportation Rune" diff --git a/code/modules/clothing/chameleon.dm b/code/modules/clothing/chameleon.dm index 90bec770bbb80..f146c86d13f01 100644 --- a/code/modules/clothing/chameleon.dm +++ b/code/modules/clothing/chameleon.dm @@ -235,7 +235,7 @@ var/obj/item/thing = target thing.update_slot_icon() - UpdateButtonIcon() + UpdateButtons() /datum/action/item_action/chameleon/change/proc/update_item(obj/item/picked_item, emp=FALSE, mob/item_holder=null) var/keepname = FALSE @@ -397,6 +397,7 @@ chameleon_action.chameleon_name = "Jumpsuit" chameleon_action.chameleon_blacklist = typecacheof(list(/obj/item/clothing/under, /obj/item/clothing/under/color, /obj/item/clothing/under/rank, /obj/item/clothing/under/changeling), only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/under/chameleon/emp_act(severity) . = ..() @@ -444,6 +445,7 @@ chameleon_action.chameleon_name = "Suit" chameleon_action.chameleon_blacklist = typecacheof(list(/obj/item/clothing/suit/armor/abductor, /obj/item/clothing/suit/changeling), only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/suit/chameleon/emp_act(severity) . = ..() @@ -488,6 +490,7 @@ chameleon_action.chameleon_name = "Glasses" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/glasses/changeling, only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/glasses/chameleon/emp_act(severity) . = ..() @@ -541,6 +544,7 @@ chameleon_action.chameleon_name = "Gloves" chameleon_action.chameleon_blacklist = typecacheof(list(/obj/item/clothing/gloves, /obj/item/clothing/gloves/color, /obj/item/clothing/gloves/changeling), only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/gloves/chameleon/emp_act(severity) . = ..() @@ -602,6 +606,7 @@ chameleon_action.chameleon_name = "Hat" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/head/changeling, only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/head/chameleon/emp_act(severity) . = ..() @@ -728,6 +733,7 @@ chameleon_action.chameleon_name = "Mask" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/mask/changeling, only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) tongue_action = new(src) if(!tongue_action.tongue_list) tongue_action.generate_tongue_list() @@ -815,6 +821,7 @@ chameleon_action.chameleon_name = "Shoes" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/shoes/changeling, only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/shoes/chameleon/emp_act(severity) . = ..() @@ -856,6 +863,7 @@ chameleon_action.chameleon_type = /obj/item/storage/backpack chameleon_action.chameleon_name = "Backpack" chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/storage/backpack/chameleon/emp_act(severity) . = ..() @@ -895,6 +903,7 @@ chameleon_action.chameleon_type = /obj/item/storage/belt chameleon_action.chameleon_name = "Belt" chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/storage/belt/chameleon/ComponentInitialize() . = ..() @@ -937,6 +946,7 @@ chameleon_action.chameleon_type = /obj/item/radio/headset chameleon_action.chameleon_name = "Headset" chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/radio/headset/chameleon/emp_act(severity) . = ..() @@ -981,6 +991,7 @@ chameleon_action.chameleon_name = "tablet" chameleon_action.chameleon_blacklist = typecacheof(list(/obj/item/modular_computer/tablet/pda/heads), only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/modular_computer/tablet/pda/chameleon/emp_act(severity) . = ..() @@ -1017,6 +1028,7 @@ chameleon_action.chameleon_type = /obj/item/stamp chameleon_action.chameleon_name = "Stamp" chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/stamp/chameleon/broken/Initialize(mapload) . = ..() @@ -1054,6 +1066,7 @@ chameleon_action.chameleon_type = /obj/item/clothing/neck chameleon_action.chameleon_name = "Neck Accessory" chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/neck/chameleon/emp_act(severity) . = ..() diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index 723509ef40772..d2b1d98325910 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -65,7 +65,7 @@ var/obj/item/food/clothing/moth_snack /obj/item/clothing/Initialize(mapload) - if(CHECK_BITFIELD(clothing_flags, VOICEBOX_TOGGLABLE)) + if(clothing_flags & VOICEBOX_TOGGLABLE) actions_types += /datum/action/item_action/toggle_voice_box . = ..() if(ispath(pocket_storage_component_path)) diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index 7eb20bdca3c0c..e40dc7664c2f1 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -479,6 +479,7 @@ chameleon_action.chameleon_name = "Glasses" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/glasses/changeling, only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/glasses/thermal/syndi/emp_act(severity) . = ..() @@ -543,10 +544,35 @@ lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE resistance_flags = LAVA_PROOF | FIRE_PROOF vision_correction = 1 // why should the eye of a god have bad vision? + var/datum/action/cooldown/scan/scan_ability /obj/item/clothing/glasses/godeye/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_NODROP, EYE_OF_GOD_TRAIT) + scan_ability = new(src) + +/obj/item/clothing/glasses/godeye/Destroy() + QDEL_NULL(scan_ability) + return ..() + + +/obj/item/clothing/glasses/godeye/equipped(mob/living/user, slot) + . = ..() + if(ishuman(user) && slot == ITEM_SLOT_EYES) + ADD_TRAIT(src, TRAIT_NODROP, EYE_OF_GOD_TRAIT) + pain(user) + scan_ability.Grant(user) + +/obj/item/clothing/glasses/godeye/dropped(mob/living/user) + . = ..() + // Behead someone, their "glasses" drop on the floor + // and thus, the god eye should no longer be sticky + REMOVE_TRAIT(src, TRAIT_NODROP, EYE_OF_GOD_TRAIT) + scan_ability.Remove(user) + +/obj/item/clothing/glasses/godeye/proc/pain(mob/living/victim) + to_chat(victim, span_userdanger("You experience blinding pain, as [src] burrows into your skull.")) + victim.emote("scream") + victim.flash_act() /obj/item/clothing/glasses/godeye/attackby(obj/item/W as obj, mob/user as mob, params) if(istype(W, src) && W != src && W.loc == user) @@ -562,6 +588,47 @@ qdel(src) ..() +/datum/action/cooldown/scan + name = "Scan" + desc = "Scan an enemy, to get their location and stagger them, increasing their time between attacks." + background_icon_state = "bg_clock" + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "scan" + + click_to_activate = TRUE + cooldown_time = 45 SECONDS + ranged_mousepointer = 'icons/effects/mouse_pointers/scan_target.dmi' + +/datum/action/cooldown/scan/IsAvailable() + return ..() && isliving(owner) + +/datum/action/cooldown/scan/Activate(atom/scanned) + StartCooldown(15 SECONDS) + + if(owner.stat != CONSCIOUS) + return FALSE + if(!isliving(scanned) || scanned == owner) + owner.balloon_alert(owner, "invalid scanned!") + return FALSE + + var/mob/living/living_owner = owner + var/mob/living/living_scanned = scanned + living_scanned.apply_status_effect(/datum/status_effect/stagger) + var/datum/status_effect/agent_pinpointer/scan_pinpointer = living_owner.apply_status_effect(/datum/status_effect/agent_pinpointer/scan) + scan_pinpointer.scan_target = living_scanned + + living_scanned.set_timed_status_effect(100 SECONDS, /datum/status_effect/jitter, only_if_higher = TRUE) + to_chat(living_scanned, span_warning("You've been staggered!")) + living_scanned.add_filter("scan", 2, list("type" = "outline", "color" = COLOR_YELLOW, "size" = 1)) + addtimer(CALLBACK(living_scanned, /atom/.proc/remove_filter, "scan"), 30 SECONDS) + + owner.playsound_local(get_turf(owner), 'sound/magic/smoke.ogg', 50, TRUE) + owner.balloon_alert(owner, "[living_scanned] scanned") + addtimer(CALLBACK(src, /atom/.proc/balloon_alert, owner, "scan recharged"), cooldown_time) + + StartCooldown() + return TRUE + /obj/item/clothing/glasses/AltClick(mob/user) if(!user.canUseTopic(src, BE_CLOSE)) return diff --git a/code/modules/clothing/glasses/hud.dm b/code/modules/clothing/glasses/hud.dm index b3beba00edd23..fd6ca4f81c575 100644 --- a/code/modules/clothing/glasses/hud.dm +++ b/code/modules/clothing/glasses/hud.dm @@ -237,6 +237,7 @@ chameleon_action.chameleon_name = "Glasses" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/glasses/changeling, only_root_path = TRUE) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/glasses/hud/security/chameleon/emp_act(severity) . = ..() @@ -340,6 +341,9 @@ var/datum/atom_hud/H = GLOB.huds[hud_type] H.add_hud_to(user) +/datum/action/item_action/switch_hud + name = "Switch HUD" + /obj/item/clothing/glasses/hud/toggle/thermal name = "thermal HUD scanner" desc = "Thermal imaging HUD in the shape of glasses." diff --git a/code/modules/clothing/head/hardhat.dm b/code/modules/clothing/head/hardhat.dm index 53a77aaffcd89..915dc9e966cb5 100644 --- a/code/modules/clothing/head/hardhat.dm +++ b/code/modules/clothing/head/hardhat.dm @@ -128,6 +128,13 @@ if(user.canUseTopic(src, BE_CLOSE)) toggle_welding_screen(user) +/obj/item/clothing/head/hardhat/weldhat/ui_action_click(mob/user, actiontype) + if(istype(actiontype, /datum/action/item_action/toggle_welding_screen)) + toggle_welding_screen(user) + return + + return ..() + /obj/item/clothing/head/utility/hardhat/welding/proc/toggle_welding_screen(mob/living/user) if(weldingvisortoggle(user)) playsound(src, 'sound/mecha/mechmove03.ogg', 50, TRUE) //Visors don't just come from nothing diff --git a/code/modules/clothing/masks/hailer.dm b/code/modules/clothing/masks/hailer.dm index 7dd01b5036195..caeb7f5ddb146 100644 --- a/code/modules/clothing/masks/hailer.dm +++ b/code/modules/clothing/masks/hailer.dm @@ -197,3 +197,7 @@ playsound(src.loc, "sound/voice/complionator/[phrase_sound].ogg", 100, 0, 4) cooldown = world.time cooldown_special = world.time + +/datum/action/item_action/halt + name = "HALT!" + diff --git a/code/modules/clothing/spacesuits/_spacesuits.dm b/code/modules/clothing/spacesuits/_spacesuits.dm index 10f1173c51507..055f8e0137e25 100644 --- a/code/modules/clothing/spacesuits/_spacesuits.dm +++ b/code/modules/clothing/spacesuits/_spacesuits.dm @@ -81,26 +81,25 @@ // Space Suit temperature regulation and power usage /obj/item/clothing/suit/space/process() - var/mob/living/carbon/human/user = src.loc - if(!user || !ishuman(user) || !(user.wear_suit == src)) + var/mob/living/carbon/human/user = loc + if(!user || !ishuman(user) || user.wear_suit != src) return // Do nothing if thermal regulators are off if(!thermal_on) return - // If we got here, thermal regulators are on. If there's no cell, turn them - // off + // If we got here, thermal regulators are on. If there's no cell, turn them off if(!cell) - toggle_spacesuit() + toggle_spacesuit(user) update_hud_icon(user) return // cell.use will return FALSE if charge is lower than THERMAL_REGULATOR_COST if(!cell.use(THERMAL_REGULATOR_COST)) - toggle_spacesuit() + toggle_spacesuit(user) update_hud_icon(user) - to_chat(user, "The thermal regulator cuts off as [cell] runs out of charge.") + to_chat(user, span_warning("The thermal regulator cuts off as [cell] runs out of charge.")) return // If we got here, it means thermals are on, the cell is in and the cell has @@ -204,20 +203,24 @@ to_chat(user, "You [cell_cover_open ? "open" : "close"] the cell cover on \the [src].") /// Toggle the space suit's thermal regulator status -/obj/item/clothing/suit/space/proc/toggle_spacesuit() +/obj/item/clothing/suit/space/proc/toggle_spacesuit(mob/toggler) // If we're turning thermal protection on, check for valid cell and for enough // charge that cell. If it's too low, we shouldn't bother with setting the // thermal protection value and should just return out early. - var/mob/living/carbon/human/user = src.loc - if(!thermal_on && !(cell && cell.charge >= THERMAL_REGULATOR_COST)) - to_chat(user, "The thermal regulator on \the [src] has no charge.") + if(!thermal_on && (!cell || cell.charge < THERMAL_REGULATOR_COST)) + if(toggler) + to_chat(toggler, span_warning("The thermal regulator on [src] has no charge.")) return thermal_on = !thermal_on min_cold_protection_temperature = thermal_on ? SPACE_SUIT_MIN_TEMP_PROTECT : SPACE_SUIT_MIN_TEMP_PROTECT_OFF - if(user) - to_chat(user, "You turn [thermal_on ? "on" : "off"] \the [src]'s thermal regulator.") - SEND_SIGNAL(src, COMSIG_SUIT_SPACE_TOGGLE) + if(toggler) + to_chat(toggler, span_notice("You turn [thermal_on ? "on" : "off"] [src]'s thermal regulator.")) + + update_action_buttons() + +/obj/item/clothing/suit/space/ui_action_click(mob/user, actiontype) + toggle_spacesuit(user) // let emags override the temperature settings /obj/item/clothing/suit/space/on_emag(mob/user) diff --git a/code/modules/clothing/spacesuits/plasmamen.dm b/code/modules/clothing/spacesuits/plasmamen.dm index a5b0dd6acb854..5b97e0f7bd70e 100644 --- a/code/modules/clothing/spacesuits/plasmamen.dm +++ b/code/modules/clothing/spacesuits/plasmamen.dm @@ -63,7 +63,7 @@ var/visor_state = "enviro_visor" var/lamp_functional = TRUE var/obj/item/clothing/head/attached_hat - actions_types = list(/datum/action/item_action/toggle_helmet_light, /datum/action/item_action/toggle_welding_screen/plasmaman) + actions_types = list(/datum/action/item_action/toggle_helmet_light, /datum/action/item_action/toggle_welding_screen) visor_vars_to_toggle = VISOR_FLASHPROTECT | VISOR_TINT flags_inv = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDEFACIALHAIR|HIDESNOUT flags_cover = HEADCOVERSMOUTH|HEADCOVERSEYES @@ -93,6 +93,13 @@ else . += "A hat can be placed on the helmet." +/obj/item/clothing/head/helmet/space/plasmaman/ui_action_click(mob/user, action) + if(istype(action, /datum/action/item_action/toggle_welding_screen)) + toggle_welding_screen(user) + return + + return ..() + /obj/item/clothing/head/helmet/space/plasmaman/proc/toggle_welding_screen(mob/living/user) if(!weldingvisortoggle(user)) return diff --git a/code/modules/events/wizard/aid.dm b/code/modules/events/wizard/aid.dm index 8525674e07a69..126940154fa06 100644 --- a/code/modules/events/wizard/aid.dm +++ b/code/modules/events/wizard/aid.dm @@ -1,5 +1,5 @@ -//in this file: Various events that directly aid the wizard. This is the "lets entice the wizard to use summon events!" file. - +// Various events that directly aid the wizard. +// This is the "lets entice the wizard to use summon events!" file. /datum/round_event_control/wizard/robelesscasting //EI NUDTH! name = "Robeless Casting" weight = 2 @@ -9,16 +9,19 @@ /datum/round_event/wizard/robelesscasting/start() - for(var/i in GLOB.mob_living_list) //Hey if a corgi has magic missle he should get the same benifit as anyone - var/mob/living/L = i - if(L.mind && L.mind.spell_list.len != 0) - var/spell_improved = FALSE - for(var/obj/effect/proc_holder/spell/S in L.mind.spell_list) - if(S.clothes_req) - S.clothes_req = 0 - spell_improved = TRUE - if(spell_improved) - to_chat(L, "You suddenly feel like you never needed those garish robes in the first place...") + // Hey, if a corgi has magic missle, he should get the same benefit as anyone + for(var/mob/living/caster as anything in GLOB.mob_living_list) + if(!length(caster.actions)) + continue + + var/spell_improved = FALSE + for(var/datum/action/cooldown/spell/spell in caster.actions) + if(spell.spell_requirements & SPELL_REQUIRES_WIZARD_GARB) + spell.spell_requirements &= ~SPELL_REQUIRES_WIZARD_GARB + spell_improved = TRUE + + if(spell_improved) + to_chat(caster, span_notice("You suddenly feel like you never needed those garish robes in the first place...")) //--// @@ -30,29 +33,16 @@ earliest_start = 0 MINUTES /datum/round_event/wizard/improvedcasting/start() - for(var/i in GLOB.mob_living_list) - var/mob/living/L = i - if(L.mind && L.mind.spell_list.len != 0) - for(var/obj/effect/proc_holder/spell/S in L.mind.spell_list) - S.name = initial(S.name) - S.spell_level++ - if(S.spell_level >= 6 || S.charge_max <= 0) //Badmin checks, these should never be a problem in normal play - continue - if(S.level_max <= 0) - continue - S.charge_max = round(initial(S.charge_max) - S.spell_level * (initial(S.charge_max) - S.cooldown_min) / S.level_max) - if(S.charge_max < S.charge_counter) - S.charge_counter = S.charge_max - switch(S.spell_level) - if(1) - S.name = "Efficient [S.name]" - if(2) - S.name = "Quickened [S.name]" - if(3) - S.name = "Free [S.name]" - if(4) - S.name = "Instant [S.name]" - if(5) - S.name = "Ludicrous [S.name]" - - to_chat(L, "You suddenly feel more competent with your casting!") + for(var/mob/living/caster as anything in GLOB.mob_living_list) + if(!length(caster.actions)) + continue + + var/upgraded_a_spell = FALSE + for(var/datum/action/cooldown/spell/spell in caster.actions) + // If improved casting has already boosted this spell further beyond, go no further + if(spell.spell_level >= spell.spell_max_level + 1) + continue + upgraded_a_spell = spell.level_spell(TRUE) + + if(upgraded_a_spell) + to_chat(caster, span_notice("You suddenly feel more competent with your casting!")) diff --git a/code/modules/events/wizard/shuffle.dm b/code/modules/events/wizard/shuffle.dm index c1185144a7e8c..53ba5b96c61a2 100644 --- a/code/modules/events/wizard/shuffle.dm +++ b/code/modules/events/wizard/shuffle.dm @@ -79,28 +79,29 @@ earliest_start = 0 MINUTES /datum/round_event/wizard/shuffleminds/start() - var/list/mobs = list() + var/list/mobs_to_swap = list() - for(var/mob/living/carbon/human/H in GLOB.alive_mob_list) - if(H.stat || !H.mind || iswizard(H)) + for(var/mob/living/carbon/human/alive_human in GLOB.alive_mob_list) + if(alive_human.stat != CONSCIOUS || !alive_human.mind || IS_WIZARD(alive_human)) continue //the wizard(s) are spared on this one - if(istype(H.get_item_by_slot(ITEM_SLOT_HEAD), /obj/item/clothing/head/costume/foilhat) || H.anti_magic_check()) - continue - mobs += H + mobs_to_swap += alive_human - if(!mobs) + if(!length(mobs_to_swap)) return - shuffle_inplace(mobs) + mobs_to_swap = shuffle(mobs_to_swap) - var/obj/effect/proc_holder/spell/targeted/mind_transfer/swapper = new /obj/effect/proc_holder/spell/targeted/mind_transfer - while(mobs.len > 1) - var/mob/living/carbon/human/H = pick(mobs) - mobs -= H - swapper.cast(list(H), mobs[mobs.len], 1) - mobs -= mobs[mobs.len] + var/datum/action/cooldown/spell/pointed/mind_transfer/swapper = new() - for(var/mob/living/carbon/human/H in GLOB.alive_mob_list) - var/datum/effect_system/smoke_spread/smoke = new - smoke.set_up(0, H.loc) + while(mobs_to_swap.len > 1) + var/mob/living/swap_to = pick_n_take(mobs_to_swap) + var/mob/living/swap_from = pick_n_take(mobs_to_swap) + + swapper.swap_minds(swap_to, swap_from) + + qdel(swapper) + + for(var/mob/living/carbon/human/alive_human in GLOB.alive_mob_list) + var/datum/effect_system/fluid_spread/smoke/smoke = new() + smoke.set_up(0, holder = alive_human, location = alive_human.loc) smoke.start() diff --git a/code/modules/instruments/items.dm b/code/modules/instruments/items.dm index 308c55f277b5d..0dbe5262e2bad 100644 --- a/code/modules/instruments/items.dm +++ b/code/modules/instruments/items.dm @@ -225,6 +225,17 @@ ..() UnregisterSignal(M, COMSIG_MOB_SAY) +/datum/action/item_action/instrument + name = "Use Instrument" + desc = "Use the instrument specified" + +/datum/action/item_action/instrument/Trigger(trigger_flags) + if(istype(target, /obj/item/instrument)) + var/obj/item/instrument/I = target + I.interact(usr) + return + return ..() + /obj/item/instrument/bikehorn name = "gilded bike horn" desc = "An exquisitely decorated bike horn, capable of honking in a variety of notes." diff --git a/code/modules/jobs/job_types/mime.dm b/code/modules/jobs/job_types/mime.dm index ea4076e5edcb0..d9dc4f9cf8999 100644 --- a/code/modules/jobs/job_types/mime.dm +++ b/code/modules/jobs/job_types/mime.dm @@ -64,8 +64,10 @@ if(visualsOnly) return + // Start our mime out with a vow of silence and the ability to break (or make) it if(H.mind) - H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mime/speak(null)) + var/datum/action/cooldown/spell/vow_of_silence/vow = new(H.mind) + vow.Grant(H) H.mind.miming = 1 /obj/item/book/mimery @@ -73,31 +75,56 @@ desc = "A primer on basic pantomime." icon_state ="bookmime" -/obj/item/book/mimery/attack_self(mob/user,) - user.set_machine(src) - var/dat = "Guide to Dank Mimery
" - dat += "Teaches one of three classic pantomime routines, allowing a practiced mime to conjure invisible objects into corporeal existence.
" - dat += "Once you have mastered your routine, this book will have no more to say to you.
" - dat += "
" - dat += "Invisible Wall
" - dat += "Invisible Chair
" - dat += "Invisible Box
" - user << browse(dat, "window=book") - -/obj/item/book/mimery/Topic(href, href_list) - ..() - if (usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED) || src.loc != usr) - return - if (!ishuman(usr)) +/obj/item/book/mimery/attack_self(mob/user) + . = ..() + if(.) return - var/mob/living/carbon/human/H = usr - if(H.is_holding(src) && H.mind) - H.set_machine(src) - if (href_list["invisible_wall"]) - H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall(null)) - if (href_list["invisible_chair"]) - H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair(null)) - if (href_list["invisible_box"]) - H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box(null)) - to_chat(usr, "The book disappears into thin air.") - qdel(src) + + var/list/spell_icons = list( + "Invisible Wall" = image(icon = 'icons/mob/actions/actions_mime.dmi', icon_state = "invisible_wall"), + "Invisible Chair" = image(icon = 'icons/mob/actions/actions_mime.dmi', icon_state = "invisible_chair"), + "Invisible Box" = image(icon = 'icons/mob/actions/actions_mime.dmi', icon_state = "invisible_box") + ) + var/picked_spell = show_radial_menu(user, src, spell_icons, custom_check = CALLBACK(src, .proc/check_menu, user), radius = 36, require_near = TRUE) + var/datum/action/cooldown/spell/picked_spell_type + switch(picked_spell) + if("Invisible Wall") + picked_spell_type = /datum/action/cooldown/spell/conjure/invisible_wall + + if("Invisible Chair") + picked_spell_type = /datum/action/cooldown/spell/conjure/invisible_chair + + if("Invisible Box") + picked_spell_type = /datum/action/cooldown/spell/conjure_item/invisible_box + + if(ispath(picked_spell_type)) + // Gives the user a vow ability too, if they don't already have one + var/datum/action/cooldown/spell/vow_of_silence/vow = locate() in user.actions + if(!vow && user.mind) + vow = new(user.mind) + vow.Grant(user) + + picked_spell_type = new picked_spell_type(user.mind || user) + picked_spell_type.Grant(user) + + to_chat(user, span_warning("The book disappears into thin air.")) + qdel(src) + + return TRUE + +/** + * Checks if we are allowed to interact with a radial menu + * + * Arguments: + * * user The human mob interacting with the menu + */ +/obj/item/book/mimery/proc/check_menu(mob/living/carbon/human/user) + if(!istype(user)) + return FALSE + if(!user.is_holding(src)) + return FALSE + if(user.incapacitated()) + return FALSE + if(!user.mind) + return FALSE +return TRUE diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index 936e5c2db5ef3..27cc3afe65120 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -49,7 +49,8 @@ /obj/item/reagent_containers/glass/bottle/necropolis_seed = 5, /obj/item/borg/upgrade/modkit/lifesteal = 5, /obj/item/shared_storage/red = 5, - /obj/item/staff/storm = 5 + /obj/item/staff/storm = 5, + /obj/item/book/granter/action/spell/summonitem = 5, ) if(..()) @@ -792,6 +793,7 @@ new /obj/item/dragons_blood(src) new /obj/item/clothing/suit/hooded/cloak/drake(src) //Drake armor crafted only by Ashwalkers now, but still available as drop for miners new /obj/item/crusher_trophy/tail_spike(src) + //new /obj/item/book/granter/action/spell/sacredflame(src) It's supposed to drop from the dragon but idk if you guys want it like that tell me in the review code // Ghost Sword - left in for other references and admin shenanigans diff --git a/code/modules/mining/minebot.dm b/code/modules/mining/minebot.dm index e03a81faa1ce8..146e2cf0e0639 100644 --- a/code/modules/mining/minebot.dm +++ b/code/modules/mining/minebot.dm @@ -520,7 +520,7 @@ button_icon_state = "trayson-meson" user.sync_lighting_plane_alpha() to_chat(user, "You toggle your meson vision [(user.sight & SEE_TURFS) ? "on" : "off"].") - UpdateButtonIcon() + UpdateButtons() /// Toggles a minebot's inbuilt light. /datum/action/innate/minedrone/toggle_light @@ -532,7 +532,7 @@ user.set_light_on(!user.light_on) to_chat(user, "You toggle your light [user.light_on ? "on" : "off"].") button_icon_state = "mech_lights_[user.light_on ? "on" : "off"]" - UpdateButtonIcon() + UpdateButtons() /// Toggles the minebot's mode from combat to mining mode, effectively switching between the minebot's plasma cutter and PKA. /datum/action/innate/minedrone/toggle_mode @@ -543,7 +543,7 @@ var/mob/living/simple_animal/hostile/mining_drone/user = owner user.toggle_mode() button_icon_state = "mech_zoom_[user.mode == MODE_COMBAT ? "on" : "off"]" - UpdateButtonIcon() + UpdateButtons() /// Allows a minebot to manually dump its own ore. /datum/action/innate/minedrone/dump_ore @@ -564,7 +564,7 @@ user.stored_scanner.toggle_on() to_chat(user, "You toggle your mining scanner [user.stored_scanner.on ? "on" : "off"].") button_icon_state = "mech_cycle_equip_[user.stored_scanner.on ? "on" : "off"]" - UpdateButtonIcon() + UpdateButtons() /**********************Minebot Upgrades**********************/ // Similar to PKA upgrades, except for minebots. Each upgrade can only be installed once and is stored in the minebot when installed. diff --git a/code/modules/mob/living/bloodcrawl.dm b/code/modules/mob/living/bloodcrawl.dm deleted file mode 100644 index d909cea4e0eb2..0000000000000 --- a/code/modules/mob/living/bloodcrawl.dm +++ /dev/null @@ -1,180 +0,0 @@ -/obj/effect/dummy/phased_mob/slaughter //Can't use the wizard one, blocked by jaunt/slow - name = "water" - icon = 'icons/effects/effects.dmi' - icon_state = "nothing" - var/canmove = 1 - density = FALSE - anchored = TRUE - invisibility = 60 - resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF - -/obj/effect/dummy/phased_mob/slaughter/relaymove(mob/living/user, direction) - forceMove(get_step(src,direction)) - -/obj/effect/dummy/phased_mob/slaughter/ex_act() - return - -/obj/effect/dummy/phased_mob/slaughter/bullet_act() - return BULLET_ACT_FORCE_PIERCE - -/obj/effect/dummy/phased_mob/slaughter/singularity_act() - return - - - -/mob/living/proc/phaseout(obj/effect/decal/cleanable/B) - if(iscarbon(src)) - var/mob/living/carbon/C = src - for(var/obj/item/I in C.held_items) - //TODO make it toggleable to either forcedrop the items, or deny - //entry when holding them - // literally only an option for carbons though - to_chat(C, "You may not hold items while blood crawling!") - return 0 - var/obj/item/bloodcrawl/B1 = new(C) - var/obj/item/bloodcrawl/B2 = new(C) - B1.icon_state = "bloodhand_left" - B2.icon_state = "bloodhand_right" - C.put_in_hands(B1) - C.put_in_hands(B2) - C.regenerate_icons() - src.notransform = TRUE - spawn(0) - bloodpool_sink(B) - src.notransform = FALSE - return 1 - -/mob/living/proc/bloodpool_sink(obj/effect/decal/cleanable/B) - var/turf/mobloc = get_turf(src.loc) - - src.visible_message("[src] sinks into the pool of blood!") - playsound(get_turf(src), 'sound/magic/enter_blood.ogg', 50, 1, -1) - // Extinguish, unbuckle, stop being pulled, set our location into the - // dummy object - var/obj/effect/dummy/phased_mob/slaughter/holder = new /obj/effect/dummy/phased_mob/slaughter(mobloc) - src.ExtinguishMob() - - // Keep a reference to whatever we're pulling, because forceMove() - // makes us stop pulling - var/pullee = src.pulling - - src.holder = holder - src.forceMove(holder) - - // if we're not pulling anyone, or we can't eat anyone - if(!pullee || src.bloodcrawl != BLOODCRAWL_EAT) - return - - // if the thing we're pulling isn't alive - if (!isliving(pullee)) - return - - var/mob/living/victim = pullee - var/kidnapped = FALSE - - if(victim.stat == CONSCIOUS) - src.visible_message("[victim] kicks free of the blood pool just before entering it!", null, "You hear splashing and struggling.") - else if(victim.reagents && victim.reagents.has_reagent(/datum/reagent/consumable/ethanol/demonsblood, needs_metabolizing = TRUE)) - visible_message("Something prevents [victim] from entering the pool!", "A strange force is blocking [victim] from entering!", "You hear a splash and a thud.") - else - victim.forceMove(src) - victim.emote("scream") - src.visible_message("[src] drags [victim] into the pool of blood!", null, "You hear a splash.") - kidnapped = TRUE - - if(kidnapped) - var/success = bloodcrawl_consume(victim) - if(!success) - to_chat(src, "You happily devour... nothing? Your meal vanished at some point!") - return 1 - -/mob/living/proc/bloodcrawl_consume(mob/living/victim) - to_chat(src, "You begin to feast on [victim]. You can not move while you are doing this.") - - var/sound - if(istype(src, /mob/living/simple_animal/slaughter)) - var/mob/living/simple_animal/slaughter/SD = src - sound = SD.feast_sound - else - sound = 'sound/magic/demon_consume.ogg' - - for(var/i in 1 to 3) - playsound(get_turf(src),sound, 50, 1) - sleep(30) - - if(!victim) - return FALSE - - if(victim.reagents && victim.reagents.has_reagent(/datum/reagent/consumable/ethanol/devilskiss, needs_metabolizing = TRUE)) - to_chat(src, "AAH! THEIR FLESH! IT BURNS!") - adjustBruteLoss(25) //I can't use adjustHealth() here because bloodcrawl affects /mob/living and adjustHealth() only affects simple mobs - var/found_bloodpool = FALSE - for(var/obj/effect/decal/cleanable/target in range(1,get_turf(victim))) - if(target.can_bloodcrawl_in()) - victim.forceMove(get_turf(target)) - victim.visible_message("[target] violently expels [victim]!") - victim.exit_blood_effect(target) - found_bloodpool = TRUE - - if(!found_bloodpool) - // Fuck it, just eject them, thanks to some split second cleaning - victim.forceMove(get_turf(victim)) - victim.visible_message("[victim] appears from nowhere, covered in blood!") - victim.exit_blood_effect() - return TRUE - - to_chat(src, "You devour [victim]. Your health is fully restored.") - src.revive(full_heal = 1) - - // No defib possible after laughter - victim.apply_damage(1000, BRUTE) - if(victim.stat != DEAD) - victim.investigate_log("has been killed by being consumed by a slaugter demon.", INVESTIGATE_DEATHS) - victim.death() - bloodcrawl_swallow(victim) - return TRUE - -/mob/living/proc/bloodcrawl_swallow(var/mob/living/victim) - qdel(victim) - -/obj/item/bloodcrawl - name = "blood crawl" - desc = "You are unable to hold anything while in this form." - icon = 'icons/effects/blood.dmi' - item_flags = ABSTRACT | DROPDEL - -/obj/item/bloodcrawl/Initialize(mapload) - . = ..() - ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) - -/mob/living/proc/exit_blood_effect(obj/effect/decal/cleanable/B) - playsound(get_turf(src), 'sound/magic/exit_blood.ogg', 50, 1, -1) - //Makes the mob have the color of the blood pool it came out of - var/newcolor = rgb(149, 10, 10) - if(istype(B, /obj/effect/decal/cleanable/xenoblood)) - newcolor = rgb(43, 186, 0) - add_atom_colour(newcolor, TEMPORARY_COLOUR_PRIORITY) - // but only for a few seconds - addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, remove_atom_colour), TEMPORARY_COLOUR_PRIORITY, newcolor), 6 SECONDS) - -/mob/living/proc/phasein(obj/effect/decal/cleanable/B) - if(src.notransform) - to_chat(src, "Finish eating first!") - return 0 - B.visible_message("[B] starts to bubble...") - if(!do_after(src, 20, target = B)) - return - if(!B) - return - forceMove(B.loc) - src.client.set_eye(src) - src.visible_message("[src] rises out of the pool of blood!") - exit_blood_effect(B) - if(iscarbon(src)) - var/mob/living/carbon/C = src - for(var/obj/item/bloodcrawl/BC in C) - BC.flags_1 = null - qdel(BC) - qdel(src.holder) - src.holder = null - return 1 diff --git a/code/modules/mob/living/brain/brain.dm b/code/modules/mob/living/brain/brain.dm index ef41226772bf9..400d43ce86227 100644 --- a/code/modules/mob/living/brain/brain.dm +++ b/code/modules/mob/living/brain/brain.dm @@ -86,8 +86,6 @@ var/obj/vehicle/sealed/mecha/M = container.mecha if(M.mouse_pointer) client.mouse_pointer_icon = M.mouse_pointer - if (client && ranged_ability && ranged_ability.ranged_mousepointer) - client.mouse_pointer_icon = ranged_ability.ranged_mousepointer /mob/living/brain/proc/get_traumas() . = list() diff --git a/code/modules/mob/living/carbon/alien/alien.dm b/code/modules/mob/living/carbon/alien/alien.dm index 594fb65d2a303..372c0cc560646 100644 --- a/code/modules/mob/living/carbon/alien/alien.dm +++ b/code/modules/mob/living/carbon/alien/alien.dm @@ -110,8 +110,10 @@ Des: Removes all infected images from the alien. return TRUE /mob/living/carbon/alien/proc/alien_evolve(mob/living/carbon/alien/new_xeno) - to_chat(src, "You begin to evolve!") - visible_message("[src] begins to twist and contort!") + visible_message( + span_alertalien("[src] begins to twist and contort!"), + span_noticealien("You begin to evolve!"), + ) new_xeno.setDir(dir) if(!alien_name_regex.Find(name)) new_xeno.name = name diff --git a/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm b/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm index 6467c13b4cfb5..86c00cb81c2c6 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm @@ -1,327 +1,384 @@ /*NOTES: These are general powers. Specific powers are stored under the appropriate alien creature type. */ - /*Alien spit now works like a taser shot. It won't home in on the target but will act the same once it does hit. Doesn't work on other aliens/AI.*/ -/obj/effect/proc_holder/alien +/datum/action/cooldown/alien name = "Alien Power" panel = "Alien" - base_action = /datum/action/spell_action/alien - action_icon = 'icons/mob/actions/actions_xeno.dmi' - action_background_icon_state = "bg_alien" + background_icon_state = "bg_alien" + icon_icon = 'icons/mob/actions/actions_xeno.dmi' + button_icon_state = "spell_default" + check_flags = AB_CHECK_CONSCIOUS + /// How much plasma this action uses. var/plasma_cost = 0 - var/check_turf = FALSE -/obj/effect/proc_holder/alien/Click() - if(!iscarbon(usr)) +/datum/action/cooldown/alien/IsAvailable() + . = ..() + if(!.) + return FALSE + if(!iscarbon(owner)) return FALSE - var/mob/living/carbon/user = usr - if(cost_check(check_turf,user)) - if(fire(user) && user) // Second check to prevent runtimes when evolving - user.adjustPlasma(-plasma_cost) + var/mob/living/carbon/carbon_owner = owner + if(carbon_owner.getPlasma() < plasma_cost) + return FALSE + return TRUE -/obj/effect/proc_holder/alien/on_gain(mob/living/carbon/user) - return +/datum/action/cooldown/alien/PreActivate(atom/target) + // Parent calls Activate(), so if parent returns TRUE, + // it means the activation happened successfuly by this point + . = ..() + if(!.) + return FALSE + // Xeno actions like "evolve" may result in our action (or our alien) being deleted + // In that case, we can just exit now as a "success" + if(QDELETED(src) || QDELETED(owner)) + return TRUE -/obj/effect/proc_holder/alien/on_lose(mob/living/carbon/user) - return + var/mob/living/carbon/carbon_owner = owner + carbon_owner.adjustPlasma(-plasma_cost) + // It'd be really annoying if click-to-fire actions stayed active, + // even if our plasma amount went under the required amount. + if(click_to_activate && carbon_owner.getPlasma() < plasma_cost) + unset_click_ability(owner, refund_cooldown = FALSE) -/obj/effect/proc_holder/alien/fire(mob/living/carbon/user) return TRUE -/obj/effect/proc_holder/alien/get_panel_text() +/datum/action/cooldown/alien/set_statpanel_format() . = ..() - if(plasma_cost > 0) - return "[plasma_cost]" + if(!islist(.)) + return + + .[PANEL_DISPLAY_STATUS] = "PLASMA - [plasma_cost]" + +/datum/action/cooldown/alien/make_structure + /// The type of structure the action makes on use + var/obj/structure/made_structure_type -/obj/effect/proc_holder/alien/proc/cost_check(check_turf = FALSE, mob/living/carbon/user, silent = FALSE) - if(user.stat) - if(!silent) - to_chat(user, "You must be conscious to do this.") +/datum/action/cooldown/alien/make_structure/IsAvailable() + . = ..() + if(!.) + return FALSE + if(!isturf(owner.loc) || isspaceturf(owner.loc)) return FALSE - if(user.getPlasma() < plasma_cost) - if(!silent) - to_chat(user, "Not enough plasma stored.") + + return TRUE + +/datum/action/cooldown/alien/make_structure/PreActivate(atom/target) + if(!check_for_duplicate()) return FALSE - if(check_turf && (!isturf(user.loc) || isspaceturf(user.loc))) - if(!silent) - to_chat(user, "Bad place for a garden!") + + if(!check_for_vents()) return FALSE + + return ..() + +/datum/action/cooldown/alien/make_structure/Activate(atom/target) + new made_structure_type(owner.loc) return TRUE -/obj/effect/proc_holder/alien/proc/check_vent_block(mob/living/user) - var/obj/machinery/atmospherics/components/unary/atmos_thing = locate() in user.loc +/// Checks if there's a duplicate structure in the owner's turf +/datum/action/cooldown/alien/make_structure/proc/check_for_duplicate() + var/obj/structure/existing_thing = locate(made_structure_type) in owner.loc + if(existing_thing) + to_chat(owner, span_warning("There is already \a [existing_thing] here!")) + return FALSE + + return TRUE + +/// Checks if there's an atmos machine (vent) in the owner's turf +/datum/action/cooldown/alien/make_structure/proc/check_for_vents() + var/obj/machinery/atmospherics/components/unary/atmos_thing = locate() in owner.loc if(atmos_thing) - var/rusure = alert(user, "Laying eggs and shaping resin here would block access to [atmos_thing]. Do you want to continue?", "Blocking Atmospheric Component", "Yes", "No") - if(rusure != "Yes") + var/are_you_sure = tgui_alert(owner, "Laying eggs and shaping resin here would block access to [atmos_thing]. Do you want to continue?", "Blocking Atmospheric Component", list("Yes", "No")) + if(are_you_sure != "Yes") + return FALSE + if(QDELETED(src) || QDELETED(owner) || !check_for_duplicate()) return FALSE + return TRUE -/obj/effect/proc_holder/alien/plant +/datum/action/cooldown/alien/make_structure/plant_weeds name = "Plant Weeds" - desc = "Alien weeds spread resin which heals any alien. Costs 50 Plasma." + desc = "Plants some alien weeds." + button_icon_state = "alien_plant" plasma_cost = 50 - check_turf = TRUE - action_icon_state = "alien_plant" + made_structure_type = /obj/structure/alien/weeds/node -/obj/effect/proc_holder/alien/plant/fire(mob/living/carbon/user) - if(locate(/obj/structure/alien/weeds/node) in get_turf(user)) - to_chat(user, "There's already a weed node here.") - return FALSE - user.visible_message("[user] has planted some alien weeds!") - new/obj/structure/alien/weeds/node(get_turf(user)) - return TRUE +/datum/action/cooldown/alien/make_structure/plant_weeds/Activate(atom/target) + owner.visible_message(span_alertalien("[owner] plants some alien weeds!")) + return ..() -/obj/effect/proc_holder/alien/whisper +/datum/action/cooldown/alien/whisper name = "Whisper" - desc = "Whisper to someone through the hivemind. Costs 10 Plasma." + desc = "Whisper to someone." + button_icon_state = "alien_whisper" plasma_cost = 10 - action_icon_state = "alien_whisper" - -/obj/effect/proc_holder/alien/whisper/fire(mob/living/carbon/user) - var/list/options = list() - for(var/mob/living/L in oview(user)) - options += L - var/mob/living/M = input("Select who to whisper to:","Whisper to?",null) as null|mob in sort_names(options) - if(!M) + +/datum/action/cooldown/alien/whisper/Activate(atom/target) + var/list/possible_recipients = list() + for(var/mob/living/recipient in oview(owner)) + possible_recipients += recipient + + if(!length(possible_recipients)) + to_chat(owner, span_noticealien("There's no one around to whisper to.")) return FALSE - var/msg = stripped_input(usr, "Message:", "Alien Whisper") - if(!msg) + + var/mob/living/chosen_recipient = tgui_input_list(owner, "Select whisper recipient", "Whisper", sort_names(possible_recipients)) + if(!chosen_recipient) return FALSE - if(CHAT_FILTER_CHECK(msg)) - to_chat(usr, "Your message contains forbidden words.") + + var/to_whisper = tgui_input_text(owner, title = "Alien Whisper") + if(QDELETED(chosen_recipient) || QDELETED(src) || QDELETED(owner) || !IsAvailable() || !to_whisper) return FALSE - msg = user.treat_message_min(msg) - log_directed_talk(user, M, msg, LOG_SAY, tag="alien whisper") - to_chat(M, "You hear a strange, alien voice in your head.[msg]") - to_chat(user, "You said: \"[msg]\" to [M]") - for(var/ded in GLOB.dead_mob_list) - if(!isobserver(ded)) + if(chosen_recipient.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) + to_chat(owner, span_warning("As you reach into [chosen_recipient]'s mind, you are stopped by a mental blockage. It seems you've been foiled.")) + return FALSE + + log_directed_talk(owner, chosen_recipient, to_whisper, LOG_SAY, tag = "alien whisper") + to_chat(chosen_recipient, "[span_noticealien("You hear a strange, alien voice in your head...")][to_whisper]") + to_chat(owner, span_noticealien("You said: \"[to_whisper]\" to [chosen_recipient]")) + for(var/mob/dead_mob as anything in GLOB.dead_mob_list) + if(!isobserver(dead_mob)) continue - var/follow_link_user = FOLLOW_LINK(ded, user) - var/follow_link_whispee = FOLLOW_LINK(ded, M) - to_chat(ded, "[follow_link_user] [user] Alien Whisper --> [follow_link_whispee] [M] [msg]") + var/follow_link_user = FOLLOW_LINK(dead_mob, owner) + var/follow_link_whispee = FOLLOW_LINK(dead_mob, chosen_recipient) + to_chat(dead_mob, "[follow_link_user] [span_name("[owner]")] [span_alertalien("Alien Whisper --> ")] [follow_link_whispee] [span_name("[chosen_recipient]")] [span_noticealien("[to_whisper]")]") + return TRUE -/obj/effect/proc_holder/alien/transfer +/datum/action/cooldown/alien/transfer name = "Transfer Plasma" desc = "Transfer Plasma to another alien." - action_icon_state = "alien_transfer" + plasma_cost = 0 + button_icon_state = "alien_transfer" -/obj/effect/proc_holder/alien/transfer/fire(mob/living/carbon/user) +/datum/action/cooldown/alien/transfer/Activate(atom/target) + var/mob/living/carbon/carbon_owner = owner var/list/mob/living/carbon/aliens_around = list() - for(var/mob/living/carbon/A in oview(user)) - if(A.getorgan(/obj/item/organ/alien/plasmavessel)) - aliens_around.Add(A) - var/mob/living/carbon/M = input("Select who to transfer to:","Transfer plasma to?",null) as mob in sort_names(aliens_around) - if(!M) - return 0 - var/amount = input("Amount:", "Transfer Plasma to [M]") as num - amount = min(abs(round(amount)), user.getPlasma()) - if(!amount) + for(var/mob/living/carbon/alien in view(owner)) + if(alien.getPlasma() == -1 || alien == owner) + continue + aliens_around += alien + + if(!length(aliens_around)) + to_chat(owner, span_noticealien("There are no other aliens around.")) return FALSE - if(!user.Adjacent(M)) - to_chat(user, "You need to be closer!") + var/mob/living/carbon/donation_target = tgui_input_list(owner, "Target to transfer to", "Plasma Donation", sort_names(aliens_around)) + if(!donation_target) return FALSE - M.adjustPlasma(amount) - user.adjustPlasma(-amount) - to_chat(M, "[user] has transferred [amount] plasma to you.") - to_chat(user, "You transfer [amount] plasma to [M]") + var/amount = tgui_input_number(owner, "Amount", "Transfer Plasma to [donation_target]", max_value = carbon_owner.getPlasma()) + if(QDELETED(donation_target) || QDELETED(src) || QDELETED(owner) || !IsAvailable() || isnull(amount) || amount <= 0) + return FALSE + + if(get_dist(owner, donation_target) > 1) + to_chat(owner, span_noticealien("You need to be closer!")) + return FALSE + + donation_target.adjustPlasma(amount) + carbon_owner.adjustPlasma(-amount) + + to_chat(donation_target, span_noticealien("[owner] has transferred [amount] plasma to you.")) + to_chat(owner, span_noticealien("You transfer [amount] plasma to [donation_target].")) return TRUE -/obj/effect/proc_holder/alien/acid +/datum/action/cooldown/alien/acid + click_to_activate = TRUE + unset_after_click = FALSE + +/datum/action/cooldown/alien/acid/corrosion name = "Corrosive Acid" - desc = "Drench an object in acid, destroying it over time. Costs 200 Plasma." + desc = "Drench an object in acid, destroying it over time." + button_icon_state = "alien_acid" plasma_cost = 200 - action_icon_state = "alien_acid" -/obj/effect/proc_holder/alien/acid/on_gain(mob/living/carbon/user) - user.add_verb(/mob/living/carbon/proc/corrosive_acid) +/datum/action/cooldown/alien/acid/corrosion/set_click_ability(mob/on_who) + . = ..() + if(!.) + return -/obj/effect/proc_holder/alien/acid/on_lose(mob/living/carbon/user) - user.remove_verb(/mob/living/carbon/proc/corrosive_acid) + to_chat(on_who, span_noticealien("You prepare to vomit acid. Click a target to acid it!")) + on_who.update_icons() -/obj/effect/proc_holder/alien/acid/proc/corrode(atom/target,mob/living/carbon/user = usr) - if(target in oview(1,user)) - if(target.acid_act(200, 100)) - user.visible_message("[user] vomits globs of vile stuff all over [target]. It begins to sizzle and melt under the bubbling mess of acid!") - return 1 - else - to_chat(user, "You cannot dissolve this object.") +/datum/action/cooldown/alien/acid/corrosion/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) + return + if(refund_cooldown) + to_chat(on_who, span_noticealien("You empty your corrosive acid glands.")) + on_who.update_icons() - return 0 - else - to_chat(src, "[target] is too far away.") - return 0 - +/datum/action/cooldown/alien/acid/corrosion/PreActivate(atom/target) + if(get_dist(owner, target) > 1) + return FALSE -/obj/effect/proc_holder/alien/acid/fire(mob/living/carbon/alien/user) - var/O = input("Select what to dissolve:","Dissolve",null) as obj|turf in oview(1,user) - if(!O || user.incapacitated()) - return 0 - else - return corrode(O,user) + return ..() -/mob/living/carbon/proc/corrosive_acid(O as obj|turf in oview(1)) // right click menu verb ugh - set name = "Corrosive Acid" +/datum/action/cooldown/alien/acid/corrosion/Activate(atom/target) + if(!target.acid_act(200, 1000)) + to_chat(owner, span_noticealien("You cannot dissolve this object.")) + return FALSE - if(!iscarbon(usr)) - return - var/mob/living/carbon/user = usr - var/obj/effect/proc_holder/alien/acid/A = locate() in user.abilities - if(!A) - return - if(user.getPlasma() > A.plasma_cost && A.corrode(O)) - user.adjustPlasma(-A.plasma_cost) + owner.visible_message( + span_alertalien("[owner] vomits globs of vile stuff all over [target]. It begins to sizzle and melt under the bubbling mess of acid!"), + span_noticealien("You vomit globs of acid over [target]. It begins to sizzle and melt."), + ) + return TRUE -/obj/effect/proc_holder/alien/neurotoxin +/datum/action/cooldown/alien/acid/neurotoxin name = "Spit Neurotoxin" - desc = "Activates your Neurotoxin glands. You can shoot paralyzing shots. Each shot costs 50 Plasma." - action_icon_state = "alien_neurotoxin_0" - active = FALSE - -/obj/effect/proc_holder/alien/neurotoxin/fire(mob/living/carbon/user) - var/message - if(active) - message = "You empty your neurotoxin gland." - remove_ranged_ability(message) - else - message = "You prepare your neurotoxin gland. Left-click to fire at a target!" - add_ranged_ability(user, message, TRUE) + desc = "Spits neurotoxin at someone, paralyzing them for a short time." + button_icon_state = "alien_neurotoxin_0" + plasma_cost = 50 -/obj/effect/proc_holder/alien/neurotoxin/update_icon() - action.button_icon_state = "alien_neurotoxin_[active]" - action.UpdateButtonIcon() +/datum/action/cooldown/alien/acid/neurotoxin/IsAvailable() + return ..() && isturf(owner.loc) -/obj/effect/proc_holder/alien/neurotoxin/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - var/p_cost = 30 - if(!iscarbon(ranged_ability_user) || ranged_ability_user.stat) - remove_ranged_ability() +/datum/action/cooldown/alien/acid/neurotoxin/set_click_ability(mob/on_who) + . = ..() + if(!.) return - var/mob/living/carbon/user = ranged_ability_user + to_chat(on_who, span_notice("You prepare your neurotoxin gland. Left-click to fire at a target!")) + + button_icon_state = "alien_neurotoxin_1" + UpdateButtons() + on_who.update_icons() - if(user.getPlasma() < p_cost) - to_chat(user, "You need at least [p_cost] plasma to spit.") - remove_ranged_ability() +/datum/action/cooldown/alien/acid/neurotoxin/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) return - var/turf/T = user.loc - var/turf/U = get_step(user, user.dir) // Get the tile infront of the move, based on their direction - if(!isturf(U) || !isturf(T)) + if(refund_cooldown) + to_chat(on_who, span_notice("You empty your neurotoxin gland.")) + + button_icon_state = "alien_neurotoxin_0" + UpdateButtons() + on_who.update_icons() + +/datum/action/cooldown/alien/acid/neurotoxin/InterceptClickOn(mob/living/caller, params, atom/target) + . = ..() + if(!.) + unset_click_ability(caller, refund_cooldown = FALSE) return FALSE - user.visible_message("[user] spits neurotoxin!", "You spit neurotoxin.") - var/obj/projectile/bullet/neurotoxin/A = new /obj/projectile/bullet/neurotoxin(user.loc) - A.preparePixelProjectile(target, user, params) - A.firer = user - A.fire() - user.newtonian_move(get_dir(U, T)) - user.adjustPlasma(-p_cost) + // We do this in InterceptClickOn() instead of Activate() + // because we use the click parameters for aiming the projectile + // (or something like that) + var/turf/user_turf = caller.loc + var/turf/target_turf = get_step(caller, target.dir) // Get the tile infront of the move, based on their direction + if(!isturf(target_turf)) + return FALSE + var/modifiers = params2list(params) + caller.visible_message( + span_danger("[caller] spits neurotoxin!"), + span_alertalien("You spit neurotoxin."), + ) + var/obj/projectile/neurotoxin/neurotoxin = new /obj/projectile/neurotoxin(caller.loc) + neurotoxin.preparePixelProjectile(target, caller, modifiers) + neurotoxin.firer = caller + neurotoxin.fire() + caller.newtonian_move(get_dir(target_turf, user_turf)) return TRUE -/obj/effect/proc_holder/alien/neurotoxin/on_lose(mob/living/carbon/user) - remove_ranged_ability() - -/obj/effect/proc_holder/alien/neurotoxin/add_ranged_ability(mob/living/user,msg,forced) - . = ..() - if(isalienadult(user)) - var/mob/living/carbon/alien/humanoid/A = user - A.drooling = TRUE - A.update_icons() - -/obj/effect/proc_holder/alien/neurotoxin/remove_ranged_ability(msg) - if(isalienadult(ranged_ability_user)) - var/mob/living/carbon/alien/humanoid/A = ranged_ability_user - A.drooling = FALSE - A.update_icons() - return ..() +// Has to return TRUE, otherwise is skipped. +/datum/action/cooldown/alien/acid/neurotoxin/Activate(atom/target) + return TRUE -/obj/effect/proc_holder/alien/resin +/datum/action/cooldown/alien/make_structure/resin name = "Secrete Resin" - desc = "Secrete tough malleable resin. Costs 55 Plasma." + desc = "Secrete tough malleable resin." + button_icon_state = "alien_resin" plasma_cost = 55 - check_turf = TRUE - var/list/structures = list( + /// A list of all structures we can make. + var/static/list/structures = list( "resin wall" = /obj/structure/alien/resin/wall, "resin membrane" = /obj/structure/alien/resin/membrane, - "resin nest" = /obj/structure/bed/nest) + "resin nest" = /obj/structure/bed/nest, + ) + +// Snowflake to check for multiple types of alien resin structures +/datum/action/cooldown/alien/make_structure/resin/check_for_duplicate() + for(var/blocker_name in structures) + var/obj/structure/blocker_type = structures[blocker_name] + if(locate(blocker_type) in owner.loc) + to_chat(owner, span_warning("There is already a resin structure there!")) + return FALSE - action_icon_state = "alien_resin" + return TRUE -/obj/effect/proc_holder/alien/resin/fire(mob/living/carbon/user) - if(locate(/obj/structure/alien/resin) in user.loc) - to_chat(user, "There is already a resin structure there.") +/datum/action/cooldown/alien/make_structure/resin/Activate(atom/target) + var/choice = tgui_input_list(owner, "Select a shape to build", "Resin building", structures) + if(isnull(choice) || QDELETED(src) || QDELETED(owner) || !check_for_duplicate() || !IsAvailable()) return FALSE - if(!check_vent_block(user)) + var/obj/structure/choice_path = structures[choice] + if(!ispath(choice_path)) return FALSE - var/choice = input("Choose what you wish to shape.","Resin building") as null|anything in structures - if(!choice) - return FALSE - if(!cost_check(check_turf,user)) - return FALSE - user.visible_message("[user] vomits up a thick purple substance and begins to shape it.", "You shape a [choice].") + owner.visible_message( + span_notice("[owner] vomits up a thick purple substance and begins to shape it."), + span_notice("You shape a [choice] out of resin."), + ) - choice = structures[choice] - new choice(get_turf(user)) + new choice_path(owner.loc) return TRUE -/obj/effect/proc_holder/alien/sneak +/datum/action/cooldown/alien/sneak name = "Sneak" desc = "Blend into the shadows to stalk your prey." - active = 0 - action_icon_state = "alien_sneak" - -/obj/effect/proc_holder/alien/sneak/fire(mob/living/carbon/alien/humanoid/user) - if(!active) - user.alpha = 75 //Still easy to see in lit areas with bright tiles, almost invisible on resin. - user.sneaking = TRUE - active = 1 - to_chat(user, "You blend into the shadows.") + button_icon_state = "alien_sneak" + /// The alpha we go to when sneaking. + var/sneak_alpha = 75 + +/datum/action/cooldown/alien/sneak/Remove(mob/living/remove_from) + if(HAS_TRAIT(remove_from, TRAIT_ALIEN_SNEAK)) + remove_from.alpha = initial(remove_from.alpha) + REMOVE_TRAIT(remove_from, TRAIT_ALIEN_SNEAK, name) + + return ..() + +/datum/action/cooldown/alien/sneak/Activate(atom/target) + if(HAS_TRAIT(owner, TRAIT_ALIEN_SNEAK)) + // It's safest to go to the initial alpha of the mob. + // Otherwise we get permanent invisbility exploits. + owner.alpha = initial(owner.alpha) + to_chat(owner, span_noticealien("You reveal yourself!")) + REMOVE_TRAIT(owner, TRAIT_ALIEN_SNEAK, name) + else - user.alpha = initial(user.alpha) - user.sneaking = FALSE - active = 0 - to_chat(user, "You reveal yourself!") + owner.alpha = sneak_alpha + to_chat(owner, span_noticealien("You blend into the shadows...")) + ADD_TRAIT(owner, TRAIT_ALIEN_SNEAK, name) + return TRUE +/// Gets the plasma level of this carbon's plasma vessel, or -1 if they don't have one /mob/living/carbon/proc/getPlasma() - var/obj/item/organ/alien/plasmavessel/vessel = getorgan(/obj/item/organ/alien/plasmavessel) + var/obj/item/organ/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel) if(!vessel) - return FALSE - return vessel.storedPlasma - + return -1 + return vessel.stored_plasma +/// Adjusts the plasma level of the carbon's plasma vessel if they have one /mob/living/carbon/proc/adjustPlasma(amount) - var/obj/item/organ/alien/plasmavessel/vessel = getorgan(/obj/item/organ/alien/plasmavessel) + var/obj/item/organ/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel) if(!vessel) return FALSE - vessel.storedPlasma = max(vessel.storedPlasma + amount,0) - vessel.storedPlasma = min(vessel.storedPlasma, vessel.max_plasma) //upper limit of max_plasma, lower limit of 0 - for(var/X in abilities) - var/obj/effect/proc_holder/alien/APH = X - if(APH.has_action) - APH.action.UpdateButtonIcon() + vessel.stored_plasma = max(vessel.stored_plasma + amount,0) + vessel.stored_plasma = min(vessel.stored_plasma, vessel.max_plasma) //upper limit of max_plasma, lower limit of 0 + for(var/datum/action/cooldown/alien/ability in actions) + ability.UpdateButtons() return TRUE /mob/living/carbon/alien/adjustPlasma(amount) . = ..() updatePlasmaDisplay() - -/mob/living/carbon/proc/usePlasma(amount) - if(getPlasma() >= amount) - adjustPlasma(-amount) - return TRUE - return FALSE diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm index 2c64eac9a6b9d..01b43ca285bad 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm @@ -6,7 +6,9 @@ icon_state = "aliend" /mob/living/carbon/alien/humanoid/drone/Initialize(mapload) - AddAbility(new/obj/effect/proc_holder/alien/evolve(null)) + var/datum/action/cooldown/alien/evolve_to_praetorian/evolution = new(src) + evolution.Grant(src) + return ..() . = ..() /mob/living/carbon/alien/humanoid/drone/create_internal_organs() @@ -15,28 +17,35 @@ internal_organs += new /obj/item/organ/alien/acid return ..() -/obj/effect/proc_holder/alien/evolve +/datum/action/cooldown/alien/evolve_to_praetorian name = "Evolve to Praetorian" desc = "Praetorian" + button_icon_state = "alien_evolve_drone" plasma_cost = 500 - action_icon_state = "alien_evolve_drone" -/obj/effect/proc_holder/alien/evolve/fire(mob/living/carbon/alien/humanoid/user) - var/obj/item/organ/alien/hivenode/node = user.getorgan(/obj/item/organ/alien/hivenode) - if(!node) //Players are Murphy's Law. We may not expect there to ever be a living xeno with no hivenode, but they _WILL_ make it happen. - to_chat(user, "Without the hivemind, you can't possibly hold the responsibility of leadership!") +/datum/action/cooldown/alien/evolve_to_praetorian/IsAvailable() + . = ..() + if(!.) return FALSE - if(node.recent_queen_death) - to_chat(user, "Your thoughts are still too scattered to take up the position of leadership.") + + if(!isturf(owner.loc)) return FALSE - if(!isturf(user.loc)) - to_chat(user, "You can't evolve here!") + if(get_alien_type(/mob/living/carbon/alien/humanoid/royal)) return FALSE - if(!get_alien_type_in_hive(/mob/living/carbon/alien/humanoid/royal)) - var/mob/living/carbon/alien/humanoid/royal/praetorian/new_xeno = new(user.loc) - user.alien_evolve(new_xeno) - return TRUE - else - to_chat(user, "We already have a living royal!") + + var/mob/living/carbon/alien/humanoid/royal/evolver = owner + var/obj/item/organ/internal/alien/hivenode/node = evolver.getorgan(/obj/item/organ/internal/alien/hivenode) + // Players are Murphy's Law. We may not expect + // there to ever be a living xeno with no hivenode, + // but they _WILL_ make it happen. + if(!node || node.recent_queen_death) return FALSE + + return TRUE + +/datum/action/cooldown/alien/evolve_to_praetorian/Activate(atom/target) + var/mob/living/carbon/alien/humanoid/evolver = owner + var/mob/living/carbon/alien/humanoid/royal/praetorian/new_xeno = new(owner.loc) + evolver.alien_evolve(new_xeno) + return TRUE diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm index be2e538500a93..2363140b1756e 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm @@ -7,9 +7,11 @@ /mob/living/carbon/alien/humanoid/royal/praetorian/Initialize(mapload) real_name = name - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/repulse/xeno(src)) - AddAbility(new /obj/effect/proc_holder/alien/royal/praetorian/evolve()) - . = ..() + var/datum/action/cooldown/spell/aoe/repulse/xeno/tail_whip = new(src) + tail_whip.Grant(src) + var/datum/action/cooldown/alien/evolve_to_queen/evolution = new(src) + evolution.Grant(src) + return ..() /mob/living/carbon/alien/humanoid/royal/praetorian/create_internal_organs() internal_organs += new /obj/item/organ/alien/plasmavessel/large @@ -18,24 +20,32 @@ internal_organs += new /obj/item/organ/alien/neurotoxin return ..() -/obj/effect/proc_holder/alien/royal/praetorian/evolve +/datum/action/cooldown/alien/evolve_to_queen name = "Evolve" - desc = "Produce an internal egg sac capable of spawning children. Only one queen can exist at a time. Costs 500 Plasma." + desc = "Produce an internal egg sac capable of spawning children. Only one queen can exist at a time." + button_icon_state = "alien_evolve_praetorian" plasma_cost = 500 - action_icon_state = "alien_evolve_praetorian" -/obj/effect/proc_holder/alien/royal/praetorian/evolve/fire(mob/living/carbon/alien/humanoid/user) - var/obj/item/organ/alien/hivenode/node = user.getorgan(/obj/item/organ/alien/hivenode) - if(!node) //Just in case this particular Praetorian gets violated and kept by the RD as a replacement for Lamarr. - to_chat(user, "Without the hivemind, you would be unfit to rule as queen!") +/datum/action/cooldown/alien/evolve_to_queen/IsAvailable() + . = ..() + if(!.) return FALSE - if(node.recent_queen_death) - to_chat(user, "You are still too burdened with guilt to evolve into a queen.") + + if(!isturf(owner.loc)) return FALSE - if(!get_alien_type_in_hive(/mob/living/carbon/alien/humanoid/royal/queen)) - var/mob/living/carbon/alien/humanoid/royal/queen/new_xeno = new(user.loc) - user.alien_evolve(new_xeno) - return TRUE - else - to_chat(user, "We already have an alive queen.") + + if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/queen)) return FALSE + + var/mob/living/carbon/alien/humanoid/royal/evolver = owner + var/obj/item/organ/internal/alien/hivenode/node = evolver.getorgan(/obj/item/organ/internal/alien/hivenode) + if(!node || node.recent_queen_death) + return FALSE + + return TRUE + +/datum/action/cooldown/alien/evolve_to_queen/Activate(atom/target) + var/mob/living/carbon/alien/humanoid/royal/evolver = owner + var/mob/living/carbon/alien/humanoid/royal/queen/new_queen = new(owner.loc) + evolver.alien_evolve(new_queen) + return TRUE diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm index 73a33ead26da4..ca5f724899a23 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm @@ -6,7 +6,9 @@ icon_state = "aliens" /mob/living/carbon/alien/humanoid/sentinel/Initialize(mapload) - AddAbility(new /obj/effect/proc_holder/alien/sneak) + var/datum/action/cooldown/alien/sneak/sneaky_beaky = new(src) + sneaky_beaky.Grant(src) + return ..() . = ..() /mob/living/carbon/alien/humanoid/sentinel/create_internal_organs() diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm index 1b19cd7eb2b4e..25d041532f679 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm @@ -19,8 +19,6 @@ var/alt_icon = 'icons/mob/alienleap.dmi' //used to switch between the two alien icon files. var/leap_on_click = FALSE COOLDOWN_DECLARE(pounce_cooldown) - var/sneaking = FALSE //For sneaky-sneaky mode and appropriate slowdown - var/drooling = FALSE //For Neurotoxic spit overlays GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list( /datum/strippable_item/hand/left, @@ -63,6 +61,6 @@ GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list( return FALSE /mob/living/carbon/alien/humanoid/check_breath(datum/gas_mixture/breath) - if(breath && breath.total_moles() > 0 && !sneaking) - playsound(get_turf(src), pick('sound/voice/lowHiss2.ogg', 'sound/voice/lowHiss3.ogg', 'sound/voice/lowHiss4.ogg'), 50, 0, -5) + if(breath?.total_moles() > 0 && !HAS_TRAIT(src, TRAIT_ALIEN_SNEAK)) + playsound(get_turf(src), pick('sound/voice/lowHiss2.ogg', 'sound/voice/lowHiss3.ogg', 'sound/voice/lowHiss4.ogg'), 50, FALSE, -5) return ..() diff --git a/code/modules/mob/living/carbon/alien/humanoid/queen.dm b/code/modules/mob/living/carbon/alien/humanoid/queen.dm index db3d5e6dc2cb3..c0e9222bbeca6 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/queen.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/queen.dm @@ -40,7 +40,6 @@ maxHealth = 400 health = 400 icon_state = "alienq" - var/datum/action/small_sprite/smallsprite = new/datum/action/small_sprite/queen() /mob/living/carbon/alien/humanoid/royal/queen/Initialize(mapload) RegisterSignal(src, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(set_countdown)) @@ -58,9 +57,12 @@ real_name = src.name - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/repulse/xeno(src)) - AddAbility(new/obj/effect/proc_holder/alien/royal/queen/promote()) + var/datum/action/cooldown/spell/aoe/repulse/xeno/tail_whip = new(src) + tail_whip.Grant(src) + var/datum/action/small_sprite/queen/smallsprite = new(src) smallsprite.Grant(src) + var/datum/action/cooldown/alien/promote/promotion = new(src) + promotion.Grant(src) return ..() /mob/living/carbon/alien/humanoid/royal/queen/create_internal_organs() @@ -69,7 +71,7 @@ internal_organs += new /obj/item/organ/alien/acid internal_organs += new /obj/item/organ/alien/neurotoxin internal_organs += new /obj/item/organ/alien/eggsac - ..() + return ..() /mob/living/carbon/alien/humanoid/royal/queen/proc/set_countdown() SIGNAL_HANDLER @@ -100,87 +102,113 @@ ..() //Queen verbs -/obj/effect/proc_holder/alien/lay_egg +/datum/action/cooldown/alien/make_structure/lay_egg name = "Lay Egg" - desc = "Lay an egg to produce huggers to impregnate prey with. Costs 75 Plasma." + desc = "Lay an egg to produce huggers to impregnate prey with." + button_icon_state = "alien_egg" plasma_cost = 75 - check_turf = TRUE - action_icon_state = "alien_egg" + made_structure_type = /obj/structure/alien/egg -/obj/effect/proc_holder/alien/lay_egg/fire(mob/living/carbon/user) - if(locate(/obj/structure/alien/egg) in get_turf(user)) - to_chat(user, "There's already an egg here.") +/datum/action/cooldown/alien/make_structure/lay_egg/Activate(atom/target) + . = ..() + owner.visible_message(span_alertalien("[owner] lays an egg!")) + +//Button to let queen choose her praetorian. +/datum/action/cooldown/alien/promote + name = "Create Royal Parasite" + desc = "Produce a royal parasite to grant one of your children the honor of being your Praetorian." + button_icon_state = "alien_queen_promote" + /// The promotion only takes plasma when completed, not on activation. + var/promotion_plasma_cost = 500 + +/datum/action/cooldown/alien/promote/set_statpanel_format() + . = ..() + if(!islist(.)) + return + + .[PANEL_DISPLAY_STATUS] = "PLASMA - [promotion_plasma_cost]" + +/datum/action/cooldown/alien/promote/IsAvailable() + . = ..() + if(!.) + return FALSE + + var/mob/living/carbon/carbon_owner = owner + if(carbon_owner.getPlasma() < promotion_plasma_cost) return FALSE - if(!check_vent_block(user)) + if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/praetorian)) return FALSE - user.visible_message("[user] has laid an egg!") - new /obj/structure/alien/egg(user.loc) return TRUE -//Button to let queen choose her praetorian. -/obj/effect/proc_holder/alien/royal/queen/promote - name = "Create Royal Parasite" - desc = "Produce a royal parasite to grant one of your children the honor of being your Praetorian. Costs 500 Plasma." - plasma_cost = 500 //Plasma cost used on promotion, not spawning the parasite. - - action_icon_state = "alien_queen_promote" - -/obj/effect/proc_holder/alien/royal/queen/promote/fire(mob/living/carbon/alien/user) - var/obj/item/queenpromote/prom - if(get_alien_type_in_hive(/mob/living/carbon/alien/humanoid/royal/praetorian/)) - to_chat(user, "You already have a Praetorian!") - return 0 - else - for(prom in user) - to_chat(user, "You discard [prom].") - qdel(prom) - return 0 - - prom = new (user.loc) - if(!user.put_in_active_hand(prom, 1)) - to_chat(user, "You must empty your hands before preparing the parasite.") - return 0 - else //Just in case telling the player only once is not enough! - to_chat(user, "Use the royal parasite on one of your children to promote her to Praetorian!") - return 0 - -/obj/item/queenpromote +/datum/action/cooldown/alien/promote/Activate(atom/target) + var/obj/item/queen_promotion/existing_promotion = locate() in owner.held_items + if(existing_promotion) + to_chat(owner, span_noticealien("You discard [existing_promotion].")) + owner.temporarilyRemoveItemFromInventory(existing_promotion) + qdel(existing_promotion) + return TRUE + + if(!owner.get_empty_held_indexes()) + to_chat(owner, span_warning("You must have an empty hand before preparing the parasite.")) + return FALSE + + var/obj/item/queen_promotion/new_promotion = new(owner.loc) + if(!owner.put_in_hands(new_promotion, del_on_fail = TRUE)) + to_chat(owner, span_noticealien("You fail to prepare a parasite.")) + return FALSE + + to_chat(owner, span_noticealien("Use [new_promotion] on one of your children to promote her to a Praetorian!")) + return TRUE + +/obj/item/queen_promotion name = "\improper royal parasite" desc = "Inject this into one of your grown children to promote her to a Praetorian!" icon_state = "alien_medal" - item_flags = ABSTRACT | DROPDEL + item_flags = NOBLUDGEON | ABSTRACT | DROPDEL icon = 'icons/mob/alien.dmi' -/obj/item/queenpromote/Initialize(mapload) +/obj/item/queen_promotion/attack(mob/living/to_promote, mob/living/carbon/alien/humanoid/queen) . = ..() - ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) + if(.) + return -/obj/item/queenpromote/attack(mob/living/M, mob/living/carbon/alien/humanoid/user) - if(!isalienadult(M) || isalienroyal(M)) - to_chat(user, "You may only use this with your adult, non-royal children!") + var/datum/action/cooldown/alien/promote/promotion = locate() in queen.actions + if(!promotion) + CRASH("[type] was created and handled by a mob ([queen]) that didn't have a promotion action associated.") + + if(!isalienadult(to_promote) || isalienroyal(to_promote)) + to_chat(queen, span_noticealien("You may only use this with your adult, non-royal children!")) return - if(get_alien_type_in_hive(/mob/living/carbon/alien/humanoid/royal/praetorian/)) - to_chat(user, "You already have a Praetorian!") + + if(!promotion.IsAvailable()) + to_chat(queen, span_noticealien("You cannot promote a child right now!")) return - var/mob/living/carbon/alien/humanoid/A = M - if(A.stat == CONSCIOUS && A.mind && A.key) - if(!user.usePlasma(500)) - to_chat(user, "You must have 500 plasma stored to use this!") - return - - to_chat(A, "The queen has granted you a promotion to Praetorian!") - user.visible_message("[A] begins to expand, twist and contort!") - var/mob/living/carbon/alien/humanoid/royal/praetorian/new_prae = new (A.loc) - A.mind.transfer_to(new_prae) - qdel(A) - qdel(src) + if(to_promote.stat != CONSCIOUS || !to_promote.mind || !to_promote.key) return - else - to_chat(user, "This child must be alert and responsive to become a Praetorian!") -/obj/item/queenpromote/attack_self(mob/user) - to_chat(user, "You discard [src].") + queen.adjustPlasma(-promotion.promotion_plasma_cost) + + to_chat(queen, span_noticealien("You have promoted [to_promote] to a Praetorian!")) + to_promote.visible_message( + span_alertalien("[to_promote] begins to expand, twist and contort!"), + span_noticealien("The queen has granted you a promotion to Praetorian!"), + ) + + var/mob/living/carbon/alien/humanoid/royal/praetorian/new_prae = new(to_promote.loc) + to_promote.mind.transfer_to(new_prae) + + qdel(to_promote) + qdel(src) + return TRUE + +/obj/item/queen_promotion/attack_self(mob/user) + to_chat(user, span_noticealien("You discard [src].")) qdel(src) + +/obj/item/queen_promotion/dropped(mob/user, silent) + if(!silent) + to_chat(user, span_noticealien("You discard [src].")) + return ..() diff --git a/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm b/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm index 42264f2534440..a5f9ee18d8c35 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm @@ -4,7 +4,7 @@ for(var/I in overlays_standing) add_overlay(I) - var/asleep = IsSleeping() + var/are_we_drooling = istype(click_intercept, /datum/action/cooldown/alien/acid) if(stat == DEAD) //If we mostly took damage from fire if(getFireLoss() > 125) @@ -12,7 +12,7 @@ else icon_state = "alien[caste]_dead" - else if((stat == UNCONSCIOUS && !asleep) || stat == HARD_CRIT || stat == SOFT_CRIT || IsParalyzed()) + else if((stat == UNCONSCIOUS && !IsSleeping()) || stat == HARD_CRIT || stat == SOFT_CRIT || IsParalyzed()) icon_state = "alien[caste]_unconscious" else if(leap_on_click) icon_state = "alien[caste]_pounce" @@ -21,11 +21,11 @@ icon_state = "alien[caste]_sleep" else if(mob_size == MOB_SIZE_LARGE) icon_state = "alien[caste]" - if(drooling) + if(are_we_drooling) add_overlay("alienspit_[caste]") else icon_state = "alien[caste]" - if(drooling) + if(are_we_drooling) add_overlay("alienspit") if(leaping) diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm index 2702d366724b5..48692498fb12e 100644 --- a/code/modules/mob/living/carbon/alien/larva/larva.dm +++ b/code/modules/mob/living/carbon/alien/larva/larva.dm @@ -33,16 +33,19 @@ //This is fine right now, if we're adding organ specific damage this needs to be updated /mob/living/carbon/alien/larva/Initialize(mapload) - - AddAbility(new/obj/effect/proc_holder/alien/hide(null)) - AddAbility(new/obj/effect/proc_holder/alien/larva_evolve(null)) - . = ..() + var/datum/action/cooldown/alien/larva_evolve/evolution = new(src) + evolution.Grant(src) + var/datum/action/cooldown/alien/hide/hide = new(src) + hide.Grant(src) + return ..() /mob/living/carbon/alien/larva/create_internal_organs() internal_organs += new /obj/item/organ/alien/plasmavessel/small/tiny ..() //This needs to be fixed +// This comment is 12 years old I hope it's fixed by now +// 14 years old idk if it's fixed /mob/living/carbon/alien/larva/get_stat_tab_status() var/list/tab_data = ..() tab_data["Progress"] = GENERATE_STAT_TEXT("[amount_grown]/[max_grown]") diff --git a/code/modules/mob/living/carbon/alien/larva/powers.dm b/code/modules/mob/living/carbon/alien/larva/powers.dm index fd9b734910674..34d39cff306d5 100644 --- a/code/modules/mob/living/carbon/alien/larva/powers.dm +++ b/code/modules/mob/living/carbon/alien/larva/powers.dm @@ -1,67 +1,103 @@ -/obj/effect/proc_holder/alien/hide +/datum/action/cooldown/alien/hide name = "Hide" - desc = "Allows aliens to hide beneath tables or certain items. Toggled on or off." + desc = "Allows you to hide beneath tables and certain objects." + button_icon_state = "alien_hide" plasma_cost = 0 + /// The layer we are on while hiding + var/hide_layer = ABOVE_NORMAL_TURF_LAYER - action_icon_state = "alien_hide" +/datum/action/cooldown/alien/hide/Activate(atom/target) + if(owner.layer == hide_layer) + owner.layer = initial(owner.layer) + owner.visible_message( + span_notice("[owner] slowly peeks up from the ground..."), + span_noticealien("You stop hiding."), + ) -/obj/effect/proc_holder/alien/hide/fire(mob/living/carbon/alien/user) - if(user.stat != CONSCIOUS) - return - - if (user.layer != ABOVE_NORMAL_TURF_LAYER) - user.layer = ABOVE_NORMAL_TURF_LAYER - user.visible_message("[user] scurries to the ground!", \ - "You are now hiding.") else - user.layer = MOB_LAYER - user.visible_message("[user] slowly peeks up from the ground...", \ - "You stop hiding.") - return 1 + owner.layer = hide_layer + owner.visible_message( + span_name("[owner] scurries to the ground!"), + span_noticealien("You are now hiding."), + ) + return TRUE -/obj/effect/proc_holder/alien/larva_evolve +/datum/action/cooldown/alien/larva_evolve name = "Evolve" desc = "Evolve into a higher alien caste." + button_icon_state = "alien_evolve_larva" plasma_cost = 0 - action_icon_state = "alien_evolve_larva" +/datum/action/cooldown/alien/larva_evolve/IsAvailable() + . = ..() + if(!.) + return FALSE + if(!islarva(owner)) + return FALSE -/obj/effect/proc_holder/alien/larva_evolve/fire(mob/living/carbon/alien/user) - if(!islarva(user)) - return - var/mob/living/carbon/alien/larva/L = user + var/mob/living/carbon/alien/larva/larva = owner + if(larva.handcuffed || larva.legcuffed) // Cuffing larvas ? Eh ? + return FALSE + if(larva.amount_grown < larva.max_grown) + return FALSE + if(larva.movement_type & VENTCRAWLING) + return FALSE + + return TRUE + +/datum/action/cooldown/alien/larva_evolve/Activate(atom/target) + var/mob/living/carbon/alien/larva/larva = owner + var/static/list/caste_options + if(!caste_options) + caste_options = list() + + // This can probably be genericized in the future. + var/mob/hunter_path = /mob/living/carbon/alien/humanoid/hunter + var/datum/radial_menu_choice/hunter = new() + hunter.name = "Hunter" + hunter.image = image(icon = initial(hunter_path.icon), icon_state = initial(hunter_path.icon_state)) + hunter.info = span_info("Hunters are the most agile caste, tasked with hunting for hosts. \ + They are faster than a human and can even pounce, but are not much tougher than a drone.") - if(L.handcuffed || L.legcuffed) // Cuffing larvas ? Eh ? - to_chat(user, "You cannot evolve when you are cuffed.") + caste_options["Hunter"] = hunter + + var/mob/sentinel_path = /mob/living/carbon/alien/humanoid/sentinel + var/datum/radial_menu_choice/sentinel = new() + sentinel.name = "Sentinel" + sentinel.image = image(icon = initial(sentinel_path.icon), icon_state = initial(sentinel_path.icon_state)) + sentinel.info = span_info("Sentinels are tasked with protecting the hive. \ + With their ranged spit, invisibility, and high health, they make formidable guardians \ + and acceptable secondhand hunters.") + + caste_options["Sentinel"] = sentinel + + var/mob/drone_path = /mob/living/carbon/alien/humanoid/drone + var/datum/radial_menu_choice/drone = new() + drone.name = "Drone" + drone.image = image(icon = initial(drone_path.icon), icon_state = initial(drone_path.icon_state)) + drone.info = span_info("Drones are the weakest and slowest of the castes, \ + but can grow into a praetorian and then queen if no queen exists, \ + and are vital to maintaining a hive with their resin secretion abilities.") + + caste_options["Drone"] = drone + + var/alien_caste = show_radial_menu(owner, owner, caste_options, radius = 38, require_near = TRUE, tooltips = TRUE) + if(QDELETED(src) || QDELETED(owner) || !IsAvailable() || !alien_caste) return - if(L.movement_type & VENTCRAWLING) - to_chat(user, "You cannot evolve while in a vent.") + if(alien_caste == null) return + var/mob/living/carbon/alien/humanoid/new_xeno + switch(alien_caste) + if("Hunter") + new_xeno = new /mob/living/carbon/alien/humanoid/hunter(larva.loc) + if("Sentinel") + new_xeno = new /mob/living/carbon/alien/humanoid/sentinel(larva.loc) + if("Drone") + new_xeno = new /mob/living/carbon/alien/humanoid/drone(larva.loc) + else + CRASH("Alien evolve was given an invalid / incorrect alien cast type. Got: [alien_caste]") - if(L.amount_grown >= L.max_grown) //TODO ~Carn - to_chat(L, "You are growing into a beautiful alien! It is time to choose a caste.") - to_chat(L, "There are three to choose from:") - to_chat(L, "Hunters are the most agile caste, tasked with hunting for hosts. They are faster than a human and can even pounce, but are not much tougher than a drone.") - to_chat(L, "Sentinels are tasked with protecting the hive. With their ranged spit, invisibility, and high health, they make formidable guardians and acceptable secondhand hunters.") - to_chat(L, "Drones are the weakest and slowest of the castes, but can grow into a praetorian and then queen if no queen exists, and are vital to maintaining a hive with their resin secretion abilities.") - var/alien_caste = alert(L, "Please choose which alien caste you shall belong to.",,"Hunter","Sentinel","Drone") - - if(user.incapacitated()) //something happened to us while we were choosing. - return - - var/mob/living/carbon/alien/humanoid/new_xeno - switch(alien_caste) - if("Hunter") - new_xeno = new /mob/living/carbon/alien/humanoid/hunter(L.loc) - if("Sentinel") - new_xeno = new /mob/living/carbon/alien/humanoid/sentinel(L.loc) - if("Drone") - new_xeno = new /mob/living/carbon/alien/humanoid/drone(L.loc) - - L.alien_evolve(new_xeno) - return 0 - else - to_chat(user, "You are not fully grown.") - return 0 + larva.alien_evolve(new_xeno) + return TRUE diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm index 97cc056fda287..987e3b28749b3 100644 --- a/code/modules/mob/living/carbon/alien/organs.dm +++ b/code/modules/mob/living/carbon/alien/organs.dm @@ -1,29 +1,6 @@ /obj/item/organ/alien icon_state = "acid" food_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/toxin/acid = 10) - var/list/alien_powers = list() - -/obj/item/organ/alien/Initialize(mapload) - . = ..() - for(var/A in alien_powers) - if(ispath(A)) - alien_powers -= A - alien_powers += new A(src) - -/obj/item/organ/alien/Destroy() - QDEL_LIST(alien_powers) - return ..() - -/obj/item/organ/alien/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) - . = ..() - for(var/obj/effect/proc_holder/alien/P in alien_powers) - M.AddAbility(P) - -/obj/item/organ/alien/Remove(mob/living/carbon/M, special = 0, pref_load = FALSE) - for(var/obj/effect/proc_holder/alien/P in alien_powers) - M.RemoveAbility(P) - return ..() - /obj/item/organ/alien/plasmavessel name = "plasma vessel" @@ -31,10 +8,14 @@ w_class = WEIGHT_CLASS_NORMAL zone = BODY_ZONE_CHEST slot = "plasmavessel" - alien_powers = list(/obj/effect/proc_holder/alien/plant, /obj/effect/proc_holder/alien/transfer) + actions_types = list( + /datum/action/cooldown/alien/make_structure/plant_weeds, + /datum/action/cooldown/alien/transfer, + ) food_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/toxin/plasma = 10) - var/storedPlasma = 100 + /// The current amount of stored plasma. + var/stored_plasma = 100 var/max_plasma = 250 var/heal_rate = 5 var/plasma_rate = 10 @@ -43,7 +24,7 @@ name = "large plasma vessel" icon_state = "plasma_large" w_class = WEIGHT_CLASS_BULKY - storedPlasma = 200 + stored_plasma = 200 max_plasma = 500 plasma_rate = 15 @@ -54,7 +35,7 @@ name = "small plasma vessel" icon_state = "plasma_small" w_class = WEIGHT_CLASS_SMALL - storedPlasma = 100 + stored_plasma = 100 max_plasma = 150 plasma_rate = 5 @@ -63,7 +44,7 @@ icon_state = "plasma_tiny" w_class = WEIGHT_CLASS_TINY max_plasma = 100 - alien_powers = list(/obj/effect/proc_holder/alien/transfer) + actions_types = list(/datum/action/cooldown/alien/transfer) /obj/item/organ/alien/plasmavessel/on_life() //If there are alien weeds on the ground then heal if needed or give some plasma @@ -104,7 +85,7 @@ zone = BODY_ZONE_HEAD slot = "hivenode" w_class = WEIGHT_CLASS_TINY - alien_powers = list(/obj/effect/proc_holder/alien/whisper) + actions_types = list(/datum/action/cooldown/alien/whisper) var/recent_queen_death = 0 //Indicates if the queen died recently, aliens are heavily weakened while this is active. /obj/item/organ/alien/hivenode/Insert(mob/living/carbon/M, special = 0, pref_load = FALSE) @@ -157,7 +138,7 @@ icon_state = "stomach-x" zone = BODY_ZONE_PRECISE_MOUTH slot = "resinspinner" - alien_powers = list(/obj/effect/proc_holder/alien/resin) + actions_types = list(/datum/action/cooldown/alien/make_structure/resin) /obj/item/organ/alien/acid @@ -165,7 +146,7 @@ icon_state = "acid" zone = BODY_ZONE_PRECISE_MOUTH slot = "acidgland" - alien_powers = list(/obj/effect/proc_holder/alien/acid) + actions_types = list(/datum/action/cooldown/alien/acid/corrosion) /obj/item/organ/alien/neurotoxin @@ -173,7 +154,7 @@ icon_state = "neurotox" zone = BODY_ZONE_PRECISE_MOUTH slot = "neurotoxingland" - alien_powers = list(/obj/effect/proc_holder/alien/neurotoxin) + actions_types = list(/datum/action/cooldown/alien/acid/neurotoxin) /obj/item/organ/alien/eggsac @@ -182,4 +163,4 @@ zone = BODY_ZONE_PRECISE_GROIN slot = "eggsac" w_class = WEIGHT_CLASS_BULKY - alien_powers = list(/obj/effect/proc_holder/alien/lay_egg) + actions_types = list(/datum/action/cooldown/alien/make_structure/lay_egg) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 2e521d1b91055..b26f6457348ca 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -386,7 +386,7 @@ var/list/tab_data = ..() var/obj/item/organ/alien/plasmavessel/vessel = getorgan(/obj/item/organ/alien/plasmavessel) if(vessel) - tab_data["Plasma Stored"] = GENERATE_STAT_TEXT("[vessel.storedPlasma]/[vessel.max_plasma]") + tab_data += "Plasma Stored: [vessel.stored_plasma]/[vessel.max_plasma]" if(locate(/obj/item/assembly/health) in src) tab_data["Health"] = GENERATE_STAT_TEXT("[health]") return tab_data diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index c235f766776a4..4fc72d4e03bfc 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -4,16 +4,6 @@ // Return the item currently in the slot ID /mob/living/carbon/human/get_item_by_slot(slot_id) switch(slot_id) - if(ITEM_SLOT_BACK) - return back - if(ITEM_SLOT_MASK) - return wear_mask - if(ITEM_SLOT_NECK) - return wear_neck - if(ITEM_SLOT_HANDCUFFED) - return handcuffed - if(ITEM_SLOT_LEGCUFFED) - return legcuffed if(ITEM_SLOT_BELT) return belt if(ITEM_SLOT_ID) @@ -24,8 +14,6 @@ return glasses if(ITEM_SLOT_GLOVES) return gloves - if(ITEM_SLOT_HEAD) - return head if(ITEM_SLOT_FEET) return shoes if(ITEM_SLOT_OCLOTHING) @@ -40,6 +28,45 @@ return s_store return null +/mob/living/carbon/human/get_slot_by_item(obj/item/looking_for) + if(looking_for == belt) + return ITEM_SLOT_BELT + + if(looking_for == wear_id) + return ITEM_SLOT_ID + + if(looking_for == ears) + return ITEM_SLOT_EARS + + if(looking_for == glasses) + return ITEM_SLOT_EYES + + if(looking_for == gloves) + return ITEM_SLOT_GLOVES + + if(looking_for == head) + return ITEM_SLOT_HEAD + + if(looking_for == shoes) + return ITEM_SLOT_FEET + + if(looking_for == wear_suit) + return ITEM_SLOT_OCLOTHING + + if(looking_for == w_uniform) + return ITEM_SLOT_ICLOTHING + + if(looking_for == r_store) + return ITEM_SLOT_RPOCKET + + if(looking_for == l_store) + return ITEM_SLOT_LPOCKET + + if(looking_for == s_store) + return ITEM_SLOT_SUITSTORE + + return ..() + /mob/living/carbon/human/proc/get_all_slots() . = get_head_slots() | get_body_slots() diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm index 21bcb156c3068..596d9b8c6ba79 100644 --- a/code/modules/mob/living/carbon/human/species_types/golems.dm +++ b/code/modules/mob/living/carbon/human/species_types/golems.dm @@ -534,9 +534,9 @@ spark_system.start() do_teleport(H, get_turf(H), 12, asoundin = 'sound/weapons/emitter2.ogg', channel = TELEPORT_CHANNEL_BLUESPACE) last_teleport = world.time - UpdateButtonIcon() //action icon looks unavailable + UpdateButtons() //action icon looks unavailable //action icon looks available again - addtimer(CALLBACK(src, PROC_REF(UpdateButtonIcon)), cooldown + 5) + addtimer(CALLBACK(src, PROC_REF(UpdateButtons)), cooldown + 5) //honk @@ -639,9 +639,12 @@ random_eligible = FALSE //Zesko claims runic golems break the game inherent_factions = list("cult") species_language_holder = /datum/language_holder/golem/runic - var/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/golem/phase_shift - var/obj/effect/proc_holder/spell/targeted/abyssal_gaze/abyssal_gaze - var/obj/effect/proc_holder/spell/targeted/dominate/dominate + /// A ref to our jaunt spell that we get on species gain. + var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/golem/jaunt + /// A ref to our gaze spell that we get on species gain. + var/datum/action/cooldown/spell/pointed/abyssal_gaze/abyssal_gaze + /// A ref to our dominate spell that we get on species gain. + var/datum/action/cooldown/spell/pointed/dominate/dominate species_chest = /obj/item/bodypart/chest/golem/cult species_head = /obj/item/bodypart/head/golem/cult @@ -656,29 +659,30 @@ var/golem_name = "[edgy_first_name] [edgy_last_name]" return golem_name -/datum/species/golem/runic/on_species_gain(mob/living/carbon/C, datum/species/old_species) +/datum/species/golem/runic/on_species_gain(mob/living/carbon/grant_to, datum/species/old_species) . = ..() - phase_shift = new - phase_shift.charge_counter = 0 - phase_shift.start_recharge() - C.AddSpell(phase_shift) - abyssal_gaze = new - abyssal_gaze.charge_counter = 0 - abyssal_gaze.start_recharge() - C.AddSpell(abyssal_gaze) - dominate = new - dominate.charge_counter = 0 - dominate.start_recharge() - C.AddSpell(dominate) + // Create our species specific spells here. + // Note we link them to the mob, not the mind, + // so they're not moved around on mindswaps + jaunt = new(grant_to) + jaunt.StartCooldown() + jaunt.Grant(grant_to) + + abyssal_gaze = new(grant_to) + abyssal_gaze.StartCooldown() + abyssal_gaze.Grant(grant_to) + + dominate = new(grant_to) + dominate.StartCooldown() + dominate.Grant(grant_to) /datum/species/golem/runic/on_species_loss(mob/living/carbon/C) - . = ..() - if(phase_shift) - C.RemoveSpell(phase_shift) - if(abyssal_gaze) - C.RemoveSpell(abyssal_gaze) - if(dominate) - C.RemoveSpell(dominate) + // Aaand cleanup our species specific spells. + // No free rides. + QDEL_NULL(jaunt) + QDEL_NULL(abyssal_gaze) + QDEL_NULL(dominate) + return ..() /datum/species/golem/runic/handle_chemicals(datum/reagent/chem, mob/living/carbon/human/H) if(istype(chem, /datum/reagent/water/holywater)) @@ -1178,8 +1182,10 @@ species_traits = list(NOBLOOD,NO_UNDERWEAR,NOEYESPRITES,NOTRANSSTING) //no mutcolors, no eye sprites inherent_traits = list(TRAIT_NOBREATH,TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_NOGUNS,TRAIT_RADIMMUNE,TRAIT_PIERCEIMMUNE,TRAIT_NODISMEMBER) - var/obj/effect/proc_holder/spell/targeted/conjure_item/snowball/ball - var/obj/effect/proc_holder/spell/aimed/cryo/cryo + /// A ref to our "throw snowball" spell we get on species gain. + var/datum/action/cooldown/spell/conjure_item/snowball/snowball + /// A ref to our cryobeam spell we get on species gain. + var/datum/action/cooldown/spell/pointed/projectile/cryo/cryo species_chest = /obj/item/bodypart/chest/golem/snow species_head = /obj/item/bodypart/head/golem/snow @@ -1197,33 +1203,23 @@ new /obj/item/food/grown/carrot(get_turf(H)) qdel(H) -/datum/species/golem/snow/on_species_gain(mob/living/carbon/C, datum/species/old_species) - . = ..() - C.weather_immunities |= "snow" - ball = new - ball.charge_counter = 0 - ball.start_recharge() - C.AddSpell(ball) - cryo = new - cryo.charge_counter = 0 - cryo.start_recharge() - C.AddSpell(cryo) - -/datum/species/golem/snow/on_species_loss(mob/living/carbon/C) +/datum/species/golem/snow/on_species_gain(mob/living/carbon/grant_to, datum/species/old_species) . = ..() - C.weather_immunities -= "snow" - if(ball) - C.RemoveSpell(ball) - if(cryo) - C.RemoveSpell(cryo) - -/obj/effect/proc_holder/spell/targeted/conjure_item/snowball - name = "Snowball" - desc = "Concentrates cryokinetic forces to create snowballs, useful for throwing at people." - item_type = /obj/item/toy/snowball - charge_max = 15 - action_icon = 'icons/obj/toy.dmi' - action_icon_state = "snowball" + ADD_TRAIT(grant_to, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT) + + snowball = new(grant_to) + snowball.StartCooldown() + snowball.Grant(grant_to) + + cryo = new(grant_to) + cryo.StartCooldown() + cryo.Grant(grant_to) + +/datum/species/golem/snow/on_species_loss(mob/living/carbon/remove_from) + REMOVE_TRAIT(remove_from, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT) + QDEL_NULL(snowball) + QDEL_NULL(cryo) + return ..() /datum/species/golem/capitalist name = "Capitalist Golem" diff --git a/code/modules/mob/living/carbon/human/species_types/psyphoza.dm b/code/modules/mob/living/carbon/human/species_types/psyphoza.dm index 00588d8e363d1..8555417ce7f3e 100644 --- a/code/modules/mob/living/carbon/human/species_types/psyphoza.dm +++ b/code/modules/mob/living/carbon/human/species_types/psyphoza.dm @@ -211,10 +211,10 @@ for(var/mob/living/L in urange(9, owner, 1)) BS.highlight_object(L, "mob", L.dir) has_cooldown_timer = TRUE - UpdateButtonIcon() + UpdateButtons() addtimer(CALLBACK(src, PROC_REF(finish_cooldown)), cooldown + sense_time) -/datum/action/item_action/organ_action/psychic_highlight/UpdateButtonIcon(status_only = FALSE, force = FALSE) +/datum/action/item_action/organ_action/psychic_highlight/UpdateButtons(status_only = FALSE, force = FALSE) . = ..() if(!IsAvailable()) button.color = transparent_when_unavailable ? rgb(128,0,0,128) : rgb(128,0,0) //Overwrite this line from the original to support my fucked up use @@ -239,7 +239,7 @@ /datum/action/item_action/organ_action/psychic_highlight/proc/finish_cooldown() has_cooldown_timer = FALSE - UpdateButtonIcon() + UpdateButtons() //Allows user to see images through walls - mostly for if this action is added to something without xray /datum/action/item_action/organ_action/psychic_highlight/proc/toggle_eyes_fowards() diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm index e35bc6b072e78..706b5dc9263e5 100644 --- a/code/modules/mob/living/carbon/inventory.dm +++ b/code/modules/mob/living/carbon/inventory.dm @@ -12,7 +12,31 @@ return handcuffed if(ITEM_SLOT_LEGCUFFED) return legcuffed - return null + return ..() + +/mob/living/carbon/get_slot_by_item(obj/item/looking_for) + if(looking_for == back) + return ITEM_SLOT_BACK + + if(back && (looking_for in back)) + return ITEM_SLOT_BACKPACK + + if(looking_for == wear_mask) + return ITEM_SLOT_MASK + + if(looking_for == wear_neck) + return ITEM_SLOT_NECK + + if(looking_for == head) + return ITEM_SLOT_HEAD + + if(looking_for == handcuffed) + return ITEM_SLOT_HANDCUFFED + + if(looking_for == legcuffed) + return ITEM_SLOT_LEGCUFFED + + return ..() /mob/living/carbon/proc/equip_in_one_of_slots(obj/item/I, list/slots, qdel_on_fail = TRUE) for(var/slot in slots) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index caf4b857864ca..1372e9540e090 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -41,8 +41,6 @@ qdel(S) else S.be_replaced() - if(ranged_ability) - ranged_ability.remove_ranged_ability(src) if(buckled) buckled.unbuckle_mob(src,force=1) @@ -633,10 +631,6 @@ clear_alert("not_enough_oxy") reload_fullscreen() . = 1 - if(mind) - for(var/S in mind.spell_list) - var/obj/effect/proc_holder/spell/spell = S - spell.updateButtonIcon() /* * Heals up the [target] to up to [heal_to] of the main damage types. @@ -1266,22 +1260,6 @@ /mob/living/proc/on_fall() return -/mob/living/proc/AddAbility(obj/effect/proc_holder/A) - abilities.Add(A) - A.on_gain(src) - if(A.has_action) - A.action.Grant(src) - -/mob/living/proc/RemoveAbility(obj/effect/proc_holder/A) - abilities.Remove(A) - A.on_lose(src) - if(A.action) - A.action.Remove(src) - -/mob/living/proc/add_abilities_to_panel() - for(var/obj/effect/proc_holder/A in abilities) - statpanel("[A.panel]",A.get_panel_text(),A) - /mob/living/lingcheck() if(mind) var/datum/antagonist/changeling/changeling = mind.has_antag_datum(/datum/antagonist/changeling) @@ -1483,11 +1461,6 @@ clear_fullscreen("remote_view", 0) update_pipe_vision() -/mob/living/update_mouse_pointer() - ..() - if (client && ranged_ability && ranged_ability.ranged_mousepointer) - client.mouse_pointer_icon = ranged_ability.ranged_mousepointer - /mob/living/vv_edit_var(var_name, var_value) switch(var_name) if (NAMEOF(src, maxHealth)) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index b8c67e9328cfe..f6a907621aedf 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -107,7 +107,6 @@ var/stun_absorption = null //converted to a list of stun absorption sources this mob has when one is added var/blood_volume = 0 //how much blood the mob has - var/obj/effect/proc_holder/ranged_ability //Any ranged ability the mob has, as a click override var/see_override = 0 //0 for no override, sets see_invisible = see_override in silicon & carbon life process via update_sight() @@ -129,8 +128,6 @@ var/last_words //used for database logging - var/list/obj/effect/proc_holder/abilities = list() - var/can_be_held = FALSE //whether this can be picked up and held. var/worn_slot_flags = NONE //if it can be held, can it be equipped to any slots? (think pAI's on head) diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm index 388d0ba0f5ccf..8e075d0409d82 100644 --- a/code/modules/mob/living/login.dm +++ b/code/modules/mob/living/login.dm @@ -24,9 +24,6 @@ if(ventcrawler) to_chat(src, "You can ventcrawl! Use alt+click on vents to quickly travel about the station.") - if(ranged_ability) - ranged_ability.add_ranged_ability(src, "You currently have [ranged_ability] active!") - var/datum/antagonist/changeling/changeling = mind.has_antag_datum(/datum/antagonist/changeling) if(changeling) changeling.regain_powers() diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index 87de7f252b1de..75e55144bde2a 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -423,7 +423,21 @@ GLOBAL_LIST_INIT(department_radio_keys, list( else . = ..() -/mob/living/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) +/** + * Living level whisper. + * + * Living mobs which whisper have their message only appear to people very close. + * + * message - the message to display + * bubble_type - the type of speech bubble that shows up when they speak (currently does nothing) + * spans - a list of spans to apply around the message + * sanitize - whether we sanitize the message + * language - typepath language to force them to speak / whisper in + * ignore_spam - whether we ignore the spam filter + * forced - string source of what forced this speech to happen, also bypasses spam filter / mutes if supplied + * filterproof - whether we ignore the word filter + */ +/mob/living/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof) say("#[message]", bubble_type, spans, sanitize, language, ignore_spam, forced) /mob/living/get_language_holder(shadow=TRUE) diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm index 0517752446447..05842a62a965b 100644 --- a/code/modules/mob/living/simple_animal/constructs.dm +++ b/code/modules/mob/living/simple_animal/constructs.dm @@ -43,8 +43,6 @@ var/seeking = FALSE var/can_repair_constructs = FALSE var/can_repair_self = FALSE - var/runetype - var/datum/action/innate/cult/create_rune/our_rune /// Theme controls color. THEME_CULT is red THEME_WIZARD is purple and THEME_HOLY is blue var/theme = THEME_CULT chat_color = "#FF6262" @@ -56,25 +54,27 @@ /mob/living/simple_animal/hostile/construct/Initialize(mapload) . = ..() - update_health_hud() - var/spellnum = 1 + AddElement(/datum/element/simple_flying) + ADD_TRAIT(src, TRAIT_HEALS_FROM_CULT_PYLONS, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT) for(var/spell in construct_spells) - var/the_spell = new spell(null) - AddSpell(the_spell) - var/obj/effect/proc_holder/spell/S = mob_spell_list[spellnum] - var/pos = 2+spellnum*31 + var/datum/action/new_spell = new spell(src) + new_spell.Grant(src) + + var/spellnum = 1 + for(var/datum/action/spell as anything in actions) + if(!(type in construct_spells)) + continue + + var/pos = 2 + spellnum * 31 if(construct_spells.len >= 4) - pos -= 31*(construct_spells.len - 4) - S.action.button.screen_loc = "6:[pos],4:-2" - S.action.button.moved = "6:[pos],4:-2" + pos -= 31 * (construct_spells.len - 4) + spell.default_button_position = "6:[pos],4:-2" // Set the default position to this random position spellnum++ - if(runetype) - our_rune = new runetype(src) - our_rune.Grant(src) - var/pos = 2+spellnum*31 - our_rune.button.screen_loc = "6:[pos],4:-2" - our_rune.button.moved = "6:[pos],4:-2" - add_overlay("glow_[icon_state]_[theme]") + update_action_buttons() + + if(icon_state) + add_overlay("glow_[icon_state]_[theme]") /mob/living/simple_animal/hostile/construct/Destroy() QDEL_NULL(our_rune) @@ -156,10 +156,10 @@ mob_size = MOB_SIZE_LARGE force_threshold = 10 construct_spells = list( - /obj/effect/proc_holder/spell/targeted/forcewall/cult, - /obj/effect/proc_holder/spell/targeted/projectile/dumbfire/juggernaut - ) - runetype = /datum/action/innate/cult/create_rune/wall + /datum/action/cooldown/spell/forcewall/cult, + /datum/action/cooldown/spell/basic_projectile/juggernaut, + /datum/action/innate/cult/create_rune/wall, + ) playstyle_string = "You are a Juggernaut. Though slow, your shell can withstand heavy punishment, \ create shield walls, rip apart enemies and walls alike, and even deflect energy weapons." @@ -222,13 +222,18 @@ attack_verb_continuous = "slashes" attack_verb_simple = "slash" attack_sound = 'sound/weapons/bladeslice.ogg' - construct_spells = list(/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift) - runetype = /datum/action/innate/cult/create_rune/tele - playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, can phase through walls, and your attacks will lower the cooldown on phasing." - - var/attack_refund = 10 //1 second per attack - var/crit_refund = 50 //5 seconds when putting a target into critical - var/kill_refund = 250 //full refund on kills + construct_spells = list( + /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift, + /datum/action/innate/cult/create_rune/tele, + ) + playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, \ + can phase through walls, and your attacks will lower the cooldown on phasing." + + // Accomplishing various things gives you a refund on jaunt, to jump in and out. + /// The seconds refunded per attack + var/attack_refund = 1 SECONDS + /// The seconds refunded when putting a target into critical + var/crit_refund = 5 SECONDS /mob/living/simple_animal/hostile/construct/wraith/AttackingTarget() //refund jaunt cooldown when attacking living targets var/prev_stat @@ -238,17 +243,24 @@ . = ..() - if(. && isnum_safe(prev_stat)) - var/mob/living/L = target - var/refund = 0 - if(QDELETED(L) || (L.stat == DEAD && prev_stat != DEAD)) //they're dead, you killed them - refund += kill_refund - else if(HAS_TRAIT(L, TRAIT_CRITICAL_CONDITION) && prev_stat == CONSCIOUS) //you knocked them into critical - refund += crit_refund - if(L.stat != DEAD && prev_stat != DEAD) - refund += attack_refund - for(var/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/S in mob_spell_list) - S.charge_counter = min(S.charge_counter + refund, S.charge_max) + if(. && isnum(prev_stat)) + var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/jaunt = locate() in actions + if(!jaunt) + return + + var/total_refund = 0 SECONDS + // they're dead, and you killed them - full refund + if(QDELETED(living_target) || (living_target.stat == DEAD && prev_stat != DEAD)) + total_refund += jaunt.cooldown_time + // you knocked them into critical + else if(HAS_TRAIT(living_target, TRAIT_CRITICAL_CONDITION) && prev_stat == CONSCIOUS) + total_refund += crit_refund + + if(living_target.stat != DEAD && prev_stat != DEAD) + total_refund += attack_refund + + jaunt.next_use_time -= total_refund + jaunt.UpdateButtons() /mob/living/simple_animal/hostile/construct/wraith/hostile //actually hostile, will move around, hit things AIStatus = AI_ON @@ -256,13 +268,19 @@ //////////////////////////Wraith-alts//////////////////////////// /mob/living/simple_animal/hostile/construct/wraith/angelic theme = THEME_HOLY - construct_spells = list(/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/angelic) + construct_spells = list( + /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/angelic, + /datum/action/innate/cult/create_rune/tele, + ) loot = list(/obj/item/ectoplasm/angelic) chat_color = "#AED2FF" /mob/living/simple_animal/hostile/construct/wraith/mystic theme = THEME_WIZARD - construct_spells = list(/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/mystic) + construct_spells = list( + /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/mystic, + /datum/action/innate/cult/create_rune/tele, + ) loot = list(/obj/item/ectoplasm/mystic) /mob/living/simple_animal/hostile/construct/wraith/noncult @@ -287,17 +305,17 @@ environment_smash = ENVIRONMENT_SMASH_WALLS attack_sound = 'sound/weapons/punch2.ogg' construct_spells = list( - /obj/effect/proc_holder/spell/aoe_turf/conjure/wall, - /obj/effect/proc_holder/spell/aoe_turf/conjure/floor, - /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone, - /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser, - /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser - ) - playstyle_string = "You are an Artificer. You are incredibly weak and fragile, but you are able to construct fortifications, \ - - use magic missile, repair allied constructs, shades, and yourself (by clicking on them), \ - and, most important of all, create new constructs by producing soulstones to capture souls, \ - and shells to place those soulstones into." + /datum/action/cooldown/spell/conjure/cult_floor, + /datum/action/cooldown/spell/conjure/cult_wall, + /datum/action/cooldown/spell/conjure/soulstone, + /datum/action/cooldown/spell/conjure/construct/lesser, + /datum/action/cooldown/spell/aoe/magic_missile/lesser, + /datum/action/innate/cult/create_rune/revive, + ) + playstyle_string = "You are an Artificer. You are incredibly weak and fragile, \ + but you are able to construct fortifications, use magic missile, and repair allied constructs, shades, \ + and yourself (by clicking on them). Additionally, and most important of all, you can create new constructs \ + by producing soulstones to capture souls, and shells to place those soulstones into." can_repair_constructs = TRUE can_repair_self = TRUE @@ -350,30 +368,33 @@ chat_color = "#AED2FF" loot = list(/obj/item/ectoplasm/angelic) construct_spells = list( - /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/purified, - /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser, - /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser - ) + /datum/action/cooldown/spell/conjure/soulstone/purified, + /datum/action/cooldown/spell/conjure/construct/lesser, + /datum/action/cooldown/spell/aoe/magic_missile/lesser, + /datum/action/innate/cult/create_rune/revive, + ) /mob/living/simple_animal/hostile/construct/artificer/mystic theme = THEME_WIZARD loot = list(/obj/item/ectoplasm/mystic) construct_spells = list( - /obj/effect/proc_holder/spell/aoe_turf/conjure/wall, - /obj/effect/proc_holder/spell/aoe_turf/conjure/floor, - /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/mystic, - /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser, - /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser - ) + /datum/action/cooldown/spell/conjure/cult_floor, + /datum/action/cooldown/spell/conjure/cult_wall, + /datum/action/cooldown/spell/conjure/soulstone/mystic, + /datum/action/cooldown/spell/conjure/construct/lesser, + /datum/action/cooldown/spell/aoe/magic_missile/lesser, + /datum/action/innate/cult/create_rune/revive, + ) /mob/living/simple_animal/hostile/construct/artificer/noncult construct_spells = list( - /obj/effect/proc_holder/spell/aoe_turf/conjure/wall, - /obj/effect/proc_holder/spell/aoe_turf/conjure/floor, - /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/noncult, - /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser, - /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser - ) + /datum/action/cooldown/spell/conjure/cult_floor, + /datum/action/cooldown/spell/conjure/cult_wall, + /datum/action/cooldown/spell/conjure/soulstone/noncult, + /datum/action/cooldown/spell/conjure/construct/lesser, + /datum/action/cooldown/spell/aoe/magic_missile/lesser, + /datum/action/innate/cult/create_rune/revive, + ) /////////////////////////////Harvester///////////////////////// /mob/living/simple_animal/hostile/construct/harvester @@ -389,8 +410,10 @@ attack_verb_continuous = "butchers" attack_verb_simple = "butcher" attack_sound = 'sound/weapons/bladeslice.ogg' - construct_spells = list(/obj/effect/proc_holder/spell/aoe_turf/area_conversion, - /obj/effect/proc_holder/spell/targeted/forcewall/cult) + construct_spells = list( + /datum/action/cooldown/spell/aoe/area_conversion, + /datum/action/cooldown/spell/forcewall/cult, + ) playstyle_string = "You are a Harvester. You are incapable of directly killing humans, but your attacks will remove their limbs: \ Bring those who still cling to this world of illusion back to the Geometer so they may know Truth. Your form and any you are pulling can pass through runed walls effortlessly." can_repair_constructs = TRUE diff --git a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm index fe819868a7832..d2df8cac69360 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm @@ -42,6 +42,12 @@ return internal_storage return ..() +/mob/living/simple_animal/drone/get_slot_by_item(obj/item/looking_for) + if(internal_storage == looking_for) + return ITEM_SLOT_DEX_STORAGE + if(head == looking_for) + return ITEM_SLOT_HEAD + return ..() /mob/living/simple_animal/drone/equip_to_slot(obj/item/I, slot) if(!slot) diff --git a/code/modules/mob/living/simple_animal/heretic_monsters.dm b/code/modules/mob/living/simple_animal/heretic_monsters.dm index 9783a615f10fa..2bebb3bd68cd1 100644 --- a/code/modules/mob/living/simple_animal/heretic_monsters.dm +++ b/code/modules/mob/living/simple_animal/heretic_monsters.dm @@ -35,16 +35,9 @@ /mob/living/simple_animal/hostile/heretic_summon/Initialize(mapload) . = ..() - add_spells() - -/** - * Add_spells - * - * Goes through spells_to_add and adds each spell to the mind. - */ -/mob/living/simple_animal/hostile/heretic_summon/proc/add_spells() - for(var/spell in spells_to_add) - AddSpell(new spell()) + for(var/spell in actions_to_add) + var/datum/action/cooldown/spell/new_spell = new spell(src) + new_spell.Grant(src) /mob/living/simple_animal/hostile/heretic_summon/raw_prophet name = "Raw Prophet" @@ -57,11 +50,11 @@ maxHealth = 50 health = 50 sight = SEE_MOBS|SEE_OBJS|SEE_TURFS - spells_to_add = list( - /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash/long, - /obj/effect/proc_holder/spell/pointed/manse_link, - /obj/effect/proc_holder/spell/targeted/telepathy/eldritch, - /obj/effect/proc_holder/spell/targeted/blind/eldritch, + actions_to_add = list( + /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/long, + /datum/action/cooldown/spell/list_target/telepathy/eldritch, + /datum/action/cooldown/spell/pointed/blind/eldritch, + /datum/action/innate/expand_sight, ) /// A assoc list of [mob/living ref] to [datum/action ref] - all the mobs linked to our mansus network. @@ -170,7 +163,7 @@ ranged_cooldown_time = 5 ranged = TRUE rapid = 1 - spells_to_add = list(/obj/effect/proc_holder/spell/targeted/worm_contract) + actions_to_add = list(/datum/action/cooldown/spell/worm_contract) ///Previous segment in the chain var/mob/living/simple_animal/hostile/heretic_summon/armsy/back ///Next segment in the chain @@ -383,9 +376,9 @@ health = 75 melee_damage = 20 sight = SEE_TURFS - spells_to_add = list( - /obj/effect/proc_holder/spell/aoe_turf/rust_conversion/small, - /obj/effect/proc_holder/spell/targeted/projectile/dumbfire/rust_wave/short, + actions_to_add = list( + /datum/action/cooldown/spell/aoe/rust_conversion/small, + /datum/action/cooldown/spell/basic_projectile/rust_wave/short, ) /mob/living/simple_animal/hostile/heretic_summon/rust_spirit/setDir(newdir) @@ -421,10 +414,10 @@ health = 75 melee_damage = 20 sight = SEE_TURFS - spells_to_add = list( - /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash, - /obj/effect/proc_holder/spell/pointed/cleave, - /obj/effect/proc_holder/spell/targeted/fire_sworn, + actions_to_add = list( + /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash, + /datum/action/cooldown/spell/pointed/cleave, + /datum/action/cooldown/spell/fire_sworn, ) /mob/living/simple_animal/hostile/heretic_summon/stalker @@ -438,8 +431,8 @@ health = 150 melee_damage = 20 sight = SEE_MOBS - spells_to_add = list( - /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash, - /obj/effect/proc_holder/spell/targeted/shapeshift/eldritch, - /obj/effect/proc_holder/spell/targeted/emplosion/eldritch, + actions_to_add = list( + /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash, + /datum/action/cooldown/spell/shapeshift/eldritch, + /datum/action/cooldown/spell/emp/eldritch, ) diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm index 35ab586b1465c..84bd14224515e 100644 --- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm +++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm @@ -3,6 +3,7 @@ #define LAYING_EGGS 2 #define MOVING_TO_TARGET 3 #define SPINNING_COCOON 4 +#define INTERACTION_SPIDER_KEY "spider_key" /mob/living/simple_animal/hostile/poison mobchatspan = "researchdirector" @@ -55,8 +56,7 @@ footstep_type = FOOTSTEP_MOB_CLAW sentience_type = SENTIENCE_OTHER // not eligible for sentience potions var/busy = SPIDER_IDLE // What a spider's doing - var/datum/action/innate/spider/lay_web/lay_web // Web action - var/obj/effect/proc_holder/spider/wrap/lesser/lesserwrap // Wrap action + //var/obj/effect/proc_holder/spider/wrap/lesser/lesserwrap // Wrap action var/web_speed = 1 // How quickly a spider lays down webs (percentage) var/mob/master // The spider's master, used by sentience var/onweb_speed @@ -76,10 +76,10 @@ /mob/living/simple_animal/hostile/poison/giant_spider/Initialize(mapload) . = ..() - lay_web = new - lay_web.Grant(src) - lesserwrap = new - AddAbility(lesserwrap) + var/datum/action/innate/spider/lay_web/webbing = new(src) + webbing.Grant(src) + var/datum/action/cooldown/wrap/wrap = new(src) + wrap.Grant(src) /mob/living/simple_animal/hostile/poison/giant_spider/mind_initialize() . = ..() @@ -278,7 +278,7 @@ // Allows nurses to heal other spiders if they're adjacent /mob/living/simple_animal/hostile/poison/giant_spider/nurse/AttackingTarget() - if(is_busy) + if(DOING_INTERACTION(src, INTERACTION_SPIDER_KEY)) return var/mob/target_mob = target if(!istype(target_mob)) @@ -293,12 +293,15 @@ visible_message("[src] begins wrapping their wounds.","You begin wrapping your wounds.") else visible_message("[src] begins wrapping the wounds of [hurt_spider].","You begin wrapping the wounds of [hurt_spider].") - is_busy = TRUE - if(do_after(src, 20, target = hurt_spider)) - hurt_spider.heal_overall_damage(20) - new /obj/effect/temp_visual/heal(get_turf(hurt_spider), "#80F5FF") - visible_message("[src] wraps the wounds of [hurt_spider].","You wrap the wounds of [hurt_spider].") - is_busy = FALSE + if(!do_after(src, 2 SECONDS, target = hurt_spider, interaction_key = INTERACTION_SPIDER_KEY)) + return + + hurt_spider.heal_overall_damage(20, 20) + new /obj/effect/temp_visual/heal(get_turf(hurt_spider), "#80F5FF") + visible_message( + span_notice("[src] wraps the wounds of [hurt_spider]."), + span_notice("You wrap the wounds of [hurt_spider]."), + ) //Handles AI nurse healing when spiders are idle /mob/living/simple_animal/hostile/poison/giant_spider/nurse/handle_automated_movement() @@ -516,116 +519,154 @@ owner.a_intent = INTENT_HELP owner.visible_message("[owner] loosens up and allows others to pass again.","You are no longer blocking others from passing around you.") -/obj/effect/proc_holder/spider/Click() - if(!istype(usr, /mob/living/simple_animal/hostile/poison/giant_spider)) - return TRUE - var/mob/living/simple_animal/hostile/poison/giant_spider/user = usr - activate(user) - return TRUE +/datum/action/innate/spider/lay_web/IsAvailable() + . = ..() + if(!.) + return FALSE + + if(DOING_INTERACTION(owner, INTERACTION_SPIDER_KEY)) + return FALSE + if(!isspider(owner)) + return FALSE + + var/mob/living/simple_animal/hostile/giant_spider/spider = owner + var/obj/structure/spider/stickyweb/web = locate() in get_turf(spider) + if(web && (!spider.web_sealer || istype(web, /obj/structure/spider/stickyweb/sealed))) + to_chat(owner, span_warning("There's already a web here!")) + return FALSE -/obj/effect/proc_holder/spider/on_lose(mob/living/carbon/user) - remove_ranged_ability() + if(!isturf(spider.loc)) + return FALSE -/obj/effect/proc_holder/spider/proc/activate(mob/living/user) return TRUE -/obj/effect/proc_holder/spider/wrap - name = "Wrap" - panel = "Spider" - desc = "Wrap something or someone in a cocoon. If it's a living being, you'll also consume them, allowing you to lay eggs." - ranged_mousepointer = 'icons/effects/wrap_target.dmi' - action_icon = 'icons/mob/actions/actions_animal.dmi' - action_icon_state = "wrap_0" - action_background_icon_state = "bg_alien" - -/obj/effect/proc_holder/spider/wrap/lesser - desc = "Wrap loose objects in a cocoon of silk to prevent them from being used" - -/obj/effect/proc_holder/spider/wrap/update_icon() - action.button_icon_state = "wrap_[active]" - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spider/wrap/activate(mob/living/user) - var/message - if(active) - message = "You no longer prepare to wrap something in a cocoon." - remove_ranged_ability(message) +/datum/action/innate/spider/lay_web/Activate() + var/turf/spider_turf = get_turf(owner) + var/mob/living/simple_animal/hostile/giant_spider/spider = owner + var/obj/structure/spider/stickyweb/web = locate() in spider_turf + if(web) + spider.visible_message( + span_notice("[spider] begins to pack more webbing onto the web."), + span_notice("You begin to seal the web."), + ) else - message = "You prepare to wrap something in a cocoon. Left-click your target to start wrapping!" - add_ranged_ability(user, message, TRUE) - return TRUE + spider.visible_message( + span_notice("[spider] begins to secrete a sticky substance."), + span_notice("You begin to lay a web."), + ) -/obj/effect/proc_holder/spider/wrap/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - if(ranged_ability_user.incapacitated() || !istype(ranged_ability_user, /mob/living/simple_animal/hostile/poison/giant_spider)) - remove_ranged_ability() - return + spider.stop_automated_movement = TRUE - var/mob/living/simple_animal/hostile/poison/giant_spider/user = ranged_ability_user + if(do_after(spider, 4 SECONDS * spider.web_speed, target = spider_turf)) + if(spider.loc == spider_turf) + if(web) + qdel(web) + new /obj/structure/spider/stickyweb/sealed(spider_turf) + new /obj/structure/spider/stickyweb(spider_turf) - if(user.Adjacent(target) && (ismob(target) || isobj(target))) - var/atom/movable/target_atom = target - if(target_atom.anchored) - return - user.cocoon_target = target_atom - INVOKE_ASYNC(user, TYPE_PROC_REF(/mob/living/simple_animal/hostile/poison/giant_spider, cocoon)) - remove_ranged_ability() - return TRUE - -/obj/effect/proc_holder/spider/throw_web - name = "Throw web" - panel = "Spider" - desc = "Throw a sticky web at potential prey to immobilize them temporarily" - ranged_mousepointer = 'icons/effects/throwweb_target.dmi' - action_icon = 'icons/mob/actions/actions_animal.dmi' - action_icon_state = "throw_web_0" - action_background_icon_state = "bg_alien" - -/obj/effect/proc_holder/spider/throw_web/activate(mob/living/user) - var/message - if(active) - message = "You discard the webbing." - remove_ranged_ability(message) - return - if(!istype(user, /mob/living/simple_animal/hostile/poison/giant_spider)) + spider.stop_automated_movement = FALSE + +/datum/action/cooldown/wrap + name = "Wrap" + desc = "Wrap something or someone in a cocoon. If it's a human or similar species, \ + you'll also consume them, allowing you to lay enriched eggs." + background_icon_state = "bg_alien" + icon_icon = 'icons/mob/actions/actions_animal.dmi' + button_icon_state = "wrap_0" + check_flags = AB_CHECK_CONSCIOUS + click_to_activate = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/wrap_target.dmi' + /// The time it takes to wrap something. + var/wrap_time = 5 SECONDS + +/datum/action/cooldown/wrap/IsAvailable() + . = ..() + if(!.) + return FALSE + if(owner.incapacitated()) + return FALSE + if(DOING_INTERACTION(owner, INTERACTION_SPIDER_KEY)) + return FALSE + return TRUE + +/datum/action/cooldown/wrap/set_click_ability(mob/on_who) + . = ..() + if(!.) return - var/mob/living/simple_animal/hostile/poison/giant_spider/spider = user - if(spider.busy != SPINNING_WEB) - spider.busy = SPINNING_WEB - spider.visible_message("[spider] begins to secrete a sticky substance.","You begin to prepare a net from webbing.") - spider.stop_automated_movement = TRUE - if(do_after(spider, 40 * spider.web_speed, spider)) - message = "You ready the completed net with your forelimbs. Left-click to throw it at a target!" - add_ranged_ability(user, message, TRUE) - spider.busy = SPIDER_IDLE - spider.stop_automated_movement = FALSE - else - to_chat(spider, "You're already spinning a web!") -/obj/effect/proc_holder/spider/throw_web/update_icon() - action.button_icon_state = "throw_web_[active]" - action.UpdateButtonIcon() + to_chat(on_who, span_notice("You prepare to wrap something in a cocoon. Left-click your target to start wrapping!")) + button_icon_state = "wrap_0" + UpdateButtons() -/obj/effect/proc_holder/spider/throw_web/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) +/datum/action/cooldown/wrap/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) return - var/turf/T = ranged_ability_user.loc - var/turf/U = get_step(ranged_ability_user, ranged_ability_user.dir) - if(!isturf(U) || !isturf(T)) + if(refund_cooldown) + to_chat(on_who, span_notice("You no longer prepare to wrap something in a cocoon.")) + button_icon_state = "wrap_1" + UpdateButtons() + +/datum/action/cooldown/wrap/Activate(atom/to_wrap) + if(!owner.Adjacent(to_wrap)) + owner.balloon_alert(owner, "must be closer!") + return FALSE + + if(!ismob(to_wrap) && !isobj(to_wrap)) + return FALSE + + if(to_wrap == owner) return FALSE - ranged_ability_user.visible_message("[ranged_ability_user] throws a web!", "You throw the web!") - var/obj/projectile/bullet/spidernet/A = new /obj/projectile/bullet/spidernet(ranged_ability_user.loc) - A.preparePixelProjectile(target, ranged_ability_user, params) - A.firer = ranged_ability_user - A.fire() - ranged_ability_user.newtonian_move(get_dir(U, T)) - remove_ranged_ability() //have to spin another net before you can use it again + if(isspider(to_wrap)) + owner.balloon_alert(owner, "can't wrap spiders!") + return FALSE + var/atom/movable/target_movable = to_wrap + if(target_movable.anchored) + return FALSE + + StartCooldown(wrap_time) + INVOKE_ASYNC(src, .proc/cocoon, to_wrap) return TRUE +/datum/action/cooldown/wrap/proc/cocoon(atom/movable/to_wrap) + owner.visible_message( + span_notice("[owner] begins to secrete a sticky substance around [to_wrap]."), + span_notice("You begin wrapping [to_wrap] into a cocoon."), + ) + + var/mob/living/simple_animal/animal_owner = owner + if(istype(animal_owner)) + animal_owner.stop_automated_movement = TRUE + + if(do_after(owner, wrap_time, target = to_wrap, interaction_key = INTERACTION_SPIDER_KEY)) + var/obj/structure/spider/cocoon/casing = new(to_wrap.loc) + if(isliving(to_wrap)) + var/mob/living/living_wrapped = to_wrap + // if they're not dead, you can consume them anyway + if(ishuman(living_wrapped) && (living_wrapped.stat != DEAD || !HAS_TRAIT(living_wrapped, TRAIT_SPIDER_CONSUMED))) + var/datum/action/innate/spider/lay_eggs/enriched/egg_power = locate() in owner.actions + if(egg_power) + egg_power.charges++ + egg_power.UpdateButtons() + owner.visible_message( + span_danger("[owner] sticks a proboscis into [living_wrapped] and sucks a viscous substance out."), + span_notice("You suck the nutriment out of [living_wrapped], feeding you enough to lay a cluster of enriched eggs."), + ) + + living_wrapped.death() //you just ate them, they're dead. + else + to_chat(owner, span_warning("[living_wrapped] cannot sate your hunger!")) + + to_wrap.forceMove(casing) + if(to_wrap.density || ismob(to_wrap)) + casing.icon_state = pick("cocoon_large1", "cocoon_large2", "cocoon_large3") + + if(istype(animal_owner)) + animal_owner.stop_automated_movement = TRUE + // Laying eggs // If a spider eats a human, they can lay eggs that can hatch into special variants of the base spiders // Otherwise, it's just basic spiders. @@ -633,58 +674,52 @@ name = "Lay Eggs" desc = "Lay a cluster of eggs, which will soon grow into more spiders. You must have a directive set and wrap a living being to do this." button_icon_state = "lay_eggs" + ///How long it takes for a broodmother to lay eggs. + var/egg_lay_time = 15 SECONDS + ///The type of egg we create + var/egg_type = /obj/effect/mob_spawn/ghost_role/spider /datum/action/innate/spider/lay_eggs/IsAvailable() - if(..()) - if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/broodmother)) - return FALSE - var/mob/living/simple_animal/hostile/poison/giant_spider/broodmother/S = owner - var/datum/antagonist/spider/spider_antag = S.mind?.has_antag_datum(/datum/antagonist/spider) - if((S.fed || S.enriched_fed) && (spider_antag?.spider_team.directive || !S.ckey)) - return TRUE + . = ..() + if(!.) + return FALSE + if(!isspider(owner)) + return FALSE + var/obj/structure/spider/eggcluster/eggs = locate() in get_turf(owner) + if(eggs) + to_chat(owner, span_warning("There is already a cluster of eggs here!")) + return FALSE + if(DOING_INTERACTION(owner, INTERACTION_SPIDER_KEY)) return FALSE + return TRUE /datum/action/innate/spider/lay_eggs/Activate() - if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/broodmother)) - return - var/mob/living/simple_animal/hostile/poison/giant_spider/broodmother/spider = owner - var/datum/antagonist/spider/spider_antag = spider.mind?.has_antag_datum(/datum/antagonist/spider) - - var/obj/structure/spider/eggcluster/cluster = locate() in get_turf(spider) - if(cluster) - to_chat(spider, "There is already a cluster of eggs here!") - else if(!(spider.fed || spider.enriched_fed)) - to_chat(spider, "You are too hungry to do this!") - else if(!spider_antag?.spider_team.directive && spider.ckey) - to_chat(spider, "You need to set a directive to do this!") - else if(spider.busy != LAYING_EGGS) - spider.busy = LAYING_EGGS - spider.visible_message("[spider] begins to lay a cluster of eggs.","You begin to lay a cluster of eggs.") - spider.stop_automated_movement = TRUE - if(do_after(spider, 50, target = get_turf(spider))) - if(spider.busy == LAYING_EGGS) - cluster = locate() in get_turf(spider) - if(!cluster || !isturf(spider.loc)) - var/obj/structure/spider/eggcluster/new_cluster = new /obj/structure/spider/eggcluster(get_turf(spider)) - if(spider.enriched_fed) // Adds an extra spawn and the potential for an enriched spawn if feeding on high quality food - new_cluster.enriched_spawns++ - new_cluster.spawns_remaining++ - spider.enriched_fed-- - else - spider.fed-- - new_cluster.grow_time *= 2 - if(spider_antag?.spider_team) //Is or was this broodmother sentient? - new_cluster.spider_team = spider_antag?.spider_team //pass that team she has along to the children - else if(spider.spider_team) //No? then it is probably a second generation broodmother that spawned for a lack of ghosts - new_cluster.spider_team = spider.spider_team //so we pass the team inherited directly via the previous broodmother - else //This is a first generation, non-sentient broodmother likely spawned by admins and laying eggs for the first time. - var/datum/team/spiders/spiders = new() - spider.spider_team = spiders //lets make sure her potentially sentient children are all on the same team - new_cluster.spider_team = spider.spider_team - new_cluster.faction = spider.faction.Copy() - UpdateButtonIcon(TRUE) - spider.busy = SPIDER_IDLE - spider.stop_automated_movement = FALSE + owner.visible_message( + span_notice("[owner] begins to lay a cluster of eggs."), + span_notice("You begin to lay a cluster of eggs."), + ) + + var/mob/living/simple_animal/hostile/giant_spider/spider = owner + spider.stop_automated_movement = TRUE + if(do_after(owner, egg_lay_time, target = get_turf(owner), interaction_key = INTERACTION_SPIDER_KEY)) + var/obj/structure/spider/eggcluster/eggs = locate() in get_turf(owner) + if(!eggs || !isturf(spider.loc)) + var/obj/effect/mob_spawn/ghost_role/spider/new_eggs = new egg_type(get_turf(spider)) + new_eggs.directive = spider.directive + new_eggs.faction = spider.faction + UpdateButtons(TRUE) + spider.stop_automated_movement = FALSE + +/datum/action/innate/spider/lay_eggs/enriched + name = "Lay Enriched Eggs" + desc = "Lay a cluster of eggs, which will soon grow into a greater spider. Requires you drain a human per cluster of these eggs." + button_icon_state = "lay_enriched_eggs" + egg_type = /obj/effect/mob_spawn/ghost_role/spider/enriched + /// How many charges we have to make eggs + var/charges = 0 + +/datum/action/innate/spider/lay_eggs/enriched/IsAvailable() + return ..() && (charges > 0) // Directive command, for giving children orders // The set directive is placed in the notes of every child spider, and said child gets the objective when they log into the mob @@ -694,25 +729,16 @@ button_icon_state = "directive" /datum/action/innate/spider/set_directive/IsAvailable() - if(..()) - if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/broodmother)) - return FALSE - return TRUE + return ..() && istype(owner, /mob/living/simple_animal/hostile/giant_spider) /datum/action/innate/spider/set_directive/Activate() - if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/broodmother)) - return - if(!owner.mind) - return - var/mob/living/simple_animal/hostile/poison/giant_spider/broodmother/S = owner - var/datum/antagonist/spider/spider_antag = S.mind.has_antag_datum(/datum/antagonist/spider) - if(!spider_antag) - spider_antag = S.mind.add_antag_datum(/datum/antagonist/spider) - var/new_directive = stripped_input(S, "Enter the new directive", "Create directive") - if(new_directive) - spider_antag.spider_team.update_directives(new_directive) - log_game("[key_name(owner)][spider_antag.spider_team.master ? " (master: [spider_antag.spider_team.master]" : ""] set its directive to: '[new_directive]'.") - S.lay_eggs.UpdateButtonIcon(TRUE) + var/mob/living/simple_animal/hostile/giant_spider/midwife/spider = owner + spider.directive = tgui_input_text(spider, "Enter the new directive", "Create directive", "[spider.directive]") + if(isnull(spider.directive) || QDELETED(src) || QDELETED(owner) || !IsAvailable()) + return FALSE + message_admins("[ADMIN_LOOKUPFLW(owner)] set its directive to: '[spider.directive]'.") + log_game("[key_name(owner)] set its directive to: '[spider.directive]'.") + return TRUE // Spider command ability for broodmothers /datum/action/innate/spider/comm @@ -741,14 +767,14 @@ var/datum/antagonist/spider/spider_antag = user.mind?.has_antag_datum(/datum/antagonist/spider) if(!spider_antag) return - for(var/mob/living/simple_animal/hostile/poison/giant_spider/M in GLOB.spidermobs) + for(var/mob/living/simple_animal/hostile/poison/giant_spider/spider as anything in GLOB.spidermobs) var/datum/antagonist/spider/target_spider_antag = M.mind?.has_antag_datum(/datum/antagonist/spider) if(spider_antag?.spider_team == target_spider_antag?.spider_team) to_chat(M, my_message) for(var/M in GLOB.dead_mob_list) var/link = FOLLOW_LINK(M, user) to_chat(M, "[link] [my_message]") - usr.log_talk(message, LOG_SAY, tag="spider command") + user.log_talk(message, LOG_SAY, tag = "spider command") // Temperature damage // Flat 10 brute if they're out of safe temperature, making them vulnerable to fire or spacing @@ -767,3 +793,4 @@ #undef LAYING_EGGS #undef MOVING_TO_TARGET #undef SPINNING_COCOON +#undef INTERACTION_SPIDER_KEY diff --git a/code/modules/mob/living/simple_animal/hostile/statue.dm b/code/modules/mob/living/simple_animal/hostile/statue.dm index 4e14b6fcf58ca..9b1036a7054b1 100644 --- a/code/modules/mob/living/simple_animal/hostile/statue.dm +++ b/code/modules/mob/living/simple_animal/hostile/statue.dm @@ -60,9 +60,12 @@ /mob/living/simple_animal/hostile/statue/Initialize(mapload, var/mob/living/creator) . = ..() // Give spells - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/flicker_lights) - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/blindness) - AddSpell(new /obj/effect/proc_holder/spell/targeted/night_vision) + var/datum/action/cooldown/spell/aoe/flicker_lights/flicker = new(src) + flicker.Grant(src) + var/datum/action/cooldown/spell/aoe/blindness/blind = new(src) + blind.Grant(src) + var/datum/action/cooldown/spell/night_vision/night_vision = new(src) + night_vision.Grant(src) // Set creator if(creator) @@ -162,68 +165,55 @@ . = ..() return . - creator +/mob/living/simple_animal/hostile/netherworld/statue/sentience_act() + faction -= "neutral" + // Statue powers // Flicker lights -/obj/effect/proc_holder/spell/aoe_turf/flicker_lights +/datum/action/cooldown/spell/aoe/flicker_lights name = "Flicker Lights" desc = "You will trigger a large amount of lights around you to flicker." - charge_max = 300 - clothes_req = 0 - range = 14 + cooldown_time = 30 SECONDS + spell_requirements = NONE + aoe_radius = 14 + +/datum/action/cooldown/spell/aoe/flicker_lights/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/obj/machinery/light/nearby_light in range(aoe_radius, center)) + if(!nearby_light.on) + continue -/obj/effect/proc_holder/spell/aoe_turf/flicker_lights/cast(list/targets,mob/user = usr) - for(var/turf/T in targets) - for(var/obj/machinery/light/L in T) - L.flicker() - return + things += nearby_light + + return things + +/datum/action/cooldown/spell/aoe/flicker_lights/cast_on_thing_in_aoe(obj/machinery/light/victim, atom/caster) + victim.flicker() //Blind AOE -/obj/effect/proc_holder/spell/aoe_turf/blindness +/datum/action/cooldown/spell/aoe/blindness name = "Blindness" desc = "Your prey will be momentarily blind for you to advance on them." - message = "You glare your eyes." - charge_max = 600 - clothes_req = 0 - range = 10 - -/obj/effect/proc_holder/spell/aoe_turf/blindness/cast(list/targets,mob/user = usr) - for(var/mob/living/L in GLOB.alive_mob_list) - var/turf/T = get_turf(L.loc) - if(T && (T in targets)) - L.adjust_blindness(4) - return - -//Toggle Night Vision -/obj/effect/proc_holder/spell/targeted/night_vision - name = "Toggle Nightvision \[ON\]" - desc = "Toggle your nightvision mode." - - charge_max = 10 - clothes_req = 0 - - message = "You toggle your night vision!" - range = -1 - include_user = 1 - -/obj/effect/proc_holder/spell/targeted/night_vision/cast(list/targets, mob/user = usr) - for(var/mob/living/target in targets) - switch(target.lighting_alpha) - if (LIGHTING_PLANE_ALPHA_VISIBLE) - target.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE - name = "Toggle Nightvision \[More]" - if (LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE) - target.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE - name = "Toggle Nightvision \[Full]" - if (LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE) - target.lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE - name = "Toggle Nightvision \[OFF]" - else - target.lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE - name = "Toggle Nightvision \[ON]" - target.update_sight() - -/mob/living/simple_animal/hostile/statue/sentience_act() - faction -= "neutral" + cooldown_time = 1 MINUTES + spell_requirements = NONE + aoe_radius = 14 + +/datum/action/cooldown/spell/aoe/blindness/cast(atom/cast_on) + cast_on.visible_message(span_danger("[cast_on] glares their eyes.")) + return ..() + +/datum/action/cooldown/spell/aoe/blindness/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/mob/living/nearby_mob in range(aoe_radius, center)) + if(nearby_mob == owner || nearby_mob == center) + continue + + things += nearby_mob + + return things + +/datum/action/cooldown/spell/aoe/blindness/cast_on_thing_in_aoe(mob/living/victim, atom/caster) + victim.blind_eyes(4) diff --git a/code/modules/mob/living/simple_animal/hostile/wizard.dm b/code/modules/mob/living/simple_animal/hostile/wizard.dm index b8699820ef2c4..009a6f1231b09 100644 --- a/code/modules/mob/living/simple_animal/hostile/wizard.dm +++ b/code/modules/mob/living/simple_animal/hostile/wizard.dm @@ -20,57 +20,60 @@ unsuitable_atmos_damage = 15 faction = list(FACTION_WIZARD) status_flags = CANPUSH - + footstep_type = FOOTSTEP_MOB_SHOE retreat_distance = 3 //out of fireball range minimum_distance = 3 del_on_death = TRUE loot = list(/obj/effect/mob_spawn/human/corpse/wizard, /obj/item/staff) - var/obj/effect/proc_holder/spell/aimed/fireball/fireball = null - var/obj/effect/proc_holder/spell/targeted/turf_teleport/blink/blink = null - var/obj/effect/proc_holder/spell/targeted/projectile/magic_missile/mm = null + var/datum/action/cooldown/spell/pointed/projectile/fireball/fireball + var/datum/action/cooldown/spell/teleport/radius_turf/blink/blink + var/datum/action/cooldown/spell/aoe/magic_missile/magic_missile var/next_cast = 0 - footstep_type = FOOTSTEP_MOB_SHOE discovery_points = 3000 /mob/living/simple_animal/hostile/wizard/Initialize(mapload) . = ..() - fireball = new /obj/effect/proc_holder/spell/aimed/fireball - fireball.clothes_req = 0 - fireball.human_req = 0 - fireball.player_lock = 0 - AddSpell(fireball) - implants += new /obj/item/implant/exile(src) + var/obj/item/implant/exile/exiled = new /obj/item/implant/exile(src) + exiled.implant(src) + + fireball = new(src) + fireball.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND) + fireball.Grant(src) - mm = new /obj/effect/proc_holder/spell/targeted/projectile/magic_missile - mm.clothes_req = 0 - mm.human_req = 0 - mm.player_lock = 0 - AddSpell(mm) + magic_missile = new(src) + magic_missile.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND) + magic_missile.Grant(src) - blink = new /obj/effect/proc_holder/spell/targeted/turf_teleport/blink - blink.clothes_req = 0 - blink.human_req = 0 - blink.player_lock = 0 + blink = new(src) + blink.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND) blink.outer_tele_radius = 3 - AddSpell(blink) + blink.Grant(src) + +/mob/living/simple_animal/hostile/wizard/Destroy() + QDEL_NULL(fireball) + QDEL_NULL(magic_missile) + QDEL_NULL(blink) + return ..() /mob/living/simple_animal/hostile/wizard/handle_automated_action() . = ..() if(target && next_cast < world.time) - if((get_dir(src,target) in list(SOUTH,EAST,WEST,NORTH)) && fireball.cast_check(0,src)) //Lined up for fireball - src.setDir(get_dir(src,target)) - fireball.perform(list(target), user = src) - next_cast = world.time + 10 //One spell per second - return . - if(mm.cast_check(0,src)) - mm.choose_targets(src) - next_cast = world.time + 10 - return . - if(blink.cast_check(0,src)) //Spam Blink when you can - blink.choose_targets(src) - next_cast = world.time + 10 - return . + if((get_dir(src, target) in list(SOUTH, EAST, WEST, NORTH)) && fireball.can_cast_spell(feedback = FALSE)) + setDir(get_dir(src, target)) + fireball.Trigger(null, target) + next_cast = world.time + 1 SECONDS + return + + if(magic_missile.IsAvailable()) + magic_missile.Trigger(null, target) + next_cast = world.time + 1 SECONDS + return + + if(blink.IsAvailable()) // Spam Blink when you can + blink.Trigger(null, src) + next_cast = world.time + 1 SECONDS + return diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 09ced6b2d584a..f2fe502601dda 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -46,12 +46,6 @@ ghostize() if(mind?.current == src) //Let's just be safe yeah? This will occasionally be cleared, but not always. Can't do it with ghostize without changing behavior mind.set_current(null) - QDEL_LIST(mob_spell_list) - for(var/datum/action/A as() in actions) - if(istype(A.target, /obj/effect/proc_holder)) - A.Remove(src) // Mind's spells' actions should only be removed - else - qdel(A) // Other actions can be safely deleted actions.Cut() return ..() @@ -305,6 +299,14 @@ /mob/proc/get_item_by_slot(slot_id) return null +/// Gets what slot the item on the mob is held in. +/// Returns null if the item isn't in any slots on our mob. +/// Does not check if the passed item is null, which may result in unexpected outcoms. +/mob/proc/get_slot_by_item(obj/item/looking_for) + if(looking_for in held_items) + return ITEM_SLOT_HANDS + return null + ///Is the mob incapacitated /mob/proc/incapacitated(ignore_restraints = FALSE, ignore_grab = FALSE, check_immobilized = FALSE) return @@ -780,19 +782,29 @@ * * Shows charge and other important info */ -/mob/proc/get_spell_stat_data(list/spells, current_tab) - var/list/stat_data = list() - for(var/obj/effect/proc_holder/spell/S in spells) - if(S.can_be_cast_by(src) && current_tab == S.panel) - client.stat_update_mode = STAT_MEDIUM_UPDATE - switch(S.charge_type) - if("recharge") - stat_data["[S.name]"] = GENERATE_STAT_TEXT("[S.charge_counter/10.0]/[S.charge_max/10]") - if("charges") - stat_data["[S.name]"] = GENERATE_STAT_TEXT("[S.charge_counter]/[S.charge_max]") - if("holdervar") - stat_data["[S.name]"] = GENERATE_STAT_TEXT("[S.holder_var_type] [S.holder_var_amount]") - return stat_data +// IDK how statpanel from tg works but it probably should be ported alongside proc_holder removals +/mob/proc/get_actions_for_statpanel() + var/list/data = list() + for(var/datum/action/cooldown/action in actions) + var/list/action_data = action.set_statpanel_format() + if(!length(action_data)) + return + + data += list(list( + // the panel the action gets displayed to + // in the future, this could probably be replaced with subtabs (a la admin tabs) + action_data[PANEL_DISPLAY_PANEL], + // the status of the action, - cooldown, charges, whatever + action_data[PANEL_DISPLAY_STATUS], + // the name of the action + action_data[PANEL_DISPLAY_NAME], + // a ref to the action button of this action for this mob + // it's a ref to the button specifically, instead of the action itself, + // because statpanel href calls click(), which the action button (not the action itself) handles + REF(action.viewers[hud_used]), + )) + + return data #define MOB_FACE_DIRECTION_DELAY 1 @@ -870,29 +882,6 @@ ghost.notify_cloning(message, sound, source, flashwindow) return ghost -///Add a spell to the mobs spell list -/mob/proc/AddSpell(obj/effect/proc_holder/spell/S) - // HACK: Preferences menu creates one of every selectable species. - // Some species, like vampires, create spells when they're made. - // The "action" is created when those spells Initialize. - // Preferences menu can create these assets at *any* time, primarily before - // the atoms SS initializes. - // That means "action" won't exist. - if (isnull(S.action)) - return - mob_spell_list += S - S.action.Grant(src) - -///Remove a spell from the mobs spell list -/mob/proc/RemoveSpell(obj/effect/proc_holder/spell/spell) - if(!spell) - return - for(var/X in mob_spell_list) - var/obj/effect/proc_holder/spell/S = X - if(istype(S, spell)) - mob_spell_list -= S - qdel(S) - ///Return any anti magic atom on this mob that matches the magic type /mob/proc/anti_magic_check(magic = TRUE, holy = FALSE, major = TRUE, self = FALSE) if(!magic && !holy) diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 97f9b47330cce..9b4cd3eb2bfd6 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -36,8 +36,9 @@ var/cached_multiplicative_actions_slowdown /// List of action hud items the user has var/list/datum/action/actions = list() - /// A special action? No idea why this lives here - var/list/datum/action/chameleon_item_actions + /// A list of chameleon actions we have specifically + /// This can be unified with the actions list + var/list/datum/action/item_action/chameleon/chameleon_item_actions /// Whether a mob is alive or dead. TODO: Move this to living - Nodrak (2019, still here) var/stat = CONSCIOUS @@ -163,15 +164,6 @@ ///A weakref to the last mob/living/carbon to push/drag/grab this mob (exclusively used by slimes friend recognition) var/datum/weakref/LAssailant = null - /** - * construct spells and mime spells. - * - * Spells that do not transfer from one mob to another and can not be lost in mindswap. - * obviously do not live in the mind - */ - var/list/mob_spell_list = list() - - /// bitflags defining which status effects can be inflicted (replaces canknockdown, canstun, etc) var/status_flags = CANSTUN|CANKNOCKDOWN|CANUNCONSCIOUS|CANPUSH diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm index 2a2dd481789ce..673f32adb59c1 100644 --- a/code/modules/mob/say.dm +++ b/code/modules/mob/say.dm @@ -20,8 +20,14 @@ return whisper(message) -///whisper a message -/mob/proc/whisper(message, datum/language/language=null) +/** + * Whisper a message. + * + * Basic level implementation just speaks the message, nothing else. + */ +/mob/proc/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof) + if(!message) + return say(message, language) //only living mobs actually whisper, everything else just talks ///The me emote verb diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index a4ef011cd21ff..6708d577e1714 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -93,8 +93,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar var/init_ringtone = "beep" /// If the device starts with its ringer on var/init_ringer_on = TRUE - /// The action for enabling/disabling the flashlight - var/datum/action/item_action/toggle_computer_light/light_action /// Stored pAI card var/obj/item/paicard/stored_pai_card /// If the device is capable of storing a pAI @@ -111,7 +109,7 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar idle_threads = list() update_id_display() if(has_light) - light_action = new(src) + add_item_action(/datum/action/item_action/toggle_computer_light) update_icon() add_messenger() @@ -153,10 +151,11 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar return ..() /obj/item/modular_computer/ui_action_click(mob/user, actiontype) - if(istype(actiontype, light_action)) + if(istype(actiontype, /datum/action/item_action/toggle_computer_light)) toggle_flashlight() - else - ..() + return + + return ..() /// From [/datum/newscaster/feed_network/proc/save_photo] /obj/item/modular_computer/proc/save_photo(icon/photo) diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/power/singularity/emitter.dm index 95c50ffb478cc..84da0bdd98fd5 100644 --- a/code/modules/power/singularity/emitter.dm +++ b/code/modules/power/singularity/emitter.dm @@ -450,7 +450,7 @@ for(var/obj/item/item in buckled_mob.held_items) if(istype(item, /obj/item/turret_control)) qdel(item) - UpdateButtonIcon() + UpdateButtons() return playsound(proto_emitter,'sound/mecha/mechmove01.ogg', 50, TRUE) name = "Switch to Automatic Firing" @@ -467,7 +467,7 @@ else //Entries in the list should only ever be items or null, so if it's not an item, we can assume it's an empty hand var/obj/item/turret_control/turret_control = new /obj/item/turret_control() buckled_mob.put_in_hands(turret_control) - UpdateButtonIcon() + UpdateButtons() /obj/item/turret_control diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index 1556116350ace..95db330e8083d 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -536,7 +536,7 @@ if(M.anti_magic_check()) M.visible_message("[src] vanishes on contact with [target]!") return BULLET_ACT_BLOCK - SEND_SIGNAL(M, COMSIG_ADD_MOOD_EVENT, src, /datum/mood_event/sapped) + SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, REF(src), /datum/mood_event/sapped) /obj/projectile/magic/necropotence name = "bolt of necropotence" @@ -545,21 +545,16 @@ /obj/projectile/magic/necropotence/on_hit(target) . = ..() - if(isliving(target)) - var/mob/living/L = target - if(L.anti_magic_check() || !L.mind || !L.mind.hasSoul) - L.visible_message("[src] vanishes on contact with [target]!") - return BULLET_ACT_BLOCK - to_chat(L, "Your body feels drained and there is a burning pain in your chest.") - L.maxHealth -= 20 - L.health = min(L.health, L.maxHealth) - if(L.maxHealth <= 0) - to_chat(L, "Your weakened soul is completely consumed by the [src]!") - L.mind.hasSoul = FALSE - for(var/obj/effect/proc_holder/spell/spell in L.mind.spell_list) - spell.charge_counter = spell.charge_max - spell.recharging = FALSE - spell.update_icon() + if(!isliving(target)) + return + + // Performs a soul tap on living targets hit. + // Takes away max health, but refreshes their spell cooldowns (if any) + var/datum/action/cooldown/spell/tap/tap = new(src) + if(tap.is_valid_target(target)) + tap.cast(target) + + qdel(tap) /obj/projectile/magic/wipe name = "bolt of possession" @@ -611,59 +606,96 @@ to_chat(M, "Your mind has managed to go unnoticed in the spirit world.") qdel(trauma) +/// Gives magic projectiles an area of effect radius that will bump into any nearby mobs /obj/projectile/magic/aoe - name = "Area Bolt" - desc = "What the fuck does this do?!" damage = 0 - var/proxdet = TRUE - martial_arts_no_deflect = FALSE + + /// The AOE radius that the projectile will trigger on people. + var/trigger_range = 1 + /// Whether our projectile will only be able to hit the original target / clicked on atom + var/can_only_hit_target = FALSE + + /// Whether our projectile leaves a trail behind it as it moves. + var/trail = FALSE + /// The duration of the trail before deleting. + var/trail_lifespan = 0 SECONDS + /// The icon the trail uses. + var/trail_icon = 'icons/obj/wizard.dmi' + /// The icon state the trail uses. + var/trail_icon_state = "trail" /obj/projectile/magic/aoe/Range() - if(proxdet) - for(var/mob/living/L in range(1, get_turf(src))) - if(L.stat != DEAD && L != firer && !L.anti_magic_check()) - return Bump(L) - ..() + if(trigger_range >= 1) + for(var/mob/living/nearby_guy in range(trigger_range, get_turf(src))) + if(nearby_guy.stat == DEAD) + continue + if(nearby_guy == firer) + continue + // Bump handles anti-magic checks for us, conveniently. + return Bump(nearby_guy) + return ..() + +/obj/projectile/magic/aoe/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE) + if(can_only_hit_target && target != original) + return FALSE + return ..() + +/obj/projectile/magic/aoe/Moved(atom/OldLoc, Dir) + . = ..() + if(trail) + create_trail() + +/// Creates and handles the trail that follows the projectile. +/obj/projectile/magic/aoe/proc/create_trail() + if(!trajectory) + return + + var/datum/point/vector/previous = trajectory.return_vector_after_increments(1, -1) + var/obj/effect/overlay/trail = new /obj/effect/overlay(previous.return_turf()) + trail.pixel_x = previous.return_px() + trail.pixel_y = previous.return_py() + trail.icon = trail_icon + trail.icon_state = trail_icon_state + //might be changed to temp overlay + trail.set_density(FALSE) + trail.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + QDEL_IN(trail, trail_lifespan) /obj/projectile/magic/aoe/lightning name = "lightning bolt" - icon_state = "tesla_projectile" //Better sprites are REALLY needed and appreciated!~ + icon_state = "tesla_projectile" //Better sprites are REALLY needed and appreciated!~ damage = 15 damage_type = BURN nodamage = FALSE speed = 0.3 - var/tesla_power = 20000 - var/tesla_range = 15 - var/tesla_flags = TESLA_MOB_DAMAGE | TESLA_MOB_STUN | TESLA_OBJ_DAMAGE - var/chain - var/mob/living/caster - -/obj/projectile/magic/aoe/lightning/New(loc, spell_level) - . = ..() - tesla_power += 5000 * spell_level - tesla_range += 2 * spell_level + /// The power of the zap itself when it electrocutes someone + var/zap_power = 20000 + /// The range of the zap itself when it electrocutes someone + var/zap_range = 15 + /// The flags of the zap itself when it electrocutes someone + var/zap_flags = ZAP_MOB_DAMAGE | ZAP_MOB_STUN | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN + /// A reference to the chain beam between the caster and the projectile + var/datum/beam/chain /obj/projectile/magic/aoe/lightning/fire(setAngle) - if(caster) - chain = caster.Beam(src, icon_state = "lightning[rand(1, 12)]") - ..() + if(firer) + chain = firer.Beam(src, icon_state = "lightning[rand(1, 12)]") + return ..() /obj/projectile/magic/aoe/lightning/on_hit(target) . = ..() - if(ismob(target)) - var/mob/M = target - if(M.anti_magic_check()) - visible_message("[src] fizzles on contact with [target]!") - qdel(src) - return BULLET_ACT_BLOCK - tesla_zap(src, tesla_range, tesla_power, tesla_flags) - qdel(src) + tesla_zap(src, zap_range, zap_power, zap_flags) /obj/projectile/magic/aoe/lightning/Destroy() - qdel(chain) - . = ..() + QDEL_NULL(chain) + return ..() + +/obj/projectile/magic/aoe/lightning/no_zap + zap_power = 10000 + zap_range = 4 + zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN /obj/projectile/magic/fireball name = "bolt of fireball" @@ -672,50 +704,82 @@ damage_type = BRUTE nodamage = FALSE - //explosion values + /// Heavy explosion range of the fireball var/exp_heavy = 0 + /// Light explosion range of the fireball var/exp_light = 2 - var/exp_flash = 3 + /// Fire radius of the fireball var/exp_fire = 2 - var/magic = TRUE - var/holy = FALSE - -/obj/projectile/magic/fireball/New(loc, spell_level) - . = ..() - exp_fire += spell_level - exp_flash += spell_level - exp_light += spell_level - exp_heavy = max(spell_level - 2, 0) + /// Flash radius of the fireball + var/exp_flash = 3 -/obj/projectile/magic/fireball/on_hit(target) +/obj/projectile/magic/fireball/on_hit(atom/target, blocked = FALSE, pierce_hit) . = ..() - if(ismob(target)) - var/mob/living/M = target - if(M.anti_magic_check()) - visible_message("[src] vanishes into smoke on contact with [target]!") - return BULLET_ACT_BLOCK - M.take_overall_damage(0,10) //between this 10 burn, the 10 brute, the explosion brute, and the onfire burn, your at about 65 damage if you stop drop and roll immediately - var/turf/T = get_turf(target) - explosion(T, -1, exp_heavy, exp_light, exp_flash, 0, flame_range = exp_fire, magic = magic, holy = holy) - -/obj/projectile/magic/fireball/infernal - name = "infernal fireball" - exp_heavy = -1 - exp_light = -1 - exp_flash = 4 - exp_fire= 5 - magic = FALSE - holy = TRUE - -/obj/projectile/magic/fireball/infernal/on_hit(target) + if(isliving(target)) + var/mob/living/mob_target = target + // between this 10 burn, the 10 brute, the explosion brute, and the onfire burn, + // you are at about 65 damage if you stop drop and roll immediately + mob_target.take_overall_damage(burn = 10) + + var/turf/target_turf = get_turf(target) + + explosion( + target_turf, + devastation_range = -1, + heavy_impact_range = exp_heavy, + light_impact_range = exp_light, + flame_range = exp_fire, + flash_range = exp_flash, + adminlog = FALSE, + explosion_cause = src, + ) + +/obj/projectile/magic/aoe/magic_missile + name = "magic missile" + icon_state = "magicm" + range = 20 + speed = 5 + trigger_range = 0 + can_only_hit_target = TRUE + nodamage = FALSE + paralyze = 6 SECONDS + hitsound = 'sound/magic/mm_hit.ogg' + + trail = TRUE + trail_lifespan = 0.5 SECONDS + trail_icon_state = "magicmd" + +/obj/projectile/magic/aoe/magic_missile/lesser + color = "red" //Looks more culty this way + range = 10 + +/obj/projectile/magic/aoe/juggernaut + name = "Gauntlet Echo" + icon_state = "cultfist" + alpha = 180 + damage = 30 + damage_type = BRUTE + knockdown = 50 + hitsound = 'sound/weapons/punch3.ogg' + trigger_range = 0 + antimagic_flags = MAGIC_RESISTANCE_HOLY + ignored_factions = list("cult") + range = 15 + speed = 7 + +/obj/projectile/magic/spell/juggernaut/on_hit(atom/target, blocked) . = ..() - if(ismob(target)) - var/mob/living/M = target - if(M.anti_magic_check()) - return BULLET_ACT_BLOCK - var/turf/T = get_turf(target) - for(var/i=0, i<50, i+=10) - addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(explosion), T, -1, exp_heavy, exp_light, exp_flash, FALSE, FALSE, FALSE, TRUE), i) + var/turf/target_turf = get_turf(src) + playsound(target_turf, 'sound/weapons/resonator_blast.ogg', 100, FALSE) + new /obj/effect/temp_visual/cult/sac(target_turf) + for(var/obj/adjacent_object in range(1, src)) + if(!adjacent_object.density) + continue + if(istype(adjacent_object, /obj/structure/destructible/cult)) + continue + + adjacent_object.take_damage(90, BRUTE, MELEE, 0) + new /obj/effect/temp_visual/cult/turf/floor(get_turf(adjacent_object)) //still magic related, but a different path diff --git a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm index 6b483e41f06d7..9e78bf659cc7e 100644 --- a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm @@ -931,7 +931,34 @@ All effects don't start immediately, but rather get worse over time; the rate is glass_desc = "Just looking at this thing makes the hair at the back of your neck stand up." -/datum/reagent/consumable/ethanol/devilskiss //If eaten by a slaughter demon, the demon will regret it. +/datum/reagent/consumable/ethanol/demonsblood/on_mob_metabolize(mob/living/metabolizer) + . = ..() + RegisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED, .proc/pre_bloodcrawl_consumed) + +/datum/reagent/consumable/ethanol/demonsblood/on_mob_end_metabolize(mob/living/metabolizer) + . = ..() + UnregisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED) + +/// Prevents the imbiber from being dragged into a pool of blood by a slaughter demon. +/datum/reagent/consumable/ethanol/demonsblood/proc/pre_bloodcrawl_consumed( + mob/living/source, + datum/action/cooldown/spell/jaunt/bloodcrawl/crawl, + mob/living/jaunter, + obj/effect/decal/cleanable/blood, +) + + SIGNAL_HANDLER + + var/turf/jaunt_turf = get_turf(jaunter) + jaunt_turf.visible_message( + span_warning("Something prevents [source] from entering [blood]!"), + blind_message = span_notice("You hear a splash and a thud.") + ) + to_chat(jaunter, span_warning("A strange force is blocking [source] from entering!")) + + return COMPONENT_STOP_CONSUMPTION + +/datum/reagent/consumable/ethanol/devilskiss name = "Devil's Kiss" description = "A creepy time!" color = "#A68310" // rgb: 166, 131, 16 @@ -943,6 +970,40 @@ All effects don't start immediately, but rather get worse over time; the rate is glass_name = "Devils Kiss" glass_desc = "A creepy time!" +/datum/reagent/consumable/ethanol/devilskiss/on_mob_metabolize(mob/living/metabolizer) + . = ..() + RegisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_CONSUMED, .proc/on_bloodcrawl_consumed) + +/datum/reagent/consumable/ethanol/devilskiss/on_mob_end_metabolize(mob/living/metabolizer) + . = ..() + UnregisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_CONSUMED) + +/// If eaten by a slaughter demon, the demon will regret it. +/datum/reagent/consumable/ethanol/devilskiss/proc/on_bloodcrawl_consumed( + mob/living/source, + datum/action/cooldown/spell/jaunt/bloodcrawl/crawl, + mob/living/jaunter, +) + + SIGNAL_HANDLER + + . = COMPONENT_STOP_CONSUMPTION + + to_chat(jaunter, span_boldwarning("AAH! THEIR FLESH! IT BURNS!")) + jaunter.apply_damage(25, BRUTE, wound_bonus = CANT_WOUND) + + for(var/obj/effect/decal/cleanable/nearby_blood in range(1, get_turf(source))) + if(!nearby_blood.can_bloodcrawl_in()) + continue + source.forceMove(get_turf(nearby_blood)) + source.visible_message(span_warning("[nearby_blood] violently expels [source]!")) + crawl.exit_blood_effect(source) + return + + // Fuck it, just eject them, thanks to some split second cleaning + source.forceMove(get_turf(source)) + source.visible_message(span_warning("[source] appears from nowhere, covered in blood!")) + crawl.exit_blood_effect(source) /datum/reagent/consumable/ethanol/vodkatonic name = "Vodka and Tonic" diff --git a/code/modules/research/nanites/nanite_programs/utility.dm b/code/modules/research/nanites/nanite_programs/utility.dm index 4a1b1f5cdf5da..9ad4d9d1688ef 100644 --- a/code/modules/research/nanites/nanite_programs/utility.dm +++ b/code/modules/research/nanites/nanite_programs/utility.dm @@ -370,7 +370,7 @@ /datum/action/innate/nanite_button/proc/update_icon(icon, color) button_icon_state = "[icon]_[color]" - UpdateButtonIcon() + UpdateButtons() /datum/nanite_program/dermal_button/toggle name = "Dermal Toggle" diff --git a/code/modules/research/xenobiology/crossbreeding/_misc.dm b/code/modules/research/xenobiology/crossbreeding/_misc.dm index 8fee94597f33f..58616cfb16645 100644 --- a/code/modules/research/xenobiology/crossbreeding/_misc.dm +++ b/code/modules/research/xenobiology/crossbreeding/_misc.dm @@ -131,7 +131,7 @@ Slimecrossing Items icon_state = "slimebarrier_thick" CanAtmosPass = ATMOS_PASS_NO opacity = TRUE - timeleft = 100 + initial_duration = 10 SECONDS //Rainbow barrier - Chilling Rainbow /obj/effect/forcefield/slimewall/rainbow diff --git a/code/modules/research/xenobiology/crossbreeding/_mobs.dm b/code/modules/research/xenobiology/crossbreeding/_mobs.dm index 0fab9be12c3b6..14e4d56fb0bb0 100644 --- a/code/modules/research/xenobiology/crossbreeding/_mobs.dm +++ b/code/modules/research/xenobiology/crossbreeding/_mobs.dm @@ -4,30 +4,36 @@ Slimecrossing Mobs Collected here for clarity. */ -//Slime transformation power - Burning Black -/obj/effect/proc_holder/spell/targeted/shapeshift/slimeform +/// Slime transformation power - from Burning Black +/datum/action/cooldown/spell/shapeshift/slime_form name = "Slime Transformation" desc = "Transform from a human to a slime, or back again!" - action_icon_state = "transformslime" - cooldown_min = 0 - charge_max = 0 + button_icon_state = "transformslime" + cooldown_time = 0 SECONDS + invocation_type = INVOCATION_NONE - shapeshift_type = /mob/living/simple_animal/slime/transformedslime + convert_damage = TRUE convert_damage_type = CLONE + possible_shapes = list(/mob/living/simple_animal/slime/transformed_slime) + + /// If TRUE, we self-delete (remove ourselves) the next time we turn back into a human var/remove_on_restore = FALSE -/obj/effect/proc_holder/spell/targeted/shapeshift/slimeform/Restore(mob/living/M) +/datum/action/cooldown/spell/shapeshift/slime_form/restore_form(mob/living/shape) + . = ..() + if(!.) + return + if(remove_on_restore) - if(M.mind) - M.mind.RemoveSpell(src) - ..() + qdel(src) -//Transformed slime - Burning Black -/mob/living/simple_animal/slime/transformedslime +/// Transformed slime - from Burning Black +/mob/living/simple_animal/slime/transformed_slime -/mob/living/simple_animal/slime/transformedslime/Reproduce() //Just in case. - to_chat(src, "I can't reproduce...") +// Just in case. +/mob/living/simple_animal/slime/transformed_slime/Reproduce() + to_chat(src, span_warning("I can't reproduce...")) // Mood return //Slime corgi - Chilling Pink diff --git a/code/modules/research/xenobiology/crossbreeding/burning.dm b/code/modules/research/xenobiology/crossbreeding/burning.dm index 9edba79689ae9..29ceb82affd7b 100644 --- a/code/modules/research/xenobiology/crossbreeding/burning.dm +++ b/code/modules/research/xenobiology/crossbreeding/burning.dm @@ -272,15 +272,14 @@ Burning extracts: effect_desc = "Transforms the user into a slime. They can transform back at will and do not lose any items." /obj/item/slimecross/burning/black/do_effect(mob/user) - var/mob/living/L = user - if(!istype(L)) + if(!isliving(user)) return - user.visible_message("[src] absorbs [user], transforming [user.p_them()] into a slime!") - var/obj/effect/proc_holder/spell/targeted/shapeshift/slimeform/S = new() - S.remove_on_restore = TRUE - user.mind.AddSpell(S) - S.cast(list(user),user) - ..() + user.visible_message(span_danger("[src] absorbs [user], transforming [user.p_them()] into a slime!")) + var/datum/action/cooldown/spell/shapeshift/slime_form/transform = new(user.mind || user) + transform.remove_on_restore = TRUE + transform.Grant(user) + transform.cast(user) + return ..() /obj/item/slimecross/burning/lightpink colour = "light pink" diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 2fde72224504b..92ab9d166d4c2 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -1,697 +1,424 @@ -#define TARGET_CLOSEST 1 -#define TARGET_RANDOM 2 - - -/obj/effect/proc_holder - var/panel = "Debug"//What panel the proc holder needs to go on. - var/active = FALSE //Used by toggle based abilities. - var/ranged_mousepointer - var/mob/living/ranged_ability_user - var/ranged_clickcd_override = -1 - var/has_action = TRUE - var/datum/action/spell_action/action = null - var/action_icon = 'icons/mob/actions/actions_spells.dmi' - var/action_icon_state = "spell_default" - var/action_background_icon_state = "bg_spell" - var/base_action = /datum/action/spell_action - -/obj/effect/proc_holder/Initialize(mapload) - . = ..() - if(has_action) - action = new base_action(src) - -/obj/effect/proc_holder/Destroy() - if(!QDELETED(action)) - qdel(action) - action = null - return ..() +/** + * # The spell action + * + * This is the base action for how many of the game's + * spells (and spell adjacent) abilities function. + * These spells function off of a cooldown-based system. + * + * ## Pre-spell checks: + * - [can_cast_spell][/datum/action/cooldown/spell/can_cast_spell] checks if the OWNER + * of the spell is able to cast the spell. + * - [is_valid_target][/datum/action/cooldown/spell/is_valid_target] checks if the TARGET + * THE SPELL IS BEING CAST ON is a valid target for the spell. NOTE: The CAST TARGET is often THE SAME as THE OWNER OF THE SPELL, + * but is not always - depending on how [Pre Activate][/datum/action/cooldown/spell/PreActivate] is resolved. + * - [can_invoke][/datum/action/cooldown/spell/can_invoke] is run in can_cast_spell to check if + * the OWNER of the spell is able to say the current invocation. + * + * ## The spell chain: + * - [before_cast][/datum/action/cooldown/spell/before_cast] is the last chance for being able + * to interrupt a spell cast. This returns a bitflag. if SPELL_CANCEL_CAST is set, the spell will not continue. + * - [spell_feedback][/datum/action/cooldown/spell/spell_feedback] is called right before cast, and handles + * invocation and sound effects. Overridable, if you want a special method of invocation or sound effects, + * or you want your spell to handle invocation / sound via special means. + * - [cast][/datum/action/cooldown/spell/cast] is where the brunt of the spell effects should be done + * and implemented. + * - [after_cast][/datum/action/cooldown/spell/after_cast] is the aftermath - final effects that follow + * the main cast of the spell. By now, the spell cooldown has already started + * + * ## Other procs called / may be called within the chain: + * - [invocation][/datum/action/cooldown/spell/invocation] handles saying any vocal (or emotive) invocations the spell + * may have, and can be overriden or extended. Called by spell_feedback. + * - [reset_spell_cooldown][/datum/action/cooldown/spell/reset_spell_cooldown] is a way to handle reverting a spell's + * cooldown and making it ready again if it fails to go off at any point. Not called anywhere by default. If you + * want to cancel a spell in before_cast and would like the cooldown restart, call this. + * + * ## Other procs of note: + * - [level_spell][/datum/action/cooldown/spell/level_spell] is where the process of adding a spell level is handled. + * this can be extended if you wish to add unique effects on level up for wizards. + * - [delevel_spell][/datum/action/cooldown/spell/delevel_spell] is where the process of removing a spell level is handled. + * this can be extended if you wish to undo unique effects on level up for wizards. + * - [update_spell_name][/datum/action/cooldown/spell/update_spell_name] updates the prefix of the spell name based on its level. + */ +/datum/action/cooldown/spell + name = "Spell" + desc = "A wizard spell." + background_icon_state = "bg_spell" + icon_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "spell_default" + check_flags = AB_CHECK_CONSCIOUS + panel = "Spells" -/obj/effect/proc_holder/proc/on_gain(mob/living/user) - return + /// The sound played on cast. + var/sound = null + /// The school of magic the spell belongs to. + /// Checked by some holy sects to punish the + /// caster for casting things that do not align + /// with their sect's alignment - see magic.dm in defines to learn more + var/school = SCHOOL_UNSET + /// If the spell uses the wizard spell rank system, the cooldown reduction per rank of the spell + var/cooldown_reduction_per_rank = 0 SECONDS + /// What is uttered when the user casts the spell + var/invocation + /// What is shown in chat when the user casts the spell, only matters for INVOCATION_EMOTE + var/invocation_self_message + /// What type of invocation the spell is. + /// Can be "none", "whisper", "shout", "emote" + var/invocation_type = INVOCATION_NONE + /// Flag for certain states that the spell requires the user be in to cast. + var/spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC + /// This determines what type of antimagic is needed to block the spell. + /// (MAGIC_RESISTANCE, MAGIC_RESISTANCE_MIND, MAGIC_RESISTANCE_HOLY) + /// If SPELL_REQUIRES_NO_ANTIMAGIC is set in Spell requirements, + /// The spell cannot be cast if the caster has any of the antimagic flags set. + var/antimagic_flags = MAGIC_RESISTANCE + /// The current spell level, if taken multiple times by a wizard + var/spell_level = 1 + /// The max possible spell level + var/spell_max_level = 5 + /// If set to a positive number, the spell will produce sparks when casted. + var/sparks_amt = 0 + /// The typepath of the smoke to create on cast. + var/smoke_type + /// The amount of smoke to create on cast. This is a range, so a value of 5 will create enough smoke to cover everything within 5 steps. + var/smoke_amt = 0 + +/datum/action/cooldown/spell/Grant(mob/grant_to) + // If our spell is mind-bound, we only wanna grant it to our mind + if(istype(target, /datum/mind)) + var/datum/mind/mind_target = target + if(mind_target.current != grant_to) + return -/obj/effect/proc_holder/proc/on_lose(mob/living/user) - return + . = ..() + if(!owner) + return -/obj/effect/proc_holder/proc/fire(mob/living/user) - return TRUE + // Register some signals so our button's icon stays up to date + if(spell_requirements & SPELL_REQUIRES_OFF_CENTCOM) + RegisterSignal(owner, COMSIG_MOVABLE_Z_CHANGED, .proc/update_icon_on_signal) + if(spell_requirements & (SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_WIZARD_GARB)) + RegisterSignal(owner, COMSIG_MOB_EQUIPPED_ITEM, .proc/update_icon_on_signal) + RegisterSignal(owner, list(COMSIG_MOB_ENTER_JAUNT, COMSIG_MOB_AFTER_EXIT_JAUNT), .proc/update_icon_on_signal) + owner.client?.stat_panel.send_message("check_spells") -/obj/effect/proc_holder/proc/get_panel_text() - return "" +/datum/action/cooldown/spell/Remove(mob/living/remove_from) -GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) //needed for the badmin verb for now + remove_from.client?.stat_panel.send_message("check_spells") + UnregisterSignal(remove_from, list( + COMSIG_MOB_AFTER_EXIT_JAUNT, + COMSIG_MOB_ENTER_JAUNT, + COMSIG_MOB_EQUIPPED_ITEM, + COMSIG_MOVABLE_Z_CHANGED, + )) -/obj/effect/proc_holder/Destroy() - QDEL_NULL(action) - if(ranged_ability_user) - remove_ranged_ability() return ..() -/obj/effect/proc_holder/singularity_act() - return +/datum/action/cooldown/spell/IsAvailable() + return ..() && can_cast_spell(feedback = FALSE) -/obj/effect/proc_holder/singularity_pull() - return +/datum/action/cooldown/spell/Trigger(trigger_flags, atom/target) + // We implement this can_cast_spell check before the parent call of Trigger() + // to allow people to click unavailable abilities to get a feedback chat message + // about why the ability is unavailable. + // It is otherwise redundant, however, as IsAvailable() checks can_cast_spell as well. + if(!can_cast_spell()) + return FALSE -/obj/effect/proc_holder/proc/InterceptClickOn(mob/living/caller, params, atom/A) - if(caller.ranged_ability != src || ranged_ability_user != caller) //I'm not actually sure how these would trigger, but, uh, safety, I guess? - to_chat(caller, "[caller.ranged_ability.name] has been disabled.") - caller.ranged_ability.remove_ranged_ability() - return TRUE //TRUE for failed, FALSE for passed. - if(ranged_clickcd_override >= 0) - ranged_ability_user.next_click = world.time + ranged_clickcd_override - else - ranged_ability_user.next_click = world.time + CLICK_CD_CLICK_ABILITY - ranged_ability_user.face_atom(A) - return FALSE + return ..() -/obj/effect/proc_holder/proc/add_ranged_ability(mob/living/user, msg, forced) - if(!user || !user.client) - return - if(user.ranged_ability && user.ranged_ability != src) - if(forced) - to_chat(user, "[user.ranged_ability.name] has been replaced by [name].") - user.ranged_ability.remove_ranged_ability() - else - return - user.ranged_ability = src - user.click_intercept = src - user.update_mouse_pointer() - ranged_ability_user = user - if(msg) - to_chat(ranged_ability_user, msg) - active = TRUE - update_icon() - -/obj/effect/proc_holder/proc/remove_ranged_ability(msg) - if(!ranged_ability_user || !ranged_ability_user.client || (ranged_ability_user.ranged_ability && ranged_ability_user.ranged_ability != src)) //To avoid removing the wrong ability - return - ranged_ability_user.ranged_ability = null - ranged_ability_user.click_intercept = null - ranged_ability_user.update_mouse_pointer() - if(msg) - to_chat(ranged_ability_user, msg) - ranged_ability_user = null - active = FALSE - update_icon() - -/obj/effect/proc_holder/spell - name = "Spell" - desc = "A wizard spell." - panel = "Spells" - var/sound = null //The sound the spell makes when it is cast - anchored = TRUE // Crap like fireball projectiles are proc_holders, this is needed so fireballs don't get blown back into your face via atmos etc. - pass_flags = PASSTABLE - density = FALSE - opacity = FALSE - - var/school = "evocation" //not relevant at now, but may be important later if there are changes to how spells work. the ones I used for now will probably be changed... maybe spell presets? lacking flexibility but with some other benefit? - var/requires_heretic_focus = FALSE //If the spell requires one of the heretic focus items to cast - var/charge_type = "recharge" //can be recharge or charges, see charge_max and charge_counter descriptions; can also be based on the holder's vars now, use "holder_var" for that - - var/charge_max = 10 SECONDS //recharge time in deciseconds if charge_type = "recharge" or starting charges if charge_type = "charges" - var/charge_counter = 0 //can only cast spells if it equals recharge, ++ each deciseconds if charge_type = "recharge" or -- each cast if charge_type = "charges" - var/still_recharging_msg = "The spell is still recharging." - var/recharging = TRUE - - var/holder_var_type = "bruteloss" //only used if charge_type equals to "holder_var" - var/holder_var_amount = 20 //same. The amount adjusted with the mob's var when the spell is used - - var/clothes_req = TRUE //see if it requires clothes - var/cult_req = FALSE //SPECIAL SNOWFLAKE clothes required for cult only spells - var/human_req = FALSE //spell can only be cast by humans - var/nonabstract_req = FALSE //spell can only be cast by mobs that are physical entities - var/stat_allowed = FALSE //see if it requires being conscious/alive, need to set to 1 for ghostpells - var/phase_allowed = FALSE // If true, the spell can be cast while phased, eg. blood crawling, ethereal jaunting - var/antimagic_allowed = FALSE // If false, the spell cannot be cast while under the effect of antimagic - var/invocation = "HURP DURP" //what is uttered when the wizard casts the spell - var/invocation_emote_self = null - var/invocation_type = INVOCATION_NONE //can be none, whisper, emote and shout - var/range = 7 //the range of the spell; outer radius for aoe spells - var/message = "" //whatever it says to the guy affected by it - var/selection_type = "view" //can be "range" or "view" - var/spell_level = 0 //if a spell can be taken multiple times, this raises - var/level_max = 4 //The max possible level_max is 4 - var/cooldown_min = 0 //This defines what spell quickened four times has as a cooldown. Make sure to set this for every spell - var/player_lock = TRUE //If it can be used by simple mobs - var/invocation_time = 0 //Time needed to cast the spell - - var/overlay = 0 - var/overlay_icon = 'icons/obj/wizard.dmi' - var/overlay_icon_state = "spell" - var/overlay_lifespan = 0 - - var/mutable_appearance/timer_overlay - var/mutable_appearance/text_overlay - var/timer_overlay_active = FALSE - var/timer_icon = 'icons/effects/cooldown.dmi' - var/timer_icon_state_active = "second" - - var/sparks_spread = 0 - var/sparks_amt = 0 //cropped at 10 - var/smoke_spread = 0 //1 - harmless, 2 - harmful - var/smoke_amt = 0 //cropped at 10 - - var/centcom_cancast = TRUE //Whether or not the spell should be allowed on z2 - - - /// Typecache of clothing needed to cast the spell. Used in actual checks. Override in Initialize if your spell requires different clothing. - /// !!Shared between instances, make a copy to modify. - var/list/casting_clothes - - /// Base typecache of clothing needed to cast spells. Do not modify, make a separate static var in subtypes if necessary. - var/static/list/casting_clothes_base - - action_icon = 'icons/mob/actions/actions_spells.dmi' - action_icon_state = "spell_default" - action_background_icon_state = "bg_spell" - base_action = /datum/action/spell_action/spell - -/obj/effect/proc_holder/spell/proc/cast_check(skipcharge = 0,mob/user = usr) //checks if the spell can be cast based on its settings; skipcharge is used when an additional cast_check is called inside the spell - if(SEND_SIGNAL(user, COMSIG_MOB_PRE_CAST_SPELL, src) & COMPONENT_CANCEL_SPELL) +/datum/action/cooldown/spell/set_click_ability(mob/on_who) + if(SEND_SIGNAL(on_who, COMSIG_MOB_SPELL_ACTIVATED, src) & SPELL_CANCEL_CAST) return FALSE - if(player_lock) - if(!user.mind || !(src in user.mind.spell_list) && !(src in user.mob_spell_list)) - to_chat(user, "You shouldn't have this spell! Something's wrong.") - return FALSE - else - if(!(src in user.mob_spell_list)) - return FALSE + return ..() - if(!do_after(user,invocation_time, target = user, progress = 1)) //checks if there is a invocation time set for this spell and cancels the spell if the user is interrupted. - to_chat(user, "You get interrupted.") +// Where the cast chain starts +/datum/action/cooldown/spell/PreActivate(atom/target) + if(!is_valid_target(target)) return FALSE - var/turf/T = get_turf(user) - if(is_centcom_level(T.z) && !centcom_cancast) //Certain spells are not allowed on the centcom zlevel - to_chat(user, "You can't cast this spell here.") - return FALSE + return Activate(target) - if(!skipcharge) - if(!charge_check(user)) - return FALSE +/// Checks if the owner of the spell can currently cast it. +/// Does not check anything involving potential targets. +/datum/action/cooldown/spell/proc/can_cast_spell(feedback = TRUE) + if(!owner) + CRASH("[type] - can_cast_spell called on a spell without an owner!") - if(user.stat && !stat_allowed) - to_chat(user, "Not when you're incapacitated.") + // Certain spells are not allowed on the centcom zlevel + var/turf/caster_turf = get_turf(owner) + if((spell_requirements & SPELL_REQUIRES_OFF_CENTCOM) && is_centcom_level(caster_turf.z)) + if(feedback) + to_chat(owner, span_warning("You can't cast [src] here!")) return FALSE - if(!antimagic_allowed) - var/antimagic = user.anti_magic_check(TRUE, FALSE, major = FALSE, self = TRUE) - if(antimagic) - if(isitem(antimagic)) - to_chat(user, "[antimagic] is interfering with your magic.") - else - to_chat(user, "Magic seems to flee from you, you can't gather enough power to cast this spell.") - return FALSE + if((spell_requirements & SPELL_REQUIRES_MIND) && !owner.mind) + // No point in feedback here, as mindless mobs aren't players + return FALSE - if(!phase_allowed && istype(user.loc, /obj/effect/dummy)) - to_chat(user, "[name] cannot be cast unless you are completely manifested in the material plane.") + if((spell_requirements & SPELL_REQUIRES_MIME_VOW) && !owner.mind?.miming) + // In the future this can be moved out of spell checks exactly + if(feedback) + to_chat(owner, span_warning("You must dedicate yourself to silence first!")) return FALSE - if(ishuman(user)) + // If the spell requires the user has no antimagic equipped, and they're holding antimagic + // that corresponds with the spell's antimagic, then they can't actually cast the spell + if((spell_requirements & SPELL_REQUIRES_NO_ANTIMAGIC) && !owner.can_cast_magic(antimagic_flags)) + if(feedback) + to_chat(owner, span_warning("Some form of antimagic is preventing you from casting [src]!")) + return FALSE - var/mob/living/carbon/human/H = user + if(!(spell_requirements & SPELL_CASTABLE_WHILE_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED)) + if(feedback) + to_chat(owner, span_warning("[src] cannot be cast unless you are completely manifested in the material plane!")) + return FALSE - if((invocation_type == "whisper" || invocation_type == "shout") && !H.can_speak_vocal()) - to_chat(user, "You can't get the words out!") - return FALSE + if(!can_invoke(feedback = feedback)) + return FALSE - if(clothes_req) //clothes check - if(!is_type_in_typecache(H.wear_suit, casting_clothes)) - to_chat(H, "I don't feel strong enough without my robe.") - return FALSE - if(!is_type_in_typecache(H.head, casting_clothes)) - to_chat(H, "I don't feel strong enough without my hat.") + if(ishuman(owner)) + if(spell_requirements & SPELL_REQUIRES_WIZARD_GARB) + var/mob/living/carbon/human/human_owner = owner + if(!(human_owner.wear_suit?.clothing_flags & CASTING_CLOTHES)) + to_chat(owner, span_warning("You don't feel strong enough without your robe!")) return FALSE - if(cult_req) //CULT_REQ CLOTHES CHECK - if(!istype(H.wear_suit, /obj/item/clothing/suit/magusred) && !istype(H.wear_suit, /obj/item/clothing/suit/hooded/cultrobes)) - to_chat(H, "I don't feel strong enough without my armor.") - return FALSE - if(!istype(H.head, /obj/item/clothing/head/wizard/magus) && !istype(H.head, /obj/item/clothing/head/hooded/cult_hoodie)) - to_chat(H, "I don't feel strong enough without my helmet.") + if(!(human_owner.head?.clothing_flags & CASTING_CLOTHES)) + to_chat(owner, span_warning("You don't feel strong enough without your hat!")) return FALSE + else - if(clothes_req || human_req) - to_chat(user, "This spell can only be cast by humans!") + // If the spell requires wizard equipment and we're not a human (can't wear robes or hats), that's just a given + if(spell_requirements & (SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_HUMAN)) + if(feedback) + to_chat(owner, span_warning("[src] can only be cast by humans!")) + return FALSE + + if(!(spell_requirements & SPELL_CASTABLE_AS_BRAIN) && isbrain(owner)) + if(feedback) + to_chat(owner, span_warning("[src] can't be cast in this state!")) return FALSE - if(nonabstract_req && (isbrain(user) || ispAI(user))) - to_chat(user, "This spell can only be cast by physical beings!") + + // Being put into a card form breaks a lot of spells, so we'll just forbid them in these states + if(ispAI(owner) || (isAI(owner) && istype(owner.loc, /obj/item/aicard))) return FALSE - if(action) - action.UpdateButtonIcon() - return TRUE -/obj/effect/proc_holder/spell/proc/use_charge(mob/user) - switch(charge_type) - if("recharge") - charge_counter = 0 //doesn't start recharging until the targets selecting ends - if("charges") - charge_counter-- //returns the charge if the targets selecting fails - if("holdervar") - adjust_var(user, holder_var_type, holder_var_amount) - start_recharge() - -/obj/effect/proc_holder/spell/proc/charge_check(mob/user, silent = FALSE) - switch(charge_type) - if("recharge") - if(charge_counter < charge_max) - if(!silent) - to_chat(user, still_recharging_msg) - return FALSE - if("charges") - if(!charge_counter) - if(!silent) - to_chat(user, "[name] has no charges left.") - return FALSE return TRUE -/obj/effect/proc_holder/spell/proc/invocation(mob/user = usr) //spelling the spell out and setting it on recharge/reducing charges amount - switch(invocation_type) - if("shout") - if(prob(50))//Auto-mute? Fuck that noise - user.say(invocation, forced = "spell") - else - user.say(replacetext(invocation," ","`"), forced = "spell") - if("whisper") - if(prob(50)) - user.whisper(invocation) - else - user.whisper(replacetext(invocation," ","`")) - if("emote") - user.visible_message(invocation, invocation_emote_self) //same style as in mob/living/emote.dm +/** + * Check if the target we're casting on is a valid target. + * For self-casted spells, the target being checked (cast_on) is the caster. + * + * Return TRUE if cast_on is valid, FALSE otherwise + */ +/datum/action/cooldown/spell/proc/is_valid_target(atom/cast_on) + return TRUE -/obj/effect/proc_holder/spell/proc/playMagSound() - playsound(get_turf(usr), sound,50,1) +// The actual cast chain occurs here, in Activate(). +// You should generally not be overriding or extending Activate() for spells. +// Defer to any of the cast chain procs instead. +/datum/action/cooldown/spell/Activate(atom/cast_on) + SHOULD_NOT_OVERRIDE(TRUE) + + // Pre-casting of the spell + // Pre-cast is the very last chance for a spell to cancel + // Stuff like target input can go here. + var/precast_result = before_cast(cast_on) + if(precast_result & SPELL_CANCEL_CAST) + return FALSE -/obj/effect/proc_holder/spell/Initialize(mapload) - . = ..() + // Spell is officially being cast + if(!(precast_result & SPELL_NO_FEEDBACK)) + // We do invocation and sound effects here, before actual cast + // That way stuff like teleports or shape-shifts can be invoked before ocurring + spell_feedback() - if(!casting_clothes_base) - casting_clothes_base = typecacheof(list(/obj/item/clothing/suit/wizrobe, - /obj/item/clothing/suit/space/hardsuit/wizard, - /obj/item/clothing/head/wizard, - /obj/item/clothing/head/helmet/space/hardsuit/wizard, - /obj/item/clothing/suit/space/hardsuit/shielded/wizard, - /obj/item/clothing/head/helmet/space/hardsuit/shielded/wizard)) + // Actually cast the spell. Main effects go here + cast(cast_on) - casting_clothes = casting_clothes_base + if(!(precast_result & SPELL_NO_IMMEDIATE_COOLDOWN)) + // The entire spell is done, start the actual cooldown at its set duration + StartCooldown() - still_recharging_msg = "[name] is still recharging." - charge_counter = charge_max + // And then proceed with the aftermath of the cast + // Final effects that happen after all the casting is done can go here + after_cast(cast_on) + UpdateButtons() -/obj/effect/proc_holder/spell/Destroy() - end_timer_animation() - qdel(action) - return ..() + return TRUE -/obj/effect/proc_holder/spell/Click() - if(cast_check()) - choose_targets() - return 1 +/** + * Actions done before the actual cast is called. + * This is the last chance to cancel the spell from being cast. + * + * Can be used for target selection or to validate checks on the caster (cast_on). + * + * Returns a bitflag. + * - SPELL_CANCEL_CAST will stop the spell from being cast. + * - SPELL_NO_FEEDBACK will prevent the spell from calling [proc/spell_feedback] on cast. (invocation, sounds) + * - SPELL_NO_IMMEDIATE_COOLDOWN will prevent the spell from starting its cooldown between cast and before after_cast. + */ +/datum/action/cooldown/spell/proc/before_cast(atom/cast_on) + SHOULD_CALL_PARENT(TRUE) + + var/sig_return = SEND_SIGNAL(src, COMSIG_SPELL_BEFORE_CAST, cast_on) + if(owner) + sig_return |= SEND_SIGNAL(owner, COMSIG_MOB_BEFORE_SPELL_CAST, src, cast_on) + return sig_return + +/** + * Actions done as the main effect of the spell. + * + * For spells without a click intercept, [cast_on] will be the owner. + * For click spells, [cast_on] is whatever the owner clicked on in casting the spell. + */ +/datum/action/cooldown/spell/proc/cast(atom/cast_on) + SHOULD_CALL_PARENT(TRUE) + + SEND_SIGNAL(src, COMSIG_SPELL_CAST, cast_on) + if(owner) + SEND_SIGNAL(owner, COMSIG_MOB_CAST_SPELL, src, cast_on) + if(owner.ckey) + owner.log_message("cast the spell [name][cast_on != owner ? " on / at [cast_on]":""].", LOG_ATTACK) + +/** + * Actions done after the main cast is finished. + * This is called after the cooldown's already begun. + * + * It can be used to apply late spell effects where order matters + * (for example, causing smoke *after* a teleport occurs in cast()) + * or to clean up variables or references post-cast. + */ +/datum/action/cooldown/spell/proc/after_cast(atom/cast_on) + SHOULD_CALL_PARENT(TRUE) + + SEND_SIGNAL(src, COMSIG_SPELL_AFTER_CAST, cast_on) + if(!owner) + return -/obj/effect/proc_holder/spell/proc/choose_targets(mob/user = usr) //depends on subtype - /targeted or /aoe_turf - return + SEND_SIGNAL(owner, COMSIG_MOB_AFTER_SPELL_CAST, src, cast_on) -/obj/effect/proc_holder/spell/proc/can_target(mob/living/target) - return TRUE + // Sparks and smoke can only occur if there's an owner to source them from. + if(sparks_amt) + do_sparks(sparks_amt, FALSE, get_turf(owner)) -/obj/effect/proc_holder/spell/proc/start_recharge() - recharging = TRUE - begin_timer_animation() - -/obj/effect/proc_holder/spell/process(delta_time) - if(recharging && charge_type == "recharge" && (charge_counter < charge_max)) - charge_counter += delta_time * 10 - update_timer_animation() - if(charge_counter >= charge_max) - end_timer_animation() - action.UpdateButtonIcon() - charge_counter = charge_max - recharging = FALSE - else - end_timer_animation() - action.UpdateButtonIcon() - charge_counter = charge_max - recharging = FALSE + if(ispath(smoke_type, /datum/effect_system/fluid_spread/smoke)) + var/datum/effect_system/fluid_spread/smoke/smoke = new smoke_type() + smoke.set_up(smoke_amt, holder = owner, location = get_turf(owner)) + smoke.start() -/obj/effect/proc_holder/spell/proc/perform(list/targets, recharge = TRUE, mob/user = usr) //if recharge is started is important for the trigger spells - if(!cast_check()) +/// Provides feedback after a spell cast occurs, in the form of a cast sound and/or invocation +/datum/action/cooldown/spell/proc/spell_feedback() + if(!owner) return - use_charge(user) - before_cast(targets) - invocation(user) - if(user?.ckey) - user.log_message("cast the spell [name].", LOG_ATTACK) - if(recharge) - start_recharge() + + if(invocation_type != INVOCATION_NONE) + invocation() if(sound) - playMagSound() - cast(targets,user=user) - after_cast(targets) - if(action) - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spell/proc/before_cast(list/targets) - if(overlay) - for(var/atom/target in targets) - var/location - if(isliving(target)) - location = target.loc - else if(isturf(target)) - location = target - var/obj/effect/overlay/spell = new /obj/effect/overlay(location) - spell.icon = overlay_icon - spell.icon_state = overlay_icon_state - spell.set_anchored(TRUE) - spell.set_density(FALSE) - QDEL_IN(spell, overlay_lifespan) - -/obj/effect/proc_holder/spell/proc/after_cast(list/targets) - for(var/atom/target in targets) - var/location - if(isliving(target)) - location = target.loc - else if(isturf(target)) - location = target - if(isliving(target) && message) - to_chat(target, "[message]") - if(sparks_spread) - do_sparks(sparks_amt, FALSE, location) - if(smoke_spread) - if(smoke_spread == 1) - var/datum/effect_system/smoke_spread/smoke = new - smoke.set_up(smoke_amt, location) - smoke.start() - else if(smoke_spread == 2) - var/datum/effect_system/smoke_spread/bad/smoke = new - smoke.set_up(smoke_amt, location) - smoke.start() - else if(smoke_spread == 3) - var/datum/effect_system/smoke_spread/sleeping/smoke = new - smoke.set_up(smoke_amt, location) - smoke.start() - - -/obj/effect/proc_holder/spell/proc/cast(list/targets,mob/user = usr) - return - -/obj/effect/proc_holder/spell/proc/revert_cast(mob/user = usr) //resets recharge or readds a charge - switch(charge_type) - if("recharge") - charge_counter = charge_max - if("charges") - charge_counter++ - if("holdervar") - adjust_var(user, holder_var_type, -holder_var_amount) - end_timer_animation() - if(action) - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spell/proc/adjust_var(mob/living/target = usr, type, amount) //handles the adjustment of the var when the spell is used. has some hardcoded types - if (!istype(target)) - return - switch(type) - if("bruteloss") - target.adjustBruteLoss(amount) - if("fireloss") - target.adjustFireLoss(amount) - if("toxloss") - target.adjustToxLoss(amount) - if("oxyloss") - target.adjustOxyLoss(amount) - if("stun") - target.AdjustStun(amount) - if("knockdown") - target.AdjustKnockdown(amount) - if("paralyze") - target.AdjustParalyzed(amount) - if("immobilize") - target.AdjustImmobilized(amount) - if("unconscious") - target.AdjustUnconscious(amount) - else - target.vars[type] += amount //I bear no responsibility for the runtimes that'll happen if you try to adjust non-numeric or even non-existent vars - -/obj/effect/proc_holder/spell/targeted //can mean aoe for mobs (limited/unlimited number) or one target mob - ranged_mousepointer = 'icons/effects/cult_target.dmi' - var/max_targets = 1 //leave 0 for unlimited targets in range, 1 for one selectable target in range, more for limited number of casts (can all target one guy, depends on target_ignore_prev) in range - var/target_ignore_prev = 1 //only important if max_targets > 1, affects if the spell can be cast multiple times at one person from one cast - var/include_user = 0 //if it includes usr in the target list - var/random_target = 0 // chooses random viable target instead of asking the caster - var/random_target_priority = TARGET_CLOSEST // if random_target is enabled how it will pick the target - var/ranged_selection_active = FALSE - -/obj/effect/proc_holder/spell/aoe_turf //affects all turfs in view or range (depends) - var/inner_radius = -1 //for all your ring spell needs - -/obj/effect/proc_holder/spell/targeted/Click() - if(ranged_selection_active) - remove_ranged_ability("You are no longer casting [src].") - return - . = ..() + playsound(get_turf(owner), sound, 50, TRUE) -/obj/effect/proc_holder/spell/targeted/choose_targets(mob/user = usr) - var/list/targets = list() - - switch(max_targets) - if(0) //unlimited - for(var/mob/living/target in view_or_range(range, user, selection_type)) - if(!can_target(target)) - continue - targets += target - if(1) //single target can be picked - if(range < 0) - targets += user +/// The invocation that accompanies the spell, called from spell_feedback() before cast(). +/datum/action/cooldown/spell/proc/invocation() + switch(invocation_type) + if(INVOCATION_SHOUT) + if(prob(50)) + owner.say(invocation, forced = "spell ([src])") else - var/possible_targets = list() - - for(var/mob/living/M in view_or_range(range, user, selection_type)) - if(!include_user && user == M) - continue - if(!can_target(M)) - continue - possible_targets += M - - //targets += input("Choose the target for the spell.", "Targeting") as mob in possible_targets - //Adds a safety check post-input to make sure those targets are actually in range. - var/mob/M - if(!random_target) - add_ranged_ability(user, "Click on a target for which to cast [src] upon.", TRUE) - return - else - switch(random_target_priority) - if(TARGET_RANDOM) - M = pick(possible_targets) - if(TARGET_CLOSEST) - for(var/mob/living/L in possible_targets) - if(M) - if(get_dist(user,L) < get_dist(user,M)) - if(los_check(user,L)) - M = L - else - if(los_check(user,L)) - M = L - if(M in view_or_range(range, user, selection_type)) - targets += M - - else - var/list/possible_targets = list() - for(var/mob/living/target in view_or_range(range, user, selection_type)) - if(!can_target(target)) - continue - possible_targets += target - for(var/i in 1 to max_targets) - if(!possible_targets.len) - break - if(target_ignore_prev) - var/target = pick(possible_targets) - possible_targets -= target - targets += target - else - targets += pick(possible_targets) - - if(!include_user && (user in targets)) - targets -= user - - if(!targets.len) //doesn't waste the spell - revert_cast(user) - return + owner.say(replacetext(invocation," ","`"), forced = "spell ([src])") - perform(targets,user=user) - -/obj/effect/proc_holder/spell/targeted/remove_ranged_ability(msg) - . = ..() - ranged_selection_active = FALSE + if(INVOCATION_WHISPER) + if(prob(50)) + owner.whisper(invocation, forced = "spell ([src])") + else + owner.whisper(replacetext(invocation," ","`"), forced = "spell ([src])") -/obj/effect/proc_holder/spell/targeted/add_ranged_ability(mob/living/user, msg, forced) - . = ..() - ranged_selection_active = TRUE + if(INVOCATION_EMOTE) + owner.visible_message(invocation, invocation_self_message) -/obj/effect/proc_holder/spell/targeted/InterceptClickOn(mob/living/caller, params, atom/A) - if(..()) +/// Checks if the current OWNER of the spell is in a valid state to say the spell's invocation +/datum/action/cooldown/spell/proc/can_invoke(feedback = TRUE) + if(spell_requirements & SPELL_CASTABLE_WITHOUT_INVOCATION) return TRUE - if(ismob(A)) - if(A == caller && !include_user) - to_chat(caller, "You cannot target yourself!") - return TRUE - - var/list/targets = list(A) - remove_ranged_ability() - - if(!targets.len) //doesn't waste the spell - revert_cast(caller) - return TRUE + if(invocation_type == INVOCATION_NONE) + return TRUE - perform(targets, user=caller) + // If you want a spell usable by ghosts for some reason, it must be INVOCATION_NONE + if(!isliving(owner)) + if(feedback) + to_chat(owner, span_warning("You need to be living to invoke [src]!")) return FALSE - return TRUE -/obj/effect/proc_holder/spell/aoe_turf/choose_targets(mob/user = usr) - var/list/targets = list() - - for(var/turf/target in view_or_range(range,user,selection_type)) - if(!can_target(target)) - continue - if(!(target in view_or_range(inner_radius,user,selection_type))) - targets += target + var/mob/living/living_owner = owner + if(invocation_type == INVOCATION_EMOTE && HAS_TRAIT(living_owner, TRAIT_EMOTEMUTE)) + if(feedback) + to_chat(owner, span_warning("You can't position your hands correctly to invoke [src]!")) + return FALSE - if(!targets.len) //doesn't waste the spell - revert_cast() - return + if((invocation_type == INVOCATION_WHISPER || invocation_type == INVOCATION_SHOUT) && !living_owner.can_speak_vocal()) + if(feedback) + to_chat(owner, span_warning("You can't get the words out to invoke [src]!")) + return FALSE - perform(targets,user=user) - -/obj/effect/proc_holder/spell/proc/updateButtonIcon(status_only, force) - action.UpdateButtonIcon(status_only, force) - -/obj/effect/proc_holder/spell/proc/can_be_cast_by(mob/caster) - if((human_req || clothes_req) && !ishuman(caster)) - return 0 - return 1 - -/obj/effect/proc_holder/spell/targeted/proc/los_check(mob/A,mob/B) - //Checks for obstacles from A to B - var/obj/dummy = new(A.loc) - dummy.pass_flags |= PASSTABLE - var/turf/previous_step = get_turf(A) - var/first_step = TRUE - for(var/turf/next_step as anything in (getline(A, B) - previous_step)) - if(first_step) - for(var/obj/blocker in previous_step) - if(!blocker.density || !(blocker.flags_1 & ON_BORDER_1)) - continue - if(blocker.CanPass(dummy, get_dir(previous_step, next_step))) - continue - return FALSE // Could not leave the first turf. - first_step = FALSE - for(var/atom/movable/movable as anything in next_step) - if(!movable.CanPass(dummy, get_dir(next_step, previous_step))) - qdel(dummy) - return FALSE - previous_step = next_step - qdel(dummy) return TRUE -/obj/effect/proc_holder/spell/proc/can_cast(mob/user = usr) - if(((!user.mind) || !(src in user.mind.spell_list)) && !(src in user.mob_spell_list)) +/// Resets the cooldown of the spell, sending COMSIG_SPELL_CAST_RESET +/// and allowing it to be used immediately (+ updating button icon accordingly) +/datum/action/cooldown/spell/proc/reset_spell_cooldown() + SEND_SIGNAL(src, COMSIG_SPELL_CAST_RESET) + next_use_time -= cooldown_time // Basically, ensures that the ability can be used now + UpdateButtons() + +/** + * Levels the spell up a single level, reducing the cooldown. + * If bypass_cap is TRUE, will level the spell up past it's set cap. + */ +/datum/action/cooldown/spell/proc/level_spell(bypass_cap = FALSE) + // Spell cannot be levelled + if(spell_max_level <= 1) return FALSE - if(!charge_check(user,TRUE)) + // Spell is at cap, and we will not bypass it + if(!bypass_cap && (spell_level >= spell_max_level)) return FALSE - if(user.stat && !stat_allowed) + spell_level++ + cooldown_time = max(cooldown_time - cooldown_reduction_per_rank, 0) + update_spell_name() + return TRUE + +/** + * Levels the spell down a single level, down to 1. + */ +/datum/action/cooldown/spell/proc/delevel_spell() + // Spell cannot be levelled + if(spell_max_level <= 1) return FALSE - if(!antimagic_allowed && user.anti_magic_check(TRUE, FALSE, major = FALSE, self = TRUE)) + if(spell_level <= 1) return FALSE - if(!ishuman(user)) - if(clothes_req || human_req) - return FALSE - if(nonabstract_req && (isbrain(user) || ispAI(user))) - return FALSE + spell_level-- + cooldown_time = min(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time)) + update_spell_name() return TRUE -//===Timer animation=== - -/obj/effect/proc_holder/spell/update_icon() - . = ..() - if(timer_overlay_active && !recharging) - end_timer_animation() - if(action) - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spell/proc/begin_timer_animation() - if(!(action?.button) || timer_overlay_active) - return - - timer_overlay_active = TRUE - timer_overlay = mutable_appearance(timer_icon, timer_icon_state_active) - timer_overlay.alpha = 180 - - if(!text_overlay) - text_overlay = image(loc = action.button) - text_overlay.maptext_width = 64 - text_overlay.maptext_height = 64 - text_overlay.maptext_x = -8 - text_overlay.maptext_y = -6 - text_overlay.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA - - if(action.owner?.client) - action.owner.client.images += text_overlay - - action.button.add_overlay(timer_overlay) - action.has_cooldown_timer = TRUE - update_timer_animation() - - START_PROCESSING(SSfastprocess, src) - -/obj/effect/proc_holder/spell/proc/update_timer_animation() - //Update map text (todo) - if(!(action?.button)) - return - text_overlay.maptext = "
[FLOOR((charge_max-charge_counter)/10, 1)]
" - -/obj/effect/proc_holder/spell/proc/end_timer_animation() - if(!(action?.button) || !timer_overlay_active) - return - timer_overlay_active = FALSE - if(action.owner?.client) - action.owner.client.images -= text_overlay - action.button.cut_overlays(timer_overlay) - timer_overlay = null - qdel(text_overlay) - text_overlay = null - action.has_cooldown_timer = FALSE - - STOP_PROCESSING(SSfastprocess, src) - -//===================== - -/obj/effect/proc_holder/spell/self //Targets only the caster. Good for buffs and heals, but probably not wise for fireballs (although they usually fireball themselves anyway, honke) - range = -1 //Duh - -/obj/effect/proc_holder/spell/self/choose_targets(mob/user = usr) - if(!user) - revert_cast() - return - perform(null,user=user) - -/obj/effect/proc_holder/spell/self/basic_heal //This spell exists mainly for debugging purposes, and also to show how casting works - name = "Lesser Heal" - desc = "Heals a small amount of brute and burn damage." - human_req = TRUE - clothes_req = FALSE - charge_max = 100 - cooldown_min = 50 - invocation = "Victus sano!" - invocation_type = INVOCATION_WHISPER - school = "restoration" - sound = 'sound/magic/staff_healing.ogg' - -/obj/effect/proc_holder/spell/self/basic_heal/cast(mob/living/carbon/human/user) //Note the lack of "list/targets" here. Instead, use a "user" var depending on mob requirements. - //Also, notice the lack of a "for()" statement that looks through the targets. This is, again, because the spell can only have a single target. - user.visible_message("A wreath of gentle light passes over [user]!", "You wreath yourself in healing light!") - user.adjustBruteLoss(-10) - user.adjustFireLoss(-10) +/** + * Updates the spell's name based on its level. + */ +/datum/action/cooldown/spell/proc/update_spell_name() + var/spell_title = "" + switch(spell_level) + if(2) + spell_title = "Efficient " + if(3) + spell_title = "Quickened " + if(4) + spell_title = "Free " + if(5) + spell_title = "Instant " + if(6) + spell_title = "Ludicrous " + + name = "[spell_title][initial(name)]" + UpdateButtons() diff --git a/code/modules/spells/spell_types/aimed.dm b/code/modules/spells/spell_types/aimed.dm deleted file mode 100644 index f54839860ac9a..0000000000000 --- a/code/modules/spells/spell_types/aimed.dm +++ /dev/null @@ -1,184 +0,0 @@ - -/obj/effect/proc_holder/spell/aimed - name = "aimed projectile spell" - var/projectile_type = /obj/projectile/magic/teleport - var/deactive_msg = "You discharge your projectile..." - var/active_msg = "You charge your projectile!" - base_icon_state = "projectile" - var/active_icon_state = "projectile" - var/list/projectile_var_overrides = list() - var/projectile_amount = 1 //Projectiles per cast. - var/current_amount = 0 //How many projectiles left. - var/projectiles_per_fire = 1 //Projectiles per fire. Probably not a good thing to use unless you override ready_projectile(). - -/obj/effect/proc_holder/spell/aimed/Click() - var/mob/living/user = usr - if(!istype(user)) - return - var/msg - if(!can_cast(user)) - msg = "You can no longer cast [name]!" - remove_ranged_ability(msg) - return - if(active) - msg = "[deactive_msg]" - if(charge_type == "recharge") - var/refund_percent = current_amount/projectile_amount - charge_counter = charge_max * refund_percent - start_recharge() - remove_ranged_ability(msg) - on_deactivation(user) - else - msg = "[active_msg] Left-click to shoot it at a target!" - current_amount = projectile_amount - add_ranged_ability(user, msg, TRUE) - on_activation(user) - -/obj/effect/proc_holder/spell/aimed/proc/on_activation(mob/user) - return - -/obj/effect/proc_holder/spell/aimed/proc/on_deactivation(mob/user) - return - -/obj/effect/proc_holder/spell/aimed/update_icon() - if(!action) - return - action.button_icon_state = "[base_icon_state][active]" - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spell/aimed/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return FALSE - var/ran_out = (current_amount <= 0) - if(!cast_check(!ran_out, ranged_ability_user)) - remove_ranged_ability() - return FALSE - var/list/targets = list(target) - perform(targets, ran_out, user = ranged_ability_user) - return TRUE - -/obj/effect/proc_holder/spell/aimed/cast(list/targets, mob/living/user) - var/target = targets[1] - var/turf/T = user.loc - var/turf/U = get_step(user, user.dir) // Get the tile infront of the move, based on their direction - if(!isturf(U) || !isturf(T)) - return FALSE - fire_projectile(user, target) - user.newtonian_move(get_dir(U, T)) - if(current_amount <= 0) - remove_ranged_ability() //Auto-disable the ability once you run out of bullets. - charge_counter = 0 - start_recharge() - on_deactivation(user) - return TRUE - -/obj/effect/proc_holder/spell/aimed/proc/fire_projectile(mob/living/user, atom/target) - current_amount-- - if(!projectile_type) - return - for(var/i in 1 to projectiles_per_fire) - var/obj/projectile/P = new projectile_type(user.loc, spell_level) - P.firer = user - P.preparePixelProjectile(target, user) - for(var/V in projectile_var_overrides) - if(P.vars[V]) - P.vv_edit_var(V, projectile_var_overrides[V]) - ready_projectile(P, target, user, i) - P.fire() - return TRUE - -/obj/effect/proc_holder/spell/aimed/proc/ready_projectile(obj/projectile/P, atom/target, mob/user, iteration) - return - -/obj/effect/proc_holder/spell/aimed/lightningbolt - name = "Lightning Bolt" - desc = "Fire a lightning bolt at your foes! It will jump between targets, but can't knock them down." - school = "evocation" - charge_max = 150 - clothes_req = FALSE - invocation = "UN'LTD P'WAH" - invocation_type = INVOCATION_SHOUT - cooldown_min = 50 - base_icon_state = "lightning" - action_icon_state = "lightning0" - sound = 'sound/magic/lightningbolt.ogg' - active = FALSE - projectile_var_overrides = list("tesla_range" = 15, "tesla_power" = 20000, "tesla_flags" = TESLA_MOB_DAMAGE) - active_msg = "You energize your hand with arcane lightning!" - deactive_msg = "You let the energy flow out of your hands back into yourself..." - projectile_type = /obj/projectile/magic/aoe/lightning - -/obj/effect/proc_holder/spell/aimed/fireball - name = "Fireball" - desc = "This spell fires an explosive fireball at a target." - school = "evocation" - charge_max = 140 - clothes_req = TRUE - invocation = "ONI SOMA" - invocation_type = INVOCATION_SHOUT - range = 20 - cooldown_min = 40 //10 deciseconds reduction per rank - projectile_type = /obj/projectile/magic/fireball - base_icon_state = "fireball" - action_icon_state = "fireball0" - sound = 'sound/magic/fireball.ogg' - active_msg = "You prepare to cast your fireball spell!" - deactive_msg = "You extinguish your fireball... for now." - active = FALSE - -/obj/effect/proc_holder/spell/aimed/spell_cards - name = "Spell Cards" - desc = "Magically sharpened rapid-fire homing cards. Send your foes to the shadow realm with their mystical power!" - school = "evocation" - charge_max = 90 - clothes_req = FALSE - invocation = "Sigi'lu M'Fan 'Tasia" - invocation_type = INVOCATION_SHOUT - range = 40 - cooldown_min = 30 - projectile_amount = 5 - projectiles_per_fire = 7 - projectile_type = /obj/projectile/spellcard - base_icon_state = "spellcard" - action_icon_state = "spellcard0" - var/datum/weakref/current_target_weakref - var/projectile_turnrate = 10 - var/projectile_pixel_homing_spread = 32 - var/projectile_initial_spread_amount = 30 - var/projectile_location_spread_amount = 12 - var/datum/component/lockon_aiming/lockon_component - ranged_clickcd_override = TRUE - -/obj/effect/proc_holder/spell/aimed/spell_cards/on_activation(mob/M) - QDEL_NULL(lockon_component) - lockon_component = M.AddComponent(/datum/component/lockon_aiming, 5, typecacheof(list(/mob/living)), 1, null, CALLBACK(src, PROC_REF(on_lockon_component))) - -/obj/effect/proc_holder/spell/aimed/spell_cards/proc/on_lockon_component(list/locked_weakrefs) - if(!length(locked_weakrefs)) - current_target_weakref = null - return - current_target_weakref = locked_weakrefs[1] - var/atom/A = current_target_weakref.resolve() - if(A) - var/mob/M = lockon_component.parent - M.face_atom(A) - -/obj/effect/proc_holder/spell/aimed/spell_cards/on_deactivation(mob/M) - QDEL_NULL(lockon_component) - -/obj/effect/proc_holder/spell/aimed/spell_cards/ready_projectile(obj/projectile/P, atom/target, mob/user, iteration) - if(current_target_weakref) - var/atom/A = current_target_weakref.resolve() - if(A && get_dist(A, user) < 7) - P.homing_turn_speed = projectile_turnrate - P.homing_inaccuracy_min = projectile_pixel_homing_spread - P.homing_inaccuracy_max = projectile_pixel_homing_spread - P.set_homing_target(current_target_weakref.resolve()) - var/rand_spr = rand() - var/total_angle = projectile_initial_spread_amount * 2 - var/adjusted_angle = total_angle - ((projectile_initial_spread_amount / projectiles_per_fire) * 0.5) - var/one_fire_angle = adjusted_angle / projectiles_per_fire - var/current_angle = iteration * one_fire_angle * rand_spr - (projectile_initial_spread_amount / 2) - P.pixel_x = rand(-projectile_location_spread_amount, projectile_location_spread_amount) - P.pixel_y = rand(-projectile_location_spread_amount, projectile_location_spread_amount) - P.preparePixelProjectile(target, user, null, current_angle) diff --git a/code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm b/code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm new file mode 100644 index 0000000000000..1d240bad61ea1 --- /dev/null +++ b/code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm @@ -0,0 +1,57 @@ +/** + * ## AOE spells + * + * A spell that iterates over atoms near the caster and casts a spell on them. + * Calls cast_on_thing_in_aoe on all atoms returned by get_things_to_cast_on by default. + */ +/datum/action/cooldown/spell/aoe + /// The max amount of targets we can affect via our AOE. 0 = unlimited + var/max_targets = 0 + /// The radius of the aoe. + var/aoe_radius = 7 + +// At this point, cast_on == owner. Either works. +/datum/action/cooldown/spell/aoe/cast(atom/cast_on) + . = ..() + // Get every atom around us to our aoe cast on + var/list/atom/things_to_cast_on = get_things_to_cast_on(cast_on) + // If we have a target limit, shuffle it (for fariness) + if(max_targets > 0) + things_to_cast_on = shuffle(things_to_cast_on) + + SEND_SIGNAL(src, COMSIG_SPELL_AOE_ON_CAST, things_to_cast_on, cast_on) + + // Now go through and cast our spell where applicable + var/num_targets = 0 + for(var/thing_to_target in things_to_cast_on) + if(max_targets > 0 && num_targets >= max_targets) + continue + + cast_on_thing_in_aoe(thing_to_target, cast_on) + num_targets++ + +/** + * Gets a list of atoms around [center] + * that are within range and affected by our aoe. + */ +/datum/action/cooldown/spell/aoe/proc/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/atom/nearby_thing in range(aoe_radius, center)) + if(nearby_thing == owner || nearby_thing == center) + continue + + things += nearby_thing + + return things + +/** + * Actually cause effects on the thing in our aoe. + * Override this for your spell! Not cast(). + * + * Arguments + * * victim - the atom being affected by our aoe + * * caster - the mob who cast the aoe + */ +/datum/action/cooldown/spell/aoe/proc/cast_on_thing_in_aoe(atom/victim, atom/caster) + SHOULD_CALL_PARENT(FALSE) + CRASH("[type] did not implement cast_on_thing_in_aoe and either has no effects or implemented the spell incorrectly.") diff --git a/code/modules/spells/spell_types/aoe_spell/area_conversion.dm b/code/modules/spells/spell_types/aoe_spell/area_conversion.dm new file mode 100644 index 0000000000000..bde25b779332e --- /dev/null +++ b/code/modules/spells/spell_types/aoe_spell/area_conversion.dm @@ -0,0 +1,25 @@ +/datum/action/cooldown/spell/aoe/area_conversion + name = "Area Conversion" + desc = "This spell instantly converts a small area around you." + background_icon_state = "bg_cult" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "areaconvert" + + school = SCHOOL_TRANSMUTATION + cooldown_time = 5 SECONDS + + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + aoe_radius = 2 + +/datum/action/cooldown/spell/aoe/area_conversion/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/turf/nearby_turf in range(aoe_radius, center)) + things += nearby_turf + + return things + +/datum/action/cooldown/spell/aoe/area_conversion/cast_on_thing_in_aoe(turf/victim, atom/caster) + playsound(victim, 'sound/items/welder.ogg', 75, TRUE) + victim.narsie_act(FALSE, TRUE, 100 - (get_dist(victim, caster) * 25)) diff --git a/code/modules/spells/spell_types/aoe_spell/knock.dm b/code/modules/spells/spell_types/aoe_spell/knock.dm new file mode 100644 index 0000000000000..fd9e4503de8fd --- /dev/null +++ b/code/modules/spells/spell_types/aoe_spell/knock.dm @@ -0,0 +1,20 @@ +/datum/action/cooldown/spell/aoe/knock + name = "Knock" + desc = "This spell opens nearby doors and closets." + button_icon_state = "knock" + + sound = 'sound/magic/knock.ogg' + school = SCHOOL_TRANSMUTATION + cooldown_time = 10 SECONDS + cooldown_reduction_per_rank = 2 SECONDS + + invocation = "AULIE OXIN FIERA" + invocation_type = INVOCATION_WHISPER + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + aoe_radius = 3 + +/datum/action/cooldown/spell/aoe/knock/get_things_to_cast_on(atom/center) + return RANGE_TURFS(aoe_radius, center) + +/datum/action/cooldown/spell/aoe/knock/cast_on_thing_in_aoe(turf/victim, atom/caster) + SEND_SIGNAL(victim, COMSIG_ATOM_MAGICALLY_UNLOCKED, src, caster) diff --git a/code/modules/spells/spell_types/aoe_spell/magic_missile.dm b/code/modules/spells/spell_types/aoe_spell/magic_missile.dm new file mode 100644 index 0000000000000..a1513c1ca897d --- /dev/null +++ b/code/modules/spells/spell_types/aoe_spell/magic_missile.dm @@ -0,0 +1,47 @@ +/datum/action/cooldown/spell/aoe/magic_missile + name = "Magic Missile" + desc = "This spell fires several, slow moving, magic projectiles at nearby targets." + button_icon_state = "magicm" + sound = 'sound/magic/magic_missile.ogg' + + school = SCHOOL_EVOCATION + cooldown_time = 20 SECONDS + cooldown_reduction_per_rank = 3.5 SECONDS + + invocation = "FORTI GY AMA" + invocation_type = INVOCATION_SHOUT + + aoe_radius = 7 + + /// The projectile type fired at all people around us + var/obj/projectile/projectile_type = /obj/projectile/magic/aoe/magic_missile + +/datum/action/cooldown/spell/aoe/magic_missile/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/mob/living/nearby_mob in view(aoe_radius, center)) + if(nearby_mob == owner || nearby_mob == center) + continue + + things += nearby_mob + + return things + +/datum/action/cooldown/spell/aoe/magic_missile/cast_on_thing_in_aoe(mob/living/victim, atom/caster) + fire_projectile(victim, caster) + +/datum/action/cooldown/spell/aoe/magic_missile/proc/fire_projectile(atom/victim, mob/caster) + var/obj/projectile/to_fire = new projectile_type() + to_fire.preparePixelProjectile(victim, caster) + to_fire.fire() + +/datum/action/cooldown/spell/aoe/magic_missile/lesser + name = "Lesser Magic Missile" + desc = "This spell fires several, slow moving, magic projectiles at nearby targets." + background_icon_state = "bg_demon" + + cooldown_time = 40 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + max_targets = 6 + projectile_type = /obj/projectile/magic/aoe/magic_missile/lesser diff --git a/code/modules/spells/spell_types/aoe_spell/repulse.dm b/code/modules/spells/spell_types/aoe_spell/repulse.dm new file mode 100644 index 0000000000000..9e24ccde61a20 --- /dev/null +++ b/code/modules/spells/spell_types/aoe_spell/repulse.dm @@ -0,0 +1,87 @@ +/datum/action/cooldown/spell/aoe/repulse + /// The max throw range of the repulsioon. + var/max_throw = 5 + /// A visual effect to be spawned on people who are thrown away. + var/obj/effect/sparkle_path = /obj/effect/temp_visual/gravpush + /// The moveforce of the throw done by the repulsion. + var/repulse_force = MOVE_FORCE_EXTREMELY_STRONG + +/datum/action/cooldown/spell/aoe/repulse/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/atom/movable/nearby_movable in view(aoe_radius, center)) + if(nearby_movable == owner || nearby_movable == center) + continue + if(nearby_movable.anchored) + continue + + things += nearby_movable + + return things + +/datum/action/cooldown/spell/aoe/repulse/cast_on_thing_in_aoe(atom/movable/victim, atom/caster) + if(ismob(victim)) + var/mob/victim_mob = victim + if(victim_mob.can_block_magic(antimagic_flags)) + return + + var/turf/throwtarget = get_edge_target_turf(caster, get_dir(caster, get_step_away(victim, caster))) + var/dist_from_caster = get_dist(victim, caster) + + if(dist_from_caster == 0) + if(isliving(victim)) + var/mob/living/victim_living = victim + victim_living.Paralyze(10 SECONDS) + victim_living.adjustBruteLoss(5) + to_chat(victim, span_userdanger("You're slammed into the floor by [caster]!")) + else + if(sparkle_path) + // Created sparkles will disappear on their own + new sparkle_path(get_turf(victim), get_dir(caster, victim)) + + if(isliving(victim)) + var/mob/living/victim_living = victim + victim_living.Paralyze(4 SECONDS) + to_chat(victim, span_userdanger("You're thrown back by [caster]!")) + + // So stuff gets tossed around at the same time. + victim.safe_throw_at(throwtarget, ((clamp((max_throw - (clamp(dist_from_caster - 2, 0, dist_from_caster))), 3, max_throw))), 1, caster, force = repulse_force) + +/datum/action/cooldown/spell/aoe/repulse/wizard + name = "Repulse" + desc = "This spell throws everything around the user away." + button_icon_state = "repulse" + sound = 'sound/magic/repulse.ogg' + + school = SCHOOL_EVOCATION + invocation = "GITTAH WEIGH" + invocation_type = INVOCATION_SHOUT + aoe_radius = 5 + + cooldown_time = 40 SECONDS + cooldown_reduction_per_rank = 6.25 SECONDS + +/datum/action/cooldown/spell/aoe/repulse/xeno + name = "Tail Sweep" + desc = "Throw back attackers with a sweep of your tail." + background_icon_state = "bg_alien" + icon_icon = 'icons/mob/actions/actions_xeno.dmi' + button_icon_state = "tailsweep" + panel = "Alien" + sound = 'sound/magic/tail_swing.ogg' + + cooldown_time = 15 SECONDS + spell_requirements = NONE + + invocation_type = INVOCATION_NONE + antimagic_flags = NONE + aoe_radius = 2 + + sparkle_path = /obj/effect/temp_visual/dir_setting/tailsweep + +/datum/action/cooldown/spell/aoe/repulse/xeno/cast(atom/cast_on) + if(iscarbon(cast_on)) + var/mob/living/carbon/carbon_caster = cast_on + playsound(get_turf(carbon_caster), 'sound/voice/hiss5.ogg', 80, TRUE, TRUE) + carbon_caster.spin(6, 1) + + return ..() diff --git a/code/modules/spells/spell_types/aoe_spell/sacred_flame.dm b/code/modules/spells/spell_types/aoe_spell/sacred_flame.dm new file mode 100644 index 0000000000000..450544a7a1f66 --- /dev/null +++ b/code/modules/spells/spell_types/aoe_spell/sacred_flame.dm @@ -0,0 +1,39 @@ +/datum/action/cooldown/spell/aoe/sacred_flame + name = "Sacred Flame" + desc = "Makes everyone around you more flammable, and lights yourself on fire." + button_icon_state = "sacredflame" + sound = 'sound/magic/fireball.ogg' + + school = SCHOOL_EVOCATION + cooldown_time = 6 SECONDS + + invocation = "FI'RAN DADISKO" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + aoe_radius = 6 + + /// The amount of firestacks to put people afflicted. + var/firestacks_to_give = 20 + +/datum/action/cooldown/spell/aoe/sacred_flame/get_things_to_cast_on(atom/center) + var/list/things = list() + for(var/mob/living/nearby_mob in view(aoe_radius, center)) + things += nearby_mob + + return things + +/datum/action/cooldown/spell/aoe/sacred_flame/cast_on_thing_in_aoe(mob/living/victim, mob/living/caster) + if(victim.can_block_magic(antimagic_flags)) + return + + victim.adjust_fire_stacks(firestacks_to_give) + // Let people who got afflicted know they're suddenly a matchstick + // But skip the caster - they'll know anyways. + if(victim != caster) + to_chat(victim, span_warning("You suddenly feel very flammable.")) + +/datum/action/cooldown/spell/aoe/sacred_flame/cast(mob/living/cast_on) + . = ..() + cast_on.ignite_mob() + to_chat(cast_on, span_danger("You feel a roaring flame build up inside you!")) diff --git a/code/modules/spells/spell_types/area_teleport.dm b/code/modules/spells/spell_types/area_teleport.dm deleted file mode 100644 index 2980830bd87f1..0000000000000 --- a/code/modules/spells/spell_types/area_teleport.dm +++ /dev/null @@ -1,89 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/area_teleport - name = "Area teleport" - desc = "This spell teleports you to a type of area of your selection." - nonabstract_req = TRUE - - var/randomise_selection = FALSE //if it lets the usr choose the teleport loc or picks it from the list - var/invocation_area = TRUE //if the invocation appends the selected area - var/sound1 = 'sound/weapons/zapbang.ogg' - var/sound2 = 'sound/weapons/zapbang.ogg' - - var/say_destination = TRUE - -/obj/effect/proc_holder/spell/targeted/area_teleport/perform(list/targets, recharge = 1,mob/living/user = usr) - var/thearea = before_cast(targets) - if(!thearea || !cast_check(1)) - revert_cast() - return - invocation(thearea,user) - if(charge_type == "recharge" && recharge) - INVOKE_ASYNC(src, PROC_REF(start_recharge)) - cast(targets,thearea,user) - after_cast(targets) - -/obj/effect/proc_holder/spell/targeted/area_teleport/before_cast(list/targets) - var/A = null - - if(!randomise_selection) - A = input("Area to teleport to", "Teleport", A) as null|anything in GLOB.teleportlocs - else - A = pick(GLOB.teleportlocs) - if(!A) - return - var/area/thearea = GLOB.teleportlocs[A] - - return thearea - -/obj/effect/proc_holder/spell/targeted/area_teleport/cast(list/targets,area/thearea,mob/user = usr) - playsound(get_turf(user), sound1, 50,1) - for(var/mob/living/target in targets) - var/list/L = list() - for(var/turf/T in get_area_turfs(thearea.type)) - if(!T.density) - var/clear = TRUE - for(var/obj/O in T) - if(O.density) - clear = FALSE - break - if(clear) - L+=T - - if(!L.len) - to_chat(usr, "The spell matrix was unable to locate a suitable teleport destination for an unknown reason. Sorry.") - return - - if(target?.buckled) - target.buckled.unbuckle_mob(target, force=1) - - var/list/tempL = L - var/attempt = null - var/success = FALSE - while(tempL.len) - attempt = pick(tempL) - do_teleport(target, attempt, channel = TELEPORT_CHANNEL_MAGIC) - if(get_turf(target) == attempt) - success = TRUE - break - else - tempL.Remove(attempt) - - if(!success) - do_teleport(target, L, channel = TELEPORT_CHANNEL_MAGIC) - playsound(get_turf(user), sound2, 50,1) - -/obj/effect/proc_holder/spell/targeted/area_teleport/invocation(area/chosenarea = null,mob/living/user = usr) - if(!invocation_area || !chosenarea) - ..() - else - var/words - if(say_destination) - words = "[invocation] [uppertext(chosenarea.name)]" - else - words = "[invocation]" - - switch(invocation_type) - if("shout") - user.say(words, forced = "spell") - playsound(user.loc, pick('sound/misc/null.ogg','sound/misc/null.ogg'), 100, 1) - if("whisper") - user.whisper(words, forced = "spell") diff --git a/code/modules/spells/spell_types/barnyard.dm b/code/modules/spells/spell_types/barnyard.dm deleted file mode 100644 index b9b8605517ea4..0000000000000 --- a/code/modules/spells/spell_types/barnyard.dm +++ /dev/null @@ -1,50 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/barnyardcurse - name = "Curse of the Barnyard" - desc = "This spell dooms an unlucky soul to possess the speech and facial attributes of a barnyard animal." - school = "transmutation" - charge_type = "recharge" - charge_max = 150 - clothes_req = FALSE - stat_allowed = FALSE - invocation = "KN'A FTAGHU, PUCK 'BTHNK!" - invocation_type = INVOCATION_SHOUT - range = 7 - cooldown_min = 30 - selection_type = "range" - var/static/list/compatible_mobs_typecache = typecacheof(list(/mob/living/carbon/human, /mob/living/carbon/monkey)) - - action_icon_state = "barn" - -/obj/effect/proc_holder/spell/targeted/barnyardcurse/cast(list/targets, mob/user = usr) - if(!length(targets)) - to_chat(user, "No target found in range.") - return - - var/mob/living/carbon/target = targets[1] - - - if(!compatible_mobs_typecache[target.type]) - to_chat(user, "You are unable to curse [target]'s head!") - return - - if(!(target in oview(range))) - to_chat(user, "[target.p_theyre(TRUE)] too far away!") - return - - if(target.anti_magic_check() || HAS_TRAIT(target, TRAIT_WARDED)) - to_chat(user, "The spell had no effect!") - target.visible_message("[target]'s face bursts into flames, which instantly burst outward, leaving [target] unharmed!", \ - "Your face starts burning up, but the flames are repulsed by your anti-magic protection!") - return - - var/list/masks = list(/obj/item/clothing/mask/pig/cursed, /obj/item/clothing/mask/cowmask/cursed, /obj/item/clothing/mask/horsehead/cursed) - - var/choice = pick(masks) - var/obj/item/clothing/mask/magichead = new choice(get_turf(target)) - target.visible_message("[target]'s face bursts into flames, and a barnyard animal's head takes its place!", \ - "Your face burns up, and shortly after the fire you realise you have the face of a barnyard animal!") - if(!target.dropItemToGround(target.wear_mask)) - qdel(target.wear_mask) - target.equip_to_slot_if_possible(magichead, ITEM_SLOT_MASK, 1, 1) - - target.flash_act() diff --git a/code/modules/spells/spell_types/blind.dm b/code/modules/spells/spell_types/blind.dm deleted file mode 100644 index 9949cf3374c35..0000000000000 --- a/code/modules/spells/spell_types/blind.dm +++ /dev/null @@ -1,49 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/blind - name = "Blind" - desc = "This spell temporarily blinds a single target." - school = "transmutation" - charge_max = 300 - clothes_req = FALSE - invocation = "STI KALY" - invocation_type = INVOCATION_WHISPER - cooldown_min = 50 //12 deciseconds reduction per rank - ranged_mousepointer = 'icons/effects/blind_target.dmi' - action_icon_state = "blind" - range = 7 - selection_type = "range" - var/duration = 300 //30 seconds - var/static/list/compatible_mobs_typecache = typecacheof(list(/mob/living/carbon/human)) - - -/obj/effect/proc_holder/spell/targeted/blind/cast(list/targets, mob/user = usr) - if(!length(targets)) - to_chat(user, "No target found in range.") - revert_cast() - return - - var/mob/living/carbon/target = targets[1] - - if(!compatible_mobs_typecache[target.type]) - to_chat(user, "You are unable to curse [target] with blindness!") - revert_cast() - return - - if(!(target in oview(range))) - to_chat(user, "[target.p_theyre(TRUE)] too far away!") - revert_cast() - return - - if(target.anti_magic_check() || HAS_TRAIT(target, TRAIT_WARDED)) - to_chat(user, "The spell had no effect!") - target.visible_message("[target]'s eyes darken, but instantly turn back to their regular color, leaving [target] unharmed!", \ - "Your eyes hurt for a moment, but the blindness is repulsed by your anti-magic protection!") - return - - target.visible_message("[target]'s eyes darken as black smoke starts coming out of them!", \ - "Your eyes hurt as they start smoking, you panic as you realise you're blind!") - target.emote("scream") - target.become_blind(MAGIC_BLIND) - addtimer(CALLBACK(src, PROC_REF(cure_blindness), target), duration) - -/obj/effect/proc_holder/spell/targeted/blind/proc/cure_blindness(mob/living/L) - L.cure_blind(MAGIC_BLIND) diff --git a/code/modules/spells/spell_types/bloodcrawl.dm b/code/modules/spells/spell_types/bloodcrawl.dm deleted file mode 100644 index eb07ecb20536a..0000000000000 --- a/code/modules/spells/spell_types/bloodcrawl.dm +++ /dev/null @@ -1,36 +0,0 @@ -/obj/effect/proc_holder/spell/bloodcrawl - name = "Blood Crawl" - desc = "Use pools of blood to phase out of existence." - charge_max = 0 - clothes_req = FALSE - //If you couldn't cast this while phased, you'd have a problem - phase_allowed = TRUE - selection_type = "range" - range = 1 - cooldown_min = 0 - overlay = null - action_icon = 'icons/mob/actions/actions_minor_antag.dmi' - action_icon_state = "bloodcrawl" - action_background_icon_state = "bg_demon" - var/phased = FALSE - -/obj/effect/proc_holder/spell/bloodcrawl/choose_targets(mob/user = usr) - for(var/obj/effect/decal/cleanable/target in view(range, get_turf(user))) - if(target.can_bloodcrawl_in()) - perform(target) - return - revert_cast() - to_chat(user, "There must be a nearby source of blood!") - -/obj/effect/proc_holder/spell/bloodcrawl/perform(obj/effect/decal/cleanable/target, recharge = 1, mob/living/user = usr) - if(istype(user)) - if(phased) - if(user.phasein(target)) - phased = FALSE - else - if(user.phaseout(target)) - phased = TRUE - start_recharge() - return - revert_cast() - to_chat(user, "You are unable to blood crawl!") diff --git a/code/modules/spells/spell_types/charge.dm b/code/modules/spells/spell_types/charge.dm deleted file mode 100644 index b15deaf470061..0000000000000 --- a/code/modules/spells/spell_types/charge.dm +++ /dev/null @@ -1,103 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/charge - name = "Charge" - desc = "This spell can be used to recharge a variety of things in your hands, from magical artifacts to electrical components. A creative wizard can even use it to grant magical power to a fellow magic user." - - school = "transmutation" - charge_max = 600 - clothes_req = FALSE - invocation = "DIRI CEL" - invocation_type = INVOCATION_WHISPER - range = -1 - cooldown_min = 400 //50 deciseconds reduction per rank - include_user = TRUE - action_icon_state = "charge" - -/obj/effect/proc_holder/spell/targeted/charge/cast(list/targets,mob/user = usr) - for(var/mob/living/L in targets) - var/list/hand_items = list(L.get_active_held_item(),L.get_inactive_held_item()) - var/charged_item = null - var/burnt_out = FALSE - - if(L.pulling && isliving(L.pulling)) - var/mob/living/M = L.pulling - if(M.mob_spell_list.len != 0 || (M.mind && M.mind.spell_list.len != 0)) - for(var/obj/effect/proc_holder/spell/S in M.mob_spell_list) - S.charge_counter = S.charge_max - if(M.mind) - for(var/obj/effect/proc_holder/spell/S in M.mind.spell_list) - S.charge_counter = S.charge_max - to_chat(M, "You feel raw magic flowing through you. It feels good!") - else - to_chat(M, "You feel very strange for a moment, but then it passes.") - burnt_out = TRUE - charged_item = M - break - for(var/obj/item in hand_items) - if(istype(item, /obj/item/spellbook)) - to_chat(L, "Glowing red letters appear on the front cover...") - to_chat(L, "[pick("NICE TRY BUT NO!","CLEVER BUT NOT CLEVER ENOUGH!", "SUCH FLAGRANT CHEESING IS WHY WE ACCEPTED YOUR APPLICATION!", "CUTE! VERY CUTE!", "YOU DIDN'T THINK IT'D BE THAT EASY, DID YOU?")]") - burnt_out = TRUE - else if(istype(item, /obj/item/book/granter/spell)) - var/obj/item/book/granter/spell/I = item - if(!I.oneuse) - to_chat(L, "This book is infinite use and can't be recharged, yet the magic has improved the book somehow...") - burnt_out = TRUE - I.pages_to_mastery-- - break - if(prob(80)) - L.visible_message("[I] catches fire!") - qdel(I) - else - I.used = FALSE - charged_item = I - break - else if(istype(item, /obj/item/gun/magic)) - var/obj/item/gun/magic/I = item - if(prob(80) && !I.can_charge) - I.max_charges-- - if(I.max_charges <= 0) - I.max_charges = 0 - burnt_out = TRUE - I.charges = I.max_charges - if(istype(item, /obj/item/gun/magic/wand) && I.max_charges != 0) - var/obj/item/gun/magic/W = item - W.icon_state = initial(W.icon_state) - I.recharge_newshot() - charged_item = I - break - else if(istype(item, /obj/item/stock_parts/cell)) - var/obj/item/stock_parts/cell/C = item - if(!C.self_recharge) - if(prob(80)) - C.maxcharge -= 200 - if(C.maxcharge <= 1) //Div by 0 protection - C.maxcharge = 1 - burnt_out = TRUE - C.charge = C.maxcharge - charged_item = C - break - else if(item.contents) - var/obj/I = null - for(I in item.contents) - if(istype(I, /obj/item/stock_parts/cell/)) - var/obj/item/stock_parts/cell/C = I - if(!C.self_recharge) - if(prob(80)) - C.maxcharge -= 200 - if(C.maxcharge <= 1) //Div by 0 protection - C.maxcharge = 1 - burnt_out = TRUE - C.charge = C.maxcharge - if(istype(C.loc, /obj/item/gun)) - var/obj/item/gun/G = C.loc - G.process_chamber() - item.update_icon() - charged_item = item - break - if(!charged_item) - to_chat(L, "You feel magical power surging through your hands, but the feeling rapidly fades...") - else if(burnt_out) - to_chat(L, "[charged_item] doesn't seem to be reacting to the spell...") - else - playsound(get_turf(L), 'sound/magic/charge.ogg', 50, 1) - to_chat(L, "[charged_item] suddenly feels very warm!") diff --git a/code/modules/spells/spell_types/cone/_cone.dm b/code/modules/spells/spell_types/cone/_cone.dm new file mode 100644 index 0000000000000..0832b01b97dfb --- /dev/null +++ b/code/modules/spells/spell_types/cone/_cone.dm @@ -0,0 +1,123 @@ +/** + * ## Cone spells + * + * Cone spells shoot off as a cone from the caster. + */ +/datum/action/cooldown/spell/cone + /// This controls how many levels the cone has. Increase this value to make a bigger cone. + var/cone_levels = 3 + /// This value determines if the cone penetrates walls. + var/respect_density = FALSE + +/datum/action/cooldown/spell/cone/cast(atom/cast_on) + . = ..() + var/list/cone_turfs = get_cone_turfs(get_turf(cast_on), cast_on.dir, cone_levels) + SEND_SIGNAL(src, COMSIG_SPELL_CONE_ON_CAST, cone_turfs, cast_on) + make_cone(cone_turfs, cast_on) + +/datum/action/cooldown/spell/cone/proc/make_cone(list/cone_turfs, atom/caster) + for(var/list/turf_list in cone_turfs) + do_cone_effects(turf_list, caster) + +/// This proc does obj, mob and turf cone effects on all targets in the passed list. +/datum/action/cooldown/spell/cone/proc/do_cone_effects(list/target_turf_list, atom/caster, level = 1) + SEND_SIGNAL(src, COMSIG_SPELL_CONE_ON_LAYER_EFFECT, target_turf_list, caster, level) + for(var/turf/target_turf as anything in target_turf_list) + if(QDELETED(target_turf)) //if turf is no longer there + continue + + do_turf_cone_effect(target_turf, caster, level) + if(!isopenturf(target_turf)) + continue + + for(var/atom/movable/movable_content as anything in target_turf) + if(isobj(movable_content)) + do_obj_cone_effect(movable_content, level) + else if(isliving(movable_content)) + do_mob_cone_effect(movable_content, level) + +///This proc deterimines how the spell will affect turfs. +/datum/action/cooldown/spell/cone/proc/do_turf_cone_effect(turf/target_turf, atom/caster, level) + return + +///This proc deterimines how the spell will affect objects. +/datum/action/cooldown/spell/cone/proc/do_obj_cone_effect(obj/target_obj, atom/caster, level) + return + +///This proc deterimines how the spell will affect mobs. +/datum/action/cooldown/spell/cone/proc/do_mob_cone_effect(mob/living/target_mob, atom/caster, level) + return + +///This proc creates a list of turfs that are hit by the cone. +/datum/action/cooldown/spell/cone/proc/get_cone_turfs(turf/starter_turf, dir_to_use, cone_levels = 3) + var/list/turfs_to_return = list() + var/turf/turf_to_use = starter_turf + var/turf/left_turf + var/turf/right_turf + var/right_dir + var/left_dir + switch(dir_to_use) + if(NORTH) + left_dir = WEST + right_dir = EAST + if(SOUTH) + left_dir = EAST + right_dir = WEST + if(EAST) + left_dir = NORTH + right_dir = SOUTH + if(WEST) + left_dir = SOUTH + right_dir = NORTH + + for(var/i in 1 to cone_levels) + var/list/level_turfs = list() + turf_to_use = get_step(turf_to_use, dir_to_use) + level_turfs += turf_to_use + if(i != 1) + left_turf = get_step(turf_to_use, left_dir) + level_turfs += left_turf + right_turf = get_step(turf_to_use, right_dir) + level_turfs += right_turf + for(var/left_i in 1 to i -calculate_cone_shape(i)) + if(left_turf.density && respect_density) + break + left_turf = get_step(left_turf, left_dir) + level_turfs += left_turf + for(var/right_i in 1 to i -calculate_cone_shape(i)) + if(right_turf.density && respect_density) + break + right_turf = get_step(right_turf, right_dir) + level_turfs += right_turf + turfs_to_return += list(level_turfs) + if(i == cone_levels) + continue + if(turf_to_use.density && respect_density) + break + return turfs_to_return + +///This proc adjusts the cones width depending on the level. +/datum/action/cooldown/spell/cone/proc/calculate_cone_shape(current_level) + var/end_taper_start = round(cone_levels * 0.8) + if(current_level > end_taper_start) + return (current_level % end_taper_start) * 2 //someone more talented and probably come up with a better formula. + else + return 2 + +/** + * ### Staggered Cone + * + * Staggered Cone spells will reach each cone level + * gradually / with a delay, instead of affecting the entire + * cone area at once. + */ +/datum/action/cooldown/spell/cone/staggered + + /// The delay between each cone level triggering. + var/delay_between_level = 0.2 SECONDS + +/datum/action/cooldown/spell/cone/staggered/make_cone(list/cone_turfs, atom/caster) + var/level_counter = 0 + for(var/list/turf_list in cone_turfs) + level_counter++ + addtimer(CALLBACK(src, .proc/do_cone_effects, turf_list, caster, level_counter), delay_between_level * level_counter) diff --git a/code/modules/spells/spell_types/cone_spells.dm b/code/modules/spells/spell_types/cone_spells.dm deleted file mode 100644 index ee0e776fa45a3..0000000000000 --- a/code/modules/spells/spell_types/cone_spells.dm +++ /dev/null @@ -1,117 +0,0 @@ -/obj/effect/proc_holder/spell/cone - name = "Cone of Nothing" - desc = "Does nothing in a cone! Wow!" - school = "evocation" - charge_max = 100 - clothes_req = FALSE - invocation = "FUKAN NOTHAN" - invocation_type = INVOCATION_SHOUT - sound = 'sound/magic/forcewall.ogg' - action_icon_state = "shield" - range = -1 - cooldown_min = 0.5 SECONDS - ///This controls how many levels the cone has, increase this value to make a bigger cone. - var/cone_levels = 3 - ///This value determines if the cone penetrates walls. - var/respect_density = FALSE - -/obj/effect/proc_holder/spell/cone/choose_targets(mob/user = usr) - perform(null, user=user) - -///This proc creates a list of turfs that are hit by the cone -/obj/effect/proc_holder/spell/cone/proc/cone_helper(var/turf/starter_turf, var/dir_to_use, var/cone_levels = 3) - var/list/turfs_to_return = list() - var/turf/turf_to_use = starter_turf - var/turf/left_turf - var/turf/right_turf - var/right_dir - var/left_dir - switch(dir_to_use) - if(NORTH) - left_dir = WEST - right_dir = EAST - if(SOUTH) - left_dir = EAST - right_dir = WEST - if(EAST) - left_dir = NORTH - right_dir = SOUTH - if(WEST) - left_dir = SOUTH - right_dir = NORTH - - - for(var/i in 1 to cone_levels) - var/list/level_turfs = list() - turf_to_use = get_step(turf_to_use, dir_to_use) - level_turfs += turf_to_use - if(i != 1) - left_turf = get_step(turf_to_use, left_dir) - level_turfs += left_turf - right_turf = get_step(turf_to_use, right_dir) - level_turfs += right_turf - for(var/left_i in 1 to i -calculate_cone_shape(i)) - if(left_turf.density && respect_density) - break - left_turf = get_step(left_turf, left_dir) - level_turfs += left_turf - for(var/right_i in 1 to i -calculate_cone_shape(i)) - if(right_turf.density && respect_density) - break - right_turf = get_step(right_turf, right_dir) - level_turfs += right_turf - turfs_to_return += list(level_turfs) - if(i == cone_levels) - continue - if(turf_to_use.density && respect_density) - break - return turfs_to_return - -/obj/effect/proc_holder/spell/cone/cast(list/targets,mob/user = usr) - var/list/cone_turfs = cone_helper(get_turf(user), user.dir, cone_levels) - for(var/list/turf_list in cone_turfs) - do_cone_effects(turf_list) - -///This proc does obj, mob and turf cone effects on all targets in a list -/obj/effect/proc_holder/spell/cone/proc/do_cone_effects(list/target_turf_list, level) - for(var/target_turf in target_turf_list) - if(!target_turf) //if turf is no longer there - continue - do_turf_cone_effect(target_turf, level) - if(isopenturf(target_turf)) - var/turf/open/open_turf = target_turf - for(var/movable_content in open_turf) - if(isobj(movable_content)) - do_obj_cone_effect(movable_content, level) - else if(isliving(movable_content)) - do_mob_cone_effect(movable_content, level) - -///This proc deterimines how the spell will affect turfs. -/obj/effect/proc_holder/spell/cone/proc/do_turf_cone_effect(turf/target_turf, level) - return - -///This proc deterimines how the spell will affect objects. -/obj/effect/proc_holder/spell/cone/proc/do_obj_cone_effect(obj/target_obj, level) - return - -///This proc deterimines how the spell will affect mobs. -/obj/effect/proc_holder/spell/cone/proc/do_mob_cone_effect(mob/living/target_mob, level) - return - -///This proc adjusts the cones width depending on the level. -/obj/effect/proc_holder/spell/cone/proc/calculate_cone_shape(current_level) - var/end_taper_start = round(cone_levels * 0.8) - if(current_level > end_taper_start) - return (current_level % end_taper_start) * 2 //someone more talented and probably come up with a better formula. - else - return 2 - -///This type of cone gradually affects each level of the cone instead of affecting the entire area at once. -/obj/effect/proc_holder/spell/cone/staggered - -/obj/effect/proc_holder/spell/cone/staggered/cast(list/targets,mob/user = usr) - var/level_counter = 0 - var/list/cone_turfs = cone_helper(get_turf(user), user.dir, cone_levels) - for(var/list/turf_list in cone_turfs) - level_counter++ - addtimer(CALLBACK(src, PROC_REF(do_cone_effects), turf_list, level_counter), 2 * level_counter) diff --git a/code/modules/spells/spell_types/conjure.dm b/code/modules/spells/spell_types/conjure.dm deleted file mode 100644 index b705d6c3b776a..0000000000000 --- a/code/modules/spells/spell_types/conjure.dm +++ /dev/null @@ -1,103 +0,0 @@ -/obj/effect/proc_holder/spell/aoe_turf/conjure - name = "Conjure" - desc = "This spell conjures objs of the specified types in range." - - var/list/summon_type = list() //determines what exactly will be summoned - //should be text, like list("/mob/living/simple_animal/bot/ed209") - - var/summon_lifespan = 0 // 0=permanent, any other time in deciseconds - var/summon_amt = 1 //amount of objects summoned - var/summon_ignore_density = FALSE //if set to TRUE, adds dense tiles to possible spawn places - var/summon_ignore_prev_spawn_points = TRUE //if set to TRUE, each new object is summoned on a new spawn point - - var/list/newVars = list() //vars of the summoned objects will be replaced with those where they meet - //should have format of list("emagged" = 1,"name" = "Wizard's Justicebot"), for example - - var/cast_sound = 'sound/items/welder.ogg' - -/obj/effect/proc_holder/spell/aoe_turf/conjure/cast(list/targets,mob/user = usr) - playsound(get_turf(user), cast_sound, 50,1) - for(var/turf/T in targets) - if(T.density && !summon_ignore_density) - targets -= T - - for(var/i in 1 to summon_amt) - if(!targets.len) - break - var/summoned_object_type = pick(summon_type) - var/spawn_place = pick(targets) - if(summon_ignore_prev_spawn_points) - targets -= spawn_place - if(ispath(summoned_object_type, /turf)) - var/turf/O = spawn_place - var/N = summoned_object_type - if(istype(O, /turf/open) && ispath(N, /turf/closed) || istype(O, /turf/open/floor/plating)) - new N(O) - else - O.ChangeTurf(N, flags = CHANGETURF_INHERIT_AIR) - else - var/atom/summoned_object = new summoned_object_type(spawn_place) - - for(var/varName in newVars) - if(varName in newVars) - summoned_object.vv_edit_var(varName, newVars[varName]) - summoned_object.flags_1 |= ADMIN_SPAWNED_1 - if(summon_lifespan) - QDEL_IN(summoned_object, summon_lifespan) - - post_summon(summoned_object, user) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/proc/post_summon(atom/summoned_object, mob/user) - return - -/obj/effect/proc_holder/spell/aoe_turf/conjure/summonEdSwarm //test purposes - Also a lot of fun - name = "Dispense Wizard Justice" - desc = "This spell dispenses wizard justice." - - summon_type = list(/mob/living/simple_animal/bot/ed209) - summon_amt = 10 - range = 3 - newVars = list("emagged" = 2, "remote_disabled" = 1,"shoot_sound" = 'sound/weapons/laser.ogg',"projectile" = /obj/projectile/beam/laser, "declare_arrests" = 0,"name" = "Wizard's Justicebot") - -/obj/effect/proc_holder/spell/aoe_turf/conjure/linkWorlds - name = "Link Worlds" - desc = "A whole new dimension for you to play with! They won't be happy about it, though." - invocation = "WTF" - clothes_req = FALSE - charge_max = 600 - cooldown_min = 200 - summon_type = list(/obj/structure/spawner/nether) - summon_amt = 1 - range = 1 - cast_sound = 'sound/weapons/marauder.ogg' - -/obj/effect/proc_holder/spell/targeted/conjure_item - name = "Summon weapon" - desc = "A generic spell that should not exist. This summons an instance of a specific type of item, or if one already exists, un-summons it. Summons into hand if possible." - invocation_type = INVOCATION_NONE - include_user = TRUE - range = -1 - clothes_req = FALSE - var/obj/item/item - var/item_type = /obj/item/banhammer - school = "conjuration" - charge_max = 150 - cooldown_min = 10 - var/delete_old = TRUE //TRUE to delete the last summoned object if it's still there, FALSE for infinite item stream weeeee - -/obj/effect/proc_holder/spell/targeted/conjure_item/cast(list/targets, mob/user = usr) - if (delete_old && item && !QDELETED(item)) - QDEL_NULL(item) - else - for(var/mob/living/carbon/C in targets) - if(C.dropItemToGround(C.get_active_held_item())) - C.put_in_hands(make_item(), TRUE) - -/obj/effect/proc_holder/spell/targeted/conjure_item/Destroy() - if(item) - qdel(item) - return ..() - -/obj/effect/proc_holder/spell/targeted/conjure_item/proc/make_item() - item = new item_type - return item diff --git a/code/modules/spells/spell_types/conjure/_conjure.dm b/code/modules/spells/spell_types/conjure/_conjure.dm new file mode 100644 index 0000000000000..9483bb57b43d1 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/_conjure.dm @@ -0,0 +1,50 @@ +/datum/action/cooldown/spell/conjure + sound = 'sound/items/welder.ogg' + school = SCHOOL_CONJURATION + + /// The radius around the caster the items will appear. 0 = spawns on top of the caster. + var/summon_radius = 7 + /// A list of types that will be created on summon. + /// The type is picked from this list, not all provided are guaranteed. + var/list/summon_type = list() + /// How long before the summons will be despawned. Set to 0 for permanent. + var/summon_lifespan = 0 + /// Amount of summons to create. + var/summon_amount = 1 + /// If TRUE, summoned objects will not be spawned in dense turfs. + var/summon_respects_density = FALSE + /// If TRUE, no two summons can be spawned in the same turf. + var/summon_respects_prev_spawn_points = TRUE + +/datum/action/cooldown/spell/conjure/cast(atom/cast_on) + . = ..() + var/list/to_summon_in = list() + for(var/turf/summon_turf in range(summon_radius, cast_on)) + if(summon_respects_density && summon_turf.density) + continue + to_summon_in += summon_turf + + for(var/i in 1 to summon_amount) + if(!length(to_summon_in)) + break + + var/atom/summoned_object_type = pick(summon_type) + var/turf/spawn_place = pick(to_summon_in) + if(summon_respects_prev_spawn_points) + to_summon_in -= spawn_place + + if(ispath(summoned_object_type, /turf)) + spawn_place.ChangeTurf(summoned_object_type, flags = CHANGETURF_INHERIT_AIR) + + else + var/atom/summoned_object = new summoned_object_type(spawn_place) + + summoned_object.flags_1 |= ADMIN_SPAWNED_1 + if(summon_lifespan > 0) + QDEL_IN(summoned_object, summon_lifespan) + + post_summon(summoned_object, cast_on) + +/// Called on atoms summoned after they are created, allows extra variable editing and such of created objects +/datum/action/cooldown/spell/conjure/proc/post_summon(atom/summoned_object, atom/cast_on) + return diff --git a/code/modules/spells/spell_types/conjure/bees.dm b/code/modules/spells/spell_types/conjure/bees.dm new file mode 100644 index 0000000000000..036abbc0f9b6f --- /dev/null +++ b/code/modules/spells/spell_types/conjure/bees.dm @@ -0,0 +1,18 @@ +/datum/action/cooldown/spell/conjure/bee + name = "Lesser Summon Bees" + desc = "This spell magically kicks a transdimensional beehive, \ + instantly summoning a swarm of bees to your location. \ + These bees are NOT friendly to anyone." + button_icon_state = "bee" + sound = 'sound/voice/moth/scream_moth.ogg' + + school = SCHOOL_CONJURATION + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 10 SECONDS + + invocation = "NOT THE BEES" + invocation_type = INVOCATION_SHOUT + + summon_radius = 3 + summon_type = list(/mob/living/simple_animal/hostile/bee/toxin) + summon_amount = 9 diff --git a/code/modules/spells/spell_types/conjure/carp.dm b/code/modules/spells/spell_types/conjure/carp.dm new file mode 100644 index 0000000000000..45007ee85037b --- /dev/null +++ b/code/modules/spells/spell_types/conjure/carp.dm @@ -0,0 +1,13 @@ +/datum/action/cooldown/spell/conjure/carp + name = "Summon Carp" + desc = "This spell conjures a simple carp." + sound = 'sound/magic/summon_karp.ogg' + + school = SCHOOL_CONJURATION + cooldown_time = 2 MINUTES + + invocation = "NOUK FHUNMM SACP RISSKA" + invocation_type = INVOCATION_SHOUT + + summon_radius = 1 + summon_type = list(/mob/living/simple_animal/hostile/carp) diff --git a/code/modules/spells/spell_types/conjure/constructs.dm b/code/modules/spells/spell_types/conjure/constructs.dm new file mode 100644 index 0000000000000..50124ce1319fa --- /dev/null +++ b/code/modules/spells/spell_types/conjure/constructs.dm @@ -0,0 +1,20 @@ +/datum/action/cooldown/spell/conjure/construct + name = "Summon Construct Shell" + desc = "This spell conjures a construct which may be controlled by Shades." + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "artificer" + sound = 'sound/magic/summonitems_generic.ogg' + + school = SCHOOL_CONJURATION + cooldown_time = 1 MINUTES + + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + summon_radius = 0 + summon_type = list(/obj/structure/constructshell) + +/datum/action/cooldown/spell/conjure/construct/lesser // Used by artificers. + name = "Create Construct Shell" + background_icon_state = "bg_demon" + cooldown_time = 3 MINUTES diff --git a/code/modules/spells/spell_types/conjure/creatures.dm b/code/modules/spells/spell_types/conjure/creatures.dm new file mode 100644 index 0000000000000..c51d4d114df00 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/creatures.dm @@ -0,0 +1,15 @@ +/datum/action/cooldown/spell/conjure/creature + name = "Summon Creature Swarm" + desc = "This spell tears the fabric of reality, allowing horrific daemons to spill forth." + sound = 'sound/magic/summonitems_generic.ogg' + + school = SCHOOL_CONJURATION + cooldown_time = 2 MINUTES + + invocation = "IA IA" + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + + summon_radius = 3 + summon_type = list(/mob/living/simple_animal/hostile/netherworld) + summon_amount = 10 diff --git a/code/modules/spells/spell_types/conjure/cult_turfs.dm b/code/modules/spells/spell_types/conjure/cult_turfs.dm new file mode 100644 index 0000000000000..7fec43aa8d404 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/cult_turfs.dm @@ -0,0 +1,29 @@ +/datum/action/cooldown/spell/conjure/cult_floor + name = "Summon Cult Floor" + desc = "This spell constructs a cult floor." + background_icon_state = "bg_cult" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "floorconstruct" + + school = SCHOOL_CONJURATION + cooldown_time = 2 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + summon_radius = 0 + summon_type = list(/turf/open/floor/engine/cult) + +/datum/action/cooldown/spell/conjure/cult_wall + name = "Summon Cult Wall" + desc = "This spell constructs a cult wall." + background_icon_state = "bg_cult" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "lesserconstruct" + + school = SCHOOL_CONJURATION + cooldown_time = 10 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + summon_radius = 0 + summon_type = list(/turf/closed/wall/mineral/cult/artificer) // We don't want artificer-based runed metal farms. diff --git a/code/modules/spells/spell_types/conjure/ed_swarm.dm b/code/modules/spells/spell_types/conjure/ed_swarm.dm new file mode 100644 index 0000000000000..db122e4c846a7 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/ed_swarm.dm @@ -0,0 +1,22 @@ +// test purposes - Also a lot of fun +/datum/action/cooldown/spell/conjure/summon_ed_swarm + name = "Dispense Wizard Justice" + desc = "This spell dispenses wizard justice." + + summon_radius = 3 + summon_type = list(/mob/living/simple_animal/bot/secbot/ed209) + summon_amount = 10 + +/datum/action/cooldown/spell/conjure/summon_ed_swarm/post_summon(atom/summoned_object, atom/cast_on) + if(!istype(summoned_object, /mob/living/simple_animal/bot/secbot/ed209)) + return + + var/mob/living/simple_animal/bot/secbot/ed209/summoned_bot = summoned_object + summoned_bot.name = "Wizard's Justicebot" + + summoned_bot.security_mode_flags = ~SECBOT_DECLARE_ARRESTS + summoned_bot.bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED + summoned_bot.bot_mode_flags |= BOT_COVER_EMAGGED + + summoned_bot.projectile = /obj/projectile/beam/laser + summoned_bot.shoot_sound = 'sound/weapons/laser.ogg' diff --git a/code/modules/spells/spell_types/conjure/invisible_chair.dm b/code/modules/spells/spell_types/conjure/invisible_chair.dm new file mode 100644 index 0000000000000..e0694898c096c --- /dev/null +++ b/code/modules/spells/spell_types/conjure/invisible_chair.dm @@ -0,0 +1,34 @@ +/datum/action/cooldown/spell/conjure/invisible_chair + name = "Invisible Chair" + desc = "The mime's performance transmutates a chair into physical reality." + background_icon_state = "bg_mime" + icon_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "invisible_chair" + panel = "Mime" + sound = null + + school = SCHOOL_MIME + cooldown_time = 30 SECONDS + invocation = "Someone does a weird gesture." // Overriden in before cast + invocation_self_message = span_notice("You conjure an invisible chair and sit down.") + invocation_type = INVOCATION_EMOTE + + spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW + antimagic_flags = NONE + spell_max_level = 1 + + summon_radius = 0 + summon_type = list(/obj/structure/chair/mime) + summon_lifespan = 25 SECONDS + +/datum/action/cooldown/spell/conjure/invisible_chair/before_cast(atom/cast_on) + . = ..() + invocation = span_notice("[cast_on] pulls out an invisible chair and sits down.") + +/datum/action/cooldown/spell/conjure/invisible_chair/post_summon(atom/summoned_object, mob/living/carbon/human/cast_on) + if(!isobj(summoned_object)) + return + + var/obj/chair = summoned_object + chair.setDir(cast_on.dir) + chair.buckle_mob(cast_on) diff --git a/code/modules/spells/spell_types/conjure/invisible_wall.dm b/code/modules/spells/spell_types/conjure/invisible_wall.dm new file mode 100644 index 0000000000000..9433fbd7df0a5 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/invisible_wall.dm @@ -0,0 +1,26 @@ +/datum/action/cooldown/spell/conjure/invisible_wall + name = "Invisible Wall" + desc = "The mime's performance transmutates a wall into physical reality." + background_icon_state = "bg_mime" + icon_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "invisible_wall" + panel = "Mime" + sound = null + + school = SCHOOL_MIME + cooldown_time = 30 SECONDS + invocation = "Someone does a weird gesture." // Overriden in before cast + invocation_self_message = span_notice("You form a wall in front of yourself.") + invocation_type = INVOCATION_EMOTE + + spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW + antimagic_flags = NONE + spell_max_level = 1 + + summon_radius = 0 + summon_type = list(/obj/effect/forcefield/mime) + summon_lifespan = 30 SECONDS + +/datum/action/cooldown/spell/conjure/invisible_wall/before_cast(atom/cast_on) + . = ..() + invocation = span_notice("[cast_on] looks as if a wall is in front of [cast_on.p_them()].") diff --git a/code/modules/spells/spell_types/conjure/link_words.dm b/code/modules/spells/spell_types/conjure/link_words.dm new file mode 100644 index 0000000000000..f227fc1a13e9a --- /dev/null +++ b/code/modules/spells/spell_types/conjure/link_words.dm @@ -0,0 +1,15 @@ +/datum/action/cooldown/spell/conjure/link_worlds + name = "Link Worlds" + desc = "A whole new dimension for you to play with! They won't be happy about it, though." + + sound = 'sound/weapons/marauder.ogg' + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 10 SECONDS + + invocation = "WTF" + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + + summon_radius = 1 + summon_type = list(/obj/structure/spawner/nether) + summon_amount = 1 diff --git a/code/modules/spells/spell_types/conjure/presents.dm b/code/modules/spells/spell_types/conjure/presents.dm new file mode 100644 index 0000000000000..057fef9b9b4a8 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/presents.dm @@ -0,0 +1,14 @@ +/datum/action/cooldown/spell/conjure/presents + name = "Conjure Presents!" + desc = "This spell lets you reach into S-space and retrieve presents! Yay!" + + school = SCHOOL_CONJURATION + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 13.75 SECONDS + + invocation = "HO HO HO" + invocation_type = INVOCATION_SHOUT + + summon_radius = 3 + summon_type = list(/obj/item/a_gift) + summon_amount = 5 diff --git a/code/modules/spells/spell_types/conjure/soulstone.dm b/code/modules/spells/spell_types/conjure/soulstone.dm new file mode 100644 index 0000000000000..cce5d1ab797ca --- /dev/null +++ b/code/modules/spells/spell_types/conjure/soulstone.dm @@ -0,0 +1,30 @@ +/datum/action/cooldown/spell/conjure/soulstone + name = "Summon Soulstone" + desc = "This spell reaches into Nar'Sie's realm, summoning one of the legendary fragments across time and space." + background_icon_state = "bg_demon" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "summonsoulstone" + + school = SCHOOL_CONJURATION + cooldown_time = 4 MINUTES + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + summon_radius = 0 + summon_type = list(/obj/item/soulstone) + +/datum/action/cooldown/spell/conjure/soulstone/cult + name = "Create Nar'sian Soulstone" + cooldown_time = 6 MINUTES + +/datum/action/cooldown/spell/conjure/soulstone/noncult + name = "Create Soulstone" + summon_type = list(/obj/item/soulstone/anybody) + +/datum/action/cooldown/spell/conjure/soulstone/purified + name = "Create Purified Soulstone" + summon_type = list(/obj/item/soulstone/anybody/purified) + +/datum/action/cooldown/spell/conjure/soulstone/mystic + name = "Create Mystic Soulstone" + summon_type = list(/obj/item/soulstone/mystic) diff --git a/code/modules/spells/spell_types/conjure/the_traps.dm b/code/modules/spells/spell_types/conjure/the_traps.dm new file mode 100644 index 0000000000000..e9717a1325329 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/the_traps.dm @@ -0,0 +1,35 @@ +/datum/action/cooldown/spell/conjure/the_traps + name = "The Traps!" + desc = "Summon a number of traps around you. They will damage and enrage any enemies that step on them." + button_icon_state = "the_traps" + + cooldown_time = 25 SECONDS + cooldown_reduction_per_rank = 5 SECONDS + + invocation = "CAVERE INSIDIAS" + invocation_type = INVOCATION_SHOUT + + summon_radius = 3 + summon_type = list( + /obj/structure/trap/stun, + /obj/structure/trap/fire, + /obj/structure/trap/chill, + /obj/structure/trap/damage, + ) + summon_lifespan = 5 MINUTES + summon_amount = 5 + + /// The amount of charges the traps spawn with. + var/trap_charges = 1 + +/datum/action/cooldown/spell/conjure/the_traps/post_summon(atom/summoned_object, atom/cast_on) + if(!istype(summoned_object, /obj/structure/trap)) + return + + var/obj/structure/trap/summoned_trap = summoned_object + summoned_trap.charges = trap_charges + + if(ismob(cast_on)) + var/mob/mob_caster = cast_on + if(mob_caster.mind) + summoned_trap.immune_minds += owner.mind diff --git a/code/modules/spells/spell_types/conjure_item/_conjure_item.dm b/code/modules/spells/spell_types/conjure_item/_conjure_item.dm new file mode 100644 index 0000000000000..5abac48b19733 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/_conjure_item.dm @@ -0,0 +1,46 @@ +/datum/action/cooldown/spell/conjure_item + school = SCHOOL_CONJURATION + invocation_type = INVOCATION_NONE + + /// Typepath of whatever item we summon + var/obj/item/item_type + /// If TRUE, we delete any previously created items when we cast the spell + var/delete_old = TRUE + /// List of weakrefs to items summoned + var/list/datum/weakref/item_refs + +/datum/action/cooldown/spell/conjure_item/Destroy() + // If we delete_old, clean up all of our items on delete + if(delete_old) + QDEL_LAZYLIST(item_refs) + + // If we don't delete_old, just let all the items be free + else + LAZYNULL(item_refs) + + return ..() + +/datum/action/cooldown/spell/conjure_item/is_valid_target(atom/cast_on) + return iscarbon(cast_on) + +/datum/action/cooldown/spell/conjure_item/cast(mob/living/carbon/cast_on) + if(delete_old && LAZYLEN(item_refs)) + QDEL_LAZYLIST(item_refs) + + var/obj/item/existing_item = cast_on.get_active_held_item() + if(existing_item) + cast_on.dropItemToGround(existing_item) + + var/obj/item/created = make_item() + if(QDELETED(created)) + CRASH("[type] tried to create an item, but failed. It's item type is [item_type].") + + cast_on.put_in_hands(created, del_on_fail = TRUE) + return ..() + +/// Instantiates the item we're conjuring and returns it. +/// Item is made in nullspace and moved out in cast(). +/datum/action/cooldown/spell/conjure_item/proc/make_item() + var/obj/item/made_item = new item_type() + LAZYADD(item_refs, WEAKREF(made_item)) + return made_item diff --git a/code/modules/spells/spell_types/conjure_item/infinite_guns.dm b/code/modules/spells/spell_types/conjure_item/infinite_guns.dm new file mode 100644 index 0000000000000..98921da4879dc --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/infinite_guns.dm @@ -0,0 +1,41 @@ +/datum/action/cooldown/spell/conjure_item/infinite_guns + school = SCHOOL_CONJURATION + cooldown_time = 1.25 MINUTES + cooldown_reduction_per_rank = 18.5 SECONDS + + invocation_type = INVOCATION_NONE + + item_type = /obj/item/gun/ballistic/rifle + // Enchanted guns self delete / do wacky stuff, anyways + delete_old = FALSE + +/datum/action/cooldown/spell/conjure_item/infinite_guns/Remove(mob/living/remove_from) + var/obj/item/existing = remove_from.is_holding_item_of_type(item_type) + if(existing) + qdel(existing) + + return ..() + +// Because enchanted guns self-delete and regenerate themselves, +// override make_item here and let's not bother with tracking their weakrefs. +/datum/action/cooldown/spell/conjure_item/infinite_guns/make_item() + return new item_type() + +/datum/action/cooldown/spell/conjure_item/infinite_guns/gun + name = "Lesser Summon Guns" + desc = "Why reload when you have infinite guns? \ + Summons an unending stream of bolt action rifles that deal little damage, \ + but will knock targets down. Requires both hands free to use. \ + Learning this spell makes you unable to learn Arcane Barrage." + button_icon_state = "bolt_action" + + item_type = /obj/item/gun/ballistic/rifle/enchanted + +/datum/action/cooldown/spell/conjure_item/infinite_guns/arcane_barrage + name = "Arcane Barrage" + desc = "Fire a torrent of arcane energy at your foes with this (powerful) spell. \ + Deals much more damage than Lesser Summon Guns, but won't knock targets down. Requires both hands free to use. \ + Learning this spell makes you unable to learn Lesser Summon Gun." + button_icon_state = "arcane_barrage" + + item_type = /obj/item/gun/ballistic/rifle/enchanted/arcane_barrage diff --git a/code/modules/spells/spell_types/conjure_item/invisible_box.dm b/code/modules/spells/spell_types/conjure_item/invisible_box.dm new file mode 100644 index 0000000000000..1e59598e514cd --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/invisible_box.dm @@ -0,0 +1,42 @@ +/datum/action/cooldown/spell/conjure_item/invisible_box + name = "Invisible Box" + desc = "The mime's performance transmutates a box into physical reality." + background_icon_state = "bg_mime" + icon_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "invisible_box" + panel = "Mime" + sound = null + + school = SCHOOL_MIME + cooldown_time = 30 SECONDS + invocation = "Someone does a weird gesture." // Overriden in before cast + invocation_self_message = span_notice("You conjure up an invisible box, large enough to store a few things.") + invocation_type = INVOCATION_EMOTE + + spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW + antimagic_flags = NONE + spell_max_level = 1 + + delete_old = FALSE + item_type = /obj/item/storage/box/mime + /// How long boxes last before going away + var/box_lifespan = 50 SECONDS + +/datum/action/cooldown/spell/conjure_item/invisible_box/before_cast(atom/cast_on) + . = ..() + invocation = span_notice("[cast_on] moves [cast_on.p_their()] hands in the shape of a cube, pressing a box out of the air.") + +/datum/action/cooldown/spell/conjure_item/invisible_box/make_item() + . = ..() + var/obj/item/made_box = . + made_box.alpha = 255 + addtimer(CALLBACK(src, .proc/cleanup_box, made_box), box_lifespan) + +/// Callback that gets rid out of box and removes the weakref from our list +/datum/action/cooldown/spell/conjure_item/invisible_box/proc/cleanup_box(obj/item/storage/box/box) + if(QDELETED(box) || !istype(box)) + return + + box.emptyStorage() + LAZYREMOVE(item_refs, WEAKREF(box)) + qdel(box) diff --git a/code/modules/spells/spell_types/conjure_item/lightning_packet.dm b/code/modules/spells/spell_types/conjure_item/lightning_packet.dm new file mode 100644 index 0000000000000..d7aeb099d54c9 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/lightning_packet.dm @@ -0,0 +1,38 @@ +/datum/action/cooldown/spell/conjure_item/spellpacket + name = "Thrown Lightning" + desc = "Forged from eldrich energies, a packet of pure power, \ + known as a spell packet will appear in your hand, that - when thrown - will stun the target." + button_icon_state = "thrownlightning" + + cooldown_time = 1 SECONDS + spell_max_level = 1 + + item_type = /obj/item/spellpacket/lightningbolt + +/datum/action/cooldown/spell/conjure_item/spellpacket/cast(mob/living/carbon/cast_on) + . = ..() + cast_on.throw_mode_on(THROW_MODE_TOGGLE) + +/obj/item/spellpacket/lightningbolt + name = "\improper Lightning bolt Spell Packet" + desc = "Some birdseed wrapped in cloth that crackles with electricity." + icon = 'icons/obj/toy.dmi' + icon_state = "snappop" + w_class = WEIGHT_CLASS_TINY + +/obj/item/spellpacket/lightningbolt/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) + . = ..() + if(.) + return + + if(isliving(hit_atom)) + var/mob/living/hit_living = hit_atom + if(!hit_living.can_block_magic()) + hit_living.electrocute_act(80, src, flags = SHOCK_ILLUSION) + qdel(src) + +/obj/item/spellpacket/lightningbolt/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = INFINITY, quickstart = TRUE) + . = ..() + if(ishuman(thrower)) + var/mob/living/carbon/human/human_thrower = thrower + human_thrower.say("LIGHTNINGBOLT!!", forced = "spell") diff --git a/code/modules/spells/spell_types/conjure_item/snowball.dm b/code/modules/spells/spell_types/conjure_item/snowball.dm new file mode 100644 index 0000000000000..bbc783a48edfe --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/snowball.dm @@ -0,0 +1,8 @@ +/datum/action/cooldown/spell/conjure_item/snowball + name = "Snowball" + desc = "Concentrates cryokinetic forces to create snowballs, useful for throwing at people." + icon_icon = 'icons/obj/toy.dmi' + button_icon_state = "snowball" + + cooldown_time = 1.5 SECONDS + item_type = /obj/item/toy/snowball diff --git a/code/modules/spells/spell_types/construct_spells.dm b/code/modules/spells/spell_types/construct_spells.dm deleted file mode 100644 index 7a0247f5b6e9c..0000000000000 --- a/code/modules/spells/spell_types/construct_spells.dm +++ /dev/null @@ -1,353 +0,0 @@ -//////////////////////////////Construct Spells///////////////////////// - -/obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser - charge_max = 3 MINUTES - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "artificer" - action_background_icon_state = "bg_demon" - -/obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser/cult - clothes_req = TRUE - charge_max = 250 SECONDS - -/obj/effect/proc_holder/spell/aoe_turf/area_conversion - name = "Area Conversion" - desc = "This spell instantly converts a small area around you." - - school = "transmutation" - charge_max = 5 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = 2 - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "areaconvert" - action_background_icon_state = "bg_cult" - -/obj/effect/proc_holder/spell/aoe_turf/area_conversion/cast(list/targets, mob/user = usr) - playsound(get_turf(user), 'sound/items/welder.ogg', 75, 1) - for(var/turf/T in targets) - T.narsie_act(FALSE, TRUE, 100 - (get_dist(user, T) * 25)) - - -/obj/effect/proc_holder/spell/aoe_turf/conjure/floor - name = "Summon Cult Floor" - desc = "This spell constructs a cult floor." - - school = "conjuration" - charge_max = 2 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = 0 - summon_type = list(/turf/open/floor/engine/cult) - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "floorconstruct" - action_background_icon_state = "bg_cult" - - -/obj/effect/proc_holder/spell/aoe_turf/conjure/wall - name = "Summon Cult Wall" - desc = "This spell constructs a cult wall." - - school = "conjuration" - charge_max = 10 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = 0 - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "lesserconstruct" - action_background_icon_state = "bg_cult" - - summon_type = list(/turf/closed/wall/mineral/cult/artificer) //we don't want artificer-based runed metal farms - -/obj/effect/proc_holder/spell/aoe_turf/conjure/door - name = "Summon Cult Door" - desc = "This spell constructs a cult Airlock." - - school = "conjuration" - charge_max = 30 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - invocation_time = 50 - range = 0 - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "airlockconstruct" - action_background_icon_state = "bg_cult" - - summon_type = list(/obj/machinery/door/airlock/cult) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/wall/reinforced - name = "Greater Construction" - desc = "This spell constructs a reinforced metal wall." - - school = "conjuration" - charge_max = 30 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = 0 - - summon_type = list(/turf/closed/wall/r_wall) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone - name = "Summon Soulstone" - desc = "This spell reaches into Nar'Sie's realm, summoning one of the legendary fragments across time and space." - - school = "conjuration" - charge_max = 4 MINUTES - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = 0 - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "summonsoulstone" - action_background_icon_state = "bg_demon" - - summon_type = list(/obj/item/soulstone) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/cult - clothes_req = TRUE - charge_max = 6 MINUTES - -/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/noncult - summon_type = list(/obj/item/soulstone/anybody) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/purified - summon_type = list(/obj/item/soulstone/anybody/purified) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/mystic - summon_type = list(/obj/item/soulstone/mystic) - -/obj/effect/proc_holder/spell/targeted/forcewall/cult - name = "Shield" - desc = "This spell creates a temporary forcefield to shield yourself and allies from incoming fire." - school = "transmutation" - charge_max = 40 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - wall_type = /obj/effect/forcefield/cult - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "cultforcewall" - action_background_icon_state = "bg_demon" - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift - name = "Phase Shift" - desc = "This spell allows you to pass through walls." - - school = "transmutation" - charge_max = 25 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - jaunt_duration = 5 SECONDS - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "phaseshift" - action_background_icon_state = "bg_demon" - jaunt_in_time = 0.6 SECONDS - jaunt_out_time = 0.6 SECONDS - jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith - jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/jaunt_steam(mobloc) - return - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/angelic - jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/angelic - jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/angelic - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/mystic - jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/mystic - jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/mystic - -/obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser - name = "Lesser Magic Missile" - desc = "This spell fires several, slow moving, magic projectiles at nearby targets." - - school = "evocation" - charge_max = 40 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - max_targets = 6 - action_icon_state = "magicm" - action_background_icon_state = "bg_demon" - proj_type = /obj/projectile/magic/spell/magic_missile/lesser - -/obj/projectile/magic/spell/magic_missile/lesser - color = "red" //Looks more culty this way - range = 10 - -/obj/projectile/magic/spell/magic_missile/lesser/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE) - if(ismob(target) && iscultist(target)) - return FALSE - return ..() - -/obj/effect/proc_holder/spell/targeted/smoke/disable - name = "Paralysing Smoke" - desc = "This spell spawns a cloud of paralysing smoke." - - school = "conjuration" - charge_max = 20 SECONDS - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = -1 - include_user = TRUE - cooldown_min = 20 //25 deciseconds reduction per rank - - smoke_spread = 3 - smoke_amt = 4 - action_icon_state = "smoke" - action_background_icon_state = "bg_cult" - - -/obj/effect/proc_holder/spell/targeted/abyssal_gaze - name = "Abyssal Gaze" - desc = "This spell instills a deep terror in your target, temporarily chilling and blinding it." - - charge_max = 75 SECONDS - range = 5 - include_user = FALSE - selection_type = "range" - stat_allowed = FALSE - - school = "evocation" - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_background_icon_state = "bg_demon" - action_icon_state = "abyssal_gaze" - -/obj/effect/proc_holder/spell/targeted/abyssal_gaze/cast(list/targets, mob/user = usr) - if(!LAZYLEN(targets)) - to_chat(user, "No target found in range.") - revert_cast() - return - - var/mob/living/carbon/target = targets[1] - - if(!(target in oview(range))) - to_chat(user, "[target] is too far away!") - revert_cast() - return - - if(target.anti_magic_check(TRUE, TRUE)) - to_chat(target, "You feel a freezing darkness closing in on you, but it rapidly dissipates.") - return - - to_chat(target, "A freezing darkness surrounds you...") - target.playsound_local(get_turf(target), 'sound/hallucinations/i_see_you1.ogg', 50, 1) - user.playsound_local(get_turf(user), 'sound/effects/ghost2.ogg', 50, 1) - target.become_blind(MAGIC_BLIND) - addtimer(CALLBACK(src, PROC_REF(cure_blindness), target), 40) - if(ishuman(targets[1])) - var/mob/living/carbon/human/humi = targets[1] - humi.adjust_coretemperature(-200) - target.adjust_bodytemperature(-200) - -/obj/effect/proc_holder/spell/targeted/abyssal_gaze/proc/cure_blindness(mob/living/L) - L.cure_blind(MAGIC_BLIND) - -/obj/effect/proc_holder/spell/targeted/dominate - name = "Dominate" - desc = "This spell dominates the mind of a lesser creature to the will of Nar'Sie, allying it only to her direct followers." - - charge_max = 1 MINUTES - range = 7 - include_user = FALSE - selection_type = "range" - stat_allowed = FALSE - - school = "evocation" - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_background_icon_state = "bg_demon" - action_icon_state = "dominate" - -/obj/effect/proc_holder/spell/targeted/dominate/cast(list/targets, mob/user = usr) - if(!LAZYLEN(targets)) - to_chat(user, "No target found in range.") - revert_cast() - return - - var/mob/living/simple_animal/S = targets[1] - - if(S.ckey) - to_chat(user, "[S] is too intelligent to dominate!") - revert_cast() - return - - if(S.stat) - to_chat(user, "[S] is dead!") - revert_cast() - return - - if(!istype(S) || S.sentience_type != SENTIENCE_ORGANIC) - to_chat(user, "[S] cannot be dominated!") - revert_cast() - return - - if(!(S in oview(range))) - to_chat(user, "[S] is too far away!") - revert_cast() - return - - S.add_atom_colour("#990000", FIXED_COLOUR_PRIORITY) - S.faction = list("cult") - playsound(get_turf(S), 'sound/effects/ghost.ogg', 100, 1) - new /obj/effect/temp_visual/cult/sac(get_turf(S)) - -/obj/effect/proc_holder/spell/targeted/dominate/can_target(mob/living/target) - if(!isanimal(target) || target.stat) - return FALSE - if("cult" in target.faction) - return FALSE - return TRUE - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/golem - charge_max = 80 SECONDS - jaunt_in_type = /obj/effect/temp_visual/dir_setting/cult/phase - jaunt_out_type = /obj/effect/temp_visual/dir_setting/cult/phase/out - - -/obj/effect/proc_holder/spell/targeted/projectile/dumbfire/juggernaut - name = "Gauntlet Echo" - desc = "Channels energy into your gauntlet - firing its essence forward in a slow moving, yet devastating, attack." - proj_type = /obj/projectile/magic/spell/juggernaut - charge_max = 35 SECONDS - clothes_req = FALSE - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_icon_state = "cultfist" - action_background_icon_state = "bg_demon" - sound = 'sound/weapons/resonator_blast.ogg' - -/obj/projectile/magic/spell/juggernaut - name = "Gauntlet Echo" - icon_state = "cultfist" - alpha = 180 - damage = 30 - damage_type = BRUTE - knockdown = 50 - hitsound = 'sound/weapons/punch3.ogg' - trigger_range = 0 - check_holy = TRUE - ignored_factions = list("cult") - range = 15 - speed = 7 - -/obj/projectile/magic/spell/juggernaut/on_hit(atom/target, blocked) - . = ..() - var/turf/T = get_turf(src) - playsound(T, 'sound/weapons/resonator_blast.ogg', 100, FALSE) - new /obj/effect/temp_visual/cult/sac(T) - for(var/obj/O in range(1, src)) - if(O.density && !istype(O, /obj/structure/destructible/cult)) - O.take_damage(90, BRUTE, MELEE, 0) - new /obj/effect/temp_visual/cult/turf/floor(get_turf(O)) diff --git a/code/modules/spells/spell_types/emplosion.dm b/code/modules/spells/spell_types/emplosion.dm deleted file mode 100644 index 9929b342d622b..0000000000000 --- a/code/modules/spells/spell_types/emplosion.dm +++ /dev/null @@ -1,17 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/emplosion - name = "Emplosion" - desc = "This spell emplodes an area." - - var/emp_heavy = 2 - var/emp_light = 3 - - action_icon_state = "emp" - sound = 'sound/weapons/zapbang.ogg' - -/obj/effect/proc_holder/spell/targeted/emplosion/cast(list/targets,mob/user = usr) - playsound(get_turf(user), sound, 50,1) - for(var/mob/living/target in targets) - if(target.anti_magic_check() && target != user) - continue - empulse(target.loc, emp_heavy, emp_light, magic=TRUE) - return diff --git a/code/modules/spells/spell_types/ethereal_jaunt.dm b/code/modules/spells/spell_types/ethereal_jaunt.dm deleted file mode 100644 index 198cd51b0ee4c..0000000000000 --- a/code/modules/spells/spell_types/ethereal_jaunt.dm +++ /dev/null @@ -1,123 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt - name = "Ethereal Jaunt" - desc = "This spell turns your form ethereal, temporarily making you invisible and able to pass through walls." - - school = "transmutation" - charge_max = 30 SECONDS - clothes_req = TRUE - invocation = "none" - invocation_type = INVOCATION_NONE - range = -1 - cooldown_min = 10 SECONDS - include_user = TRUE - nonabstract_req = TRUE - action_icon_state = "jaunt" - /// For how long are we jaunting? - var/jaunt_duration = 5 SECONDS - /// For how long we become immobilized after exiting the jaunt. - var/jaunt_in_time = 0.5 SECONDS - /// For how long we become immobilized when using this spell. - var/jaunt_out_time = 0 SECONDS - /// Visual for jaunting - var/jaunt_in_type = /obj/effect/temp_visual/wizard - /// Visual for exiting the jaunt - var/jaunt_out_type = /obj/effect/temp_visual/wizard/out - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/cast(list/targets,mob/user = usr) //magnets, so mostly hardcoded - play_sound("enter",user) - for(var/mob/living/target in targets) - INVOKE_ASYNC(src, PROC_REF(do_jaunt), target) - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/do_jaunt(mob/living/target) - target.notransform = 1 - var/turf/mobloc = get_turf(target) - var/obj/effect/dummy/phased_mob/spell_jaunt/holder = new /obj/effect/dummy/phased_mob/spell_jaunt(mobloc) - new jaunt_out_type(mobloc, target.dir) - target.ExtinguishMob() - target.forceMove(holder) - target.reset_perspective(holder) - target.notransform=0 //mob is safely inside holder now, no need for protection. - target.ignore_slowdown(JAUNT_TRAIT) - jaunt_steam(mobloc) - if(jaunt_out_time) - //ADD_TRAIT(target, TRAIT_IMMOBILIZED, type) - sleep(jaunt_out_time) - //REMOVE_TRAIT(target, TRAIT_IMMOBILIZED, type) - - sleep(jaunt_duration) - - target.unignore_slowdown(JAUNT_TRAIT) - if(target.loc != holder) //mob warped out of the warp - qdel(holder) - return - mobloc = get_turf(target.loc) - jaunt_steam(mobloc) - ADD_TRAIT(target, TRAIT_IMMOBILIZED, type) - holder.reappearing = 1 - play_sound("exit",target) - sleep(25 - jaunt_in_time) - new jaunt_in_type(mobloc, holder.dir) - target.setDir(holder.dir) - sleep(jaunt_in_time) - qdel(holder) - if(!QDELETED(target)) - if(mobloc.density) - for(var/direction in GLOB.alldirs) - var/turf/T = get_step(mobloc, direction) - if(T) - if(target.Move(T)) - break - REMOVE_TRAIT(target, TRAIT_IMMOBILIZED, type) - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/jaunt_steam(mobloc) - var/datum/effect_system/steam_spread/steam = new /datum/effect_system/steam_spread() - steam.set_up(10, 0, mobloc) - steam.start() - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/play_sound(type,mob/living/target) - switch(type) - if("enter") - playsound(get_turf(target), 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1) - if("exit") - playsound(get_turf(target), 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) - -/obj/effect/dummy/phased_mob/spell_jaunt - name = "water" - icon = 'icons/effects/effects.dmi' - icon_state = "nothing" - var/reappearing = FALSE - var/movedelay = 0 - var/movespeed = 2 - density = FALSE - anchored = TRUE - invisibility = 60 - resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF - -/obj/effect/dummy/phased_mob/spell_jaunt/Destroy() - // Eject contents if deleted somehow - for(var/atom/movable/AM in src) - AM.forceMove(get_turf(src)) - return ..() - -/obj/effect/dummy/phased_mob/spell_jaunt/relaymove(mob/living/user, direction) - if ((movedelay > world.time) || reappearing || !direction) - return - var/turf/newLoc = get_step(src,direction) - setDir(direction) - - movedelay = world.time + movespeed - - if(newLoc.flags_1 & NOJAUNT_1) - to_chat(user, "Some strange aura is blocking the way.") - return - if (newLoc.is_holy()) - to_chat(user, "Holy energies block your path!") - return - - forceMove(newLoc) - -/obj/effect/dummy/phased_mob/spell_jaunt/ex_act(blah) - return - -/obj/effect/dummy/phased_mob/spell_jaunt/bullet_act(blah) - return BULLET_ACT_FORCE_PIERCE diff --git a/code/modules/spells/spell_types/explosion.dm b/code/modules/spells/spell_types/explosion.dm deleted file mode 100644 index 201c11048b571..0000000000000 --- a/code/modules/spells/spell_types/explosion.dm +++ /dev/null @@ -1,16 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/explosion - name = "Explosion" - desc = "This spell explodes an area." - - var/ex_severe = 1 - var/ex_heavy = 2 - var/ex_light = 3 - var/ex_flash = 4 - -/obj/effect/proc_holder/spell/targeted/explosion/cast(list/targets,mob/user = usr) - for(var/mob/living/target in targets) - if(target.anti_magic_check()) - continue - explosion(target.loc,ex_severe,ex_heavy,ex_light,ex_flash, magic = TRUE) - - return diff --git a/code/modules/spells/spell_types/forcewall.dm b/code/modules/spells/spell_types/forcewall.dm deleted file mode 100644 index aab1f8961772a..0000000000000 --- a/code/modules/spells/spell_types/forcewall.dm +++ /dev/null @@ -1,40 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/forcewall - name = "Forcewall" - desc = "Create a magical barrier that only you can pass through." - school = "transmutation" - charge_max = 100 - clothes_req = FALSE - invocation = "TARCOL MINTI ZHERI" - invocation_type = INVOCATION_SHOUT - sound = 'sound/magic/forcewall.ogg' - action_icon_state = "shield" - range = -1 - include_user = TRUE - cooldown_min = 50 //12 deciseconds reduction per rank - var/wall_type = /obj/effect/forcefield/wizard - -/obj/effect/proc_holder/spell/targeted/forcewall/cast(list/targets,mob/user = usr) - new wall_type(get_turf(user), null, user) - if(user.dir == SOUTH || user.dir == NORTH) - new wall_type(get_step(user, EAST), null, user) - new wall_type(get_step(user, WEST), null, user) - else - new wall_type(get_step(user, NORTH), null, user) - new wall_type(get_step(user, SOUTH), null, user) - - -/obj/effect/forcefield/wizard - var/mob/wizard - -/obj/effect/forcefield/wizard/Initialize(mapload, ntimeleft, mob/summoner) - . = ..() - wizard = summoner - -/obj/effect/forcefield/wizard/CanAllowThrough(atom/movable/mover, border_dir) - . = ..() - if(mover == wizard) - return TRUE - if(isliving(mover)) - var/mob/living/living_mover = mover - if(living_mover.anti_magic_check(major = FALSE)) - return TRUE diff --git a/code/modules/spells/spell_types/genetic.dm b/code/modules/spells/spell_types/genetic.dm deleted file mode 100644 index 07bac681beb14..0000000000000 --- a/code/modules/spells/spell_types/genetic.dm +++ /dev/null @@ -1,45 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/genetic - name = "Genetic" - desc = "This spell inflicts a set of mutations and disabilities upon the target." - - var/list/active_on = list() - var/list/traits = list() //disabilities - var/list/mutations = list() //mutation defines - var/duration = 100 //deciseconds - /* - Disabilities - 1st bit - ? - 2nd bit - ? - 3rd bit - ? - 4th bit - ? - 5th bit - ? - 6th bit - ? - */ - -/obj/effect/proc_holder/spell/targeted/genetic/cast(list/targets,mob/user = usr) - playMagSound() - for(var/mob/living/carbon/target in targets) - if(target.anti_magic_check()) - continue - if(!target.has_dna()) - continue - for(var/A in mutations) - target.dna.add_mutation(A) - for(var/A in traits) - ADD_TRAIT(target, A, GENETICS_SPELL) - active_on += target - if(duration < charge_max) - addtimer(CALLBACK(src, PROC_REF(remove), target), duration, TIMER_OVERRIDE|TIMER_UNIQUE) - -/obj/effect/proc_holder/spell/targeted/genetic/Destroy() - . = ..() - for(var/V in active_on) - remove(V) - -/obj/effect/proc_holder/spell/targeted/genetic/proc/remove(mob/living/carbon/target) - active_on -= target - if(!QDELETED(target) && target.has_dna()) - for(var/A in mutations) - target.dna.remove_mutation(A) - for(var/A in traits) - REMOVE_TRAIT(target, A, GENETICS_SPELL) diff --git a/code/modules/spells/spell_types/godhand.dm b/code/modules/spells/spell_types/godhand.dm deleted file mode 100644 index 6ac95d96eb844..0000000000000 --- a/code/modules/spells/spell_types/godhand.dm +++ /dev/null @@ -1,204 +0,0 @@ -/obj/item/melee/touch_attack - name = "\improper outstretched hand" - desc = "High Five?" - var/catchphrase = "High Five!" - var/on_use_sound = null - var/obj/effect/proc_holder/spell/targeted/touch/attached_spell - icon = 'icons/obj/items_and_weapons.dmi' - lefthand_file = 'icons/mob/inhands/misc/touchspell_lefthand.dmi' - righthand_file = 'icons/mob/inhands/misc/touchspell_righthand.dmi' - icon_state = "syndballoon" - item_state = null - item_flags = NEEDS_PERMIT | ABSTRACT | DROPDEL | ISWEAPON - w_class = WEIGHT_CLASS_HUGE - force = 0 - throwforce = 0 - throw_range = 0 - throw_speed = 0 - var/charges = 1 - -/obj/item/melee/touch_attack/Initialize(mapload, obj/effect/proc_holder/spell/targeted/touch/_spell) - . = ..() - ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) - if(istype(_spell)) - attached_spell = _spell - -/obj/item/melee/touch_attack/attack(mob/target, mob/living/carbon/user) - if(!iscarbon(user)) //Look ma, no hands - return - if(!(user.mobility_flags & MOBILITY_USE)) - to_chat(user, "You can't reach out!") - return - ..() - -/obj/item/melee/touch_attack/afterattack(atom/target, mob/user, proximity) - . = ..() - if(!proximity) - return - if(charges > 0) - use_charge(user) - - -/obj/item/melee/touch_attack/proc/use_charge(mob/living/user, whisper = FALSE) - if(QDELETED(src)) - return - - if(catchphrase) - if(whisper) - user.say("#[catchphrase]", forced = "spell") - else - user.say(catchphrase, forced = "spell") - if(!isnull(on_use_sound)) - playsound(get_turf(user), on_use_sound, 50, TRUE) - if(--charges <= 0) - attached_spell.use_charge() - qdel(src) - -/obj/item/melee/touch_attack/Destroy() - if(attached_spell) - attached_spell.on_hand_destroy(src) - return ..() - -/obj/item/melee/touch_attack/disintegrate - name = "\improper disintegrating touch" - desc = "This hand of mine glows with an awesome power!" - catchphrase = "EI NATH!!" - on_use_sound = 'sound/magic/disintegrate.ogg' - icon_state = "disintegrate" - item_state = "disintegrate" - -/obj/item/melee/touch_attack/disintegrate/afterattack(atom/target, mob/living/carbon/user, proximity) - if(!proximity || target == user || !ismob(target) || !iscarbon(user) || !(user.mobility_flags & MOBILITY_USE)) //exploding after touching yourself would be bad - return - if(!user.can_speak_vocal()) - to_chat(user, "You can't get the words out!") - return - var/mob/M = target - do_sparks(4, FALSE, M.loc) - for(var/mob/living/L in viewers(7, get_turf(src))) - if(L != user) - L.flash_act(affect_silicon = FALSE) - var/atom/A = M.anti_magic_check() - if(A) - if(isitem(A)) - target.visible_message("[target]'s [A] glows brightly as it wards off the spell!") - user.visible_message("The feedback blows [user]'s arm off!","The spell bounces from [M]'s skin back into your arm!") - user.flash_act() - var/obj/item/bodypart/part = user.get_holding_bodypart_of_item(src) - if(part) - part.dismember() - return ..() - var/obj/item/clothing/suit/hooded/bloated_human/suit = M.get_item_by_slot(ITEM_SLOT_OCLOTHING) - if(istype(suit)) - M.visible_message("[M]'s [suit] explodes off of them into a puddle of gore!") - M.dropItemToGround(suit) - qdel(suit) - new /obj/effect/gibspawner(M.loc) - return ..() - M.gib() - return ..() - -/obj/item/melee/touch_attack/fleshtostone - name = "\improper petrifying touch" - desc = "That's the bottom line, because flesh to stone said so!" - catchphrase = "STAUN EI!!" - on_use_sound = 'sound/magic/fleshtostone.ogg' - icon_state = "fleshtostone" - item_state = "fleshtostone" - -/obj/item/melee/touch_attack/fleshtostone/afterattack(atom/target, mob/living/carbon/user, proximity) - if(!proximity || target == user || !isliving(target) || !iscarbon(user)) //getting hard after touching yourself would also be bad - return - if(!(user.mobility_flags & MOBILITY_USE)) - to_chat(user, "You can't reach out!") - return - if(!user.can_speak_vocal()) - to_chat(user, "You can't get the words out!") - return - var/mob/living/M = target - if(M.anti_magic_check()) - to_chat(user, "The spell can't seem to affect [M]!") - to_chat(M, "You feel your flesh turn to stone for a moment, then revert back!") - ..() - return - M.Stun(40) - M.petrify() - return ..() - - -/obj/item/melee/touch_attack/megahonk - name = "\improper honkmother's blessing" - desc = "You've got a feeling they won't be laughing after this one. Honk honk." - catchphrase = "HONKDOOOOUKEN!" - on_use_sound = 'sound/items/airhorn.ogg' - icon = 'icons/mecha/mecha_equipment.dmi' - icon_state = "mecha_honker" - -/obj/item/melee/touch_attack/megahonk/afterattack(atom/target, mob/living/carbon/user, proximity) - if(!proximity || !iscarbon(target) || !iscarbon(user) || user.handcuffed) - return - playsound(get_turf(target), on_use_sound,100,1) - for(var/mob/living/carbon/M in (hearers(1, target) - user)) //3x3 around the target, not affecting the user - if(ishuman(M)) - var/mob/living/carbon/human/H = M - if(istype(H.ears, /obj/item/clothing/ears/earmuffs)) - continue - var/mul = (M==target ? 1 : 0.5) - to_chat(M, "HONK") - M.SetSleeping(0) - M.stuttering += 20*mul - M.adjustEarDamage(0, 30*mul) - M.Knockdown(60*mul) - if(prob(40)) - M.Knockdown(200*mul) - else - M.Jitter(500*mul) - - . = ..() - -/obj/item/melee/touch_attack/megahonk/attack_self(mob/user) - . = ..() - to_chat(user, "\The [src] disappears, to honk another day.") - qdel(src) - -/obj/item/melee/touch_attack/bspie - name = "\improper bluespace pie" - desc = "A thing you can barely comprehend as you hold it in your hand. You're fairly sure you could fit an entire body inside." - on_use_sound = 'sound/magic/demon_consume.ogg' - icon = 'icons/obj/food/piecake.dmi' - icon_state = "frostypie" - color = "#000077" - -/obj/item/melee/touch_attack/bspie/attack_self(mob/user) - . = ..() - to_chat(user, "You smear \the [src] on your chest! ") - qdel(src) - -/obj/item/melee/touch_attack/bspie/afterattack(atom/target, mob/living/carbon/user, proximity) - if(!proximity || !iscarbon(target) || !iscarbon(user) || user.handcuffed) - return - if(target == user) - to_chat(user, "You smear \the [src] on your chest!") - qdel(src) - return - var/mob/living/carbon/M = target - - user.visible_message("[user] is trying to stuff [M]\s body into \the [src]!") - if(do_after(user, 25 SECONDS, M)) - var/name = M.real_name - var/obj/item/food/pie/cream/body/pie = new(get_turf(M)) - pie.name = "\improper [name] [pie.name]" - - . = ..() - - M.forceMove(pie) - -/obj/item/melee/touch_attack/mutation - catchphrase = null - var/datum/mutation/parent_mutation - -/obj/item/melee/touch_attack/mutation/Initialize(_mapload, obj/effect/proc_holder/spell/targeted/touch/_spell, datum/mutation/_parent) - . = ..() - if(!istype(_parent)) - return INITIALIZE_HINT_QDEL - parent_mutation = _parent diff --git a/code/modules/spells/spell_types/infinite_guns.dm b/code/modules/spells/spell_types/infinite_guns.dm deleted file mode 100644 index 2330e9338810c..0000000000000 --- a/code/modules/spells/spell_types/infinite_guns.dm +++ /dev/null @@ -1,27 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/infinite_guns - name = "Lesser Summon Guns" - desc = "Why reload when you have infinite guns? Summons an unending stream of bolt action rifles that deal little damage, but will knock targets down. Requires both hands free to use. Learning this spell makes you unable to learn Arcane Barrage." - invocation_type = INVOCATION_NONE - include_user = TRUE - range = -1 - - school = "conjuration" - charge_max = 750 - clothes_req = TRUE - cooldown_min = 10 //Gun wizard - action_icon_state = "bolt_action" - var/summon_path = /obj/item/gun/ballistic/rifle/boltaction/enchanted - -/obj/effect/proc_holder/spell/targeted/infinite_guns/cast(list/targets, mob/user = usr) - for(var/mob/living/carbon/C in targets) - C.drop_all_held_items() - var/GUN = new summon_path - C.put_in_hands(GUN) - -/obj/effect/proc_holder/spell/targeted/infinite_guns/gun - -/obj/effect/proc_holder/spell/targeted/infinite_guns/arcane_barrage - name = "Arcane Barrage" - desc = "Fire a torrent of arcane energy at your foes with this (powerful) spell. Deals much more damage than Lesser Summon Guns, but won't knock targets down. Requires both hands free to use. Learning this spell makes you unable to learn Lesser Summon Gun." - action_icon_state = "arcane_barrage" - summon_path = /obj/item/gun/ballistic/rifle/boltaction/enchanted/arcane_barrage diff --git a/code/modules/spells/spell_types/jaunt/_jaunt.dm b/code/modules/spells/spell_types/jaunt/_jaunt.dm new file mode 100644 index 0000000000000..af311947366b6 --- /dev/null +++ b/code/modules/spells/spell_types/jaunt/_jaunt.dm @@ -0,0 +1,92 @@ +/** + * ## Jaunt spells + * + * A basic subtype for jaunt related spells. + * Jaunt spells put their caster in a dummy + * phased_mob effect that allows them to float + * around incorporeally. + * + * Doesn't actually implement any behavior on cast to + * enter or exit the jaunt - that must be done via subtypes. + * + * Use enter_jaunt() and exit_jaunt() as wrappers. + */ +/datum/action/cooldown/spell/jaunt + school = SCHOOL_TRANSMUTATION + + invocation_type = INVOCATION_NONE + + /// What dummy mob type do we put jaunters in on jaunt? + var/jaunt_type = /obj/effect/dummy/phased_mob + +/datum/action/cooldown/spell/jaunt/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + var/area/owner_area = get_area(owner) + var/turf/owner_turf = get_turf(owner) + if(!owner_area || !owner_turf) + return FALSE // nullspaced? + + if(owner_area.area_flags & NOTELEPORT) + if(feedback) + to_chat(owner, span_danger("Some dull, universal force is stopping you from jaunting here.")) + return FALSE + + if(owner_turf?.turf_flags & NOJAUNT) + if(feedback) + to_chat(owner, span_danger("An otherwordly force is preventing you from jaunting here.")) + return FALSE + + return isliving(owner) + + +/** + * Places the [jaunter] in a jaunt holder mob + * If [loc_override] is supplied, + * the jaunt will be moved to that turf to start at + * + * Returns the holder mob that was created + */ +/datum/action/cooldown/spell/jaunt/proc/enter_jaunt(mob/living/jaunter, turf/loc_override) + var/obj/effect/dummy/phased_mob/jaunt = new jaunt_type(loc_override || get_turf(jaunter), jaunter) + spell_requirements |= SPELL_CASTABLE_WHILE_PHASED + ADD_TRAIT(jaunter, TRAIT_MAGICALLY_PHASED, REF(src)) + + // This needs to happen at the end, after all the traits and stuff is handled + SEND_SIGNAL(jaunter, COMSIG_MOB_ENTER_JAUNT, src, jaunt) + return jaunt + +/** + * Ejects the [unjaunter] from jaunt + * If [loc_override] is supplied, + * the jaunt will be moved to that turf + * before ejecting the unjaunter + * + * Returns TRUE on successful exit, FALSE otherwise + */ +/datum/action/cooldown/spell/jaunt/proc/exit_jaunt(mob/living/unjaunter, turf/loc_override) + var/obj/effect/dummy/phased_mob/jaunt = unjaunter.loc + if(!istype(jaunt)) + return FALSE + + if(jaunt.jaunter != unjaunter) + CRASH("Jaunt spell attempted to exit_jaunt with an invalid unjaunter, somehow.") + + if(loc_override) + jaunt.forceMove(loc_override) + jaunt.eject_jaunter() + spell_requirements &= ~SPELL_CASTABLE_WHILE_PHASED + REMOVE_TRAIT(unjaunter, TRAIT_MAGICALLY_PHASED, REF(src)) + + // Ditto - this needs to happen at the end, after all the traits and stuff is handled + SEND_SIGNAL(unjaunter, COMSIG_MOB_AFTER_EXIT_JAUNT, src) + return TRUE + +/// Simple helper to check if the passed mob is currently jaunting or not +/datum/action/cooldown/spell/jaunt/proc/is_jaunting(mob/living/user) + return istype(user.loc, /obj/effect/dummy/phased_mob) + +/datum/action/cooldown/spell/jaunt/Remove(mob/living/remove_from) + exit_jaunt(remove_from) + return ..() diff --git a/code/modules/spells/spell_types/jaunt/bloodcrawl.dm b/code/modules/spells/spell_types/jaunt/bloodcrawl.dm new file mode 100644 index 0000000000000..365234c649a54 --- /dev/null +++ b/code/modules/spells/spell_types/jaunt/bloodcrawl.dm @@ -0,0 +1,315 @@ +/** + * ### Blood Crawl + * + * Lets the caster enter and exit pools of blood. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl + name = "Blood Crawl" + desc = "Allows you to phase in and out of existance via pools of blood." + background_icon_state = "bg_demon" + icon_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "bloodcrawl" + + spell_requirements = NONE + + /// The time it takes to enter blood + var/enter_blood_time = 0 SECONDS + /// The time it takes to exit blood + var/exit_blood_time = 2 SECONDS + /// The radius around us that we look for blood in + var/blood_radius = 1 + /// If TRUE, we equip "blood crawl" hands to the jaunter to prevent using items + var/equip_blood_hands = TRUE + +/datum/action/cooldown/spell/jaunt/bloodcrawl/cast(mob/living/cast_on) + . = ..() + for(var/obj/effect/decal/cleanable/blood_nearby in range(blood_radius, get_turf(cast_on))) + if(blood_nearby.can_bloodcrawl_in()) + return do_bloodcrawl(blood_nearby, cast_on) + + reset_spell_cooldown() + to_chat(cast_on, span_warning("There must be a nearby source of blood!")) + +/** + * Attempts to enter or exit the passed blood pool. + * Returns TRUE if we successfully entered or exited said pool, FALSE otherwise + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/do_bloodcrawl(obj/effect/decal/cleanable/blood, mob/living/jaunter) + if(is_jaunting(jaunter)) + . = try_exit_jaunt(blood, jaunter) + else + . = try_enter_jaunt(blood, jaunter) + + if(!.) + reset_spell_cooldown() + to_chat(jaunter, span_warning("You are unable to blood crawl!")) + +/** + * Attempts to enter the passed blood pool. + * If forced is TRUE, it will override enter_blood_time. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/try_enter_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter, forced = FALSE) + if(!forced) + if(enter_blood_time > 0 SECONDS) + blood.visible_message(span_warning("[jaunter] starts to sink into [blood]!")) + if(!do_after(jaunter, enter_blood_time, target = blood)) + return FALSE + + // The actual turf we enter + var/turf/jaunt_turf = get_turf(blood) + + // Begin the jaunt + jaunter.notransform = TRUE + var/obj/effect/dummy/phased_mob/holder = enter_jaunt(jaunter, jaunt_turf) + if(!holder) + jaunter.notransform = FALSE + return FALSE + + if(equip_blood_hands && iscarbon(jaunter)) + jaunter.drop_all_held_items() + // Give them some bloody hands to prevent them from doing things + var/obj/item/bloodcrawl/left_hand = new(jaunter) + var/obj/item/bloodcrawl/right_hand = new(jaunter) + left_hand.icon_state = "bloodhand_right" // Icons swapped intentionally.. + right_hand.icon_state = "bloodhand_left" // ..because perspective, or something + jaunter.put_in_hands(left_hand) + jaunter.put_in_hands(right_hand) + + blood.visible_message(span_warning("[jaunter] sinks into [blood]!")) + playsound(jaunt_turf, 'sound/magic/enter_blood.ogg', 50, TRUE, -1) + jaunter.extinguish_mob() + + jaunter.notransform = FALSE + return TRUE + +/** + * Attempts to Exit the passed blood pool. + * If forced is TRUE, it will override exit_blood_time, and if we're currently consuming someone. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/try_exit_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter, forced = FALSE) + if(!forced) + if(jaunter.notransform) + to_chat(jaunter, span_warning("You cannot exit yet!!")) + return FALSE + + if(exit_blood_time > 0 SECONDS) + blood.visible_message(span_warning("[blood] starts to bubble...")) + if(!do_after(jaunter, exit_blood_time, target = blood)) + return FALSE + + if(!exit_jaunt(jaunter, get_turf(blood))) + return FALSE + + if(equip_blood_hands && iscarbon(jaunter)) + for(var/obj/item/bloodcrawl/blood_hand in jaunter.held_items) + jaunter.temporarilyRemoveItemFromInventory(blood_hand, force = TRUE) + qdel(blood_hand) + + blood.visible_message(span_boldwarning("[jaunter] rises out of [blood]!")) + return TRUE + +/datum/action/cooldown/spell/jaunt/bloodcrawl/exit_jaunt(mob/living/unjaunter, turf/loc_override) + . = ..() + if(!.) + return + + exit_blood_effect(unjaunter) + +/// Adds an coloring effect to mobs which exit blood crawl. +/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/exit_blood_effect(mob/living/exited) + var/turf/landing_turf = get_turf(exited) + playsound(landing_turf, 'sound/magic/exit_blood.ogg', 50, TRUE, -1) + + // Make the mob have the color of the blood pool it came out of + var/obj/effect/decal/cleanable/came_from = locate() in landing_turf + var/new_color = came_from?.get_blood_color() + if(!new_color) + return + + exited.add_atom_colour(new_color, TEMPORARY_COLOUR_PRIORITY) + // ...but only for a few seconds + addtimer(CALLBACK(exited, /atom/.proc/remove_atom_colour, TEMPORARY_COLOUR_PRIORITY, new_color), 6 SECONDS) + +/** + * Slaughter demon's blood crawl + * Allows the blood crawler to consume people they are dragging. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon + name = "Voracious Blood Crawl" + desc = "Allows you to phase in and out of existance via pools of blood. If you are dragging someone in critical or dead, \ + they will be consumed by you, fully healing you." + /// The sound played when someone's consumed. + var/consume_sound = 'sound/magic/demon_consume.ogg' + +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/try_enter_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter) + // Save this before the actual jaunt + var/atom/coming_with = jaunter.pulling + + // Does the actual jaunt + . = ..() + if(!.) + return + + var/turf/jaunt_turf = get_turf(jaunter) + // if we're not pulling anyone, or we can't what we're pulling + if(!isliving(coming_with)) + return + + var/mob/living/victim = coming_with + + if(victim.stat == CONSCIOUS) + jaunt_turf.visible_message( + span_warning("[victim] kicks free of [blood] just before entering it!"), + blind_message = span_notice("You hear splashing and struggling."), + ) + return FALSE + + if(SEND_SIGNAL(victim, COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED, src, jaunter, blood) & COMPONENT_STOP_CONSUMPTION) + return FALSE + + victim.forceMove(jaunter) + victim.emote("scream") + jaunt_turf.visible_message( + span_boldwarning("[jaunter] drags [victim] into [blood]!"), + blind_message = span_notice("You hear a splash."), + ) + + jaunter.notransform = TRUE + consume_victim(victim, jaunter) + jaunter.notransform = FALSE + + return TRUE + +/** + * Consumes the [victim] from the [jaunter], fully healing them + * and calling [proc/on_victim_consumed] if successful. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/proc/consume_victim(mob/living/victim, mob/living/jaunter) + on_victim_start_consume(victim, jaunter) + + for(var/i in 1 to 3) + playsound(get_turf(jaunter), consume_sound, 50, TRUE) + if(!do_after(jaunter, 3 SECONDS, victim)) + to_chat(jaunter, span_danger("You lose your victim!")) + return FALSE + if(QDELETED(src)) + return FALSE + + if(SEND_SIGNAL(victim, COMSIG_LIVING_BLOOD_CRAWL_CONSUMED, src, jaunter) & COMPONENT_STOP_CONSUMPTION) + return FALSE + + jaunter.revive(full_heal = TRUE, admin_revive = FALSE) + + // No defib possible after laughter + victim.apply_damage(1000, BRUTE, wound_bonus = CANT_WOUND) + victim.death() + on_victim_consumed(victim, jaunter) + +/** + * Called when a victim starts to be consumed. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/proc/on_victim_start_consume(mob/living/victim, mob/living/jaunter) + to_chat(jaunter, span_danger("You begin to feast on [victim]... You can not move while you are doing this.")) + +/** + * Called when a victim is successfully consumed. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/proc/on_victim_consumed(mob/living/victim, mob/living/jaunter) + to_chat(jaunter, span_danger("You devour [victim]. Your health is fully restored.")) + qdel(victim) + +/** + * Laughter demon's blood crawl + * All mobs consumed are revived after the demon is killed. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny + name = "Friendly Blood Crawl" + desc = "Allows you to phase in and out of existance via pools of blood. If you are dragging someone in critical or dead - I mean, \ + sleeping, when entering a blood pool, they will be invited to a party and fully heal you!" + consume_sound = 'sound/misc/scary_horn.ogg' + + // Keep the people we hug! + var/list/mob/living/consumed_mobs = list() + +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/Destroy() + consumed_mobs.Cut() + return ..() + +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/Grant(mob/grant_to) + . = ..() + if(owner) + RegisterSignal(owner, COMSIG_LIVING_DEATH, .proc/on_death) + +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/Remove(mob/living/remove_from) + UnregisterSignal(remove_from, COMSIG_LIVING_DEATH) + return ..() + +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/on_victim_start_consume(mob/living/victim, mob/living/jaunter) + to_chat(jaunter, span_clown("You invite [victim] to your party! You can not move while you are doing this.")) + +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/on_victim_consumed(mob/living/victim, mob/living/jaunter) + to_chat(jaunter, span_clown("[victim] joins your party! Your health is fully restored.")) + consumed_mobs += victim + RegisterSignal(victim, COMSIG_MOB_STATCHANGE, .proc/on_victim_statchange) + RegisterSignal(victim, COMSIG_PARENT_QDELETING, .proc/on_victim_deleted) + +/** + * Signal proc for COMSIG_LIVING_DEATH and COMSIG_PARENT_QDELETING + * + * If our demon is deleted or destroyed, expel all of our consumed mobs + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/proc/on_death(datum/source) + SIGNAL_HANDLER + + var/turf/release_turf = get_turf(source) + for(var/mob/living/friend as anything in consumed_mobs) + + // Unregister the signals first + UnregisterSignal(friend, list(COMSIG_MOB_STATCHANGE, COMSIG_PARENT_QDELETING)) + + friend.forceMove(release_turf) + if(!friend.revive(full_heal = TRUE, admin_revive = TRUE)) + continue + friend.grab_ghost(force = TRUE) + playsound(release_turf, consumed_mobs, 50, TRUE, -1) + to_chat(friend, span_clown("You leave [source]'s warm embrace, and feel ready to take on the world.")) + + +/** + * Handle signal from a consumed mob changing stat. + * + * A signal handler for if one of the laughter demon's consumed mobs has + * changed stat. If they're no longer dead (because they were dead when + * swallowed), eject them so they can't rip their way out from the inside. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/proc/on_victim_statchange(mob/living/victim, new_stat) + SIGNAL_HANDLER + + if(new_stat == DEAD) + return + // Someone we've eaten has spontaneously revived; maybe regen coma, maybe a changeling + victim.forceMove(get_turf(victim)) + victim.visible_message(span_warning("[victim] falls out of the air, covered in blood, with a confused look on their face.")) + exit_blood_effect(victim) + + consumed_mobs -= victim + UnregisterSignal(victim, COMSIG_MOB_STATCHANGE) + +/** + * Handle signal from a consumed mob being deleted. Clears any references. + */ +/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/proc/on_victim_deleted(datum/source) + SIGNAL_HANDLER + + consumed_mobs -= source + +/// Bloodcrawl "hands", prevent the user from holding items in bloodcrawl +/obj/item/bloodcrawl + name = "blood crawl" + desc = "You are unable to hold anything while in this form." + icon = 'icons/effects/blood.dmi' + item_flags = ABSTRACT | DROPDEL + +/obj/item/bloodcrawl/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) diff --git a/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm b/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm new file mode 100644 index 0000000000000..2c89ba21c7f84 --- /dev/null +++ b/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm @@ -0,0 +1,256 @@ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt + name = "Ethereal Jaunt" + desc = "This spell turns your form ethereal, temporarily making you invisible and able to pass through walls." + button_icon_state = "jaunt" + sound = 'sound/magic/ethereal_enter.ogg' + + cooldown_time = 30 SECONDS + cooldown_reduction_per_rank = 5 SECONDS + + jaunt_type = /obj/effect/dummy/phased_mob/spell_jaunt + + var/exit_jaunt_sound = 'sound/magic/ethereal_exit.ogg' + /// For how long are we jaunting? + var/jaunt_duration = 5 SECONDS + /// For how long we become immobilized after exiting the jaunt. + var/jaunt_in_time = 0.5 SECONDS + /// For how long we become immobilized when using this spell. + var/jaunt_out_time = 0 SECONDS + /// Visual for jaunting + var/obj/effect/jaunt_in_type = /obj/effect/temp_visual/wizard + /// Visual for exiting the jaunt + var/obj/effect/jaunt_out_type = /obj/effect/temp_visual/wizard/out + /// List of valid exit points + var/list/exit_point_list + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/enter_jaunt(mob/living/jaunter) + . = ..() + if(!.) + return + + var/turf/cast_turf = get_turf(.) + new jaunt_out_type(cast_turf, jaunter.dir) + jaunter.extinguish_mob() + do_steam_effects(cast_turf) + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/cast(mob/living/cast_on) + . = ..() + do_jaunt(cast_on) + +/** + * Begin the jaunt, and the entire jaunt chain. + * Puts cast_on in the phased mob holder here. + * + * Calls do_jaunt_out: + * - if jaunt_out_time is set to more than 0, + * Or immediately calls start_jaunt: + * - if jaunt_out_time = 0 + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_jaunt(mob/living/cast_on) + // Makes sure they don't die or get jostled or something during the jaunt entry + // Honestly probably not necessary anymore, but better safe than sorry + cast_on.notransform = TRUE + var/obj/effect/dummy/phased_mob/holder = enter_jaunt(cast_on) + cast_on.notransform = FALSE + + if(!holder) + CRASH("[type] attempted do_jaunt but failed to create a jaunt holder via enter_jaunt.") + + if(jaunt_out_time > 0) + ADD_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src)) + addtimer(CALLBACK(src, .proc/do_jaunt_out, cast_on, holder), jaunt_out_time) + else + start_jaunt(cast_on, holder) + +/** + * The wind-up to the jaunt. + * Optional, only called if jaunt_out_time is set. + * + * Calls start_jaunt. + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_jaunt_out(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder) + if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src)) + return + + REMOVE_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src)) + start_jaunt(cast_on, holder) + +/** + * The actual process of starting the jaunt. + * Sets up the signals and exit points and allows + * the caster to actually start moving around. + * + * Calls stop_jaunt after the jaunt runs out. + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/start_jaunt(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder) + if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src)) + return + + LAZYINITLIST(exit_point_list) + RegisterSignal(holder, COMSIG_MOVABLE_MOVED, .proc/update_exit_point, target) + addtimer(CALLBACK(src, .proc/stop_jaunt, cast_on, holder, get_turf(holder)), jaunt_duration) + +/** + * The stopping of the jaunt. + * Unregisters and signals and places + * the jaunter on the turf they will exit at. + * + * Calls do_jaunt_in: + * - immediately, if jaunt_in_time >= 2.5 seconds + * - 2.5 seconds - jaunt_in_time seconds otherwise + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/stop_jaunt(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder, turf/start_point) + if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src)) + return + + UnregisterSignal(holder, COMSIG_MOVABLE_MOVED) + // The caster escaped our holder somehow? + if(cast_on.loc != holder) + qdel(holder) + return + + // Pick an exit turf to deposit the jaunter + var/turf/found_exit + for(var/turf/possible_exit as anything in exit_point_list) + if(possible_exit.is_blocked_turf_ignore_climbable()) + continue + found_exit = possible_exit + break + + // No valid exit was found + if(!found_exit) + // It's possible no exit was found, because we literally didn't even move + if(get_turf(cast_on) != start_point) + to_chat(cast_on, span_danger("Unable to find an unobstructed space, you find yourself ripped back to where you started.")) + // Either way, default to where we started + found_exit = start_point + + exit_point_list = null + holder.forceMove(found_exit) + do_steam_effects(found_exit) + holder.reappearing = TRUE + if(exit_jaunt_sound) + playsound(found_exit, exit_jaunt_sound, 50, TRUE) + + ADD_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src)) + + if(2.5 SECONDS - jaunt_in_time <= 0) + do_jaunt_in(cast_on, holder, found_exit) + else + addtimer(CALLBACK(src, .proc/do_jaunt_in, cast_on, holder, found_exit), 2.5 SECONDS - jaunt_in_time) + +/** + * The wind-up (wind-out?) of exiting the jaunt. + * Optional, only called if jaunt_in_time is above 2.5 seconds. + * + * Calls end_jaunt. + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_jaunt_in(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder, turf/final_point) + if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src)) + return + + new jaunt_in_type(final_point, holder.dir) + cast_on.setDir(holder.dir) + + if(jaunt_in_time > 0) + addtimer(CALLBACK(src, .proc/end_jaunt, cast_on, holder, final_point), jaunt_in_time) + else + end_jaunt(cast_on, holder, final_point) + +/** + * Finally, the actual veritable end of the jaunt chains. + * Deletes the phase holder, ejecting the caster at final_point. + * + * If the final_point is dense for some reason, + * tries to put the caster in an adjacent turf. + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/end_jaunt(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder, turf/final_point) + if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src)) + return + cast_on.notransform = TRUE + exit_jaunt(cast_on) + cast_on.notransform = FALSE + + REMOVE_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src)) + + if(final_point.density) + var/list/aside_turfs = get_adjacent_open_turfs(final_point) + if(length(aside_turfs)) + cast_on.forceMove(pick(aside_turfs)) + +/** + * Updates the exit point of the jaunt + * + * Called when the jaunting mob holder moves, this updates the backup exit-jaunt + * location, in case the jaunt ends with the mob still in a wall. Five + * spots are kept in the list, in case the last few changed since we passed + * by (doors closing, engineers building walls, etc) + */ +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/update_exit_point(mob/living/source) + SIGNAL_HANDLER + + var/turf/location = get_turf(source) + if(location.is_blocked_turf_ignore_climbable()) + return + exit_point_list.Insert(1, location) + if(length(exit_point_list) >= 5) + exit_point_list.Cut(5) + +/// Does some steam effects from the jaunt at passed loc. +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_steam_effects(turf/loc) + var/datum/effect_system/steam_spread/steam = new() + steam.set_up(10, FALSE, loc) + steam.start() + + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift + name = "Phase Shift" + desc = "This spell allows you to pass through walls." + background_icon_state = "bg_demon" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "phaseshift" + + cooldown_time = 25 SECONDS + spell_requirements = NONE + + jaunt_duration = 5 SECONDS + jaunt_in_time = 0.6 SECONDS + jaunt_out_time = 0.6 SECONDS + jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith + jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/do_steam_effects(mobloc) + return + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/angelic + name = "Purified Phase Shift" + jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/angelic + jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/angelic + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/mystic + name = "Mystic Phase Shift" + jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/mystic + jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/mystic + +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/golem + name = "Runic Phase Shift" + cooldown_time = 80 SECONDS + jaunt_in_type = /obj/effect/temp_visual/dir_setting/cult/phase + jaunt_out_type = /obj/effect/temp_visual/dir_setting/cult/phase/out + + +/// The dummy that holds people jaunting. Maybe one day we can replace it. +/obj/effect/dummy/phased_mob/spell_jaunt + movespeed = 2 //quite slow. + /// Whether we're currently reappearing - we can't move if so + var/reappearing = FALSE + +/obj/effect/dummy/phased_mob/spell_jaunt/phased_check(mob/living/user, direction) + if(reappearing) + return + . = ..() + if(!.) + return + if (locate(/obj/effect/blessing) in .) + to_chat(user, span_warning("Holy energies block your path!")) + return null diff --git a/code/modules/spells/spell_types/jaunt/shadow_walk.dm b/code/modules/spells/spell_types/jaunt/shadow_walk.dm new file mode 100644 index 0000000000000..64405faf99377 --- /dev/null +++ b/code/modules/spells/spell_types/jaunt/shadow_walk.dm @@ -0,0 +1,82 @@ +/datum/action/cooldown/spell/jaunt/shadow_walk + name = "Shadow Walk" + desc = "Grants unlimited movement in darkness." + background_icon_state = "bg_alien" + icon_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "ninja_cloak" + + spell_requirements = NONE + jaunt_type = /obj/effect/dummy/phased_mob/shadow + +/datum/action/cooldown/spell/jaunt/shadow_walk/cast(mob/living/cast_on) + . = ..() + if(is_jaunting(cast_on)) + exit_jaunt(cast_on) + return + + var/turf/cast_turf = get_turf(cast_on) + if(cast_turf.get_lumcount() >= SHADOW_SPECIES_LIGHT_THRESHOLD) + to_chat(cast_on, span_warning("It isn't dark enough here!")) + return + + playsound(cast_turf, 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1) + cast_on.visible_message(span_boldwarning("[cast_on] melts into the shadows!")) + cast_on.SetAllImmobility(0) + cast_on.setStaminaLoss(0, FALSE) + enter_jaunt(cast_on) + +/obj/effect/dummy/phased_mob/shadow + name = "shadows" + /// The amount that shadow heals us per SSobj tick (times delta_time) + var/healing_rate = 1.5 + +/obj/effect/dummy/phased_mob/shadow/Initialize(mapload) + . = ..() + START_PROCESSING(SSobj, src) + +/obj/effect/dummy/phased_mob/shadow/Destroy() + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/effect/dummy/phased_mob/shadow/process(delta_time) + var/turf/T = get_turf(src) + var/light_amount = T.get_lumcount() + if(!jaunter || jaunter.loc != src) + qdel(src) + return + + if(light_amount < 0.2 && !QDELETED(jaunter) && isliving(jaunter)) //heal in the dark + var/mob/living/living_jaunter = jaunter + living_jaunter.heal_overall_damage((healing_rate * delta_time), (healing_rate * delta_time), 0, BODYTYPE_ORGANIC) + + check_light_level() + +/obj/effect/dummy/phased_mob/shadow/relaymove(mob/living/user, direction) + var/turf/oldloc = loc + . = ..() + if(loc != oldloc) + check_light_level() + +/obj/effect/dummy/phased_mob/shadow/phased_check(mob/living/user, direction) + . = ..() + if(. && isspaceturf(.)) + to_chat(user, span_warning("It really would not be wise to go into space.")) + return FALSE + +/obj/effect/dummy/phased_mob/shadow/proc/check_light_level() + var/turf/T = get_turf(src) + var/light_amount = T.get_lumcount() + if(light_amount > 0.2) // jaunt ends + eject_jaunter(TRUE) + +/obj/effect/dummy/phased_mob/shadow/eject_jaunter(forced_out = FALSE) + var/turf/reveal_turf = get_turf(src) + + if(istype(reveal_turf)) + if(forced_out) + reveal_turf.visible_message(span_boldwarning("[jaunter] is revealed by the light!")) + else + reveal_turf.visible_message(span_boldwarning("[jaunter] emerges from the darkness!")) + playsound(reveal_turf, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) + + return ..() diff --git a/code/modules/spells/spell_types/knock.dm b/code/modules/spells/spell_types/knock.dm deleted file mode 100644 index 34fa4c026041f..0000000000000 --- a/code/modules/spells/spell_types/knock.dm +++ /dev/null @@ -1,32 +0,0 @@ -/obj/effect/proc_holder/spell/aoe_turf/knock - name = "Knock" - desc = "This spell opens nearby doors and closets." - - school = "transmutation" - charge_max = 100 - clothes_req = FALSE - invocation = "AULIE OXIN FIERA" - invocation_type = INVOCATION_WHISPER - range = 3 - cooldown_min = 20 //20 deciseconds reduction per rank - - action_icon_state = "knock" - -/obj/effect/proc_holder/spell/aoe_turf/knock/cast(list/targets,mob/user = usr) - SEND_SOUND(user, sound('sound/magic/knock.ogg')) - for(var/turf/T in targets) - for(var/obj/machinery/door/door in T.contents) - INVOKE_ASYNC(src, PROC_REF(open_door), door) - for(var/obj/structure/closet/C in T.contents) - INVOKE_ASYNC(src, PROC_REF(open_closet), C) - -/obj/effect/proc_holder/spell/aoe_turf/knock/proc/open_door(var/obj/machinery/door/door) - if(istype(door, /obj/machinery/door/airlock)) - var/obj/machinery/door/airlock/A = door - A.locked = FALSE - A.wires.ui_update() - door.open() - -/obj/effect/proc_holder/spell/aoe_turf/knock/proc/open_closet(var/obj/structure/closet/C) - C.locked = FALSE - C.open() diff --git a/code/modules/spells/spell_types/lichdom.dm b/code/modules/spells/spell_types/lichdom.dm deleted file mode 100644 index 65ceadb56e6a2..0000000000000 --- a/code/modules/spells/spell_types/lichdom.dm +++ /dev/null @@ -1,160 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/lichdom - name = "Bind Soul" - desc = "A dark necromantic pact that can forever bind your soul to an \ - item of your choosing. So long as both your body and the item remain \ - intact and on the same plane you can revive from death, though the time \ - between reincarnations grows steadily with use, along with the weakness \ - that the new skeleton body will experience upon 'birth'. Note that \ - becoming a lich destroys all internal organs except the brain." - school = "necromancy" - charge_max = 10 - clothes_req = FALSE - centcom_cancast = FALSE - invocation = "NECREM IMORTIUM!" - invocation_type = INVOCATION_SHOUT - range = -1 - level_max = 0 //cannot be improved - cooldown_min = 10 - include_user = TRUE - - action_icon = 'icons/mob/actions/actions_spells.dmi' - action_icon_state = "skeleton" - -/obj/effect/proc_holder/spell/targeted/lichdom/cast(list/targets,mob/user = usr) - for(var/mob/M in targets) - var/list/hand_items = list() - if(iscarbon(M)) - hand_items = list(M.get_active_held_item(),M.get_inactive_held_item()) - if(!hand_items.len) - to_chat(M, "You must hold an item you wish to make your phylactery...") - return - if(!M.mind.hasSoul) - to_chat(user, "You do not possess a soul.") - return - - var/obj/item/marked_item - - for(var/obj/item/item in hand_items) - // I ensouled the nuke disk once. But it's probably a really - // mean tactic, so probably should discourage it. - if((item.item_flags & ABSTRACT) || HAS_TRAIT(item, TRAIT_NODROP) || SEND_SIGNAL(item, COMSIG_ITEM_IMBUE_SOUL, user)) - continue - marked_item = item - to_chat(M, "You begin to focus your very being into [item]...") - break - - if(!marked_item) - to_chat(M, "None of the items you hold are suitable for emplacement of your fragile soul.") - return - - playsound(user, 'sound/effects/pope_entry.ogg', 100) - - if(!do_after(M, 50, target=marked_item, timed_action_flags = IGNORE_HELD_ITEM)) - to_chat(M, "Your soul snaps back to your body as you stop ensouling [marked_item]!") - return - - marked_item.name = "ensouled [marked_item.name]" - marked_item.desc += "\nA terrible aura surrounds this item, its very existence is offensive to life itself..." - marked_item.add_atom_colour("#003300", ADMIN_COLOUR_PRIORITY) - - new /obj/item/phylactery(marked_item, M.mind) - - to_chat(M, "With a hideous feeling of emptiness you watch in horrified fascination as skin sloughs off bone! Blood boils, nerves disintegrate, eyes boil in their sockets! As your organs crumble to dust in your fleshless chest you come to terms with your choice. You're a lich!") - M.mind.hasSoul = FALSE - M.set_species(/datum/species/skeleton) - if(ishuman(M)) - var/mob/living/carbon/human/H = M - H.dropItemToGround(H.w_uniform) - H.dropItemToGround(H.wear_suit) - H.dropItemToGround(H.head) - H.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(H), ITEM_SLOT_OCLOTHING) - H.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(H), ITEM_SLOT_HEAD) - H.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(H), ITEM_SLOT_ICLOTHING) - - // you only get one phylactery. - M.mind.RemoveSpell(src) - - -/obj/item/phylactery - name = "phylactery" - desc = "Stores souls. Revives liches. Also repels mosquitos." - icon = 'icons/obj/projectiles.dmi' - icon_state = "bluespace" - color = "#003300" - light_color = "#003300" - light_system = MOVABLE_LIGHT - light_range = 3 - var/lon_range = 3 - var/resurrections = 0 - var/datum/mind/mind - var/respawn_time = 1800 - - var/static/active_phylacteries = 0 - -/obj/item/phylactery/Initialize(mapload, datum/mind/newmind) - . = ..() - mind = newmind - name = "phylactery of [mind.name]" - - active_phylacteries++ - AddElement(/datum/element/point_of_interest) - START_PROCESSING(SSobj, src) - if(initial(SSticker.mode.round_ends_with_antag_death)) - SSticker.mode.round_ends_with_antag_death = FALSE - -/obj/item/phylactery/Destroy(force=FALSE) - STOP_PROCESSING(SSobj, src) - active_phylacteries-- - if(!active_phylacteries) - SSticker.mode.round_ends_with_antag_death = initial(SSticker.mode.round_ends_with_antag_death) - . = ..() - -/obj/item/phylactery/process() - if(QDELETED(mind)) - qdel(src) - return - - if(!mind.current || (mind.current && mind.current.stat == DEAD)) - addtimer(CALLBACK(src, PROC_REF(rise)), respawn_time, TIMER_UNIQUE) - -/obj/item/phylactery/proc/rise() - if(mind.current && mind.current.stat != DEAD) - return "[mind] already has a living body: [mind.current]" - - var/turf/item_turf = get_turf(src) - if(!item_turf) - return "[src] is not at a turf? NULLSPACE!?" - - var/mob/old_body = mind.current - var/mob/living/carbon/human/lich = new(item_turf) - - lich.equip_to_slot_or_del(new /obj/item/clothing/shoes/sandal/magic(lich), ITEM_SLOT_FEET) - lich.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(lich), ITEM_SLOT_ICLOTHING) - lich.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(lich), ITEM_SLOT_OCLOTHING) - lich.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(lich), ITEM_SLOT_HEAD) - - lich.real_name = mind.name - mind.transfer_to(lich) - mind.grab_ghost(force=TRUE) - lich.hardset_dna(null,null,lich.real_name,null, new /datum/species/skeleton,null) - to_chat(lich, "Your bones clatter and shudder as you are pulled back into this world!") - var/turf/body_turf = get_turf(old_body) - lich.Paralyze(200 + 200*resurrections) - resurrections++ - if(old_body?.loc) - if(iscarbon(old_body)) - var/mob/living/carbon/C = old_body - for(var/obj/item/W in C) - C.dropItemToGround(W) - for(var/X in C.internal_organs) - var/obj/item/organ/I = X - I.Remove(C) - I.forceMove(body_turf) - var/wheres_wizdo = dir2text(get_dir(body_turf, item_turf)) - if(wheres_wizdo) - old_body.visible_message("Suddenly [old_body.name]'s corpse falls to pieces! You see a strange energy rise from the remains, and speed off towards the [wheres_wizdo]!") - body_turf.Beam(item_turf,icon_state="lichbeam", time = 10 + 10 * resurrections) - old_body.dust() - - - return "Respawn of [mind] successful." diff --git a/code/modules/spells/spell_types/lightning.dm b/code/modules/spells/spell_types/lightning.dm deleted file mode 100644 index f6f139c041d70..0000000000000 --- a/code/modules/spells/spell_types/lightning.dm +++ /dev/null @@ -1,86 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/tesla - name = "Tesla Blast" - desc = "Charge up a tesla arc and release it at a random nearby target! You can move freely while it charges. The arc jumps between targets and can knock them down." - charge_type = "recharge" - charge_max = 300 - clothes_req = TRUE - invocation = "UN'LTD P'WAH!" - invocation_type = INVOCATION_SHOUT - range = 7 - cooldown_min = 30 - selection_type = "view" - random_target = TRUE - var/ready = FALSE - var/static/mutable_appearance/halo - var/sound/Snd // so far only way i can think of to stop a sound, thank MSO for the idea. - - action_icon_state = "lightning" - -/obj/effect/proc_holder/spell/targeted/tesla/Click() - if(!ready && cast_check()) - StartChargeup() - return TRUE - -/obj/effect/proc_holder/spell/targeted/tesla/proc/StartChargeup(mob/user = usr) - ready = TRUE - to_chat(user, "You start gathering the power.") - Snd = new/sound('sound/magic/lightning_chargeup.ogg',channel = 7) - halo = halo || mutable_appearance('icons/effects/effects.dmi', "electricity", EFFECTS_LAYER) - user.add_overlay(halo) - playsound(get_turf(user), Snd, 50, 0) - if(do_after(user, 10 SECONDS, timed_action_flags = UNINTERRUPTIBLE)) - if(ready && cast_check(skipcharge=1)) - choose_targets() - else - revert_cast(user, 0) - else - revert_cast(user, 0) - -/obj/effect/proc_holder/spell/targeted/tesla/proc/Reset(mob/user = usr) - ready = FALSE - user.cut_overlay(halo) - -/obj/effect/proc_holder/spell/targeted/tesla/revert_cast(mob/user = usr, message = 1) - if(message) - to_chat(user, "No target found in range.") - Reset(user) - ..() - -/obj/effect/proc_holder/spell/targeted/tesla/cast(list/targets, mob/user = usr) - ready = FALSE - var/mob/living/carbon/target = targets[1] - Snd=sound(null, repeat = 0, wait = 1, channel = Snd.channel) //byond, why you suck? - playsound(get_turf(user),Snd,50,0)// Sorry MrPerson, but the other ways just didn't do it the way i needed to work, this is the only way. - if(get_dist(user,target)>range) - to_chat(user, "[target.p_theyre(TRUE)] too far away!") - Reset(user) - return - - playsound(get_turf(user), 'sound/magic/lightningbolt.ogg', 50, 1) - user.Beam(target,icon_state="lightning[rand(1,12)]", time = 5) - - Bolt(user,target,30,5,user) - Reset(user) - -/obj/effect/proc_holder/spell/targeted/tesla/proc/Bolt(mob/origin,mob/target,bolt_energy,bounces,mob/user = usr) - origin.Beam(target,icon_state="lightning[rand(1,12)]", time = 5) - var/mob/living/carbon/current = target - if(current.anti_magic_check()) - playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, 1, -1) - current.visible_message("[current] absorbs the spell, remaining unharmed!", "You absorb the spell, remaining unharmed!") - else if(bounces < 1) - current.electrocute_act(bolt_energy,"Lightning Bolt",flags = SHOCK_NOGLOVES) - playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, 1, -1) - else - current.electrocute_act(bolt_energy,"Lightning Bolt",flags = SHOCK_NOGLOVES) - playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, 1, -1) - var/list/possible_targets = new - for(var/mob/living/M in view_or_range(range,target,"view")) - if(user == M || target == M && los_check(current,M)) // || origin == M ? Not sure double shockings is good or not - continue - possible_targets += M - if(!possible_targets.len) - return - var/mob/living/next = pick(possible_targets) - if(next) - Bolt(current,next,max((bolt_energy-5),5),bounces-1,user) diff --git a/code/modules/spells/spell_types/list_targets/_list_targets.dm b/code/modules/spells/spell_types/list_targets/_list_targets.dm new file mode 100644 index 0000000000000..d595552e98795 --- /dev/null +++ b/code/modules/spells/spell_types/list_targets/_list_targets.dm @@ -0,0 +1,41 @@ +/** + * ## List Target spells + * + * These spells will prompt the user with a tgui list + * of all nearby targets that they select on to cast. + * + * To add effects on cast, override "cast(atom/cast_on)". + * The cast_on atom is the atom that was selected by the list. + */ +/datum/action/cooldown/spell/list_target + /// The message displayed as the title of the tgui target input list. + var/choose_target_message = "Choose a target." + /// Radius around the caster that living targets are picked to choose from + var/target_radius = 7 + +/datum/action/cooldown/spell/list_target/PreActivate(atom/caster) + var/list/list_targets = get_list_targets(caster, target_radius) + if(!length(list_targets)) + caster.balloon_alert(caster, "no targets nearby!") + return FALSE + + var/atom/chosen = tgui_input_list(caster, choose_target_message, name, sort_names(list_targets)) + if(QDELETED(src) || QDELETED(caster) || QDELETED(chosen) || !can_cast_spell()) + return FALSE + + if(get_dist(chosen, caster) > target_radius) + caster.balloon_alert(caster, "they're too far!") + return FALSE + + return Activate(chosen) + +/// Get a list of living targets in radius of the center to put in the target list. +/datum/action/cooldown/spell/list_target/proc/get_list_targets(atom/center, target_radius = 7) + var/list/things = list() + for(var/mob/living/nearby_living in view(target_radius, center)) + if(nearby_living == owner || nearby_living == center) + continue + + things += nearby_living + + return things diff --git a/code/modules/spells/spell_types/list_targets/telepathy.dm b/code/modules/spells/spell_types/list_targets/telepathy.dm new file mode 100644 index 0000000000000..161b24f46d815 --- /dev/null +++ b/code/modules/spells/spell_types/list_targets/telepathy.dm @@ -0,0 +1,51 @@ +/datum/action/cooldown/spell/list_target/telepathy + name = "Telepathy" + desc = "Telepathically transmits a message to the target." + icon_icon = 'icons/mob/actions/actions_revenant.dmi' + button_icon_state = "r_transmit" + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE_MIND + + choose_target_message = "Choose a target to whisper to." + + /// The message we send to the next person via telepathy. + var/message + /// The span surrounding the telepathy message + var/telepathy_span = "notice" + /// The bolded span surrounding the telepathy message + var/bold_telepathy_span = "boldnotice" + +/datum/action/cooldown/spell/list_target/telepathy/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + message = tgui_input_text(owner, "What do you wish to whisper to [cast_on]?", "[src]") + if(QDELETED(src) || QDELETED(owner) || QDELETED(cast_on) || !can_cast_spell()) + return . | SPELL_CANCEL_CAST + + if(!message) + reset_spell_cooldown() + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/list_target/telepathy/cast(mob/living/cast_on) + . = ..() + log_directed_talk(owner, cast_on, message, LOG_SAY, name) + + var/formatted_message = "[message]" + + to_chat(owner, "You transmit to [cast_on]: [formatted_message]") + if(!cast_on.can_block_magic(antimagic_flags, charge_cost = 0)) //hear no evil + to_chat(cast_on, "You hear something behind you talking... [formatted_message]") + + for(var/mob/dead/ghost as anything in GLOB.dead_mob_list) + if(!isobserver(ghost)) + continue + + var/from_link = FOLLOW_LINK(ghost, owner) + var/from_mob_name = "[owner] [src]:" + var/to_link = FOLLOW_LINK(ghost, cast_on) + var/to_mob_name = span_name("[cast_on]") + + to_chat(ghost, "[from_link] [from_mob_name] [formatted_message] [to_link] [to_mob_name]") diff --git a/code/modules/spells/spell_types/mime.dm b/code/modules/spells/spell_types/mime.dm deleted file mode 100644 index 2f4947a5a2835..0000000000000 --- a/code/modules/spells/spell_types/mime.dm +++ /dev/null @@ -1,242 +0,0 @@ -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall - name = "Invisible Wall" - desc = "The mime's performance transmutates a wall into physical reality." - school = "mime" - panel = JOB_NAME_MIME - summon_type = list(/obj/effect/forcefield/mime) - invocation_type = "emote" - invocation_emote_self = "You form a wall in front of yourself." - summon_lifespan = 100 - charge_max = 300 - clothes_req = FALSE - antimagic_allowed = TRUE - range = 0 - cast_sound = null - human_req = TRUE - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "invisible_wall" - action_background_icon_state = "bg_mime" - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall/Click() - if(usr && usr.mind) - if(!usr.mind.miming) - to_chat(usr, "You must dedicate yourself to silence first.") - return - invocation = "[usr.real_name] looks as if a wall is in front of [usr.p_them()]." - else - invocation_type ="none" - ..() - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair - name = "Invisible Chair" - desc = "The mime's performance transmutates a chair into physical reality." - school = "mime" - panel = JOB_NAME_MIME - summon_type = list(/obj/structure/chair/mime) - invocation_type = "emote" - invocation_emote_self = "You conjure an invisible chair and sit down." - summon_lifespan = 250 - charge_max = 300 - clothes_req = FALSE - antimagic_allowed = TRUE - range = 0 - cast_sound = null - human_req = TRUE - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "invisible_chair" - action_background_icon_state = "bg_mime" - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair/Click() - if(usr && usr.mind) - if(!usr.mind.miming) - to_chat(usr, "You must dedicate yourself to silence first.") - return - invocation = "[usr.real_name] pulls out an invisible chair and sits down." - else - invocation_type ="none" - ..() - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair/cast(list/targets,mob/user = usr) - ..() - var/turf/T = user.loc - for (var/obj/structure/chair/A in T) - if (is_type_in_list(A, summon_type)) - A.setDir(user.dir) - A.buckle_mob(user) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box - name = "Invisible Box" - desc = "The mime's performance transmutates a box into physical reality." - school = "mime" - panel = JOB_NAME_MIME - summon_type = list(/obj/item/storage/box/mime) - invocation_type = "emote" - invocation_emote_self = "You conjure up an invisible box, large enough to store a few things." - summon_lifespan = 500 - charge_max = 300 - clothes_req = FALSE - antimagic_allowed = TRUE - range = 0 - cast_sound = null - human_req = TRUE - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "invisible_box" - action_background_icon_state = "bg_mime" - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box/cast(list/targets,mob/user = usr) - ..() - var/turf/T = user.loc - for (var/obj/item/storage/box/mime/B in T) - user.put_in_hands(B) - B.alpha = 255 - addtimer(CALLBACK(B, TYPE_PROC_REF(/obj/item/storage/box/mime, emptyStorage), FALSE), (summon_lifespan - 1)) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box/Click() - if(usr && usr.mind) - if(!usr.mind.miming) - to_chat(usr, "You must dedicate yourself to silence first.") - return - invocation = "[usr.real_name] moves [usr.p_their()] hands in the shape of a cube, pressing a box out of the air." - else - invocation_type ="none" - ..() - - -/obj/effect/proc_holder/spell/targeted/mime/speak - name = "Speech" - desc = "Make or break a vow of silence." - school = "mime" - panel = JOB_NAME_MIME - clothes_req = FALSE - human_req = TRUE - antimagic_allowed = TRUE - charge_max = 3000 - range = -1 - include_user = TRUE - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "mime_speech" - action_background_icon_state = "bg_mime" - -/obj/effect/proc_holder/spell/targeted/mime/speak/Click() - if(!usr) - return - if(!ishuman(usr)) - return - var/mob/living/carbon/human/H = usr - if(H.mind.miming) - still_recharging_msg = "You can't break your vow of silence that fast!" - else - still_recharging_msg = "You'll have to wait before you can give your vow of silence again!" - ..() - -/obj/effect/proc_holder/spell/targeted/mime/speak/cast(list/targets,mob/user = usr) - for(var/mob/living/carbon/human/H in targets) - H.mind.miming=!H.mind.miming - if(H.mind.miming) - to_chat(H, "You make a vow of silence.") - SEND_SIGNAL(H, COMSIG_CLEAR_MOOD_EVENT, "vow") - else - SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "vow", /datum/mood_event/broken_vow) - to_chat(H, "You break your vow of silence.") - for(var/datum/objective/crew/vow/obj in H.mind.crew_objectives) - obj.broken = TRUE - -// These spells can only be gotten from the "Guide for Advanced Mimery series" for Mime Traitors. - -/obj/effect/proc_holder/spell/targeted/forcewall/mime - name = "Invisible Blockade" - desc = "Form an invisible three tile wide blockade." - school = "mime" - panel = JOB_NAME_MIME - wall_type = /obj/effect/forcefield/mime/advanced - invocation_type = "emote" - invocation_emote_self = "You form a blockade in front of yourself." - charge_max = 600 - sound = null - clothes_req = FALSE - antimagic_allowed = TRUE - range = -1 - include_user = TRUE - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "invisible_blockade" - action_background_icon_state = "bg_mime" - -/obj/effect/proc_holder/spell/targeted/forcewall/mime/Click() - if(usr && usr.mind) - if(!usr.mind.miming) - to_chat(usr, "You must dedicate yourself to silence first.") - return - invocation = "[usr.real_name] looks as if a blockade is in front of [usr.p_them()]." - else - invocation_type ="none" - ..() - -/obj/effect/proc_holder/spell/targeted/mime/finger_guns - name = "Finger Guns" - desc = "Shoot a mimed bullet from your fingers that stuns and does some damage." - school = "mime" - panel = JOB_NAME_MIME - charge_max = 300 - range = -1 - clothes_req = FALSE - antimagic_allowed = TRUE - include_user = TRUE - invocation_type = "emote" - invocation_emote_self = "You fire your finger gun!" - sound = null - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "finger_guns0" - action_background_icon_state = "bg_mime" - -/obj/effect/proc_holder/spell/targeted/mime/finger_guns/Click() - if(!usr) - return - if(!ishuman(usr)) - return - if(usr?.mind) - if(!usr.mind.miming) - to_chat(usr, "You must dedicate yourself to silence first.") - return - var/obj/item/gun/ballistic/revolver/mime/magic/N = new(usr) - if(usr.put_in_hands(N)) - to_chat(usr, "You form your fingers into a gun.") - else - qdel(N) - to_chat(usr, "You don't have any free hands to make fingerguns with.") - ..() - -/obj/item/book/granter/spell/mimery_blockade - spell = /obj/effect/proc_holder/spell/targeted/forcewall/mime - spellname = "Invisible Blockade" - name = "Guide to Advanced Mimery Vol 1" - desc = "The pages don't make any sound when turned." - icon_state ="bookmime" - remarks = list("...") - -/obj/item/book/granter/spell/mimery_blockade/attack_self(mob/user) - . = ..() - if(!.) - return - if(!locate(/obj/effect/proc_holder/spell/targeted/mime/speak) in user.mind.spell_list) - user.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mime/speak) - -/obj/item/book/granter/spell/mimery_guns - spell = /obj/effect/proc_holder/spell/targeted/mime/finger_guns - spellname = "Finger Guns" - name = "Guide to Advanced Mimery Vol 2" - desc = "There aren't any words written..." - icon_state ="bookmime" - remarks = list("...") - -/obj/item/book/granter/spell/mimery_guns/attack_self(mob/user) - . = ..() - if(!.) - return - if(!locate(/obj/effect/proc_holder/spell/targeted/mime/speak) in user.mind.spell_list) - user.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mime/speak) diff --git a/code/modules/spells/spell_types/mind_transfer.dm b/code/modules/spells/spell_types/mind_transfer.dm deleted file mode 100644 index 3f220dcc0fcc7..0000000000000 --- a/code/modules/spells/spell_types/mind_transfer.dm +++ /dev/null @@ -1,108 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/mind_transfer - name = "Mind Transfer" - desc = "This spell allows the user to switch bodies with a target." - - school = "transmutation" - charge_max = 600 - clothes_req = FALSE - invocation = "GIN'YU CAPAN" - invocation_type = INVOCATION_WHISPER - range = 1 - cooldown_min = 200 //100 deciseconds reduction per rank - var/unconscious_amount_caster = 400 //how much the caster is stunned for after the spell - var/unconscious_amount_victim = 400 //how much the victim is stunned for after the spell - - action_icon_state = "mindswap" - -/* -Urist: I don't feel like figuring out how you store object spells so I'm leaving this for you to do. -Make sure spells that are removed from spell_list are actually removed and deleted when mind transferring. -Also, you never added distance checking after target is selected. I've went ahead and did that. -*/ -/obj/effect/proc_holder/spell/targeted/mind_transfer/cast(list/targets, mob/living/user = usr, distanceoverride, silent = FALSE) - if(!targets.len) - if(!silent) - to_chat(user, "No mind found!") - return - - if(targets.len > 1) - if(!silent) - to_chat(user, "Too many minds! You're not a hive damnit!") - return - - var/mob/living/target = targets[1] - - var/t_He = target.p_they(TRUE) - var/t_is = target.p_are() - - if(!(target in oview(range)) && !distanceoverride)//If they are not in overview after selection. Do note that !() is necessary for in to work because ! takes precedence over it. - if(!silent) - to_chat(user, "[t_He] [t_is] too far away!") - return - - if(ismegafauna(target)) - if(!silent) - to_chat(user, "This creature is too powerful to control!") - return - - if(target.stat == DEAD) - if(!silent) - to_chat(user, "You don't particularly want to be dead!") - return - - if(!target.key || !target.mind) - if(!silent) - to_chat(user, "[t_He] appear[target.p_s()] to be catatonic! Not even magic can affect [target.p_their()] vacant mind.") - return - - if(user.suiciding) - if(!silent) - to_chat(user, "You're killing yourself! You can't concentrate enough to do this!") - return - - var/datum/mind/TM = target.mind - if(target.anti_magic_check() || TM.has_antag_datum(/datum/antagonist/wizard) || TM.has_antag_datum(/datum/antagonist/cult) || TM.has_antag_datum(/datum/antagonist/changeling) || TM.has_antag_datum(/datum/antagonist/rev) || target.key[1] == "@") - if(!silent) - to_chat(user, "[target.p_their(TRUE)] mind is resisting your spell!") - return - - if(istype(target.get_item_by_slot(ITEM_SLOT_HEAD), /obj/item/clothing/head/costume/foilhat)) - to_chat(target, "Your protective headgear successfully deflects mind controlling brainwaves!") - to_chat(user, "[target.p_their(TRUE)] mind is protected by a strange ward on their headgear!") - return - - if(istype(target, /mob/living/simple_animal/hostile/holoparasite)) - var/mob/living/simple_animal/hostile/holoparasite/stand = target - if(stand.summoner) - if(stand.summoner == user) - if(!silent) - to_chat(user, "Swapping minds with your own guardian would just put you back into your own head!") - return - else - target = stand.summoner - - if(istype(target, /mob/living/simple_animal/slaughter)) //No. - to_chat(user, "Your mind recoils from the infernal hellfire of [target]'s soul!") - user.Unconscious(unconscious_amount_caster) - return - - var/mob/living/victim = target//The target of the spell whos body will be transferred to. - var/mob/living/caster = user//The wizard/whomever doing the body transferring. - - //MIND TRANSFER BEGIN - var/mob/dead/observer/ghost = victim.ghostize(FALSE) - caster.mind.transfer_to(victim) - - ghost.mind.transfer_to(caster) - if(ghost.key) - caster.key = ghost.key //have to transfer the key since the mind was not active - qdel(ghost) - - //MIND TRANSFER END - - //Here we knock both mobs out for a time. - caster.Unconscious(unconscious_amount_caster) - victim.Unconscious(unconscious_amount_victim) - SEND_SOUND(caster, sound('sound/magic/mandswap.ogg')) - SEND_SOUND(victim, sound('sound/magic/mandswap.ogg'))// only the caster and victim hear the sounds, that way no one knows for sure if the swap happened - return TRUE diff --git a/code/modules/spells/spell_types/personality_commune.dm b/code/modules/spells/spell_types/personality_commune.dm deleted file mode 100644 index f7fc09f8320f5..0000000000000 --- a/code/modules/spells/spell_types/personality_commune.dm +++ /dev/null @@ -1,36 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/personality_commune - name = "Personality Commune" - desc = "Sends thoughts to your alternate consciousness." - charge_max = 0 - clothes_req = FALSE - range = -1 - include_user = TRUE - action_icon_state = "telepathy" - action_background_icon_state = "bg_spell" - var/datum/brain_trauma/severe/split_personality/trauma - var/flufftext = "You hear an echoing voice in the back of your head..." - -/obj/effect/proc_holder/spell/targeted/personality_commune/New(datum/brain_trauma/severe/split_personality/T) - . = ..() - trauma = T - -// Pillaged and adapted from telepathy code -/obj/effect/proc_holder/spell/targeted/personality_commune/cast(list/targets, mob/user) - if(!istype(trauma)) - to_chat(user, "Something is wrong; Either due a bug or admemes, you are trying to cast this spell without a split personality!") - return - var/msg = stripped_input(usr, "What would you like to tell your other self?", null , "") - if(!msg) - charge_counter = charge_max - return - if(CHAT_FILTER_CHECK(msg)) - to_chat(usr, "Your message contains forbidden words.") - return - msg = user.treat_message_min(msg) - to_chat(user, "You concentrate and send thoughts to your other self: [msg]") - to_chat(trauma.owner, "[flufftext] [msg]") - log_directed_talk(user, trauma.owner, msg, LOG_SAY ,"[name]") - for(var/ded in GLOB.dead_mob_list) - if(!isobserver(ded)) - continue - to_chat(ded, "[FOLLOW_LINK(ded, user)] [user] [name]: \"[msg]\" to [trauma]") diff --git a/code/modules/spells/spell_types/pointed.dm b/code/modules/spells/spell_types/pointed.dm deleted file mode 100644 index 423187508fac5..0000000000000 --- a/code/modules/spells/spell_types/pointed.dm +++ /dev/null @@ -1,75 +0,0 @@ -/obj/effect/proc_holder/spell/pointed - name = "pointed spell" - ranged_mousepointer = 'icons/effects/throw_target.dmi' - /// Message showing to the spell owner upon deactivating pointed spell. - var/deactive_msg = "You dispel the magic..." - /// Message showing to the spell owner upon activating pointed spell. - var/active_msg = "You prepare to use the spell on a target..." - /// Default icon for the pointed spell, used for active/inactive states switching. - base_icon_state = "projectile" - -/obj/effect/proc_holder/spell/pointed/Click() - var/mob/living/user = usr - if(!istype(user)) - return - var/msg - if(!can_cast(user)) - msg = "You can no longer cast [name]!" - remove_ranged_ability(msg) - return - if(active) - msg = "[deactive_msg]" - remove_ranged_ability(msg) - on_deactivation(user) - else - msg = "[active_msg] Left-click to activate spell on a target!" - add_ranged_ability(user, msg, TRUE) - on_activation(user) - -/** - * - * What happens upon pointed spell activation. - * - * user mob The mob interacting owning the spell. - * - **/ -/obj/effect/proc_holder/spell/pointed/proc/on_activation(mob/user) - return - - /** - * - * What happens upon pointed spell deactivation. - * - * user mob The mob interacting owning the spell. - * - **/ -/obj/effect/proc_holder/spell/pointed/proc/on_deactivation(mob/user) - return - -/obj/effect/proc_holder/spell/pointed/update_icon() - if(!action) - return - action.button_icon_state = "[base_icon_state][active]" - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return FALSE - if(!intercept_check(ranged_ability_user, target)) - return FALSE - if(!cast_check(user = ranged_ability_user)) - return FALSE - perform(list(target), user = ranged_ability_user) - remove_ranged_ability() - return TRUE - - /** - * - * Specific spell checks for InterceptClickOn() targets. - * - * user mob The mob using the ranged spell via intercept. - * target atom The atom being targeted by the spell via intercept. - * - **/ -/obj/effect/proc_holder/spell/pointed/proc/intercept_check(mob/user, atom/target) - return TRUE diff --git a/code/modules/spells/spell_types/pointed/_pointed.dm b/code/modules/spells/spell_types/pointed/_pointed.dm new file mode 100644 index 0000000000000..4f5bbf2349e50 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/_pointed.dm @@ -0,0 +1,181 @@ +/** + * ## Pointed spells + * + * These spells override the caster's click, + * allowing them to cast the spell on whatever is clicked on. + * + * To add effects on cast, override "cast(atom/cast_on)". + * The cast_on atom is the person who was clicked on. + */ +/datum/action/cooldown/spell/pointed + click_to_activate = TRUE + + /// The base icon state of the spell's button icon, used for editing the icon "on" and "off" + var/base_icon_state + /// Message showing to the spell owner upon activating pointed spell. + var/active_msg + /// Message showing to the spell owner upon deactivating pointed spell. + var/deactive_msg + /// The casting range of our spell + var/cast_range = 7 + /// Variable dictating if the spell will use turf based aim assist + var/aim_assist = TRUE + +/datum/action/cooldown/spell/pointed/New(Target) + . = ..() + if(!active_msg) + active_msg = "You prepare to use [src] on a target..." + if(!deactive_msg) + deactive_msg = "You dispel [src]." + +/datum/action/cooldown/spell/pointed/set_click_ability(mob/on_who) + . = ..() + if(!.) + return + + on_activation(on_who) + +// Note: Destroy() calls Remove(), Remove() calls unset_click_ability() if our spell is active. +/datum/action/cooldown/spell/pointed/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) + return + + on_deactivation(on_who, refund_cooldown = refund_cooldown) + +/datum/action/cooldown/spell/pointed/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + on_deactivation(owner, refund_cooldown = FALSE) + +/// Called when the spell is activated / the click ability is set to our spell +/datum/action/cooldown/spell/pointed/proc/on_activation(mob/on_who) + SHOULD_CALL_PARENT(TRUE) + + to_chat(on_who, span_notice("[active_msg] Left-click to cast the spell on a target!")) + if(base_icon_state) + button_icon_state = "[base_icon_state]1" + UpdateButtons() + return TRUE + +/// Called when the spell is deactivated / the click ability is unset from our spell +/datum/action/cooldown/spell/pointed/proc/on_deactivation(mob/on_who, refund_cooldown = TRUE) + SHOULD_CALL_PARENT(TRUE) + + if(refund_cooldown) + // Only send the "deactivation" message if they're willingly disabling the ability + to_chat(on_who, span_notice("[deactive_msg]")) + if(base_icon_state) + button_icon_state = "[base_icon_state]0" + UpdateButtons() + return TRUE + +/datum/action/cooldown/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/click_target) + + var/atom/aim_assist_target + if(aim_assist && isturf(click_target)) + // Find any human in the list. We aren't picky, it's aim assist after all + aim_assist_target = locate(/mob/living/carbon/human) in click_target + if(!aim_assist_target) + // If we didn't find a human, we settle for any living at all + aim_assist_target = locate(/mob/living) in click_target + + return ..(caller, params, aim_assist_target || click_target) + +/datum/action/cooldown/spell/pointed/is_valid_target(atom/cast_on) + if(cast_on == owner) + to_chat(owner, span_warning("You cannot cast [src] on yourself!")) + return FALSE + + if(get_dist(owner, cast_on) > cast_range) + to_chat(owner, span_warning("[cast_on.p_theyre(TRUE)] too far away!")) + return FALSE + + return TRUE + +/** + * ### Pointed projectile spells + * + * Pointed spells that, instead of casting a spell directly on the target that's clicked, + * will instead fire a projectile pointed at the target's direction. + */ +/datum/action/cooldown/spell/pointed/projectile + /// What projectile we create when we shoot our spell. + var/obj/projectile/magic/projectile_type = /obj/projectile/magic/teleport + /// How many projectiles we can fire per cast. Not all at once, per click, kinda like charges + var/projectile_amount = 1 + /// How many projectiles we have yet to fire, based on projectile_amount + var/current_amount = 0 + /// How many projectiles we fire every fire_projectile() call. + /// Unwise to change without overriding or extending ready_projectile. + var/projectiles_per_fire = 1 + +/datum/action/cooldown/spell/pointed/projectile/New(Target) + . = ..() + if(projectile_amount > 1) + unset_after_click = FALSE + +/datum/action/cooldown/spell/pointed/projectile/is_valid_target(atom/cast_on) + return TRUE + +/datum/action/cooldown/spell/pointed/projectile/on_activation(mob/on_who) + . = ..() + if(!.) + return + + current_amount = projectile_amount + +/datum/action/cooldown/spell/pointed/projectile/on_deactivation(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(projectile_amount > 1 && current_amount) + StartCooldown(cooldown_time * ((projectile_amount - current_amount) / projectile_amount)) + current_amount = 0 + +// cast_on is a turf, or atom target, that we clicked on to fire at. +/datum/action/cooldown/spell/pointed/projectile/cast(atom/cast_on) + . = ..() + if(!isturf(owner.loc)) + return FALSE + + var/turf/caster_turf = get_turf(owner) + // Get the tile infront of the caster, based on their direction + var/turf/caster_front_turf = get_step(owner, owner.dir) + + fire_projectile(cast_on) + owner.newtonian_move(get_dir(caster_front_turf, caster_turf)) + if(current_amount <= 0) + unset_click_ability(owner, refund_cooldown = FALSE) + + return TRUE + +/datum/action/cooldown/spell/pointed/projectile/after_cast(atom/cast_on) + . = ..() + if(current_amount > 0) + // We still have projectiles to cast! + // Reset our cooldown and let them fire away + reset_spell_cooldown() + +/datum/action/cooldown/spell/pointed/projectile/proc/fire_projectile(atom/target) + current_amount-- + for(var/i in 1 to projectiles_per_fire) + var/obj/projectile/to_fire = new projectile_type() + ready_projectile(to_fire, target, owner, i) + to_fire.fire() + return TRUE + +/datum/action/cooldown/spell/pointed/projectile/proc/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + to_fire.firer = owner + to_fire.fired_from = get_turf(owner) + to_fire.preparePixelProjectile(target, owner) + RegisterSignal(to_fire, COMSIG_PROJECTILE_ON_HIT, .proc/on_cast_hit) + + if(istype(to_fire, /obj/projectile/magic)) + var/obj/projectile/magic/magic_to_fire = to_fire + magic_to_fire.antimagic_flags = antimagic_flags + +/// Signal proc for whenever the projectile we fire hits someone. +/// Pretty much relays to the spell when the projectile actually hits something. +/datum/action/cooldown/spell/pointed/projectile/proc/on_cast_hit(atom/source, mob/firer, atom/hit, angle) + SIGNAL_HANDLER + + SEND_SIGNAL(src, COMSIG_SPELL_PROJECTILE_HIT, hit, firer, source) diff --git a/code/modules/spells/spell_types/pointed/abyssal_gaze.dm b/code/modules/spells/spell_types/pointed/abyssal_gaze.dm new file mode 100644 index 0000000000000..462e9e53a8409 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/abyssal_gaze.dm @@ -0,0 +1,53 @@ +/datum/action/cooldown/spell/pointed/abyssal_gaze + name = "Abyssal Gaze" + desc = "This spell instills a deep terror in your target, temporarily chilling and blinding it." + ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' + background_icon_state = "bg_demon" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "abyssal_gaze" + + school = SCHOOL_EVOCATION + cooldown_time = 75 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = NONE + antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY + + cast_range = 5 + active_msg = "You prepare to instill a deep terror in a target..." + + /// The duration of the blind on our target + var/blind_duration = 4 SECONDS + /// The amount of temperature we take from our target + var/amount_to_cool = 200 + +/datum/action/cooldown/spell/pointed/abyssal_gaze/is_valid_target(atom/cast_on) + return iscarbon(target) + +/datum/action/cooldown/spell/pointed/abyssal_gaze/cast(mob/living/carbon/cast_on) + . = ..() + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(owner, span_warning("The spell had no effect!")) + to_chat(cast_on, span_warning("You feel a freezing darkness closing in on you, but it rapidly dissipates.")) + return FALSE + + to_chat(cast_on, span_userdanger("A freezing darkness surrounds you...")) + cast_on.playsound_local(get_turf(cast_on), 'sound/hallucinations/i_see_you1.ogg', 50, 1) + owner.playsound_local(get_turf(owner), 'sound/effects/ghost2.ogg', 50, 1) + cast_on.become_blind(ABYSSAL_GAZE_BLIND) + addtimer(CALLBACK(src, .proc/cure_blindness, cast_on), blind_duration) + if(ishuman(cast_on)) + var/mob/living/carbon/human/human_cast_on = cast_on + human_cast_on.adjust_coretemperature(-amount_to_cool) + cast_on.adjust_bodytemperature(-amount_to_cool) + +/** + * cure_blidness: Cures Abyssal Gaze blindness from the target + * + * Arguments: + * * target The mob that is being cured of the blindness. + */ +/datum/action/cooldown/spell/pointed/abyssal_gaze/proc/cure_blindness(mob/living/carbon/cast_on) + if(QDELETED(cast_on) || !istype(cast_on)) + return + + cast_on.cure_blind(ABYSSAL_GAZE_BLIND) diff --git a/code/modules/spells/spell_types/pointed/barnyard.dm b/code/modules/spells/spell_types/pointed/barnyard.dm new file mode 100644 index 0000000000000..b6fce6521555a --- /dev/null +++ b/code/modules/spells/spell_types/pointed/barnyard.dm @@ -0,0 +1,55 @@ +/datum/action/cooldown/spell/pointed/barnyardcurse + name = "Curse of the Barnyard" + desc = "This spell dooms an unlucky soul to possess the speech and facial attributes of a barnyard animal." + button_icon_state = "barn" + ranged_mousepointer = 'icons/effects/mouse_pointers/barn_target.dmi' + + school = SCHOOL_TRANSMUTATION + cooldown_time = 15 SECONDS + cooldown_reduction_per_rank = 3 SECONDS + + invocation = "KN'A FTAGHU, PUCK 'BTHNK!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + active_msg = "You prepare to curse a target..." + deactive_msg = "You dispel the curse." + +/datum/action/cooldown/spell/pointed/barnyardcurse/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return FALSE + if(!ishuman(cast_on)) + return FALSE + + var/mob/living/carbon/human/human_target = cast_on + if(!human_target.wear_mask) + return TRUE + + return !(human_target.wear_mask.type in GLOB.cursed_animal_masks) + +/datum/action/cooldown/spell/pointed/barnyardcurse/cast(mob/living/carbon/human/cast_on) + . = ..() + if(cast_on.can_block_magic(antimagic_flags)) + cast_on.visible_message( + span_danger("[cast_on]'s face bursts into flames, which instantly burst outward, leaving [cast_on.p_them()] unharmed!"), + span_danger("Your face starts burning up, but the flames are repulsed by your anti-magic protection!"), + ) + to_chat(owner, span_warning("The spell had no effect!")) + return FALSE + + var/chosen_type = pick(GLOB.cursed_animal_masks) + var/obj/item/clothing/mask/animal/cursed_mask = new chosen_type(get_turf(target)) + + cast_on.visible_message( + span_danger("[target]'s face bursts into flames, and a barnyard animal's head takes its place!"), + span_userdanger("Your face burns up, and shortly after the fire you realise you have the face of a [cursed_mask.animal_type]!"), + ) + + // Can't drop? Nuke it + if(!cast_on.dropItemToGround(cast_on.wear_mask)) + qdel(cast_on.wear_mask) + + cast_on.equip_to_slot_if_possible(cursed_mask, ITEM_SLOT_MASK, TRUE, TRUE) + cast_on.flash_act() + return TRUE diff --git a/code/modules/spells/spell_types/pointed/blind.dm b/code/modules/spells/spell_types/pointed/blind.dm new file mode 100644 index 0000000000000..cd044e5cb4013 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/blind.dm @@ -0,0 +1,51 @@ +/datum/action/cooldown/spell/pointed/blind + name = "Blind" + desc = "This spell temporarily blinds a single target." + button_icon_state = "blind" + ranged_mousepointer = 'icons/effects/mouse_pointers/blind_target.dmi' + + sound = 'sound/magic/blind.ogg' + school = SCHOOL_TRANSMUTATION + cooldown_time = 30 SECONDS + cooldown_reduction_per_rank = 6.25 SECONDS + + invocation = "STI KALY" + invocation_type = INVOCATION_WHISPER + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + active_msg = "You prepare to blind a target..." + + /// The amount of blind to apply + var/eye_blind_amount = 10 + /// The amount of blurriness to apply + var/eye_blurry_amount = 20 + /// The duration of the blind mutation placed on the person + var/blind_mutation_duration = 30 SECONDS + +/datum/action/cooldown/spell/pointed/blind/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return FALSE + if(!ishuman(cast_on)) + return FALSE + + var/mob/living/carbon/human/human_target = cast_on + return !human_target.is_blind() + +/datum/action/cooldown/spell/pointed/blind/cast(mob/living/carbon/human/cast_on) + . = ..() + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(cast_on, span_notice("Your eye itches, but it passes momentarily.")) + to_chat(owner, span_warning("The spell had no effect!")) + return FALSE + + to_chat(cast_on, span_warning("Your eyes cry out in pain!")) + cast_on.blind_eyes(eye_blind_amount) + cast_on.blur_eyes(eye_blurry_amount) + if(cast_on.dna && blind_mutation_duration > 0 SECONDS) + cast_on.dna.add_mutation(/datum/mutation/human/blind) + addtimer(CALLBACK(src, .proc/fix_eyes, cast_on), blind_mutation_duration) + return TRUE + +/datum/action/cooldown/spell/pointed/blind/proc/fix_eyes(mob/living/carbon/human/cast_on) + cast_on.dna?.remove_mutation(/datum/mutation/human/blind) diff --git a/code/modules/spells/spell_types/pointed/dominate.dm b/code/modules/spells/spell_types/pointed/dominate.dm new file mode 100644 index 0000000000000..f5b8aaa9c8c5a --- /dev/null +++ b/code/modules/spells/spell_types/pointed/dominate.dm @@ -0,0 +1,49 @@ +/datum/action/cooldown/spell/pointed/dominate + name = "Dominate" + desc = "This spell dominates the mind of a lesser creature to the will of Nar'Sie, \ + allying it only to her direct followers." + background_icon_state = "bg_demon" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "dominate" + ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' + + school = SCHOOL_EVOCATION + cooldown_time = 1 MINUTES + invocation_type = INVOCATION_NONE + spell_requirements = NONE + // An UNHOLY, MAGIC SPELL that INFLUECNES THE MIND - all things work here, logically + antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY|MAGIC_RESISTANCE_MIND + + cast_range = 7 + active_msg = "You prepare to dominate the mind of a target..." + +/datum/action/cooldown/spell/pointed/dominate/is_valid_target(atom/cast_on) + if(!isanimal(cast_on)) + return FALSE + + var/mob/living/simple_animal/animal = cast_on + if(animal.mind) + return FALSE + if(animal.stat == DEAD) + return FALSE + if(animal.sentience_type != SENTIENCE_ORGANIC) + return FALSE + if("cult" in animal.faction) + return FALSE + if(HAS_TRAIT(animal, TRAIT_HOLY)) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/dominate/cast(mob/living/simple_animal/cast_on) + . = ..() + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(cast_on, span_warning("Your feel someone attempting to subject your mind to terrible machinations!")) + to_chat(owner, span_warning("[cast_on] resists your domination!")) + return FALSE + + var/turf/cast_turf = get_turf(cast_on) + cast_on.add_atom_colour("#990000", FIXED_COLOUR_PRIORITY) + cast_on.faction |= "cult" + playsound(cast_turf, 'sound/effects/ghost.ogg', 100, TRUE) + new /obj/effect/temp_visual/cult/sac(cast_turf) diff --git a/code/modules/spells/spell_types/pointed/finger_guns.dm b/code/modules/spells/spell_types/pointed/finger_guns.dm new file mode 100644 index 0000000000000..9c495d27d755e --- /dev/null +++ b/code/modules/spells/spell_types/pointed/finger_guns.dm @@ -0,0 +1,48 @@ +/datum/action/cooldown/spell/pointed/projectile/finger_guns + name = "Finger Guns" + desc = "Shoot up to three mimed bullets from your fingers that damage and mute their targets. \ + Can't be used if you have something in your hands." + background_icon_state = "bg_mime" + icon_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "finger_guns0" + panel = "Mime" + sound = null + + school = SCHOOL_MIME + cooldown_time = 30 SECONDS + + invocation = "" + invocation_type = INVOCATION_EMOTE + invocation_self_message = span_danger("You fire your finger gun!") + + spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW + antimagic_flags = NONE + spell_max_level = 1 + + base_icon_state = "finger_guns" + active_msg = "You draw your fingers!" + deactive_msg = "You put your fingers at ease. Another time." + cast_range = 20 + projectile_type = /obj/projectile/bullet/mime + projectile_amount = 3 + +/datum/action/cooldown/spell/pointed/projectile/finger_guns/can_invoke(feedback = TRUE) + if(invocation_type == INVOCATION_EMOTE) + if(!ishuman(owner)) + return FALSE + + var/mob/living/carbon/human/human_owner = owner + if(human_owner.incapacitated()) + if(feedback) + to_chat(owner, span_warning("You can't properly point your fingers while incapacitated.")) + return FALSE + if(human_owner.get_active_held_item()) + if(feedback) + to_chat(owner, span_warning("You can't properly fire your finger guns with something in your hand.")) + return FALSE + + return ..() + +/datum/action/cooldown/spell/pointed/projectile/finger_guns/before_cast(atom/cast_on) + . = ..() + invocation = span_notice("[cast_on] fires [cast_on.p_their()] finger gun!") diff --git a/code/modules/spells/spell_types/pointed/fireball.dm b/code/modules/spells/spell_types/pointed/fireball.dm new file mode 100644 index 0000000000000..47fd05c0f4680 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/fireball.dm @@ -0,0 +1,23 @@ +/datum/action/cooldown/spell/pointed/projectile/fireball + name = "Fireball" + desc = "This spell fires an explosive fireball at a target." + button_icon_state = "fireball0" + + sound = 'sound/magic/fireball.ogg' + school = SCHOOL_EVOCATION + cooldown_time = 6 SECONDS + cooldown_reduction_per_rank = 1 SECONDS // 1 second reduction per rank + + invocation = "ONI SOMA!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + base_icon_state = "fireball" + active_msg = "You prepare to cast your fireball spell!" + deactive_msg = "You extinguish your fireball... for now." + cast_range = 8 + projectile_type = /obj/projectile/magic/fireball + +/datum/action/cooldown/spell/pointed/projectile/fireball/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + . = ..() + to_fire.range = (6 + 2 * spell_level) diff --git a/code/modules/spells/spell_types/pointed/lightning_bolt.dm b/code/modules/spells/spell_types/pointed/lightning_bolt.dm new file mode 100644 index 0000000000000..e88e35235718f --- /dev/null +++ b/code/modules/spells/spell_types/pointed/lightning_bolt.dm @@ -0,0 +1,43 @@ +/datum/action/cooldown/spell/pointed/projectile/lightningbolt + name = "Lightning Bolt" + desc = "Fire a lightning bolt at your foes! It will jump between targets, but can't knock them down." + button_icon_state = "lightning0" + + sound = 'sound/magic/lightningbolt.ogg' + school = SCHOOL_EVOCATION + cooldown_time = 10 SECONDS + cooldown_reduction_per_rank = 2 SECONDS + + invocation = "P'WAH, UNLIM'TED P'WAH!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + base_icon_state = "lightning" + active_msg = "You energize your hands with arcane lightning!" + deactive_msg = "You let the energy flow out of your hands back into yourself..." + projectile_type = /obj/projectile/magic/aoe/lightning + + /// The range the bolt itself (different to the range of the projectile) + var/bolt_range = 15 + /// The power of the bolt itself + var/bolt_power = 20000 + /// The flags the bolt itself takes when zapping someone + var/bolt_flags = ZAP_MOB_DAMAGE + +/datum/action/cooldown/spell/pointed/projectile/lightningbolt/Grant(mob/grant_to) + . = ..() + ADD_TRAIT(owner, TRAIT_TESLA_SHOCKIMMUNE, type) + +/datum/action/cooldown/spell/pointed/projectile/lightningbolt/Remove(mob/living/remove_from) + REMOVE_TRAIT(remove_from, TRAIT_TESLA_SHOCKIMMUNE, type) + return ..() + +/datum/action/cooldown/spell/pointed/projectile/lightningbolt/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + . = ..() + if(!istype(to_fire, /obj/projectile/magic/aoe/lightning)) + return + + var/obj/projectile/magic/aoe/lightning/bolt = to_fire + bolt.zap_range = bolt_range + bolt.zap_power = bolt_power + bolt.zap_flags = bolt_flags diff --git a/code/modules/spells/spell_types/pointed/mind_transfer.dm b/code/modules/spells/spell_types/pointed/mind_transfer.dm new file mode 100644 index 0000000000000..bf31d402a4c22 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/mind_transfer.dm @@ -0,0 +1,125 @@ +/datum/action/cooldown/spell/pointed/mind_transfer + name = "Mind Swap" + desc = "This spell allows the user to switch bodies with a target next to him." + button_icon_state = "mindswap" + ranged_mousepointer = 'icons/effects/mouse_pointers/mindswap_target.dmi' + + school = SCHOOL_TRANSMUTATION + cooldown_time = 60 SECONDS + cooldown_reduction_per_rank = 10 SECONDS + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_MIND|SPELL_CASTABLE_AS_BRAIN + antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND + + invocation = "GIN'YU CAPAN" + invocation_type = INVOCATION_WHISPER + + active_msg = "You prepare to swap minds with a target..." + deactive_msg = "You dispel mind swap." + cast_range = 1 + + /// If TRUE, we cannot mindswap into mobs with minds if they do not currently have a key / player. + var/target_requires_key = TRUE + /// For how long is the caster stunned for after the spell + var/unconscious_amount_caster = 40 SECONDS + /// For how long is the victim stunned for after the spell + var/unconscious_amount_victim = 40 SECONDS + /// List of mobs we cannot mindswap into. + var/static/list/mob/living/blacklisted_mobs = typecacheof(list( + /mob/living/brain, + /mob/living/silicon/pai, + /mob/living/simple_animal/hostile/imp/slaughter, + /mob/living/simple_animal/hostile/megafauna, + )) + +/datum/action/cooldown/spell/pointed/mind_transfer/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!isliving(owner)) + return FALSE + if(owner.suiciding) + if(feedback) + to_chat(owner, span_warning("You're killing yourself! You can't concentrate enough to do this!")) + return FALSE + return TRUE + +/datum/action/cooldown/spell/pointed/mind_transfer/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return FALSE + + if(!isliving(cast_on)) + to_chat(owner, span_warning("You can only swap minds with living beings!")) + return FALSE + if(is_type_in_typecache(cast_on, blacklisted_mobs)) + to_chat(owner, span_warning("This creature is too [pick("powerful", "strange", "arcane", "obscene")] to control!")) + return FALSE + if(isguardian(cast_on)) + var/mob/living/simple_animal/hostile/guardian/stand = cast_on + if(stand.summoner && stand.summoner == owner) + to_chat(owner, span_warning("Swapping minds with your own guardian would just put you back into your own head!")) + return FALSE + + var/mob/living/living_target = cast_on + if(living_target.stat == DEAD) + to_chat(owner, span_warning("You don't particularly want to be dead!")) + return FALSE + if(!living_target.mind) + to_chat(owner, span_warning("[living_target.p_theyve(TRUE)] doesn't appear to have a mind to swap into!")) + return FALSE + if(!living_target.key && target_requires_key) + to_chat(owner, span_warning("[living_target.p_theyve(TRUE)] appear[living_target.p_s()] to be catatonic! \ + Not even magic can affect [living_target.p_their()] vacant mind.")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/mind_transfer/cast(mob/living/cast_on) + . = ..() + swap_minds(owner, cast_on) + +/datum/action/cooldown/spell/pointed/mind_transfer/proc/swap_minds(mob/living/caster, mob/living/cast_on) + + var/mob/living/to_swap = cast_on + if(isguardian(cast_on)) + var/mob/living/simple_animal/hostile/guardian/stand = cast_on + if(stand.summoner) + to_swap = stand.summoner + + var/datum/mind/mind_to_swap = to_swap.mind + if(to_swap.can_block_magic(antimagic_flags) \ + || mind_to_swap.has_antag_datum(/datum/antagonist/wizard) \ + || mind_to_swap.has_antag_datum(/datum/antagonist/cult) \ + || mind_to_swap.has_antag_datum(/datum/antagonist/changeling) \ + || mind_to_swap.has_antag_datum(/datum/antagonist/rev) \ + || mind_to_swap.key?[1] == "@" \ + ) + to_chat(caster, span_warning("[to_swap.p_their(TRUE)] mind is resisting your spell!")) + return FALSE + + // MIND TRANSFER BEGIN + + var/datum/mind/caster_mind = caster.mind + var/datum/mind/to_swap_mind = to_swap.mind + + var/to_swap_key = to_swap.key + + caster_mind.transfer_to(to_swap) + to_swap_mind.transfer_to(caster) + + // Just in case the swappee's key wasn't grabbed by transfer_to... + if(to_swap_key) + caster.key = to_swap_key + + // MIND TRANSFER END + + // Now we knock both mobs out for a time. + caster.Unconscious(unconscious_amount_caster) + to_swap.Unconscious(unconscious_amount_victim) + + // Only the caster and victim hear the sounds, + // that way no one knows for sure if the swap happened + SEND_SOUND(caster, sound('sound/magic/mandswap.ogg')) + SEND_SOUND(to_swap, sound('sound/magic/mandswap.ogg')) + + return TRUE diff --git a/code/modules/spells/spell_types/pointed/spell_cards.dm b/code/modules/spells/spell_types/pointed/spell_cards.dm new file mode 100644 index 0000000000000..3ee5924fcfe07 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/spell_cards.dm @@ -0,0 +1,82 @@ +/datum/action/cooldown/spell/pointed/projectile/spell_cards + name = "Spell Cards" + desc = "Blazing hot rapid-fire homing cards. Send your foes to the shadow realm with their mystical power!" + button_icon_state = "spellcard0" + click_cd_override = 1 + + school = SCHOOL_EVOCATION + cooldown_time = 5 SECONDS + cooldown_reduction_per_rank = 1 SECONDS + + invocation = "Sigi'lu M'Fan 'Tasia!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + base_icon_state = "spellcard" + cast_range = 40 + projectile_type = /obj/projectile/magic/spellcard + projectile_amount = 5 + projectiles_per_fire = 7 + + /// A weakref to the mob we're currently targeting with the lockon component. + var/datum/weakref/current_target_weakref + /// The turn rate of the spell cards in flight. (They track onto locked on targets) + var/projectile_turnrate = 10 + /// The homing spread of the spell cards in flight. + var/projectile_pixel_homing_spread = 32 + /// The initial spread of the spell cards when fired. + var/projectile_initial_spread_amount = 30 + /// The location spread of the spell cards when fired. + var/projectile_location_spread_amount = 12 + /// A ref to our lockon component, which is created and destroyed on activation and deactivation. + var/datum/component/lockon_aiming/lockon_component + +/datum/action/cooldown/spell/pointed/projectile/spell_cards/Destroy() + QDEL_NULL(lockon_component) + return ..() + +/datum/action/cooldown/spell/pointed/projectile/spell_cards/on_activation(mob/on_who) + . = ..() + if(!.) + return + + QDEL_NULL(lockon_component) + lockon_component = owner.AddComponent( \ + /datum/component/lockon_aiming, \ + range = 5, \ + typecache = GLOB.typecache_living, \ + amount = 1, \ + when_locked = CALLBACK(src, .proc/on_lockon_component)) + +/datum/action/cooldown/spell/pointed/projectile/spell_cards/proc/on_lockon_component(list/locked_weakrefs) + if(!length(locked_weakrefs)) + current_target_weakref = null + return + current_target_weakref = locked_weakrefs[1] + var/atom/real_target = current_target_weakref.resolve() + if(real_target) + owner.face_atom(real_target) + +/datum/action/cooldown/spell/pointed/projectile/spell_cards/on_deactivation(mob/on_who, refund_cooldown = TRUE) + . = ..() + QDEL_NULL(lockon_component) + +/datum/action/cooldown/spell/pointed/projectile/spell_cards/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + . = ..() + if(current_target_weakref) + var/atom/real_target = current_target_weakref?.resolve() + if(real_target && get_dist(real_target, user) < 7) + to_fire.homing_turn_speed = projectile_turnrate + to_fire.homing_inaccuracy_min = projectile_pixel_homing_spread + to_fire.homing_inaccuracy_max = projectile_pixel_homing_spread + to_fire.set_homing_target(real_target) + + var/rand_spr = rand() + var/total_angle = projectile_initial_spread_amount * 2 + var/adjusted_angle = total_angle - ((projectile_initial_spread_amount / projectiles_per_fire) * 0.5) + var/one_fire_angle = adjusted_angle / projectiles_per_fire + var/current_angle = iteration * one_fire_angle * rand_spr - (projectile_initial_spread_amount / 2) + + to_fire.pixel_x = rand(-projectile_location_spread_amount, projectile_location_spread_amount) + to_fire.pixel_y = rand(-projectile_location_spread_amount, projectile_location_spread_amount) + to_fire.preparePixelProjectile(target, user, null, current_angle) diff --git a/code/modules/spells/spell_types/projectile.dm b/code/modules/spells/spell_types/projectile.dm deleted file mode 100644 index aaf468899fab7..0000000000000 --- a/code/modules/spells/spell_types/projectile.dm +++ /dev/null @@ -1,130 +0,0 @@ - - -/obj/projectile/magic/spell - name = "custom spell projectile" - var/list/ignored_factions //Do not hit these - var/check_holy = FALSE - var/check_antimagic = FALSE - var/trigger_range = 0 //How far we do we need to be to hit - var/linger = FALSE //Can't hit anything but the intended target - - var/trail = FALSE //if it leaves a trail - var/trail_lifespan = 0 //deciseconds - var/trail_icon = 'icons/obj/wizard.dmi' - var/trail_icon_state = "trail" - -//todo unify this and magic/aoe under common path -/obj/projectile/magic/spell/Range() - if(trigger_range > 1) - for(var/mob/living/L in range(trigger_range, get_turf(src))) - if(can_hit_target(L, ignore_loc = TRUE)) - return Bump(L) - . = ..() - -/obj/projectile/magic/spell/Moved(atom/OldLoc, Dir) - . = ..() - if(trail) - create_trail() - -/obj/projectile/magic/spell/proc/create_trail() - if(!trajectory) - return - var/datum/point/vector/previous = trajectory.return_vector_after_increments(1,-1) - var/obj/effect/overlay/trail = new /obj/effect/overlay(previous.return_turf()) - trail.pixel_x = previous.return_px() - trail.pixel_y = previous.return_py() - trail.icon = trail_icon - trail.icon_state = trail_icon_state - //might be changed to temp overlay - trail.set_density(FALSE) - trail.mouse_opacity = MOUSE_OPACITY_TRANSPARENT - QDEL_IN(trail, trail_lifespan) - -/obj/projectile/magic/spell/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE) - . = ..() - if(linger && target != original) - return FALSE - if(ismob(target) && !direct_target) //Unsure about the direct target, i guess it could always skip these. - var/mob/M = target - if(M.anti_magic_check(check_antimagic, check_holy)) - return FALSE - if(ignored_factions?.len && faction_check(M.faction,ignored_factions)) - return FALSE - - -//NEEDS MAJOR CODE CLEANUP. - -/obj/effect/proc_holder/spell/targeted/projectile - name = "Projectile" - desc = "This spell summons projectiles which try to hit the targets." - - - - var/proj_type = /obj/projectile/magic/spell //IMPORTANT use only subtypes of this - - - var/update_projectile = FALSE //So you want to admin abuse magic bullets ? This is for you - //Below only apply if update_projectile is true - var/proj_icon = 'icons/obj/projectiles.dmi' - var/proj_icon_state = "spell" - var/proj_name = "a spell projectile" - var/proj_trail = FALSE //if it leaves a trail - var/proj_trail_lifespan = 0 //deciseconds - var/proj_trail_icon = 'icons/obj/wizard.dmi' - var/proj_trail_icon_state = "trail" - var/proj_lingering = FALSE //if it lingers or disappears upon hitting an obstacle - var/proj_homing = TRUE //if it follows the target - var/proj_insubstantial = FALSE //if it can pass through dense objects or not - var/proj_trigger_range = 0 //the range from target at which the projectile triggers cast(target) - var/proj_lifespan = 15 //in deciseconds * proj_step_delay - var/proj_step_delay = 1 //lower = faster - var/list/ignore_factions = list() //Faction types that will be ignored - var/check_antimagic = TRUE - var/check_holy = FALSE - -/obj/effect/proc_holder/spell/targeted/projectile/proc/fire_projectile(atom/target, mob/user) - var/obj/projectile/magic/spell/projectile = new proj_type(null, spell_level) - - if(update_projectile) - //Generally these should already be set on the projectile, this is mostly here for varedited spells. - projectile.icon = proj_icon - projectile.icon_state = proj_icon_state - projectile.name = proj_name - if(proj_insubstantial) - projectile.movement_type |= PHASING - if(proj_homing) - projectile.homing = TRUE - projectile.homing_turn_speed = 360 //Perfect tracking - if(proj_lingering) - projectile.linger = TRUE - projectile.trigger_range = proj_trigger_range - projectile.ignored_factions = ignore_factions - projectile.range = proj_lifespan - projectile.speed = proj_step_delay - projectile.trail = proj_trail - projectile.trail_lifespan = proj_trail_lifespan - projectile.trail_icon = proj_trail_icon - projectile.trail_icon_state = proj_trail_icon_state - - projectile.preparePixelProjectile(target,user) - if(projectile.homing) - projectile.set_homing_target(target) - projectile.fire() - -/obj/effect/proc_holder/spell/targeted/projectile/cast(list/targets, mob/user = usr) - playMagSound() - for(var/atom/target in targets) - fire_projectile(target, user) - -//This one just pops one projectile in direction user is facing, irrelevant of max_targets etc -/obj/effect/proc_holder/spell/targeted/projectile/dumbfire - name = "Dumbfire projectile" - -/obj/effect/proc_holder/spell/targeted/projectile/dumbfire/choose_targets(mob/user = usr) - var/turf/T = get_turf(user) - for(var/i in 1 to range-1) - var/turf/new_turf = get_step(T, user.dir) - if(new_turf.density) - break - T = new_turf - perform(list(T),user = user) diff --git a/code/modules/spells/spell_types/projectile/_basic_projectile.dm b/code/modules/spells/spell_types/projectile/_basic_projectile.dm new file mode 100644 index 0000000000000..f9bd303f56f1d --- /dev/null +++ b/code/modules/spells/spell_types/projectile/_basic_projectile.dm @@ -0,0 +1,29 @@ +/** + * ## Basic Projectile spell + * + * Simply fires specified projectile type the direction the caster is facing. + * + * Behavior could / should probably be unified with pointed projectile spells + * and aoe projectile spells in the future. + */ +/datum/action/cooldown/spell/basic_projectile + /// How far we try to fire the basic projectile. Blocked by dense objects. + var/projectile_range = 7 + /// The projectile type fired at all people around us + var/obj/projectile/projectile_type = /obj/projectile/magic/aoe/magic_missile + +/datum/action/cooldown/spell/basic_projectile/cast(atom/cast_on) + . = ..() + var/turf/target_turf = get_turf(cast_on) + for(var/i in 1 to projectile_range - 1) + var/turf/next_turf = get_step(target_turf, cast_on.dir) + if(next_turf.density) + break + target_turf = next_turf + + fire_projectile(target_turf, cast_on) + +/datum/action/cooldown/spell/basic_projectile/proc/fire_projectile(atom/target, atom/caster) + var/obj/projectile/to_fire = new projectile_type() + to_fire.preparePixelProjectile(target, caster) + to_fire.fire() diff --git a/code/modules/spells/spell_types/projectile/juggernaut.dm b/code/modules/spells/spell_types/projectile/juggernaut.dm new file mode 100644 index 0000000000000..443c9cf62e5cd --- /dev/null +++ b/code/modules/spells/spell_types/projectile/juggernaut.dm @@ -0,0 +1,12 @@ +/datum/action/cooldown/spell/basic_projectile/juggernaut + name = "Gauntlet Echo" + desc = "Channels energy into your gauntlet - firing its essence forward in a slow moving, yet devastating, attack." + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "cultfist" + background_icon_state = "bg_demon" + sound = 'sound/weapons/resonator_blast.ogg' + + cooldown_time = 35 SECONDS + spell_requirements = NONE + + projectile_type = /obj/projectile/magic/aoe/juggernaut diff --git a/code/modules/spells/spell_types/rightandwrong.dm b/code/modules/spells/spell_types/rightandwrong.dm index 7e5371b6d878a..1b0738ecbb6e0 100644 --- a/code/modules/spells/spell_types/rightandwrong.dm +++ b/code/modules/spells/spell_types/rightandwrong.dm @@ -50,15 +50,15 @@ GLOBAL_LIST_INIT(summoned_guns, list( //if you add anything that isn't covered by the typepaths below, add it to summon_magic_objective_types GLOBAL_LIST_INIT(summoned_magic, list( - /obj/item/book/granter/spell/fireball, - /obj/item/book/granter/spell/smoke, - /obj/item/book/granter/spell/blind, - /obj/item/book/granter/spell/mindswap, - /obj/item/book/granter/spell/forcewall, - /obj/item/book/granter/spell/knock, - /obj/item/book/granter/spell/barnyard, - /obj/item/book/granter/spell/charge, - /obj/item/book/granter/spell/summonitem, + /obj/item/book/granter/action/spell/fireball, + /obj/item/book/granter/action/spell/smoke, + /obj/item/book/granter/action/spell/blind, + /obj/item/book/granter/action/spell/mindswap, + /obj/item/book/granter/action/spell/forcewall, + /obj/item/book/granter/action/spell/knock, + /obj/item/book/granter/action/spell/barnyard, + /obj/item/book/granter/action/spell/charge, + /obj/item/book/granter/action/spell/summonitem, /obj/item/gun/magic/wand, /obj/item/gun/magic/wand/death, /obj/item/gun/magic/wand/resurrection, diff --git a/code/modules/spells/spell_types/rod_form.dm b/code/modules/spells/spell_types/rod_form.dm deleted file mode 100644 index 28be733dafe77..0000000000000 --- a/code/modules/spells/spell_types/rod_form.dm +++ /dev/null @@ -1,56 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/rod_form - name = "Rod Form" - desc = "Take on the form of an immovable rod, destroying all in your path. Purchasing this spell multiple times will also increase the rod's damage and travel range." - clothes_req = TRUE - human_req = FALSE - charge_max = 250 - cooldown_min = 100 - range = -1 - include_user = TRUE - invocation = "CLANG!" - invocation_type = INVOCATION_SHOUT - action_icon_state = "immrod" - -/obj/effect/proc_holder/spell/targeted/rod_form/cast(list/targets,mob/user = usr) - var/area/A = get_area(user) - if(istype(A, /area/wizard_station)) - to_chat(user, "You know better than to trash Wizard Federation property. Best wait until you leave to use [src].") - return - for(var/mob/living/M in targets) - var/turf/start = get_turf(M) - var/obj/effect/immovablerod/wizard/W = new(start, get_ranged_target_turf(start, M.dir, (15 + spell_level * 3))) - W.wizard = M - W.max_distance += spell_level * 3 //You travel farther when you upgrade the spell - W.damage_bonus += spell_level * 20 //You do more damage when you upgrade the spell - W.start_turf = start - M.forceMove(W) - M.notransform = TRUE - M.status_flags |= GODMODE - -//Wizard Version of the Immovable Rod - -/obj/effect/immovablerod/wizard - var/max_distance = 13 - var/damage_bonus = 0 - var/turf/start_turf - notify = FALSE - -/obj/effect/immovablerod/wizard/Moved() - . = ..() - if(get_dist(start_turf, get_turf(src)) >= max_distance && !QDELETED(src)) - qdel(src) - -/obj/effect/immovablerod/wizard/Destroy() - if(wizard) - wizard.status_flags &= ~GODMODE - wizard.notransform = FALSE - wizard.forceMove(get_turf(src)) - return ..() - -/obj/effect/immovablerod/wizard/penetrate(mob/living/L) - if(L.anti_magic_check()) - L.visible_message("[src] hits [L], but it bounces back, then vanishes!" , "[src] hits you... but it bounces back, then vanishes!" , "You hear a weak, sad, CLANG.") - qdel(src) - return - L.visible_message("[L] is penetrated by an immovable rod!" , "The rod penetrates you!" , "You hear a CLANG!") - L.adjustBruteLoss(70 + damage_bonus) diff --git a/code/modules/spells/spell_types/santa.dm b/code/modules/spells/spell_types/santa.dm deleted file mode 100644 index 350206eaf7391..0000000000000 --- a/code/modules/spells/spell_types/santa.dm +++ /dev/null @@ -1,16 +0,0 @@ -//Santa spells! -/obj/effect/proc_holder/spell/aoe_turf/conjure/presents - name = "Conjure Presents!" - desc = "This spell lets you reach into S-space and retrieve presents! Yay!" - school = "santa" - charge_max = 600 - clothes_req = FALSE - antimagic_allowed = TRUE - invocation = "HO HO HO" - invocation_type = INVOCATION_SHOUT - range = 3 - cooldown_min = 50 - - summon_type = list("/obj/item/a_gift") - summon_lifespan = 0 - summon_amt = 5 diff --git a/code/modules/spells/spell_types/self/basic_heal.dm b/code/modules/spells/spell_types/self/basic_heal.dm new file mode 100644 index 0000000000000..a4acba2d88451 --- /dev/null +++ b/code/modules/spells/spell_types/self/basic_heal.dm @@ -0,0 +1,27 @@ +// This spell exists mainly for debugging purposes, and also to show how casting works +/datum/action/cooldown/spell/basic_heal + name = "Lesser Heal" + desc = "Heals a small amount of brute and burn damage to the caster." + + sound = 'sound/magic/staff_healing.ogg' + school = SCHOOL_RESTORATION + cooldown_time = 10 SECONDS + cooldown_reduction_per_rank = 1.25 SECONDS + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_HUMAN + + invocation = "Victus sano!" + invocation_type = INVOCATION_WHISPER + + /// Amount of brute to heal to the spell caster on cast + var/brute_to_heal = 10 + /// Amount of burn to heal to the spell caster on cast + var/burn_to_heal = 10 + +/datum/action/cooldown/spell/basic_heal/cast(mob/living/cast_on) + . = ..() + cast_on.visible_message( + span_warning("A wreath of gentle light passes over [cast_on]!"), + span_notice("You wreath yourself in healing light!"), + ) + cast_on.adjustBruteLoss(-brute_to_heal, FALSE) + cast_on.adjustFireLoss(-burn_to_heal) diff --git a/code/modules/spells/spell_types/self/charge.dm b/code/modules/spells/spell_types/self/charge.dm new file mode 100644 index 0000000000000..87d7ae287d337 --- /dev/null +++ b/code/modules/spells/spell_types/self/charge.dm @@ -0,0 +1,58 @@ +/datum/action/cooldown/spell/charge + name = "Charge" + desc = "This spell can be used to recharge a variety of things in your hands, \ + from magical artifacts to electrical components. A creative wizard can even use it \ + to grant magical power to a fellow magic user." + button_icon_state = "charge" + + sound = 'sound/magic/charge.ogg' + school = SCHOOL_TRANSMUTATION + cooldown_time = 60 SECONDS + cooldown_reduction_per_rank = 5 SECONDS + + invocation = "DIRI CEL" + invocation_type = INVOCATION_WHISPER + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + +/datum/action/cooldown/spell/charge/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/datum/action/cooldown/spell/charge/cast(mob/living/cast_on) + . = ..() + + // Charge people we're pulling first and foremost + if(isliving(cast_on.pulling)) + var/mob/living/pulled_living = cast_on.pulling + var/pulled_has_spells = FALSE + + for(var/datum/action/cooldown/spell/spell in pulled_living.actions) + spell.reset_spell_cooldown() + pulled_has_spells = TRUE + + if(pulled_has_spells) + to_chat(pulled_living, span_notice("You feel raw magic flowing through you. It feels good!")) + to_chat(cast_on, span_notice("[pulled_living] suddenly feels very warm!")) + return + + to_chat(pulled_living, span_notice("You feel very strange for a moment, but then it passes.")) + + // Then charge their main hand item, then charge their offhand item + var/obj/item/to_charge = cast_on.get_active_held_item() || cast_on.get_inactive_held_item() + if(!to_charge) + to_chat(cast_on, span_notice("You feel magical power surging through your hands, but the feeling rapidly fades.")) + return + + var/charge_return = SEND_SIGNAL(to_charge, COMSIG_ITEM_MAGICALLY_CHARGED, src, cast_on) + + if(QDELETED(to_charge)) + to_chat(cast_on, span_warning("[src] seems to react adversely with [to_charge]!")) + return + + if(charge_return & COMPONENT_ITEM_BURNT_OUT) + to_chat(cast_on, span_warning("[to_charge] seems to react negatively to [src], becoming uncomfortably warm!")) + + else if(charge_return & COMPONENT_ITEM_CHARGED) + to_chat(cast_on, span_notice("[to_charge] suddenly feels very warm!")) + + else + to_chat(cast_on, span_notice("[to_charge] doesn't seem to be react to [src].")) diff --git a/code/modules/spells/spell_types/self/disable_tech.dm b/code/modules/spells/spell_types/self/disable_tech.dm new file mode 100644 index 0000000000000..543daa467791e --- /dev/null +++ b/code/modules/spells/spell_types/self/disable_tech.dm @@ -0,0 +1,30 @@ +/datum/action/cooldown/spell/emp + name = "Emplosion" + desc = "This spell emplodes an area." + button_icon_state = "emp" + sound = 'sound/weapons/zapbang.ogg' + + school = SCHOOL_EVOCATION + + /// The heavy radius of the EMP + var/emp_heavy = 2 + /// The light radius of the EMP + var/emp_light = 3 + +/datum/action/cooldown/spell/emp/cast(atom/cast_on) + . = ..() + empulse(get_turf(cast_on), emp_heavy, emp_light) + +/datum/action/cooldown/spell/emp/disable_tech + name = "Disable Tech" + desc = "This spell disables all weapons, cameras and most other technology in range." + sound = 'sound/magic/disable_tech.ogg' + + cooldown_time = 40 SECONDS + cooldown_reduction_per_rank = 5 SECONDS + + invocation = "NEC CANTIO" + invocation_type = INVOCATION_SHOUT + + emp_heavy = 6 + emp_light = 10 diff --git a/code/modules/spells/spell_types/self/forcewall.dm b/code/modules/spells/spell_types/self/forcewall.dm new file mode 100644 index 0000000000000..e037c1ae689d9 --- /dev/null +++ b/code/modules/spells/spell_types/self/forcewall.dm @@ -0,0 +1,66 @@ +/datum/action/cooldown/spell/forcewall + name = "Forcewall" + desc = "Create a magical barrier that only you can pass through." + button_icon_state = "shield" + + sound = 'sound/magic/forcewall.ogg' + school = SCHOOL_TRANSMUTATION + cooldown_time = 10 SECONDS + cooldown_reduction_per_rank = 1.25 SECONDS + + invocation = "TARCOL MINTI ZHERI" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + /// The typepath to the wall we create on cast. + var/wall_type = /obj/effect/forcefield/wizard + +/datum/action/cooldown/spell/forcewall/cast(atom/cast_on) + . = ..() + new wall_type(get_turf(owner), owner) + + if(owner.dir == SOUTH || owner.dir == NORTH) + new wall_type(get_step(owner, EAST), owner, antimagic_flags) + new wall_type(get_step(owner, WEST), owner, antimagic_flags) + + else + new wall_type(get_step(owner, NORTH), owner, antimagic_flags) + new wall_type(get_step(owner, SOUTH), owner, antimagic_flags) + +/datum/action/cooldown/spell/forcewall/cult + name = "Shield" + desc = "This spell creates a temporary forcefield to shield yourself and allies from incoming fire." + background_icon_state = "bg_demon" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "cultforcewall" + + cooldown_time = 40 SECONDS + invocation_type = INVOCATION_NONE + + wall_type = /obj/effect/forcefield/cult + +/datum/action/cooldown/spell/forcewall/mime + name = "Invisible Blockade" + desc = "Form an invisible three tile wide blockade." + background_icon_state = "bg_mime" + icon_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "invisible_blockade" + panel = "Mime" + sound = null + + school = SCHOOL_MIME + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 0 SECONDS + spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW + antimagic_flags = NONE + + invocation = "" + invocation_type = INVOCATION_EMOTE + invocation_self_message = span_notice("You form a blockade in front of yourself.") + spell_max_level = 1 + + wall_type = /obj/effect/forcefield/mime/advanced + +/datum/action/cooldown/spell/forcewall/mime/before_cast(atom/cast_on) + . = ..() + invocation = span_notice("[cast_on] looks as if a blockade is in front of [cast_on.p_them()].") diff --git a/code/modules/spells/spell_types/self/lichdom.dm b/code/modules/spells/spell_types/self/lichdom.dm new file mode 100644 index 0000000000000..69325f9df97ab --- /dev/null +++ b/code/modules/spells/spell_types/self/lichdom.dm @@ -0,0 +1,83 @@ +/datum/action/cooldown/spell/lichdom + name = "Bind Soul" + desc = "A spell that binds your soul to an item in your hands. \ + Binding your soul to an item will turn you into an immortal Lich. \ + So long as the item remains intact, you will revive from death, \ + no matter the circumstances." + icon_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "skeleton" + + school = SCHOOL_NECROMANCY + cooldown_time = 1 SECONDS + + invocation = "NECREM IMORTIUM!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_OFF_CENTCOM|SPELL_REQUIRES_MIND + spell_max_level = 1 + +/datum/action/cooldown/spell/lichdom/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + + // We call this here so we can get feedback if they try to cast it when they shouldn't. + if(!is_valid_target(owner)) + if(feedback) + to_chat(owner, span_warning("You don't have a soul to bind!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/lichdom/is_valid_target(atom/cast_on) + return isliving(cast_on) && !HAS_TRAIT(owner, TRAIT_NO_SOUL) + +/datum/action/cooldown/spell/lichdom/cast(mob/living/cast_on) + var/obj/item/marked_item = cast_on.get_active_held_item() + if(!marked_item || marked_item.item_flags & ABSTRACT) + return + if(HAS_TRAIT(marked_item, TRAIT_NODROP)) + to_chat(cast_on, span_warning("[marked_item] is stuck to your hand - it wouldn't be a wise idea to place your soul into it.")) + return + // I ensouled the nuke disk once. + // But it's a really mean tactic, so we probably should disallow it. + if(SEND_SIGNAL(marked_item, COMSIG_ITEM_IMBUE_SOUL, src, cast_on) & COMPONENT_BLOCK_IMBUE) + to_chat(cast_on, span_warning("[marked_item] is not suitable for emplacement of your fragile soul.")) + return + + . = ..() + playsound(cast_on, 'sound/effects/pope_entry.ogg', 100) + + to_chat(cast_on, span_green("You begin to focus your very being into [marked_item]...")) + if(!do_after(cast_on, 5 SECONDS, target = marked_item, timed_action_flags = IGNORE_HELD_ITEM)) + to_chat(cast_on, span_warning("Your soul snaps back to your body as you stop ensouling [marked_item]!")) + return + + marked_item.AddComponent(/datum/component/phylactery, cast_on.mind) + + cast_on.set_species(/datum/species/skeleton) + to_chat(cast_on, span_userdanger("With a hideous feeling of emptiness you watch in horrified fascination \ + as skin sloughs off bone! Blood boils, nerves disintegrate, eyes boil in their sockets! \ + As your organs crumble to dust in your fleshless chest you come to terms with your choice. \ + You're a lich!")) + + if(iscarbon(cast_on)) + var/mob/living/carbon/carbon_cast_on = cast_on + var/obj/item/organ/internal/brain/lich_brain = carbon_cast_on.getorganslot(ORGAN_SLOT_BRAIN) + if(lich_brain) // This prevents MMIs being used to stop lich revives + lich_brain.organ_flags &= ~ORGAN_VITAL + lich_brain.decoy_override = TRUE + + if(ishuman(cast_on)) + var/mob/living/carbon/human/human_cast_on = cast_on + human_cast_on.dropItemToGround(human_cast_on.w_uniform) + human_cast_on.dropItemToGround(human_cast_on.wear_suit) + human_cast_on.dropItemToGround(human_cast_on.head) + human_cast_on.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(human_cast_on), ITEM_SLOT_OCLOTHING) + human_cast_on.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(human_cast_on), ITEM_SLOT_HEAD) + human_cast_on.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(human_cast_on), ITEM_SLOT_ICLOTHING) + + + // No soul. You just sold it + ADD_TRAIT(cast_on, TRAIT_NO_SOUL, LICH_TRAIT) + // You only get one phylactery. + qdel(src) diff --git a/code/modules/spells/spell_types/self/lightning.dm b/code/modules/spells/spell_types/self/lightning.dm new file mode 100644 index 0000000000000..7423fb8a374a6 --- /dev/null +++ b/code/modules/spells/spell_types/self/lightning.dm @@ -0,0 +1,128 @@ +/datum/action/cooldown/spell/tesla + name = "Tesla Blast" + desc = "Charge up a tesla arc and release it at random nearby targets! \ + You can move freely while it charges. The arc jumps between targets and can knock them down." + button_icon_state = "lightning" + + cooldown_time = 30 SECONDS + cooldown_reduction_per_rank = 6.75 SECONDS + + invocation = "UN'LTD P'WAH!" + invocation_type = INVOCATION_SHOUT + school = SCHOOL_EVOCATION + + /// Whether we're currently channelling a tesla blast or not + var/currently_channeling = FALSE + /// How long it takes to channel the zap. + var/channel_time = 10 SECONDS + /// The radius around (either the caster or people shocked) to which the tesla blast can reach + var/shock_radius = 7 + /// The halo that appears around the caster while charging the spell + var/static/mutable_appearance/halo + /// The sound played while charging the spell + /// Quote: "the only way i can think of to stop a sound, thank MSO for the idea." + var/sound/charge_sound + +/datum/action/cooldown/spell/tesla/Remove(mob/living/remove_from) + reset_tesla(remove_from) + return ..() + +/datum/action/cooldown/spell/tesla/set_statpanel_format() + . = ..() + if(!islist(.)) + return + + if(currently_channeling) + .[PANEL_DISPLAY_STATUS] = "CHANNELING" + +/datum/action/cooldown/spell/tesla/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(currently_channeling) + if(feedback) + to_chat(owner, span_warning("You're already channeling [src]!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/tesla/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + to_chat(cast_on, span_notice("You start gathering power...")) + charge_sound = new /sound('sound/magic/lightning_chargeup.ogg', channel = 7) + halo ||= mutable_appearance('icons/effects/effects.dmi', "electricity", EFFECTS_LAYER) + cast_on.add_overlay(halo) + playsound(get_turf(cast_on), charge_sound, 50, FALSE) + + currently_channeling = TRUE + if(!do_after(cast_on, channel_time, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_HELD_ITEM))) + reset_tesla(cast_on) + return . | SPELL_CANCEL_CAST + + return TRUE + +/datum/action/cooldown/spell/tesla/reset_spell_cooldown() + reset_tesla(owner) + return ..() + +/// Resets the tesla effect. +/datum/action/cooldown/spell/tesla/proc/reset_tesla(atom/to_reset) + to_reset.cut_overlay(halo) + currently_channeling = FALSE + +/datum/action/cooldown/spell/tesla/cast(atom/cast_on) + . = ..() + + // byond, why you suck? + charge_sound = sound(null, repeat = 0, wait = 1, channel = charge_sound.channel) + // Sorry MrPerson, but the other ways just didn't do it the way i needed to work, this is the only way. + playsound(get_turf(cast_on), charge_sound, 50, FALSE) + + var/mob/living/carbon/to_zap_first = get_target(cast_on) + if(QDELETED(to_zap_first)) + cast_on.balloon_alert(cast_on, "no targets nearby!") + reset_spell_cooldown() + return FALSE + + playsound(get_turf(cast_on), 'sound/magic/lightningbolt.ogg', 50, TRUE) + zap_target(cast_on, to_zap_first) + reset_tesla(cast_on) + return TRUE + +/// Zaps a target, the bolt originating from origin. +/datum/action/cooldown/spell/tesla/proc/zap_target(atom/origin, mob/living/carbon/to_zap, bolt_energy = 30, bounces = 5) + origin.Beam(to_zap, icon_state = "lightning[rand(1,12)]", time = 0.5 SECONDS) + playsound(get_turf(to_zap), 'sound/magic/lightningshock.ogg', 50, TRUE, -1) + + if(to_zap.can_block_magic(antimagic_flags)) + to_zap.visible_message( + span_warning("[to_zap] absorbs the spell, remaining unharmed!"), + span_userdanger("You absorb the spell, remaining unharmed!"), + ) + + else + to_zap.electrocute_act(bolt_energy, "Lightning Bolt", flags = SHOCK_NOGLOVES) + + if(bounces >= 1) + var/mob/living/carbon/to_zap_next = get_target(to_zap) + if(!QDELETED(to_zap_next)) + zap_target(to_zap, to_zap_next, max((bolt_energy - 5), 5), bounces - 1) + +/// Get a target in view of us to zap next. Returns a carbon, or null if none were found. +/datum/action/cooldown/spell/tesla/proc/get_target(atom/center) + var/list/possibles = list() + for(var/mob/living/carbon/to_check in view(shock_radius, center)) + if(to_check == center || to_check == owner) + continue + if(!length(get_path_to(center, to_check, max_distance = shock_radius, simulated_only = FALSE))) + continue + + possibles += to_check + + if(!length(possibles)) + return null + + return pick(possibles) diff --git a/code/modules/spells/spell_types/self/mime_vow.dm b/code/modules/spells/spell_types/self/mime_vow.dm new file mode 100644 index 0000000000000..553c9394f57f4 --- /dev/null +++ b/code/modules/spells/spell_types/self/mime_vow.dm @@ -0,0 +1,24 @@ +/datum/action/cooldown/spell/vow_of_silence + name = "Speech" + desc = "Make (or break) a vow of silence." + background_icon_state = "bg_mime" + icon_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "mime_speech" + panel = "Mime" + + school = SCHOOL_MIME + cooldown_time = 5 MINUTES + + spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIND + spell_max_level = 1 + +/datum/action/cooldown/spell/vow_of_silence/cast(mob/living/carbon/human/cast_on) + . = ..() + cast_on.mind.miming = !cast_on.mind.miming + if(cast_on.mind.miming) + to_chat(cast_on, span_notice("You make a vow of silence.")) + SEND_SIGNAL(cast_on, COMSIG_CLEAR_MOOD_EVENT, "vow") + else + to_chat(cast_on, span_notice("You break your vow of silence.")) + SEND_SIGNAL(cast_on, COMSIG_ADD_MOOD_EVENT, "vow", /datum/mood_event/broken_vow) + cast_on.update_action_buttons_icon() diff --git a/code/modules/spells/spell_types/self/mutate.dm b/code/modules/spells/spell_types/self/mutate.dm new file mode 100644 index 0000000000000..0cc578809d655 --- /dev/null +++ b/code/modules/spells/spell_types/self/mutate.dm @@ -0,0 +1,49 @@ +/// A spell type that adds mutations to the caster temporarily. +/datum/action/cooldown/spell/apply_mutations + button_icon_state = "mutate" + sound = 'sound/magic/mutate.ogg' + + school = SCHOOL_TRANSMUTATION + + /// A list of all mutations we add on cast + var/list/mutations_to_add = list() + /// The duration the mutations will last afetr cast (keep this above the minimum cooldown) + var/mutation_duration = 10 SECONDS + +/datum/action/cooldown/spell/apply_mutations/New(Target) + . = ..() + spell_requirements |= SPELL_REQUIRES_HUMAN // The spell involves mutations, so it always require human / dna + +/datum/action/cooldown/spell/apply_mutations/Remove(mob/living/remove_from) + remove_mutations(remove_from) + return ..() + +/datum/action/cooldown/spell/apply_mutations/is_valid_target(atom/cast_on) + var/mob/living/carbon/human/human_caster = cast_on // Requires human anyways + return !!human_caster.dna + +/datum/action/cooldown/spell/apply_mutations/cast(mob/living/carbon/human/cast_on) + . = ..() + for(var/mutation in mutations_to_add) + cast_on.dna.add_mutation(mutation) + addtimer(CALLBACK(src, .proc/remove_mutations, cast_on), mutation_duration, TIMER_DELETE_ME) + +/// Removes the mutations we added from casting our spell +/datum/action/cooldown/spell/apply_mutations/proc/remove_mutations(mob/living/carbon/human/cast_on) + if(QDELETED(cast_on) || !is_valid_target(cast_on)) + return + + for(var/mutation in mutations_to_add) + cast_on.dna.remove_mutation(mutation) + +/datum/action/cooldown/spell/apply_mutations/mutate + name = "Mutate" + desc = "This spell causes you to turn into a hulk and gain laser vision for a short while." + cooldown_time = 40 SECONDS + cooldown_reduction_per_rank = 2.5 SECONDS + + invocation = "BIRUZ BENNAR" + invocation_type = INVOCATION_SHOUT + + mutations_to_add = list(/datum/mutation/human/laser_eyes, /datum/mutation/human/hulk) + mutation_duration = 30 SECONDS diff --git a/code/modules/spells/spell_types/self/night_vision.dm b/code/modules/spells/spell_types/self/night_vision.dm new file mode 100644 index 0000000000000..331842ae0c5ec --- /dev/null +++ b/code/modules/spells/spell_types/self/night_vision.dm @@ -0,0 +1,39 @@ +//Toggle Night Vision +/datum/action/cooldown/spell/night_vision + name = "Toggle Nightvision" + desc = "Toggle your nightvision mode." + + cooldown_time = 1 SECONDS + spell_requirements = NONE + + /// The span the "toggle" message uses when sent to the user + var/toggle_span = "notice" + +/datum/action/cooldown/spell/night_vision/New(Target) + . = ..() + name = "[name] \[ON\]" + +/datum/action/cooldown/spell/night_vision/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/datum/action/cooldown/spell/night_vision/cast(mob/living/cast_on) + . = ..() + to_chat(cast_on, "You toggle your night vision.") + + var/next_mode_text = "" + switch(cast_on.lighting_alpha) + if (LIGHTING_PLANE_ALPHA_VISIBLE) + cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE + next_mode_text = "More" + if (LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE) + cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE + next_mode_text = "Full" + if (LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE) + cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE + next_mode_text = "OFF" + else + cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE + next_mode_text = "ON" + + cast_on.update_sight() + name = "[initial(name)] \[[next_mode_text]\]" diff --git a/code/modules/spells/spell_types/self/personality_commune.dm b/code/modules/spells/spell_types/self/personality_commune.dm new file mode 100644 index 0000000000000..67e794c966832 --- /dev/null +++ b/code/modules/spells/spell_types/self/personality_commune.dm @@ -0,0 +1,54 @@ +// This can probably be changed to use mind linker at some point +/datum/action/cooldown/spell/personality_commune + name = "Personality Commune" + desc = "Sends thoughts to your alternate consciousness." + button_icon_state = "telepathy" + cooldown_time = 0 SECONDS + spell_requirements = NONE + + /// Fluff text shown when a message is sent to the pair + var/fluff_text = span_boldnotice("You hear an echoing voice in the back of your head...") + /// The message to send to the corresponding person on cast + var/to_send + +/datum/action/cooldown/spell/personality_commune/New(Target) + . = ..() + if(!istype(target, /datum/brain_trauma/severe/split_personality)) + stack_trace("[type] was created on a target that isn't a /datum/brain_trauma/severe/split_personality, this doesn't work.") + qdel(src) + +/datum/action/cooldown/spell/personality_commune/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/datum/action/cooldown/spell/personality_commune/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + var/datum/brain_trauma/severe/split_personality/trauma = target + if(!istype(trauma)) // hypothetically impossible but you never know + return . | SPELL_CANCEL_CAST + + to_send = tgui_input_text(cast_on, "What would you like to tell your other self?", "Commune") + if(QDELETED(src) || QDELETED(trauma)|| QDELETED(cast_on) || QDELETED(trauma.owner) || !can_cast_spell()) + return . | SPELL_CANCEL_CAST + if(!to_send) + reset_cooldown() + return . | SPELL_CANCEL_CAST + + return TRUE + +// Pillaged and adapted from telepathy code +/datum/action/cooldown/spell/personality_commune/cast(mob/living/cast_on) + . = ..() + var/datum/brain_trauma/severe/split_personality/trauma = target + + var/user_message = span_boldnotice("You concentrate and send thoughts to your other self:") + var/user_message_body = span_notice("[to_send]") + to_chat(cast_on, "[user_message] [user_message_body]") + to_chat(trauma.owner, "[fluff_text] [user_message_body]") + log_directed_talk(cast_on, trauma.owner, to_send, LOG_SAY, "[name]") + for(var/dead_mob in GLOB.dead_mob_list) + if(!isobserver(dead_mob)) + continue + to_chat(dead_mob, "[FOLLOW_LINK(dead_mob, cast_on)] [span_boldnotice("[cast_on] [name]:")] [span_notice("\"[to_send]\" to")] [span_name("[trauma]")]") diff --git a/code/modules/spells/spell_types/self/rod_form.dm b/code/modules/spells/spell_types/self/rod_form.dm new file mode 100644 index 0000000000000..5c9d03f47716c --- /dev/null +++ b/code/modules/spells/spell_types/self/rod_form.dm @@ -0,0 +1,149 @@ +/// The base distance a wizard rod will go without upgrades. +#define BASE_WIZ_ROD_RANGE 13 + +/datum/action/cooldown/spell/rod_form + name = "Rod Form" + desc = "Take on the form of an immovable rod, destroying all in your path. \ + Purchasing this spell multiple times will also increase the rod's damage and travel range." + button_icon_state = "immrod" + + school = SCHOOL_TRANSMUTATION + cooldown_time = 25 SECONDS + cooldown_reduction_per_rank = 3.75 SECONDS + + invocation = "CLANG!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_OFF_CENTCOM + + /// The extra distance we travel per additional spell level. + var/distance_per_spell_rank = 3 + /// The extra damage we deal per additional spell level. + var/damage_per_spell_rank = 20 + /// The max distance the rod goes on cast + var/rod_max_distance = BASE_WIZ_ROD_RANGE + /// The damage bonus applied to the rod on cast + var/rod_damage_bonus = 0 + +/datum/action/cooldown/spell/rod_form/cast(atom/cast_on) + . = ..() + // The destination turf of the rod - just a bit over the max range we calculated, for safety + var/turf/distant_turf = get_ranged_target_turf(get_turf(cast_on), cast_on.dir, (rod_max_distance + 2)) + + new /obj/effect/immovablerod/wizard( + get_turf(cast_on), + distant_turf, + null, + FALSE, + cast_on, + rod_max_distance, + rod_damage_bonus, + ) + +/datum/action/cooldown/spell/rod_form/level_spell(bypass_cap = FALSE) + . = ..() + if(!.) + return FALSE + + rod_max_distance += distance_per_spell_rank + rod_damage_bonus += damage_per_spell_rank + return TRUE + +/datum/action/cooldown/spell/rod_form/delevel_spell() + . = ..() + if(!.) + return FALSE + + rod_max_distance -= distance_per_spell_rank + rod_damage_bonus -= damage_per_spell_rank + return TRUE + +/// Wizard Version of the Immovable Rod. +/obj/effect/immovablerod/wizard + notify = FALSE + loopy_rod = TRUE + dnd_style_level_up = FALSE + /// The wizard who's piloting our rod. + var/datum/weakref/our_wizard + /// The distance the rod will go. + var/max_distance = BASE_WIZ_ROD_RANGE + /// The damage bonus of the rod when it smacks people. + var/damage_bonus = 0 + /// The turf the rod started from, to calcuate distance. + var/turf/start_turf +/obj/effect/immovablerod/wizard/Initialize(mapload, atom/target_atom, atom/specific_target, force_looping = FALSE, mob/living/wizard, max_distance = BASE_WIZ_ROD_RANGE, damage_bonus = 0) + . = ..() + if(wizard) + set_wizard(wizard) + start_turf = get_turf(src) + src.max_distance = max_distance + src.damage_bonus = damage_bonus +/obj/effect/immovablerod/wizard/Destroy(force) + start_turf = null + return ..() +/obj/effect/immovablerod/wizard/Move() + if(get_dist(start_turf, get_turf(src)) >= max_distance) + stop_travel() + return + return ..() +/obj/effect/immovablerod/wizard/penetrate(mob/living/penetrated) + if(penetrated.can_block_magic()) + penetrated.visible_message( + span_danger("[src] hits [penetrated], but it bounces back, then vanishes!"), + span_userdanger("[src] hits you... but it bounces back, then vanishes!"), + span_danger("You hear a weak, sad, CLANG.") + ) + stop_travel() + return + penetrated.visible_message( + span_danger("[penetrated] is penetrated by an immovable rod!"), + span_userdanger("The [src] penetrates you!"), + span_danger("You hear a CLANG!"), + ) + penetrated.adjustBruteLoss(70 + damage_bonus) +/obj/effect/immovablerod/wizard/suplex_rod(mob/living/strongman) + var/mob/living/wizard = our_wizard?.resolve() + if(QDELETED(wizard)) + return ..() // There's no wizard in this rod? It's pretty much a normal rod at this point + strongman.visible_message( + span_boldwarning("[src] transforms into [wizard] as [strongman] suplexes them!"), + span_warning("As you grab [src], it suddenly turns into [wizard] as you suplex them!") + ) + to_chat(wizard, span_boldwarning("You're suddenly jolted out of rod-form as [strongman] somehow manages to grab you, slamming you into the ground!")) + stop_travel() + wizard.Stun(6 SECONDS) + wizard.apply_damage(25, BRUTE) + return TRUE +/** + * Called when the wizard rod reaches it's maximum distance + * or is otherwise stopped by something. + * Dumps out the wizard, and deletes. + */ +/obj/effect/immovablerod/wizard/proc/stop_travel() + eject_wizard() + qdel(src) +/** + * Set wizard as our_wizard, placing them in the rod + * and preparing them for travel. + */ +/obj/effect/immovablerod/wizard/proc/set_wizard(mob/living/wizard) + our_wizard = WEAKREF(wizard) + wizard.forceMove(src) + wizard.notransform = TRUE + wizard.status_flags |= GODMODE + ADD_TRAIT(wizard, TRAIT_MAGICALLY_PHASED, REF(src)) + +/** + * Eject our current wizard, removing them from the rod + * and fixing all of the variables we changed. + */ +/obj/effect/immovablerod/wizard/proc/eject_wizard() + var/mob/living/wizard = our_wizard?.resolve() + if(QDELETED(wizard)) + return + wizard.status_flags &= ~GODMODE + wizard.notransform = FALSE + wizard.forceMove(get_turf(src)) + our_wizard = null + REMOVE_TRAIT(wizard, TRAIT_MAGICALLY_PHASED, REF(src)) + +#undef BASE_WIZ_ROD_RANGE diff --git a/code/modules/spells/spell_types/self/smoke.dm b/code/modules/spells/spell_types/self/smoke.dm new file mode 100644 index 0000000000000..b2c7e924f191e --- /dev/null +++ b/code/modules/spells/spell_types/self/smoke.dm @@ -0,0 +1,37 @@ +/// Basic smoke spell. +/datum/action/cooldown/spell/smoke + name = "Smoke" + desc = "This spell spawns a cloud of smoke at your location. \ + People within will begin to choke and drop their items." + button_icon_state = "smoke" + + school = SCHOOL_CONJURATION + cooldown_time = 12 SECONDS + cooldown_reduction_per_rank = 2.5 SECONDS + + invocation_type = INVOCATION_NONE + + smoke_type = /datum/effect_system/fluid_spread/smoke/bad + smoke_amt = 4 + +/// Chaplain smoke. +/datum/action/cooldown/spell/smoke/lesser + name = "Holy Smoke" + desc = "This spell spawns a small cloud of smoke at your location." + + school = SCHOOL_HOLY + cooldown_time = 36 SECONDS + spell_requirements = NONE + + smoke_type = /datum/effect_system/fluid_spread/smoke + smoke_amt = 2 + +/// Unused smoke that makes people sleep. Used to be for cult? +/datum/action/cooldown/spell/smoke/disable + name = "Paralysing Smoke" + desc = "This spell spawns a cloud of paralysing smoke." + background_icon_state = "bg_cult" + + cooldown_time = 20 SECONDS + + smoke_type = /datum/effect_system/fluid_spread/smoke/sleeping diff --git a/code/modules/spells/spell_types/self/soultap.dm b/code/modules/spells/spell_types/self/soultap.dm new file mode 100644 index 0000000000000..57932ad8288b9 --- /dev/null +++ b/code/modules/spells/spell_types/self/soultap.dm @@ -0,0 +1,63 @@ + +/** + * SOUL TAP! + * + * Trades 20 max health for a refresh on all of your spells. + * I was considering making it depend on the cooldowns of your spells, but I want to support "Big spell wizard" with this loadout. + * The two spells that sound most problematic with this is mindswap and lichdom, + * but soul tap requires clothes for mindswap and lichdom takes your soul. + */ +/datum/action/cooldown/spell/tap + name = "Soul Tap" + desc = "Fuel your spells using your own soul!" + button_icon_state = "soultap" + + // I could see why this wouldn't be necromancy, but messing with souls or whatever. Ectomancy? + school = SCHOOL_NECROMANCY + cooldown_time = 1 SECONDS + invocation = "AT ANY COST!" + invocation_type = INVOCATION_SHOUT + spell_max_level = 1 + + /// The amount of health we take on tap + var/tap_health_taken = 20 + +/datum/action/cooldown/spell/tap/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + + // We call this here so we can get feedback if they try to cast it when they shouldn't. + if(!is_valid_target(owner)) + if(feedback) + to_chat(owner, span_warning("You have no soul to tap into!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/tap/is_valid_target(atom/cast_on) + return isliving(cast_on) && !HAS_TRAIT(owner, TRAIT_NO_SOUL) + +/datum/action/cooldown/spell/tap/cast(mob/living/cast_on) + . = ..() + cast_on.maxHealth -= tap_health_taken + cast_on.health = min(cast_on.health, cast_on.maxHealth) + + for(var/datum/action/cooldown/spell/spell in cast_on.actions) + spell.reset_spell_cooldown() + + // If the tap took all of our life, we die and lose our soul! + if(cast_on.maxHealth <= 0) + to_chat(cast_on, span_userdanger("Your weakened soul is completely consumed by the tap!")) + ADD_TRAIT(cast_on, TRAIT_NO_SOUL, MAGIC_TRAIT) + + cast_on.visible_message(span_danger("[cast_on] suddenly dies!"), ignored_mobs = cast_on) + cast_on.death() + + // If the next tap will kill us, give us a heads-up + else if(cast_on.maxHealth - tap_health_taken <= 0) + to_chat(cast_on, span_bolddanger("Your body feels incredibly drained, and the burning is hard to ignore!")) + + // Otherwise just give them some feedback + else + to_chat(cast_on, span_danger("Your body feels drained and there is a burning pain in your chest.")) diff --git a/code/modules/spells/spell_types/self/spacetime_distortion.dm b/code/modules/spells/spell_types/self/spacetime_distortion.dm new file mode 100644 index 0000000000000..d71cb6713bc8b --- /dev/null +++ b/code/modules/spells/spell_types/self/spacetime_distortion.dm @@ -0,0 +1,168 @@ +// This could probably be an aoe spell but it's a little cursed, so I'm not touching it +/datum/action/cooldown/spell/spacetime_dist + name = "Spacetime Distortion" + desc = "Entangle the strings of space-time in an area around you, \ + randomizing the layout and making proper movement impossible. The strings vibrate..." + sound = 'sound/effects/magic.ogg' + button_icon_state = "spacetime" + + school = SCHOOL_EVOCATION + cooldown_time = 30 SECONDS + spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_OFF_CENTCOM + spell_max_level = 1 + + /// Weather we're ready to cast again yet or not + var/ready = TRUE + /// The radius of the scramble around the caster + var/scramble_radius = 7 + /// The duration of the scramble + var/duration = 15 SECONDS + /// A lazylist of all scramble effects this spell has created. + var/list/effects + +/datum/action/cooldown/spell/spacetime_dist/Destroy() + QDEL_LAZYLIST(effects) + return ..() + +/datum/action/cooldown/spell/spacetime_dist/can_cast_spell(feedback = TRUE) + return ..() && ready + +/datum/action/cooldown/spell/spacetime_dist/set_statpanel_format() + . = ..() + if(!islist(.)) + return + + if(!ready) + .[PANEL_DISPLAY_STATUS] = "NOT READY" + +/datum/action/cooldown/spell/spacetime_dist/cast(atom/cast_on) + . = ..() + var/list/turf/to_switcharoo = get_targets_to_scramble(cast_on) + if(!length(to_switcharoo)) + to_chat(cast_on, span_warning("For whatever reason, the strings nearby aren't keen on being tangled.")) + reset_spell_cooldown() + return + + ready = FALSE + + for(var/turf/swap_a as anything in to_switcharoo) + var/turf/swap_b = to_switcharoo[swap_a] + var/obj/effect/cross_action/spacetime_dist/effect_a = new /obj/effect/cross_action/spacetime_dist(swap_a, antimagic_flags) + var/obj/effect/cross_action/spacetime_dist/effect_b = new /obj/effect/cross_action/spacetime_dist(swap_b, antimagic_flags) + effect_a.linked_dist = effect_b + effect_a.add_overlay(swap_b.photograph()) + effect_b.linked_dist = effect_a + effect_b.add_overlay(swap_a.photograph()) + effect_b.set_light(4, 30, "#c9fff5") + LAZYADD(effects, effect_a) + LAZYADD(effects, effect_b) + +/datum/action/cooldown/spell/spacetime_dist/after_cast() + . = ..() + addtimer(CALLBACK(src, .proc/clean_turfs), duration) + +/// Callback which cleans up our effects list after the duration expires. +/datum/action/cooldown/spell/spacetime_dist/proc/clean_turfs() + QDEL_LAZYLIST(effects) + ready = TRUE + +/** + * Gets a list of turfs around the center atom to scramble. + * + * Returns an assoc list of [turf] to [turf]. These pairs are what turfs are + * swapped between one another when the cast is done. + */ +/datum/action/cooldown/spell/spacetime_dist/proc/get_targets_to_scramble(atom/center) + // Get turfs around the center + var/list/turfs = spiral_range_turfs(scramble_radius, center) + if(!length(turfs)) + return + + var/list/turf_steps = list() + + // Go through the turfs we got and pair them up + // This is where we determine what to swap where + var/num_to_scramble = round(length(turfs) * 0.5) + for(var/i in 1 to num_to_scramble) + turf_steps[pick_n_take(turfs)] = pick_n_take(turfs) + + // If there's any turfs unlinked with a friend, + // just randomly swap it with any turf in the area + if(length(turfs)) + var/turf/loner = pick(turfs) + var/area/caster_area = get_area(center) + turf_steps[loner] = get_turf(pick(caster_area.contents)) + + return turf_steps + + +/obj/effect/cross_action + name = "cross me" + desc = "for crossing" + anchored = TRUE + +/obj/effect/cross_action/spacetime_dist + name = "spacetime distortion" + desc = "A distortion in spacetime. You can hear faint music..." + icon_state = "" + /// A flags which save people from being thrown about + var/antimagic_flags = MAGIC_RESISTANCE + var/obj/effect/cross_action/spacetime_dist/linked_dist + var/busy = FALSE + var/sound + var/walks_left = 50 //prevents the game from hanging in extreme cases (such as minigun fire) + +/obj/effect/cross_action/singularity_act() + return + +/obj/effect/cross_action/singularity_pull() + return + +/obj/effect/cross_action/spacetime_dist/Initialize(mapload, flags = MAGIC_RESISTANCE) + . = ..() + setDir(pick(GLOB.cardinals)) + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = .proc/on_entered, + ) + AddElement(/datum/element/connect_loc, loc_connections) + antimagic_flags = flags + +/obj/effect/cross_action/spacetime_dist/proc/walk_link(atom/movable/AM) + if(ismob(AM)) + var/mob/M = AM + if(M.can_block_magic(antimagic_flags, charge_cost = 0)) + return + if(linked_dist && walks_left > 0) + flick("purplesparkles", src) + linked_dist.get_walker(AM) + walks_left-- + +/obj/effect/cross_action/spacetime_dist/proc/get_walker(atom/movable/AM) + busy = TRUE + flick("purplesparkles", src) + AM.forceMove(get_turf(src)) + playsound(get_turf(src),sound,70,FALSE) + busy = FALSE + +/obj/effect/cross_action/spacetime_dist/proc/on_entered(datum/source, atom/movable/AM) + SIGNAL_HANDLER + if(!busy) + walk_link(AM) + +/obj/effect/cross_action/spacetime_dist/attackby(obj/item/W, mob/user, params) + if(user.temporarilyRemoveItemFromInventory(W)) + walk_link(W) + else + walk_link(user) + +//ATTACK HAND IGNORING PARENT RETURN VALUE +/obj/effect/cross_action/spacetime_dist/attack_hand(mob/user, list/modifiers) + walk_link(user) + +/obj/effect/cross_action/spacetime_dist/attack_paw(mob/user, list/modifiers) + walk_link(user) + +/obj/effect/cross_action/spacetime_dist/Destroy() + busy = TRUE + linked_dist = null + return ..() diff --git a/code/modules/spells/spell_types/self/stop_time.dm b/code/modules/spells/spell_types/self/stop_time.dm new file mode 100644 index 0000000000000..cab47375eb3a4 --- /dev/null +++ b/code/modules/spells/spell_types/self/stop_time.dm @@ -0,0 +1,30 @@ +/datum/action/cooldown/spell/timestop + name = "Stop Time" + desc = "This spell stops time for everyone except for you, \ + allowing you to move freely while your enemies and even projectiles are frozen." + button_icon_state = "time" + + school = SCHOOL_FORBIDDEN // Fucking with time is not appreciated by anyone + cooldown_time = 50 SECONDS + cooldown_reduction_per_rank = 10 SECONDS + + invocation = "TOKI YO TOMARE!" + invocation_type = INVOCATION_SHOUT + + /// The radius / range of the time stop. + var/timestop_range = 2 + /// The duration of the time stop. + var/timestop_duration = 10 SECONDS + +/datum/action/cooldown/spell/timestop/Grant(mob/grant_to) + . = ..() + if(owner) + ADD_TRAIT(owner, TRAIT_TIME_STOP_IMMUNE, REF(src)) + +/datum/action/cooldown/spell/timestop/Remove(mob/remove_from) + REMOVE_TRAIT(remove_from, TRAIT_TIME_STOP_IMMUNE, REF(src)) + return ..() + +/datum/action/cooldown/spell/timestop/cast(atom/cast_on) + . = ..() + new /obj/effect/timestop/magic(get_turf(cast_on), timestop_range, timestop_duration, list(cast_on)) diff --git a/code/modules/spells/spell_types/self/summon_item.dm b/code/modules/spells/spell_types/self/summon_item.dm new file mode 100644 index 0000000000000..761c2c7efada7 --- /dev/null +++ b/code/modules/spells/spell_types/self/summon_item.dm @@ -0,0 +1,154 @@ +/datum/action/cooldown/spell/summonitem + name = "Instant Summons" + desc = "This spell can be used to recall a previously marked item to your hand from anywhere in the universe." + button_icon_state = "summons" + + school = SCHOOL_TRANSMUTATION + cooldown_time = 10 SECONDS + + invocation = "GAR YOK" + invocation_type = INVOCATION_WHISPER + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + spell_max_level = 1 //cannot be improved + + ///The obj marked for recall + var/obj/marked_item + +/datum/action/cooldown/spell/summonitem/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/// Set the passed object as our marked item +/datum/action/cooldown/spell/summonitem/proc/mark_item(obj/to_mark) + name = "Recall [to_mark]" + marked_item = to_mark + RegisterSignal(marked_item, COMSIG_PARENT_QDELETING, .proc/on_marked_item_deleted) + +/// Unset our current marked item +/datum/action/cooldown/spell/summonitem/proc/unmark_item() + name = initial(name) + UnregisterSignal(marked_item, COMSIG_PARENT_QDELETING) + marked_item = null + +/// Signal proc for COMSIG_PARENT_QDELETING on our marked item, unmarks our item if it's deleted +/datum/action/cooldown/spell/summonitem/proc/on_marked_item_deleted(datum/source) + SIGNAL_HANDLER + + if(owner) + to_chat(owner, span_boldwarning("You sense your marked item has been destroyed!")) + unmark_item() + +/datum/action/cooldown/spell/summonitem/cast(mob/living/cast_on) + . = ..() + if(QDELETED(marked_item)) + try_link_item(cast_on) + return + + if(marked_item == cast_on.get_active_held_item()) + try_unlink_item(cast_on) + return + + try_recall_item(cast_on) + +/// If we don't have a marked item, attempts to mark the caster's held item. +/datum/action/cooldown/spell/summonitem/proc/try_link_item(mob/living/caster) + var/obj/item/potential_mark = caster.get_active_held_item() + if(!potential_mark) + if(caster.get_inactive_held_item()) + to_chat(caster, span_warning("You must hold the desired item in your hands to mark it for recall!")) + else + to_chat(caster, span_warning("You aren't holding anything that can be marked for recall!")) + return FALSE + + var/link_message = "" + if(potential_mark.item_flags & ABSTRACT) + return FALSE + if(SEND_SIGNAL(potential_mark, COMSIG_ITEM_MARK_RETRIEVAL, src, caster) & COMPONENT_BLOCK_MARK_RETRIEVAL) + return FALSE + if(HAS_TRAIT(potential_mark, TRAIT_NODROP)) + link_message += "Though it feels redundant... " + + link_message += "You mark [potential_mark] for recall." + to_chat(caster, span_notice(link_message)) + mark_item(potential_mark) + return TRUE + +/// If we have a marked item and it's in our hand, we will try to unlink it +/datum/action/cooldown/spell/summonitem/proc/try_unlink_item(mob/living/caster) + to_chat(caster, span_notice("You begin removing the mark on [marked_item]...")) + if(!do_after(caster, 5 SECONDS, marked_item)) + to_chat(caster, span_notice("You decide to keep [marked_item] marked.")) + return FALSE + + to_chat(caster, span_notice("You remove the mark on [marked_item] to use elsewhere.")) + unmark_item() + return TRUE + +/// Recalls our marked item to the caster. May bring some unexpected things along. +/datum/action/cooldown/spell/summonitem/proc/try_recall_item(mob/living/caster) + var/obj/item_to_retrieve = marked_item + + if(item_to_retrieve.loc) + // I don't want to know how someone could put something + // inside itself but these are wizards so let's be safe + var/infinite_recursion = 0 + + // if it's in something, you get the whole thing. + while(!isturf(item_to_retrieve.loc) && infinite_recursion < 10) + if(isitem(item_to_retrieve.loc)) + var/obj/item/mark_loc = item_to_retrieve.loc + // Being able to summon abstract things because + // your item happened to get placed there is a no-no + if(mark_loc.item_flags & ABSTRACT) + break + + // If its on someone, properly drop it + if(ismob(item_to_retrieve.loc)) + var/mob/holding_mark = item_to_retrieve.loc + + // Items in silicons warp the whole silicon + if(issilicon(holding_mark)) + holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly disappears!")) + holding_mark.forceMove(caster.loc) + holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly appears!")) + item_to_retrieve = null + break + + holding_mark.dropItemToGround(item_to_retrieve) + + else if(isobj(item_to_retrieve.loc)) + var/obj/retrieved_item = item_to_retrieve.loc + // Can't bring anchored things + if(retrieved_item.anchored) + return + // Edge cases for moving certain machinery... + if(istype(retrieved_item, /obj/machinery/portable_atmospherics)) + var/obj/machinery/portable_atmospherics/atmos_item = retrieved_item + atmos_item.disconnect() + atmos_item.update_appearance() + + // Otherwise bring the whole thing with us + item_to_retrieve = retrieved_item + + infinite_recursion += 1 + + else + // Organs are usually stored in nullspace + if(isorgan(item_to_retrieve)) + var/obj/item/organ/organ = item_to_retrieve + if(organ.owner) + // If this code ever runs I will be happy + log_combat(caster, organ.owner, "magically removed [organ.name] from", addition = "COMBAT MODE: [uppertext(caster.combat_mode)]") + organ.Remove(organ.owner) + + if(!item_to_retrieve) + return + + item_to_retrieve.loc?.visible_message(span_warning("[item_to_retrieve] suddenly disappears!")) + + if(isitem(item_to_retrieve) && caster.put_in_hands(item_to_retrieve)) + item_to_retrieve.loc.visible_message(span_warning("[item_to_retrieve] suddenly appears in [caster]'s hand!")) + else + item_to_retrieve.forceMove(caster.drop_location()) + item_to_retrieve.loc.visible_message(span_warning("[item_to_retrieve] suddenly appears!")) + playsound(get_turf(item_to_retrieve), 'sound/magic/summonitems_generic.ogg', 50, TRUE) diff --git a/code/modules/spells/spell_types/self/voice_of_god.dm b/code/modules/spells/spell_types/self/voice_of_god.dm new file mode 100644 index 0000000000000..ae4c46a3bb73a --- /dev/null +++ b/code/modules/spells/spell_types/self/voice_of_god.dm @@ -0,0 +1,50 @@ +/datum/action/cooldown/spell/voice_of_god + name = "Voice of God" + desc = "Speak with an incredibly compelling voice, forcing listeners to obey your commands." + icon_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "voice_of_god" + sound = 'sound/magic/clockwork/invoke_general.ogg' + + cooldown_time = 120 SECONDS // Varies depending on command + invocation = "" // Handled by the VOICE OF GOD itself + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + antimagic_flags = NONE + + /// The command to deliver on cast + var/command + /// The modifier to the cooldown, after cast + var/cooldown_mod = 1 + /// The modifier put onto the power of the command + var/power_mod = 1 + /// A list of spans to apply to commands given + var/list/spans = list("colossus", "yell") + +/datum/action/cooldown/spell/voice_of_god/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + command = tgui_input_text(cast_on, "Speak with the Voice of God", "Command") + if(QDELETED(src) || QDELETED(cast_on) || !can_cast_spell()) + return . | SPELL_CANCEL_CAST + if(!command) + reset_spell_cooldown() + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/voice_of_god/cast(atom/cast_on) + . = ..() + var/command_cooldown = voice_of_god(uppertext(command), cast_on, spans, base_multiplier = power_mod) + cooldown_time = (command_cooldown * cooldown_mod) + +// "Invocation" is done by the actual voice of god proc +/datum/action/cooldown/spell/voice_of_god/invocation() + return + +/datum/action/cooldown/spell/voice_of_god/clown + name = "Voice of Clown" + desc = "Speak with an incredibly funny voice, startling people into obeying you for a brief moment." + sound = 'sound/misc/scary_horn.ogg' + cooldown_mod = 0.5 + power_mod = 0.1 + spans = list("clown") diff --git a/code/modules/spells/spell_types/shadow_walk.dm b/code/modules/spells/spell_types/shadow_walk.dm deleted file mode 100644 index 992c303edecda..0000000000000 --- a/code/modules/spells/spell_types/shadow_walk.dm +++ /dev/null @@ -1,101 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/shadowwalk - name = "Shadow Walk" - desc = "Grants unlimited movement in darkness." - charge_max = 0 - clothes_req = FALSE - antimagic_allowed = TRUE - phase_allowed = TRUE - selection_type = "range" - range = -1 - include_user = TRUE - cooldown_min = 0 - overlay = null - action_icon = 'icons/mob/actions/actions_minor_antag.dmi' - action_icon_state = "ninja_cloak" - action_background_icon_state = "bg_alien" - -/obj/effect/proc_holder/spell/targeted/shadowwalk/cast(list/targets,mob/living/user = usr) - var/L = user.loc - if(istype(user.loc, /obj/effect/dummy/phased_mob/shadow)) - var/obj/effect/dummy/phased_mob/shadow/S = L - S.end_jaunt(FALSE) - return - else - var/turf/T = get_turf(user) - var/light_amount = T.get_lumcount() - if(light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD) - playsound(get_turf(user), 'sound/magic/ethereal_enter.ogg', 50, 1, -1) - visible_message("[user] melts into the shadows!") - user.SetAllImmobility(0) - user.setStaminaLoss(0, 0) - var/obj/effect/dummy/phased_mob/shadow/S2 = new(get_turf(user.loc)) - user.forceMove(S2) - S2.jaunter = user - else - to_chat(user, "It isn't dark enough here!") - -/obj/effect/dummy/phased_mob/shadow - name = "darkness" - icon = 'icons/effects/effects.dmi' - icon_state = "nothing" - var/canmove = TRUE - var/mob/living/jaunter - density = FALSE - anchored = TRUE - invisibility = 60 - resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF - -/obj/effect/dummy/phased_mob/shadow/relaymove(mob/living/user, direction) - var/turf/newLoc = get_step(src,direction) - if(isspaceturf(newLoc)) - to_chat(user, "It really would not be wise to go into space.") - return - if(newLoc.get_lumcount() > SHADOW_SPECIES_LIGHT_THRESHOLD && newLoc.is_blocked_turf()) - to_chat(user, "It wouldn't be wise to move here while incorporeal, I may become trapped.") - return - forceMove(newLoc) - check_light_level() - -/obj/effect/dummy/phased_mob/shadow/proc/check_light_level() - var/turf/T = get_turf(src) - var/light_amount = T.get_lumcount() - if(light_amount > SHADOW_SPECIES_LIGHT_THRESHOLD) // jaunt ends - end_jaunt(TRUE) - else if (light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD && (!QDELETED(jaunter))) //heal in the dark - jaunter.heal_overall_damage(1,1, 0, BODYTYPE_ORGANIC) - -/obj/effect/dummy/phased_mob/shadow/proc/end_jaunt(forced = FALSE) - if(jaunter) - if(forced) - visible_message("[jaunter] is revealed by the light!") - else - visible_message("[jaunter] emerges from the darkness!") - jaunter.forceMove(get_turf(src)) - playsound(get_turf(jaunter), 'sound/magic/ethereal_exit.ogg', 50, 1, -1) - jaunter = null - qdel(src) - -/obj/effect/dummy/phased_mob/shadow/Initialize(mapload) - . = ..() - START_PROCESSING(SSobj, src) - -/obj/effect/dummy/phased_mob/shadow/Destroy() - STOP_PROCESSING(SSobj, src) - . = ..() - -/obj/effect/dummy/phased_mob/shadow/process() - if(!jaunter) - qdel(src) - if(jaunter.loc != src) - qdel(src) - check_light_level() - -/obj/effect/dummy/phased_mob/shadow/ex_act() - return - -/obj/effect/dummy/phased_mob/shadow/bullet_act() - return BULLET_ACT_FORCE_PIERCE - -/obj/effect/dummy/phased_mob/shadow/singularity_act() - return - diff --git a/code/modules/spells/spell_types/shapeshift.dm b/code/modules/spells/spell_types/shapeshift.dm deleted file mode 100644 index 4c01f64823dd3..0000000000000 --- a/code/modules/spells/spell_types/shapeshift.dm +++ /dev/null @@ -1,185 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/shapeshift - name = "Shapechange" - desc = "Take on the shape of another for a time to use their natural abilities. Once you've made your choice it cannot be changed." - clothes_req = FALSE - human_req = FALSE - charge_max = 200 - cooldown_min = 50 - range = -1 - include_user = TRUE - invocation = "RAC'WA NO!" - invocation_type = INVOCATION_SHOUT - action_icon_state = "shapeshift" - - var/revert_on_death = TRUE - var/die_with_shapeshifted_form = TRUE - var/convert_damage = TRUE //If you want to convert the caster's health to the shift, and vice versa. - var/convert_damage_type = BRUTE //Since simplemobs don't have advanced damagetypes, what to convert damage back into. - - var/shapeshift_type - var/list/possible_shapes = list(/mob/living/simple_animal/mouse,\ - /mob/living/simple_animal/pet/dog/corgi,\ - /mob/living/simple_animal/hostile/carp/ranged/chaos,\ - /mob/living/simple_animal/bot/ed209,\ - /mob/living/simple_animal/hostile/poison/giant_spider/hunter/viper/wizard,\ - /mob/living/simple_animal/hostile/construct/juggernaut) - -/obj/effect/proc_holder/spell/targeted/shapeshift/cast(list/targets,mob/user = usr) - if(src in user.mob_spell_list) - user.mob_spell_list.Remove(src) - user.mind.AddSpell(src) - if(user.buckled) - user.buckled.unbuckle_mob(src,force=TRUE) - for(var/mob/living/M in targets) - if(!shapeshift_type) - var/list/animal_list = list() - for(var/path in possible_shapes) - var/mob/living/simple_animal/A = path - animal_list[initial(A.name)] = path - var/new_shapeshift_type = input(M, "Choose Your Animal Form!", "It's Morphing Time!", null) as null|anything in sort_list(animal_list) - if(shapeshift_type) - return - shapeshift_type = new_shapeshift_type - if(!shapeshift_type) //If you aren't gonna decide I am! - shapeshift_type = pick(animal_list) - shapeshift_type = animal_list[shapeshift_type] - - var/obj/shapeshift_holder/S = locate() in M - if(S) - Restore(M) - else - Shapeshift(M) - - -/obj/effect/proc_holder/spell/targeted/shapeshift/proc/Shapeshift(mob/living/caster) - var/obj/shapeshift_holder/H = locate() in caster - if(H) - to_chat(caster, "You're already shapeshifted!") - return - - var/mob/living/shape = new shapeshift_type(caster.loc) - H = new(shape,src,caster) - - clothes_req = FALSE - human_req = FALSE - -/obj/effect/proc_holder/spell/targeted/shapeshift/proc/Restore(mob/living/shape) - var/obj/shapeshift_holder/H = locate() in shape - if(!H) - return - - H.restore() - - clothes_req = initial(clothes_req) - human_req = initial(human_req) - -/obj/effect/proc_holder/spell/targeted/shapeshift/dragon - name = "Dragon Form" - desc = "Take on the shape a lesser ash drake." - invocation = "RAAAAAAAAWR!" - convert_damage = FALSE - - - shapeshift_type = /mob/living/simple_animal/hostile/megafauna/dragon/lesser - - -/obj/shapeshift_holder - name = "Shapeshift holder" - resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ON_FIRE | UNACIDABLE | ACID_PROOF - var/mob/living/stored - var/mob/living/shape - var/restoring = FALSE - var/datum/soullink/shapeshift/slink - var/obj/effect/proc_holder/spell/targeted/shapeshift/source - -/obj/shapeshift_holder/Initialize(mapload,obj/effect/proc_holder/spell/targeted/shapeshift/source,mob/living/caster, convert_damage = FALSE) - . = ..() - src.source = source - shape = loc - if(!istype(shape)) - CRASH("shapeshift holder created outside mob/living") - stored = caster - if(stored.mind) - stored.mind.transfer_to(shape) - stored.forceMove(src) - stored.notransform = TRUE - if(convert_damage || istype(source) && source.convert_damage) - var/damage_percent = (stored.maxHealth - stored.health)/stored.maxHealth; - var/damapply = damage_percent * shape.maxHealth; - - shape.apply_damage(damapply, source.convert_damage_type, forced = TRUE); - - slink = soullink(/datum/soullink/shapeshift, stored , shape) - slink.source = src - -/obj/shapeshift_holder/Destroy() - if(!restoring) - restore() - stored = null - shape = null - . = ..() - -/obj/shapeshift_holder/Moved() - . = ..() - if(!restoring || QDELETED(src)) - restore() - -/obj/shapeshift_holder/handle_atom_del(atom/A) - if(A == stored && !restoring) - restore() - -/obj/shapeshift_holder/Exited(atom/movable/gone, direction) - . = ..() - if(stored == gone && !restoring) - restore() - -/obj/shapeshift_holder/proc/casterDeath() - //Something kills the stored caster through direct damage. - if(source.revert_on_death) - restore(death=TRUE) - else - shape.investigate_log("has been killed whilst shapeshifted.", INVESTIGATE_DEATHS) - shape.death() - -/obj/shapeshift_holder/proc/shapeDeath(death=TRUE) - //Shape dies. - if(death || istype(source) && source.die_with_shapeshifted_form) - if(death || istype(source) && source.revert_on_death) - restore(death=TRUE) - else - restore() - -/obj/shapeshift_holder/proc/restore(death=FALSE, convert_damage = TRUE) - if(!stored) //somehow this proc is getting called twice and it runtimes on the second pass because stored has been hit with qdel() - return FALSE - restoring = TRUE - qdel(slink) - stored.forceMove(get_turf(src)) - stored.notransform = FALSE - if(shape.mind) - shape.mind.transfer_to(stored) - if(death) - stored.death() - else if(convert_damage || (istype(source) && source.convert_damage)) - var/original_blood_volume = stored.blood_volume - stored.revive(full_heal = TRUE) - - var/damage_percent = (shape.maxHealth - shape.health)/shape.maxHealth; - var/damapply = stored.maxHealth * damage_percent - - stored.apply_damage(damapply, (istype(source) ? source.convert_damage_type : BRUTE), forced = TRUE) //brute is the default damage convert - stored.blood_volume = original_blood_volume - if(!QDELETED(shape)) - qdel(shape) - qdel(src) - -/datum/soullink/shapeshift - var/obj/shapeshift_holder/source - -/datum/soullink/shapeshift/ownerDies(gibbed, mob/living/owner) - if(source) - source.casterDeath(gibbed) - -/datum/soullink/shapeshift/sharerDies(gibbed, mob/living/sharer) - if(source) - source.shapeDeath(!gibbed) diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm new file mode 100644 index 0000000000000..df154f2cedb6a --- /dev/null +++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm @@ -0,0 +1,244 @@ +/datum/action/cooldown/spell/shapeshift + school = SCHOOL_TRANSMUTATION + + /// Whehter we revert to our human form on death. + var/revert_on_death = TRUE + /// Whether we die when our shapeshifted form is killed + var/die_with_shapeshifted_form = TRUE + /// Whether we convert our health from one form to another + var/convert_damage = TRUE + /// If convert damage is true, the damage type we deal when converting damage back and forth + var/convert_damage_type = BRUTE + + /// Our chosen type + var/mob/living/shapeshift_type + /// All possible types we can become + var/list/atom/possible_shapes + +/datum/action/cooldown/spell/shapeshift/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/datum/action/cooldown/spell/shapeshift/proc/is_shifted(mob/living/cast_on) + return locate(/obj/shapeshift_holder) in cast_on + +/datum/action/cooldown/spell/shapeshift/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + if(shapeshift_type) + return + + if(length(possible_shapes) == 1) + shapeshift_type = possible_shapes[1] + return + + var/list/shape_names_to_types = list() + var/list/shape_names_to_image = list() + if(!length(shape_names_to_types) || !length(shape_names_to_image)) + for(var/atom/path as anything in possible_shapes) + var/shape_name = initial(path.name) + shape_names_to_types[shape_name] = path + shape_names_to_image[shape_name] = image(icon = initial(path.icon), icon_state = initial(path.icon_state)) + + var/picked_type = show_radial_menu( + cast_on, + cast_on, + shape_names_to_image, + custom_check = CALLBACK(src, .proc/check_menu, cast_on), + radius = 38, + ) + + if(!picked_type) + return . | SPELL_CANCEL_CAST + + var/atom/shift_type = shape_names_to_types[picked_type] + if(!ispath(shift_type)) + return . | SPELL_CANCEL_CAST + + shapeshift_type = shift_type || pick(possible_shapes) + if(QDELETED(src) || QDELETED(owner) || !can_cast_spell(feedback = FALSE)) + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/shapeshift/cast(mob/living/cast_on) + . = ..() + cast_on.buckled?.unbuckle_mob(cast_on, force = TRUE) + + var/currently_ventcrawling = (cast_on.movement_type & VENTCRAWLING) + + // Do the shift back or forth + if(is_shifted(cast_on)) + restore_form(cast_on) + else + do_shapeshift(cast_on) + + // The shift is done, let's make sure they're in a valid state now + // If we're not ventcrawling, we don't need to mind + if(!currently_ventcrawling) + return + + // We are ventcrawling - can our new form support ventcrawling? + if(HAS_TRAIT(cast_on, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(cast_on, TRAIT_VENTCRAWLER_NUDE)) + return + + // Uh oh. You've shapeshifted into something that can't fit into a vent, while ventcrawling. + eject_from_vents(cast_on) + +/// Whenever someone shapeshifts within a vent, +/// and enters a state in which they are no longer a ventcrawler, +/// they are brutally ejected from the vents. In the form of gibs. +/datum/action/cooldown/spell/shapeshift/proc/eject_from_vents(mob/living/cast_on) + var/obj/machinery/atmospherics/pipe_you_die_in = cast_on.loc + var/datum/pipeline/our_pipeline + var/pipenets = pipe_you_die_in.return_pipenets() + if(islist(pipenets)) + our_pipeline = pipenets[1] + else + our_pipeline = pipenets + + to_chat(cast_on, span_userdanger("Casting [src] inside of [pipe_you_die_in] quickly turns you into a bloody mush!")) + var/obj/effect/gib_type = isalien(cast_on) ? /obj/effect/gibspawner/xeno : /obj/effect/gibspawner/generic + + for(var/obj/machinery/atmospherics/components/unary/possible_vent in range(10, get_turf(cast_on))) + if(length(possible_vent.parents) && possible_vent.parents[1] == our_pipeline) + new gib_type(get_turf(possible_vent)) + playsound(possible_vent, 'sound/effects/reee.ogg', 75, TRUE) + + priority_announce("We detected a pipe blockage around [get_area(get_turf(cast_on))], please dispatch someone to investigate.", "Central Command") + cast_on.death() + qdel(cast_on) + +/datum/action/cooldown/spell/shapeshift/proc/check_menu(mob/living/caster) + if(QDELETED(src)) + return FALSE + if(QDELETED(caster)) + return FALSE + + return !caster.incapacitated() + +/datum/action/cooldown/spell/shapeshift/proc/do_shapeshift(mob/living/caster) + if(is_shifted(caster)) + to_chat(caster, span_warning("You're already shapeshifted!")) + CRASH("[type] called do_shapeshift while shapeshifted.") + + var/mob/living/new_shape = new shapeshift_type(caster.loc) + var/obj/shapeshift_holder/new_shape_holder = new(new_shape, src, caster) + + spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB) + + return new_shape_holder + +/datum/action/cooldown/spell/shapeshift/proc/restore_form(mob/living/caster) + var/obj/shapeshift_holder/current_shift = is_shifted(caster) + if(QDELETED(current_shift)) + return + + var/mob/living/restored_player = current_shift.stored + + current_shift.restore() + spell_requirements = initial(spell_requirements) // Miiight mess with admin stuff. + + return restored_player + +// Maybe one day, this can be a component or something +// Until then, this is what holds data between wizard and shapeshift form whenever shapeshift is cast. +/obj/shapeshift_holder + name = "Shapeshift holder" + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ON_FIRE | UNACIDABLE | ACID_PROOF + var/mob/living/stored + var/mob/living/shape + var/restoring = FALSE + var/datum/action/cooldown/spell/shapeshift/source + +/obj/shapeshift_holder/Initialize(mapload, datum/action/cooldown/spell/shapeshift/_source, mob/living/caster) + . = ..() + source = _source + shape = loc + if(!istype(shape)) + stack_trace("shapeshift holder created outside mob/living") + return INITIALIZE_HINT_QDEL + stored = caster + if(stored.mind) + stored.mind.transfer_to(shape) + stored.forceMove(src) + stored.notransform = TRUE + if(source.convert_damage) + var/damage_percent = (stored.maxHealth - stored.health) / stored.maxHealth; + var/damapply = damage_percent * shape.maxHealth; + + shape.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND); + shape.blood_volume = stored.blood_volume; + + RegisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/shape_death) + RegisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/caster_death) + +/obj/shapeshift_holder/Destroy() + // restore_form manages signal unregistering. If restoring is TRUE, we've already unregistered the signals and we're here + // because restore() qdel'd src. + if(!restoring) + restore() + stored = null + shape = null + return ..() + +/obj/shapeshift_holder/Moved() + . = ..() + if(!restoring && !QDELETED(src)) + restore() + +/obj/shapeshift_holder/handle_atom_del(atom/A) + if(A == stored && !restoring) + restore() + +/obj/shapeshift_holder/Exited(atom/movable/gone, direction) + if(stored == gone && !restoring) + restore() + +/obj/shapeshift_holder/proc/caster_death() + SIGNAL_HANDLER + + //Something kills the stored caster through direct damage. + if(source.revert_on_death) + restore(death = TRUE) + else + shape.death() + +/obj/shapeshift_holder/proc/shape_death() + SIGNAL_HANDLER + + //Shape dies. + if(source.die_with_shapeshifted_form) + if(source.revert_on_death) + restore(death = TRUE) + else + restore() + +/obj/shapeshift_holder/proc/restore(death=FALSE) + // Destroy() calls this proc if it hasn't been called. Unregistering here prevents multiple qdel loops + // when caster and shape both die at the same time. + UnregisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH)) + UnregisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH)) + restoring = TRUE + stored.forceMove(shape.loc) + stored.notransform = FALSE + if(shape.mind) + shape.mind.transfer_to(stored) + if(death) + stored.death() + else if(source.convert_damage) + stored.revive(full_heal = TRUE, admin_revive = FALSE) + + var/damage_percent = (shape.maxHealth - shape.health)/shape.maxHealth; + var/damapply = stored.maxHealth * damage_percent + + stored.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus=CANT_WOUND) + if(source.convert_damage) + stored.blood_volume = shape.blood_volume; + + // This guard is important because restore() can also be called on COMSIG_PARENT_QDELETING for shape, as well as on death. + // This can happen in, for example, [/proc/wabbajack] where the mob hit is qdel'd. + if(!QDELETED(shape)) + QDEL_NULL(shape) + + qdel(src) + return stored diff --git a/code/modules/spells/spell_types/shapeshift/dragon.dm b/code/modules/spells/spell_types/shapeshift/dragon.dm new file mode 100644 index 0000000000000..b58315bb73a95 --- /dev/null +++ b/code/modules/spells/spell_types/shapeshift/dragon.dm @@ -0,0 +1,7 @@ +/datum/action/cooldown/spell/shapeshift/dragon + name = "Dragon Form" + desc = "Take on the shape a lesser ash drake." + invocation = "RAAAAAAAAWR!" + spell_requirements = NONE + + possible_shapes = list(/mob/living/simple_animal/hostile/megafauna/dragon/lesser) diff --git a/code/modules/spells/spell_types/shapeshift/polar_bear.dm b/code/modules/spells/spell_types/shapeshift/polar_bear.dm new file mode 100644 index 0000000000000..73f0bae94969b --- /dev/null +++ b/code/modules/spells/spell_types/shapeshift/polar_bear.dm @@ -0,0 +1,7 @@ +/datum/action/cooldown/spell/shapeshift/polar_bear + name = "Polar Bear Form" + desc = "Take on the shape of a polar bear." + invocation = "RAAAAAAAAWR!" + spell_requirements = NONE + + possible_shapes = list(/mob/living/simple_animal/hostile/asteroid/polarbear/lesser) diff --git a/code/modules/spells/spell_types/shapeshift/shapechange.dm b/code/modules/spells/spell_types/shapeshift/shapechange.dm new file mode 100644 index 0000000000000..a858ac414d96e --- /dev/null +++ b/code/modules/spells/spell_types/shapeshift/shapechange.dm @@ -0,0 +1,22 @@ +/datum/action/cooldown/spell/shapeshift/wizard + name = "Wild Shapeshift" + desc = "Take on the shape of another for a time to use their natural abilities. \ + Once you've made your choice, it cannot be changed." + button_icon_state = "shapeshift" + + school = SCHOOL_TRANSMUTATION + cooldown_time = 20 SECONDS + cooldown_reduction_per_rank = 3.75 SECONDS + + invocation = "RAC'WA NO!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + possible_shapes = list( + /mob/living/simple_animal/mouse, + /mob/living/simple_animal/pet/dog/corgi, + /mob/living/simple_animal/hostile/carp/ranged/chaos, + /mob/living/simple_animal/bot/secbot/ed209, + /mob/living/simple_animal/hostile/giant_spider/viper/wizard, + /mob/living/simple_animal/hostile/construct/juggernaut/mystic, + ) diff --git a/code/modules/spells/spell_types/soultap.dm b/code/modules/spells/spell_types/soultap.dm deleted file mode 100644 index 7aa33c856e151..0000000000000 --- a/code/modules/spells/spell_types/soultap.dm +++ /dev/null @@ -1,33 +0,0 @@ -#define HEALTH_LOST_PER_SOUL_TAP 20 - -//SOUL TAP!// -//Trades 20 max health for a refresh on all of your spells. I was considering making it depend on the cooldowns of your spells, but I want to support "Big spell wizard" with this loadout. -//the two spells that sound most problematic with this is mindswap and lichdom, but soul tap requires clothes for mindswap and lichdom takes your soul. - -/obj/effect/proc_holder/spell/self/tap - name = "Soul Tap" - desc = "Fuel your spells using your own soul!" - school = "necromancy" //i could see why this wouldn't be necromancy but messing with souls or whatever. ectomancy? - charge_max = 10 - invocation = "AT ANY COST!" - invocation_type = INVOCATION_SHOUT - level_max = 0 - cooldown_min = 10 - - action_icon = 'icons/mob/actions/actions_spells.dmi' - action_icon_state = "soultap" - -/obj/effect/proc_holder/spell/self/tap/cast(mob/living/user = usr) - if(!user.mind.hasSoul) - to_chat(user, "You do not possess a soul to tap into!") - return - to_chat(user, "Your body feels drained and there is a burning pain in your chest.") - user.maxHealth -= HEALTH_LOST_PER_SOUL_TAP - user.health = min(user.health, user.maxHealth) - if(user.maxHealth <= 0) - to_chat(user, "Your weakened soul is completely consumed by the tap!") - user.mind.hasSoul = FALSE - for(var/obj/effect/proc_holder/spell/spell in user.mind.spell_list) - spell.charge_counter = spell.charge_max - spell.recharging = FALSE - spell.update_icon() diff --git a/code/modules/spells/spell_types/spacetime_distortion.dm b/code/modules/spells/spell_types/spacetime_distortion.dm deleted file mode 100644 index 7011b69db2ee7..0000000000000 --- a/code/modules/spells/spell_types/spacetime_distortion.dm +++ /dev/null @@ -1,133 +0,0 @@ -/obj/effect/proc_holder/spell/spacetime_dist - name = "Spacetime Distortion" - desc = "Entangle the strings of space-time in an area around you, randomizing the layout and making proper movement impossible. The strings vibrate..." - charge_max = 450 - var/duration = 150 - range = 7 - var/list/effects - var/ready = TRUE - centcom_cancast = FALSE - sound = 'sound/effects/magic.ogg' - cooldown_min = 300 - invocation = "ZYAR INCANTUS" - invocation_type = INVOCATION_SHOUT - clothes_req = FALSE - level_max = 0 - action_icon_state = "spacetime" - -/obj/effect/proc_holder/spell/spacetime_dist/can_cast(mob/user = usr) - if(ready) - return ..() - return FALSE - -/obj/effect/proc_holder/spell/spacetime_dist/choose_targets(mob/user = usr) - var/list/turfs = spiral_range_turfs(range, user) - if(!turfs.len) - revert_cast() - return - - ready = FALSE - var/list/turf_steps = list() - var/length = round(turfs.len * 0.5) - for(var/i in 1 to length) - turf_steps[pick_n_take(turfs)] = pick_n_take(turfs) - if(turfs.len > 0) - var/turf/loner = pick(turfs) - var/area/A = get_area(user) - turf_steps[loner] = get_turf(pick(A.contents)) - - perform(turf_steps,user=user) - -/obj/effect/proc_holder/spell/spacetime_dist/after_cast(list/targets) - addtimer(CALLBACK(src, PROC_REF(clean_turfs)), duration) - -/obj/effect/proc_holder/spell/spacetime_dist/cast(list/targets, mob/user = usr) - effects = list() - for(var/V in targets) - var/turf/T0 = V - var/turf/T1 = targets[V] - var/obj/effect/cross_action/spacetime_dist/STD0 = new /obj/effect/cross_action/spacetime_dist(T0) - var/obj/effect/cross_action/spacetime_dist/STD1 = new /obj/effect/cross_action/spacetime_dist(T1) - STD0.linked_dist = STD1 - STD0.add_overlay(T1.photograph()) - STD1.linked_dist = STD0 - STD1.add_overlay(T0.photograph()) - STD1.set_light(4, 30, "#c9fff5") - effects += STD0 - effects += STD1 - -/obj/effect/proc_holder/spell/spacetime_dist/proc/clean_turfs() - for(var/effect in effects) - qdel(effect) - effects.Cut() - effects = null - ready = TRUE - -/obj/effect/cross_action - name = "cross me" - desc = "for crossing" - anchored = TRUE - -/obj/effect/cross_action/spacetime_dist - name = "spacetime distortion" - desc = "A distortion in spacetime. You can hear faint music..." - icon_state = "" - var/obj/effect/cross_action/spacetime_dist/linked_dist - var/busy = FALSE - var/sound - var/walks_left = 50 //prevents the game from hanging in extreme cases (such as minigun fire) - -/obj/effect/cross_action/singularity_act() - return - -/obj/effect/cross_action/singularity_pull() - return - -/obj/effect/cross_action/spacetime_dist/Initialize(mapload) - . = ..() - setDir(pick(GLOB.cardinals)) - var/static/list/loc_connections = list( - COMSIG_ATOM_ENTERED = PROC_REF(on_entered), - ) - AddElement(/datum/element/connect_loc, loc_connections) - -/obj/effect/cross_action/spacetime_dist/proc/walk_link(atom/movable/AM) - if(ismob(AM)) - var/mob/M = AM - if(M.anti_magic_check(major = FALSE)) - return - if(linked_dist && walks_left > 0) - flick("purplesparkles", src) - linked_dist.get_walker(AM) - walks_left-- - -/obj/effect/cross_action/spacetime_dist/proc/get_walker(atom/movable/AM) - busy = TRUE - flick("purplesparkles", src) - AM.forceMove(get_turf(src)) - playsound(get_turf(src),sound,70,0) - busy = FALSE - -/obj/effect/cross_action/spacetime_dist/proc/on_entered(datum/source, atom/movable/AM) - SIGNAL_HANDLER - - if(!busy) - walk_link(AM) - -/obj/effect/cross_action/spacetime_dist/attackby(obj/item/W, mob/user, params) - if(user.temporarilyRemoveItemFromInventory(W)) - walk_link(W) - else - walk_link(user) - -//ATTACK HAND IGNORING PARENT RETURN VALUE -/obj/effect/cross_action/spacetime_dist/attack_hand(mob/user) - walk_link(user) - -/obj/effect/cross_action/spacetime_dist/attack_paw(mob/user) - walk_link(user) - -/obj/effect/cross_action/spacetime_dist/Destroy() - busy = TRUE - linked_dist = null - return ..() diff --git a/code/modules/spells/spell_types/summonitem.dm b/code/modules/spells/spell_types/summonitem.dm deleted file mode 100644 index abf00e699a10c..0000000000000 --- a/code/modules/spells/spell_types/summonitem.dm +++ /dev/null @@ -1,110 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/summonitem - name = "Instant Summons" - desc = "This spell can be used to recall a previously marked item to your hand from anywhere in the universe." - school = "transmutation" - charge_max = 100 - clothes_req = FALSE - invocation = "GAR YOK" - invocation_type = INVOCATION_WHISPER - range = -1 - level_max = 0 //cannot be improved - cooldown_min = 100 - include_user = TRUE - - var/obj/marked_item - var/allow_change = TRUE - - action_icon_state = "summons" - -/obj/effect/proc_holder/spell/targeted/summonitem/cast(list/targets,mob/user = usr) - for(var/mob/living/L in targets) - var/list/hand_items = list(L.get_active_held_item(),L.get_inactive_held_item()) - var/message - - if(!marked_item && allow_change) //linking item to the spell - message = "" - for(var/obj/item/item in hand_items) - if(item.item_flags & ABSTRACT) - continue - if(SEND_SIGNAL(item, COMSIG_ITEM_MARK_RETRIEVAL) & COMPONENT_BLOCK_MARK_RETRIEVAL) - continue - if(HAS_TRAIT(item, TRAIT_NODROP)) - message += "Though it feels redundant, " - marked_item = item - message += "You mark [item] for recall." - name = "Recall [item]" - break - - if(!marked_item) - if(hand_items) - message = "You aren't holding anything that can be marked for recall." - else - message = "You must hold the desired item in your hands to mark it for recall." - - else if(marked_item && (marked_item in hand_items) && allow_change) //unlinking item to the spell - message = "You remove the mark on [marked_item] to use elsewhere." - name = "Instant Summons" - marked_item = null - - else if(marked_item && QDELETED(marked_item)) //the item was destroyed at some point - message = "You sense your marked item has been destroyed!" - name = "Instant Summons" - marked_item = null - if(!allow_change) - qdel(src) - return - - else //Getting previously marked item - var/obj/item_to_retrieve = marked_item - var/infinite_recursion = 0 //I don't want to know how someone could put something inside itself but these are wizards so let's be safe - - if(!item_to_retrieve.loc) - if(isorgan(item_to_retrieve)) // Organs are usually stored in nullspace - var/obj/item/organ/organ = item_to_retrieve - if(organ.owner) - // If this code ever runs I will be happy - log_combat(L, organ.owner, "magically removed [organ.name] from", addition="INTENT: [uppertext(L.a_intent)]") - organ.Remove(organ.owner) - else - while(!isturf(item_to_retrieve.loc) && infinite_recursion < 10) //if it's in something you get the whole thing. - if(isitem(item_to_retrieve.loc)) - var/obj/item/I = item_to_retrieve.loc - if(I.item_flags & ABSTRACT) //Being able to summon abstract things because your item happened to get placed there is a no-no - break - if(ismob(item_to_retrieve.loc)) //If its on someone, properly drop it - var/mob/M = item_to_retrieve.loc - - if(issilicon(M)) //Items in silicons warp the whole silicon - M.loc.visible_message("[M] suddenly disappears!") - M.forceMove(L.loc) - M.loc.visible_message("[M] suddenly appears!") - item_to_retrieve = null - break - M.dropItemToGround(item_to_retrieve) - - else - if(istype(item_to_retrieve.loc, /obj/machinery/portable_atmospherics/)) //Edge cases for moved machinery - var/obj/machinery/portable_atmospherics/P = item_to_retrieve.loc - P.disconnect() - P.update_icon() - - item_to_retrieve = item_to_retrieve.loc - - infinite_recursion += 1 - - if(!item_to_retrieve) - return - - if(item_to_retrieve.loc) - item_to_retrieve.loc.visible_message("The [item_to_retrieve.name] suddenly disappears!") - if(!L.put_in_hands(item_to_retrieve)) - item_to_retrieve.forceMove(L.drop_location()) - item_to_retrieve.loc.visible_message("The [item_to_retrieve.name] suddenly appears!") - playsound(get_turf(L), 'sound/magic/summonitems_generic.ogg', 50, 1) - else - item_to_retrieve.loc.visible_message("The [item_to_retrieve.name] suddenly appears in [L]'s hand!") - playsound(get_turf(L), 'sound/magic/summonitems_generic.ogg', 50, 1) - - - if(message) - to_chat(L, message) diff --git a/code/modules/spells/spell_types/telepathy.dm b/code/modules/spells/spell_types/telepathy.dm deleted file mode 100644 index 3bb63f14e93df..0000000000000 --- a/code/modules/spells/spell_types/telepathy.dm +++ /dev/null @@ -1,38 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/telepathy - name = "Telepathy" - desc = "Telepathically transmits a message to the target." - charge_max = 0 - clothes_req = 0 - range = 7 - include_user = 0 - action_icon = 'icons/mob/actions/actions_revenant.dmi' - action_icon_state = "r_transmit" - action_background_icon_state = "bg_spell" - var/notice = "notice" - var/boldnotice = "boldnotice" - var/magic_check = FALSE - var/holy_check = FALSE - -/obj/effect/proc_holder/spell/targeted/telepathy/cast(list/targets, mob/living/user = usr) - for(var/mob/living/M in targets) - if(istype(M.get_item_by_slot(ITEM_SLOT_HEAD), /obj/item/clothing/head/costume/foilhat)) - to_chat(user, "It appears the target's mind is ironclad! No getting a message in there!") - return - var/msg = tgui_input_text(usr, "What do you wish to tell [M]?", "Telepathy") - if(!length(msg)) - revert_cast(user) - return - if(CHAT_FILTER_CHECK(msg)) - to_chat(user, "Your message contains forbidden words.") - return - msg = user.treat_message_min(msg) - log_directed_talk(user, M, msg, LOG_SAY, "[name]") - to_chat(user, "You transmit to [M]: [msg]") - if(!M.anti_magic_check(magic_check, holy_check)) //hear no evil - to_chat(M, "You hear something behind you talking... [msg]") - for(var/ded in GLOB.dead_mob_list) - if(!isobserver(ded)) - continue - var/follow_rev = FOLLOW_LINK(ded, user) - var/follow_whispee = FOLLOW_LINK(ded, M) - to_chat(ded, "[follow_rev] [user] [name]: \"[msg]\" to [follow_whispee] [M]") diff --git a/code/modules/spells/spell_types/touch/_touch.dm b/code/modules/spells/spell_types/touch/_touch.dm new file mode 100644 index 0000000000000..d17ef04387d21 --- /dev/null +++ b/code/modules/spells/spell_types/touch/_touch.dm @@ -0,0 +1,265 @@ +/datum/action/cooldown/spell/touch + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED + sound = 'sound/items/welder.ogg' + invocation = "High Five!" + invocation_type = INVOCATION_SHOUT + + /// Typepath of what hand we create on initial cast. + var/obj/item/melee/touch_attack/hand_path = /obj/item/melee/touch_attack + /// Ref to the hand we currently have deployed. + var/obj/item/melee/touch_attack/attached_hand + /// The message displayed to the person upon creating the touch hand + var/draw_message = span_notice("You channel the power of the spell to your hand.") + /// The message displayed upon willingly dropping / deleting / cancelling the touch hand before using it + var/drop_message = span_notice("You draw the power out of your hand.") + +/datum/action/cooldown/spell/touch/Destroy() + // If we have an owner, the hand is cleaned up in Remove(), which Destroy() calls. + if(!owner) + QDEL_NULL(attached_hand) + return ..() + +/datum/action/cooldown/spell/touch/Remove(mob/living/remove_from) + remove_hand(remove_from) + return ..() + +/datum/action/cooldown/spell/touch/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE) + . = ..() + if(!button) + return + if(attached_hand) + button.color = COLOR_GREEN + +/datum/action/cooldown/spell/touch/set_statpanel_format() + . = ..() + if(!islist(.)) + return + + if(attached_hand) + .[PANEL_DISPLAY_STATUS] = "ACTIVE" + +/datum/action/cooldown/spell/touch/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!iscarbon(owner)) + return FALSE + var/mob/living/carbon/carbon_owner = owner + if(!(carbon_owner.mobility_flags & MOBILITY_USE)) + return FALSE + return TRUE + +/datum/action/cooldown/spell/touch/is_valid_target(atom/cast_on) + return iscarbon(cast_on) + +/** + * Creates a new hand_path hand and equips it to the caster. + * + * If the equipping action fails, reverts the cooldown and returns FALSE. + * Otherwise, registers signals and returns TRUE. + */ +/datum/action/cooldown/spell/touch/proc/create_hand(mob/living/carbon/cast_on) + var/obj/item/melee/touch_attack/new_hand = new hand_path(cast_on, src) + if(!cast_on.put_in_hands(new_hand, del_on_fail = TRUE)) + reset_spell_cooldown() + if (cast_on.usable_hands == 0) + to_chat(cast_on, span_warning("You dont have any usable hands!")) + else + to_chat(cast_on, span_warning("Your hands are full!")) + return FALSE + + attached_hand = new_hand + RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK, .proc/on_hand_hit) + RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK_SECONDARY, .proc/on_secondary_hand_hit) + RegisterSignal(attached_hand, COMSIG_PARENT_QDELETING, .proc/on_hand_deleted) + RegisterSignal(attached_hand, COMSIG_ITEM_DROPPED, .proc/on_hand_dropped) + to_chat(cast_on, draw_message) + return TRUE + +/** + * Unregisters any signals and deletes the hand currently summoned by the spell. + * + * If reset_cooldown_after is TRUE, we will additionally refund the cooldown of the spell. + * If reset_cooldown_after is FALSE, we will instead just start the spell's cooldown + */ +/datum/action/cooldown/spell/touch/proc/remove_hand(mob/living/hand_owner, reset_cooldown_after = FALSE) + if(!QDELETED(attached_hand)) + UnregisterSignal(attached_hand, list(COMSIG_ITEM_AFTERATTACK, COMSIG_ITEM_AFTERATTACK_SECONDARY, COMSIG_PARENT_QDELETING, COMSIG_ITEM_DROPPED)) + hand_owner?.temporarilyRemoveItemFromInventory(attached_hand) + QDEL_NULL(attached_hand) + + if(reset_cooldown_after) + if(hand_owner) + to_chat(hand_owner, drop_message) + reset_spell_cooldown() + else + StartCooldown() + +// Touch spells don't go on cooldown OR give off an invocation until the hand is used itself. +/datum/action/cooldown/spell/touch/before_cast(atom/cast_on) + return ..() | SPELL_NO_FEEDBACK | SPELL_NO_IMMEDIATE_COOLDOWN + +/datum/action/cooldown/spell/touch/cast(mob/living/carbon/cast_on) + if(!QDELETED(attached_hand) && (attached_hand in cast_on.held_items)) + remove_hand(cast_on, reset_cooldown_after = TRUE) + return + + create_hand(cast_on) + return ..() + +/** + * Signal proc for [COMSIG_ITEM_AFTERATTACK] from our attached hand. + * + * When our hand hits an atom, we can cast do_hand_hit() on them. + */ +/datum/action/cooldown/spell/touch/proc/on_hand_hit(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters) + SIGNAL_HANDLER + + if(!proximity_flag) + return + if(victim == caster) + return + if(!can_cast_spell(feedback = FALSE)) + return + + INVOKE_ASYNC(src, .proc/do_hand_hit, source, victim, caster) + +/** + * Signal proc for [COMSIG_ITEM_AFTERATTACK_SECONDARY] from our attached hand. + * + * Same as on_hand_hit, but for if right-click was used on hit. + */ +/datum/action/cooldown/spell/touch/proc/on_secondary_hand_hit(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters) + SIGNAL_HANDLER + + if(!proximity_flag) + return + if(victim == caster) + return + if(!can_cast_spell(feedback = FALSE)) + return + + INVOKE_ASYNC(src, .proc/do_secondary_hand_hit, source, victim, caster) + +/** + * Calls cast_on_hand_hit() from the caster onto the victim. + */ +/datum/action/cooldown/spell/touch/proc/do_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + SEND_SIGNAL(src, COMSIG_SPELL_TOUCH_HAND_HIT, victim, caster, hand) + if(!cast_on_hand_hit(hand, victim, caster)) + return + + log_combat(caster, victim, "cast the touch spell [name] on", hand) + spell_feedback() + remove_hand(caster) + +/** + * Calls do_secondary_hand_hit() from the caster onto the victim. + */ +/datum/action/cooldown/spell/touch/proc/do_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + var/secondary_result = cast_on_secondary_hand_hit(hand, victim, caster) + switch(secondary_result) + // Continue will remove the hand here and stop + if(SECONDARY_ATTACK_CONTINUE_CHAIN) + log_combat(caster, victim, "cast the touch spell [name] on", hand, "(secondary / alt cast)") + spell_feedback() + remove_hand(caster) + + // Call normal will call the normal cast proc + if(SECONDARY_ATTACK_CALL_NORMAL) + do_hand_hit(hand, victim, caster) + + // Cancel chain will do nothing, + if(SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return + +/** + * The actual process of casting the spell on the victim from the caster. + * + * Override / extend this to implement casting effects. + * Return TRUE on a successful cast to use up the hand (delete it) + * Return FALSE to do nothing and let them keep the hand in hand + */ +/datum/action/cooldown/spell/touch/proc/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + return FALSE + +/** + * For any special casting effects done if the user right-clicks + * on touch spell instead of left-clicking + * + * Return SECONDARY_ATTACK_CALL_NORMAL to call the normal cast_on_hand_hit + * Return SECONDARY_ATTACK_CONTINUE_CHAIN to prevent the normal cast_on_hand_hit from calling, but still use up the hand + * Return SECONDARY_ATTACK_CANCEL_CHAIN to prevent the spell from being used + */ +/datum/action/cooldown/spell/touch/proc/cast_on_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + return SECONDARY_ATTACK_CALL_NORMAL + +/** + * Signal proc for [COMSIG_PARENT_QDELETING] from our attached hand. + * + * If our hand is deleted for a reason unrelated to our spell, + * unlink it (clear refs) and revert the cooldown + */ +/datum/action/cooldown/spell/touch/proc/on_hand_deleted(datum/source) + SIGNAL_HANDLER + + remove_hand(reset_cooldown_after = TRUE) + +/** + * Signal proc for [COMSIG_ITEM_DROPPED] from our attached hand. + * + * If our caster drops the hand, remove the hand / revert the cast + * Basically gives them an easy hotkey to lose their hand without needing to click the button + */ +/datum/action/cooldown/spell/touch/proc/on_hand_dropped(datum/source, mob/living/dropper) + SIGNAL_HANDLER + + remove_hand(dropper, reset_cooldown_after = TRUE) + +/obj/item/melee/touch_attack + name = "\improper outstretched hand" + desc = "High Five?" + icon = 'icons/obj/items_and_weapons.dmi' + lefthand_file = 'icons/mob/inhands/misc/touchspell_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/touchspell_righthand.dmi' + icon_state = "latexballon" + inhand_icon_state = null + item_flags = NEEDS_PERMIT | ABSTRACT + w_class = WEIGHT_CLASS_HUGE + force = 0 + throwforce = 0 + throw_range = 0 + throw_speed = 0 + /// A weakref to what spell made us. + var/datum/weakref/spell_which_made_us + +/obj/item/melee/touch_attack/Initialize(mapload, datum/action/cooldown/spell/spell) + . = ..() + + if(spell) + spell_which_made_us = WEAKREF(spell) + +/obj/item/melee/touch_attack/attack(mob/target, mob/living/carbon/user) + if(!iscarbon(user)) //Look ma, no hands + return TRUE + if(!(user.mobility_flags & MOBILITY_USE)) + to_chat(user, span_warning("You can't reach out!")) + return TRUE + return ..() + +/** + * When the hand component of a touch spell is qdel'd, (the hand is dropped or otherwise lost), + * the cooldown on the spell that made it is automatically refunded. + * + * However, if you want to consume the hand and not give a cooldown, + * such as adding a unique behavior to the hand specifically, this function will do that. + */ +/obj/item/melee/touch_attack/mansus_fist/proc/remove_hand_with_no_refund(mob/holder) + var/datum/action/cooldown/spell/touch/hand_spell = spell_which_made_us?.resolve() + if(!QDELETED(hand_spell)) + hand_spell.remove_hand(holder, reset_cooldown_after = FALSE) + return + + // We have no spell associated for some reason, just delete us as normal. + holder.temporarilyRemoveItemFromInventory(src, force = TRUE) + qdel(src) diff --git a/code/modules/spells/spell_types/touch/duffelbag_curse.dm b/code/modules/spells/spell_types/touch/duffelbag_curse.dm new file mode 100644 index 0000000000000..9accad2c09069 --- /dev/null +++ b/code/modules/spells/spell_types/touch/duffelbag_curse.dm @@ -0,0 +1,86 @@ +/datum/action/cooldown/spell/touch/duffelbag + name = "Bestow Cursed Duffel Bag" + desc = "A spell that summons a duffel bag demon on the target, slowing them down and slowly eating them." + button_icon_state = "duffelbag_curse" + sound = 'sound/magic/mm_hit.ogg' + + school = SCHOOL_CONJURATION + cooldown_time = 6 SECONDS + cooldown_reduction_per_rank = 1 SECONDS + + invocation = "HU'SWCH H'ANS!!" + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + hand_path = /obj/item/melee/touch_attack/duffelbag + +/datum/action/cooldown/spell/touch/duffelbag/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!iscarbon(victim)) + return FALSE + + var/mob/living/carbon/duffel_victim = victim + var/static/list/elaborate_backstory = list( + "spacewar origin story", + "military background", + "corporate connections", + "life in the colonies", + "anti-government activities", + "upbringing on the space farm", + "fond memories with your buddy Keith", + ) + if(duffel_victim.can_block_magic(antimagic_flags)) + to_chat(caster, span_warning("The spell can't seem to affect [duffel_victim]!")) + to_chat(duffel_victim, span_warning("You really don't feel like talking about your [pick(elaborate_backstory)] with complete strangers today.")) + return TRUE + + // To get it started, stun and knockdown the person being hit + duffel_victim.flash_act() + duffel_victim.Immobilize(5 SECONDS) + duffel_victim.apply_damage(80, STAMINA) + duffel_victim.Knockdown(5 SECONDS) + + // If someone's already cursed, don't try to give them another + if(HAS_TRAIT(duffel_victim, TRAIT_DUFFEL_CURSE_PROOF)) + to_chat(caster, span_warning("The burden of [duffel_victim]'s duffel bag becomes too much, shoving them to the floor!")) + to_chat(duffel_victim, span_warning("The weight of this bag becomes overburdening!")) + return TRUE + + // However if they're uncursed, they're fresh for getting a cursed bag + var/obj/item/storage/backpack/duffelbag/cursed/conjured_duffel = new get_turf(victim) + duffel_victim.visible_message( + span_danger("A growling duffel bag appears on [duffel_victim]!"), + span_danger("You feel something attaching itself to you, and a strong desire to discuss your [pick(elaborate_backstory)] at length!"), + ) + + // This duffelbag is now cuuuurrrsseed! Equip it on them + ADD_TRAIT(conjured_duffel, TRAIT_DUFFEL_CURSE_PROOF, CURSED_ITEM_TRAIT(conjured_duffel.name)) + conjured_duffel.pickup(duffel_victim) + conjured_duffel.forceMove(duffel_victim) + + // Put it on their back first + if(duffel_victim.dropItemToGround(duffel_victim.back)) + duffel_victim.equip_to_slot_if_possible(conjured_duffel, ITEM_SLOT_BACK, TRUE, TRUE) + return TRUE + + // If the back equip failed, put it in their hands first + if(duffel_victim.put_in_hands(conjured_duffel)) + return TRUE + + // If they had no empty hands, try to put it in their inactive hand first + duffel_victim.dropItemToGround(duffel_victim.get_inactive_held_item()) + if(duffel_victim.put_in_hands(conjured_duffel)) + return TRUE + + // If their inactive hand couldn't be emptied or found, put it in their active hand + duffel_victim.dropItemToGround(duffel_victim.get_active_held_item()) + if(duffel_victim.put_in_hands(conjured_duffel)) + return TRUE + + // Well, we failed to give them the duffel bag, + // but technically we still stunned them so that's something + return TRUE + +/obj/item/melee/touch_attack/duffelbag + name = "\improper burdening touch" + desc = "Where is the bar from here?" + icon_state = "duffelcurse" + inhand_icon_state = "duffelcurse" diff --git a/code/modules/spells/spell_types/touch/flesh_to_stone.dm b/code/modules/spells/spell_types/touch/flesh_to_stone.dm new file mode 100644 index 0000000000000..c6693c7c904e3 --- /dev/null +++ b/code/modules/spells/spell_types/touch/flesh_to_stone.dm @@ -0,0 +1,33 @@ +/datum/action/cooldown/spell/touch/flesh_to_stone + name = "Flesh to Stone" + desc = "This spell charges your hand with the power to turn victims into inert statues for a long period of time." + button_icon_state = "statue" + sound = 'sound/magic/fleshtostone.ogg' + + school = SCHOOL_TRANSMUTATION + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 10 SECONDS + + invocation = "STAUN EI!!" + + hand_path = /obj/item/melee/touch_attack/flesh_to_stone + +/datum/action/cooldown/spell/touch/flesh_to_stone/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!isliving(victim)) + return FALSE + + var/mob/living/living_victim = victim + if(living_victim.can_block_magic(antimagic_flags)) + to_chat(caster, span_warning("The spell can't seem to affect [victim]!")) + to_chat(victim, span_warning("You feel your flesh turn to stone for a moment, then revert back!")) + return TRUE + + living_victim.Stun(4 SECONDS) + living_victim.petrify() + return TRUE + +/obj/item/melee/touch_attack/flesh_to_stone + name = "\improper petrifying touch" + desc = "That's the bottom line, because flesh to stone said so!" + icon_state = "fleshtostone" + inhand_icon_state = "fleshtostone" diff --git a/code/modules/spells/spell_types/touch/smite.dm b/code/modules/spells/spell_types/touch/smite.dm new file mode 100644 index 0000000000000..d39ddb3e30ce4 --- /dev/null +++ b/code/modules/spells/spell_types/touch/smite.dm @@ -0,0 +1,55 @@ +/datum/action/cooldown/spell/touch/smite + name = "Smite" + desc = "This spell charges your hand with an unholy energy \ + that can be used to cause a touched victim to violently explode." + button_icon_state = "gib" + sound = 'sound/magic/disintegrate.ogg' + + school = SCHOOL_EVOCATION + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 10 SECONDS + + invocation = "EI NATH!!" + sparks_amt = 4 + + hand_path = /obj/item/melee/touch_attack/smite + +/datum/action/cooldown/spell/touch/smite/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!isliving(victim)) + return FALSE + + do_sparks(sparks_amt, FALSE, get_turf(victim)) + for(var/mob/living/nearby_spectator in view(caster, 7)) + if(nearby_spectator == caster) + continue + nearby_spectator.flash_act(affect_silicon = FALSE) + + var/mob/living/living_victim = victim + if(living_victim.can_block_magic(antimagic_flags)) + caster.visible_message( + span_warning("The feedback blows [caster]'s arm off!"), + span_userdanger("The spell bounces from [living_victim]'s skin back into your arm!"), + ) + caster.flash_act() + var/obj/item/bodypart/to_dismember = caster.get_holding_bodypart_of_item(hand) + to_dismember?.dismember() + return TRUE + + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + var/obj/item/clothing/suit/worn_suit = human_victim.wear_suit + if(istype(worn_suit, /obj/item/clothing/suit/hooded/bloated_human)) + human_victim.visible_message(span_danger("[victim]'s [worn_suit] explodes off of them into a puddle of gore!")) + human_victim.dropItemToGround(worn_suit) + qdel(worn_suit) + new /obj/effect/gibspawner(get_turf(victim)) + return TRUE + + living_victim.gib() + return TRUE + +/obj/item/melee/touch_attack/smite + name = "\improper smiting touch" + desc = "This hand of mine glows with an awesome power!" + icon_state = "disintegrate" + inhand_icon_state = "disintegrate" diff --git a/code/modules/spells/spell_types/touch_attacks.dm b/code/modules/spells/spell_types/touch_attacks.dm deleted file mode 100644 index 9af90c350c286..0000000000000 --- a/code/modules/spells/spell_types/touch_attacks.dm +++ /dev/null @@ -1,99 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/touch - var/hand_path = /obj/item/melee/touch_attack - var/obj/item/melee/touch_attack/attached_hand = null - var/drawmessage = "You channel the power of the spell to your hand." - var/dropmessage = "You draw the power out of your hand." - invocation_type = INVOCATION_NONE //you scream on connecting, not summoning - include_user = TRUE - range = -1 - //Checks - var/spell_used = FALSE - -/obj/effect/proc_holder/spell/targeted/touch/Destroy() - remove_hand() - to_chat(usr, "The power of the spell dissipates from your hand.") - ..() - -/obj/effect/proc_holder/spell/targeted/touch/proc/remove_hand() - QDEL_NULL(attached_hand) - if(!spell_used) - charge_counter = charge_max - -/obj/effect/proc_holder/spell/targeted/touch/proc/on_hand_destroy(obj/item/melee/touch_attack/hand) - if(hand != attached_hand) - CRASH("Incorrect touch spell hand.") - //Start recharging. - attached_hand = null - recharging = TRUE - action.UpdateButtonIcon() - -/obj/effect/proc_holder/spell/targeted/touch/cast(list/targets,mob/user = usr) - if(!QDELETED(attached_hand)) - remove_hand() - to_chat(user, "[dropmessage]") - return - - for(var/mob/living/carbon/target in targets) - if(!attached_hand && charge_hand(target)) - recharging = FALSE - return - -/obj/effect/proc_holder/spell/targeted/touch/charge_check(mob/user,silent = FALSE) - if(!QDELETED(attached_hand)) //Charge doesn't matter when putting the hand away. - return TRUE - return ..() - -/obj/effect/proc_holder/spell/targeted/touch/proc/create_hand() - return new hand_path(null, src) - -/obj/effect/proc_holder/spell/targeted/touch/proc/charge_hand(mob/living/carbon/user) - attached_hand = create_hand() - if(!user.put_in_hands(attached_hand)) - remove_hand() - if (user.usable_hands == 0) - to_chat(user, "You dont have any usable hands!") - else - to_chat(user, "Your hands are full!") - return FALSE - spell_used = FALSE - to_chat(user, "[drawmessage]") - return TRUE - - -/obj/effect/proc_holder/spell/targeted/touch/disintegrate - name = "Disintegrate" - desc = "This spell charges your hand with vile energy that can be used to violently explode victims." - hand_path = /obj/item/melee/touch_attack/disintegrate - - school = "evocation" - charge_max = 600 - clothes_req = TRUE - cooldown_min = 200 //100 deciseconds reduction per rank - - action_icon_state = "gib" - -/obj/effect/proc_holder/spell/targeted/touch/flesh_to_stone - name = "Flesh to Stone" - desc = "This spell charges your hand with the power to turn victims into inert statues for a long period of time." - hand_path = /obj/item/melee/touch_attack/fleshtostone - - school = "transmutation" - charge_max = 600 - clothes_req = TRUE - cooldown_min = 200 //100 deciseconds reduction per rank - - action_icon_state = "statue" - sound = 'sound/magic/fleshtostone.ogg' - -/obj/effect/proc_holder/spell/targeted/touch/mutation - clothes_req = FALSE - var/datum/mutation/parent_mutation - -/obj/effect/proc_holder/spell/targeted/touch/mutation/Initialize(_mapload, datum/mutation/_parent) - . = ..() - if(!istype(_parent)) - return INITIALIZE_HINT_QDEL - parent_mutation = _parent - -/obj/effect/proc_holder/spell/targeted/touch/mutation/create_hand() - return new hand_path(null, src, parent_mutation) diff --git a/code/modules/spells/spell_types/trigger.dm b/code/modules/spells/spell_types/trigger.dm deleted file mode 100644 index c13c96686c6df..0000000000000 --- a/code/modules/spells/spell_types/trigger.dm +++ /dev/null @@ -1,26 +0,0 @@ -/obj/effect/proc_holder/spell/pointed/trigger - name = "Trigger" - desc = "This spell triggers another spell or a few." - var/list/linked_spells = list() //those are just referenced by the trigger spell and are unaffected by it directly - var/list/starting_spells = list() //those are added on New() to contents from default spells and are deleted when the trigger spell is deleted to prevent memory leaks - -/obj/effect/proc_holder/spell/pointed/trigger/Initialize(mapload) - . = ..() - for(var/spell in starting_spells) - var/spell_to_add = text2path(spell) - new spell_to_add(src) //should result in adding to contents, needs testing - -/obj/effect/proc_holder/spell/pointed/trigger/Destroy() - for(var/spell in contents) - qdel(spell) - linked_spells = null - starting_spells = null - return ..() - -/obj/effect/proc_holder/spell/pointed/trigger/cast(list/targets, mob/user = usr) - playMagSound() - for(var/mob/living/target in targets) - for(var/obj/effect/proc_holder/spell/spell in contents) - spell.perform(list(target),0) - for(var/obj/effect/proc_holder/spell/spell in linked_spells) - spell.perform(list(target),0) diff --git a/code/modules/spells/spell_types/turf_teleport.dm b/code/modules/spells/spell_types/turf_teleport.dm deleted file mode 100644 index 3dbe76e5cddc4..0000000000000 --- a/code/modules/spells/spell_types/turf_teleport.dm +++ /dev/null @@ -1,38 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/turf_teleport - name = "Turf Teleport" - desc = "This spell teleports the target to the turf in range." - nonabstract_req = TRUE - - var/inner_tele_radius = 1 - var/outer_tele_radius = 2 - - var/include_space = FALSE //whether it includes space tiles in possible teleport locations - var/include_dense = FALSE //whether it includes dense tiles in possible teleport locations - var/sound1 = 'sound/weapons/zapbang.ogg' - var/sound2 = 'sound/weapons/zapbang.ogg' - -/obj/effect/proc_holder/spell/targeted/turf_teleport/cast(list/targets,mob/user = usr) - playsound(get_turf(user), sound1, 50,1) - for(var/mob/living/target in targets) - var/list/turfs = new/list() - for(var/turf/T as() in (RANGE_TURFS(outer_tele_radius, target)-RANGE_TURFS(inner_tele_radius, target))) - if(isspaceturf(T) && !include_space) - continue - if(T.density && !include_dense) - continue - if(T.x>world.maxx-outer_tele_radius || T.xworld.maxy-outer_tele_radius || T.yYou are unable to speak!
") - return FALSE - return TRUE - -/obj/effect/proc_holder/spell/voice_of_god/choose_targets(mob/user = usr) - perform(user=user) -/obj/effect/proc_holder/spell/voice_of_god/perform(list/targets, recharge = 1, mob/user = usr) - command = input(user, "Speak with the Voice of God", "Command") - if(QDELETED(src) || QDELETED(user)) - return - if(!command) - revert_cast(user) - return - ..() - -/obj/effect/proc_holder/spell/voice_of_god/cast(list/targets, mob/user = usr) - playsound(get_turf(user), speech_sound, 300, 1, 5) - var/cooldown = voice_of_god(uppertext(command), user, spans, base_multiplier = power_mod) - charge_max = (cooldown * cooldown_mod) - -/obj/effect/proc_holder/spell/voice_of_god/clown - name = "Voice of Clown" - desc = "Speak with an incredibly funny voice, startling people into obeying you for a brief moment." - power_mod = 0.1 - cooldown_mod = 0.5 - spans = list("clown") - speech_sound = 'sound/spookoween/scary_horn2.ogg' diff --git a/code/modules/spells/spell_types/wizard.dm b/code/modules/spells/spell_types/wizard.dm deleted file mode 100644 index 467d6bfef5882..0000000000000 --- a/code/modules/spells/spell_types/wizard.dm +++ /dev/null @@ -1,377 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/projectile/magic_missile - name = "Magic Missile" - desc = "This spell fires several, slow moving, magic projectiles at nearby targets." - - school = "evocation" - charge_max = 200 - clothes_req = TRUE - invocation = "FORTI GY AMA" - invocation_type = INVOCATION_SHOUT - range = 7 - cooldown_min = 60 //35 deciseconds reduction per rank - max_targets = 0 - proj_type = /obj/projectile/magic/spell/magic_missile - action_icon_state = "magicm" - sound = 'sound/magic/magic_missile.ogg' - -/obj/projectile/magic/spell/magic_missile - name = "a magic missile" - icon_state = "magicm" - range = 20 - speed = 5 - trigger_range = 0 - linger = TRUE - paralyze = 60 - hitsound = 'sound/magic/mm_hit.ogg' - - trail = TRUE - trail_lifespan = 5 - trail_icon_state = "magicmd" - -/obj/projectile/magic/spell/magic_missile/New(loc, spell_level) - . = ..() - paralyze += spell_level * 10 - -/obj/effect/proc_holder/spell/targeted/genetic/mutate - name = "Mutate" - desc = "This spell causes you to turn into a hulk and gain laser vision for a short while." - - school = "transmutation" - charge_max = 400 - clothes_req = TRUE - invocation = "BIRUZ BENNAR" - invocation_type = INVOCATION_SHOUT - range = -1 - include_user = TRUE - - mutations = list(LASEREYES, HULK) - duration = 300 - cooldown_min = 300 //25 deciseconds reduction per rank - - action_icon_state = "mutate" - sound = 'sound/magic/mutate.ogg' - - -/obj/effect/proc_holder/spell/targeted/smoke - name = "Smoke" - desc = "This spell spawns a cloud of choking smoke at your location." - - school = "conjuration" - charge_max = 120 - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = -1 - include_user = TRUE - cooldown_min = 20 //25 deciseconds reduction per rank - - smoke_spread = 2 - smoke_amt = 4 - - action_icon_state = "smoke" - -/obj/effect/proc_holder/spell/targeted/smoke/lesser //Chaplain smoke book - name = "Smoke" - desc = "This spell spawns a small cloud of choking smoke at your location." - - school = "conjuration" - charge_max = 360 - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = -1 - include_user = TRUE - - smoke_spread = 1 - smoke_amt = 2 - - action_icon_state = "smoke" - -/obj/effect/proc_holder/spell/targeted/emplosion/disable_tech - name = "Disable Tech" - desc = "This spell disables all weapons, cameras and most other technology in range." - charge_max = 400 - clothes_req = TRUE - invocation = "NEC CANTIO" - invocation_type = INVOCATION_SHOUT - range = -1 - include_user = TRUE - cooldown_min = 200 //50 deciseconds reduction per rank - - emp_heavy = 6 - emp_light = 10 - sound = 'sound/magic/disable_tech.ogg' - -/obj/effect/proc_holder/spell/targeted/turf_teleport/blink - name = "Blink" - desc = "This spell randomly teleports you a short distance." - - school = "abjuration" - charge_max = 20 - clothes_req = TRUE - invocation = "none" - invocation_type = INVOCATION_NONE - range = -1 - include_user = TRUE - cooldown_min = 5 //4 deciseconds reduction per rank - - - smoke_spread = 1 - smoke_amt = 0 - - inner_tele_radius = 0 - outer_tele_radius = 6 - - action_icon_state = "blink" - sound1 = 'sound/magic/blink.ogg' - sound2 = 'sound/magic/blink.ogg' - -/obj/effect/proc_holder/spell/targeted/turf_teleport/blink/cult - name = "quickstep" - - charge_max = 100 - clothes_req = FALSE - clothes_req = TRUE - -/obj/effect/proc_holder/spell/targeted/area_teleport/teleport - name = "Teleport" - desc = "This spell teleports you to an area of your selection." - - school = "abjuration" - charge_max = 600 - clothes_req = TRUE - invocation = "SCYAR NILA" - invocation_type = INVOCATION_SHOUT - range = -1 - include_user = TRUE - cooldown_min = 200 //100 deciseconds reduction per rank - action_icon_state = "teleport" - - smoke_spread = 1 - smoke_amt = 2 - sound1 = 'sound/magic/teleport_diss.ogg' - sound2 = 'sound/magic/teleport_app.ogg' - -/obj/effect/proc_holder/spell/targeted/area_teleport/teleport/santa - name = "Santa Teleport" - - invocation = "HO HO HO" - clothes_req = FALSE - say_destination = FALSE // Santa moves in mysterious ways - -/obj/effect/proc_holder/spell/aoe_turf/conjure/timestop - name = "Stop Time" - desc = "This spell stops time for everyone except for you, allowing you to move freely while your enemies and even projectiles are frozen." - charge_max = 500 - clothes_req = TRUE - invocation = "TOKI WO TOMARE" - invocation_type = INVOCATION_SHOUT - range = 0 - cooldown_min = 100 - summon_amt = 1 - action_icon_state = "time" - - summon_type = list(/obj/effect/timestop/wizard) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/carp - name = "Summon Carp" - desc = "This spell conjures a simple carp." - - school = "conjuration" - charge_max = 1200 - clothes_req = TRUE - invocation = "NOUK FHUNMM SACP RISSKA" - invocation_type = INVOCATION_SHOUT - range = 1 - - summon_type = list(/mob/living/simple_animal/hostile/carp) - cast_sound = 'sound/magic/summon_karp.ogg' - - -/obj/effect/proc_holder/spell/aoe_turf/conjure/construct - name = "Artificer" - desc = "This spell conjures a construct which may be controlled by Shades." - - school = "conjuration" - charge_max = 600 - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - range = 0 - - summon_type = list(/obj/structure/constructshell) - - action_icon_state = "artificer" - cast_sound = 'sound/magic/summonitems_generic.ogg' - - -/obj/effect/proc_holder/spell/aoe_turf/conjure/creature - name = "Summon Creature Swarm" - desc = "This spell tears the fabric of reality, allowing horrific daemons to spill forth." - - school = "conjuration" - charge_max = 1200 - clothes_req = FALSE - invocation = "IA IA" - invocation_type = INVOCATION_SHOUT - summon_amt = 10 - range = 3 - - summon_type = list(/mob/living/simple_animal/hostile/netherworld) - cast_sound = 'sound/magic/summonitems_generic.ogg' - -/obj/effect/proc_holder/spell/aoe_turf/conjure/creature/cult - name = "Summon Creatures (DANGEROUS)" - clothes_req = TRUE - charge_max = 5000 - summon_amt = 2 - -/obj/effect/proc_holder/spell/aoe_turf/conjure/creature/bee - name = "Lesser summon bees" - desc = "This spell magically kicks a transdimensional beehive, instantly summoning a swarm of bees to your location. These bees are NOT friendly to anyone." - charge_max = 600 - clothes_req = TRUE - invocation = "NOT THE BEES" - summon_amt = 9 - action_icon_state = "bee" - cooldown_min = 20 SECONDS - - summon_type = /mob/living/simple_animal/hostile/poison/bees/toxin - cast_sound = 'sound/voice/moth/scream_moth.ogg' - -/obj/effect/proc_holder/spell/aoe_turf/repulse - name = "Repulse" - desc = "This spell throws everything around the user away." - charge_max = 400 - clothes_req = TRUE - invocation = "GITTAH WEIGH" - invocation_type = INVOCATION_SHOUT - range = 5 - cooldown_min = 150 - selection_type = "view" - sound = 'sound/magic/repulse.ogg' - var/maxthrow = 5 - var/sparkle_path = /obj/effect/temp_visual/gravpush - var/anti_magic_check = TRUE - var/repulse_force = MOVE_FORCE_EXTREMELY_STRONG - - action_icon_state = "repulse" - -/obj/effect/proc_holder/spell/aoe_turf/repulse/cast(list/targets,mob/user = usr, var/stun_amt = 40) - var/list/thrownatoms = list() - var/atom/throwtarget - var/distfromcaster - playMagSound() - for(var/turf/T in targets) //Done this way so things don't get thrown all around hilariously. - for(var/atom/movable/AM in T) - thrownatoms += AM - - stun_amt += 10 * spell_level - maxthrow = 5 + spell_level - - for(var/am in thrownatoms) - var/atom/movable/AM = am - if(AM == user || AM.anchored) - continue - - if(ismob(AM)) - var/mob/M = AM - if(M.anti_magic_check(anti_magic_check, FALSE)) - continue - - throwtarget = get_edge_target_turf(user, get_dir(user, get_step_away(AM, user))) - distfromcaster = get_dist(user, AM) - if(distfromcaster == 0) - if(isliving(AM)) - var/mob/living/M = AM - M.Paralyze(100) - M.adjustBruteLoss(5) - to_chat(M, "You're slammed into the floor by [user]!") - else - new sparkle_path(get_turf(AM), get_dir(user, AM)) //created sparkles will disappear on their own - if(isliving(AM)) - var/mob/living/M = AM - M.Paralyze(stun_amt) - to_chat(M, "You're thrown back by [user]!") - AM.safe_throw_at(throwtarget, ((clamp((maxthrow - (clamp(distfromcaster - 2, 0, distfromcaster))), 3, maxthrow))), 1,user, force = repulse_force)//So stuff gets tossed around at the same time. - -/obj/effect/proc_holder/spell/aoe_turf/repulse/xeno //i fixed conflicts only to find out that this is in the WIZARD file instead of the xeno file?! - name = "Tail Sweep" - desc = "Throw back attackers with a sweep of your tail." - sound = 'sound/magic/tail_swing.ogg' - charge_max = 150 - clothes_req = FALSE - antimagic_allowed = TRUE - range = 2 - cooldown_min = 150 - invocation_type = INVOCATION_NONE - sparkle_path = /obj/effect/temp_visual/dir_setting/tailsweep - action_icon = 'icons/mob/actions/actions_xeno.dmi' - action_icon_state = "tailsweep" - action_background_icon_state = "bg_alien" - anti_magic_check = FALSE - -/obj/effect/proc_holder/spell/aoe_turf/repulse/xeno/cast(list/targets,mob/user = usr) - if(iscarbon(user)) - var/mob/living/carbon/C = user - playsound(C.loc, 'sound/voice/hiss5.ogg', 80, 1, 1) - C.spin(6,1) - ..(targets, user, 60) - -/obj/effect/proc_holder/spell/targeted/sacred_flame - name = "Sacred Flame" - desc = "Makes everyone around you more flammable, and lights yourself on fire." - charge_max = 60 - clothes_req = FALSE - invocation = "FI'RAN DADISKO" - invocation_type = INVOCATION_SHOUT - max_targets = 0 - range = 6 - include_user = TRUE - selection_type = "view" - action_icon_state = "sacredflame" - sound = 'sound/magic/fireball.ogg' - -/obj/effect/proc_holder/spell/targeted/sacred_flame/cast(list/targets, mob/user = usr) - for(var/mob/living/L in targets) - if(L.anti_magic_check(TRUE, TRUE)) - continue - L.adjust_fire_stacks(20) - if(isliving(user)) - var/mob/living/U = user - if(!U.anti_magic_check(TRUE, TRUE)) - U.IgniteMob() - -/obj/effect/proc_holder/spell/targeted/conjure_item/spellpacket - name = "Thrown Lightning" - desc = "Forged from eldrich energies, a packet of pure power, known as a spell packet will appear in your hand, that when thrown will stun the target." - clothes_req = TRUE - item_type = /obj/item/spellpacket/lightningbolt - charge_max = 10 - action_icon_state = "thrownlightning" - -/obj/effect/proc_holder/spell/targeted/conjure_item/spellpacket/cast(list/targets, mob/user = usr) - ..() - for(var/mob/living/carbon/C in targets) - C.throw_mode_on(THROW_MODE_TOGGLE) - -/obj/item/spellpacket/lightningbolt - name = "\improper Lightning bolt Spell Packet" - desc = "Some birdseed wrapped in cloth that crackles with electricity." - icon = 'icons/obj/toy.dmi' - icon_state = "snappop" - w_class = WEIGHT_CLASS_TINY - -/obj/item/spellpacket/lightningbolt/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) - if(!..()) - if(isliving(hit_atom)) - var/mob/living/M = hit_atom - if(!M.anti_magic_check()) - M.electrocute_act(80, src, flags = SHOCK_ILLUSION) - qdel(src) - -/obj/item/spellpacket/lightningbolt/throw_at(atom/target, range, speed, mob/thrower, spin=TRUE, diagonals_first = FALSE, datum/callback/callback, force = INFINITY, quickstart = TRUE) - . = ..() - if(ishuman(thrower)) - var/mob/living/carbon/human/H = thrower - H.say("LIGHTNINGBOLT!!", forced = "spell") diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 89679c50c9ae9..654ab483fb241 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -112,10 +112,15 @@ #include "icon_smoothing_unit_test.dm" #include "merge_type.dm" #include "metabolizing.dm" +#include "mindbound_actions.dm" #include "missing_icons.dm" #include "ntnetwork_tests.dm" #include "preference_species.dm" #include "projectiles.dm" +#include "spell_invocations.dm" +#include "spell_mindswap.dm" +#include "spell_names.dm" +#include "spell_shapeshift.dm" #include "stat_mc.dm" #include "subsystem_init.dm" #include "subsystem_metric_sanity.dm" @@ -127,6 +132,7 @@ #include "unit_test.dm" #include "random_ruin_mapsize.dm" #include "walls_have_sheets.dm" +#include "wizard_loadout.dm" #include "worn_icons.dm" #ifdef REFERENCE_TRACKING_DEBUG //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter diff --git a/code/modules/unit_tests/mindbound_actions.dm b/code/modules/unit_tests/mindbound_actions.dm new file mode 100644 index 0000000000000..b404124144091 --- /dev/null +++ b/code/modules/unit_tests/mindbound_actions.dm @@ -0,0 +1,30 @@ +/** + * Tests that actions assigned to a mob's mind + * are successfuly transferred when their mind is transferred to a new mob. + */ +/datum/unit_test/actions_moved_on_mind_transfer + +/datum/unit_test/actions_moved_on_mind_transfer/Run() + + var/mob/living/carbon/human/wizard = allocate(/mob/living/carbon/human) + var/mob/living/simple_animal/pet/dog/corgi/wizard_dog = allocate(/mob/living/simple_animal/pet/dog/corgi) + wizard.mind_initialize() + + var/datum/action/cooldown/spell/pointed/projectile/fireball/fireball = new(wizard.mind) + fireball.Grant(wizard) + var/datum/action/cooldown/spell/aoe/magic_missile/missile = new(wizard.mind) + missile.Grant(wizard) + var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/jaunt = new(wizard.mind) + jaunt.Grant(wizard) + + var/datum/mind/wizard_mind = wizard.mind + wizard_mind.transfer_to(wizard_dog) + + TEST_ASSERT_EQUAL(wizard_dog.mind, wizard_mind, "Mind transfer failed to occur, which invalidates the test.") + + for(var/datum/action/cooldown/spell/remaining_spell in wizard.actions) + Fail("Spell: [remaining_spell] failed to transfer minds when a mind transfer occured.") + + qdel(fireball) + qdel(missile) + qdel(jaunt) diff --git a/code/modules/unit_tests/spell_invocations.dm b/code/modules/unit_tests/spell_invocations.dm new file mode 100644 index 0000000000000..f72e5277eacfd --- /dev/null +++ b/code/modules/unit_tests/spell_invocations.dm @@ -0,0 +1,26 @@ +/** + * Validates that all spells have a correct + * invocation type and invocation setup. + */ +/datum/unit_test/spell_invocations + +/datum/unit_test/spell_invocations/Run() + + var/list/types_to_test = subtypesof(/datum/action/cooldown/spell) + + for(var/datum/action/cooldown/spell/spell_type as anything in types_to_test) + var/spell_name = initial(spell_type.name) + var/invoke_type = initial(spell_type.invocation_type) + switch(invoke_type) + if(INVOCATION_EMOTE) + if(isnull(initial(spell_type.invocation_self_message))) + Fail("Spell: [spell_name] ([spell_type]) set emote invocation type but did not set a self message.") + if(isnull(initial(spell_type.invocation))) + Fail("Spell: [spell_name] ([spell_type]) set emote invocation type but did not set an invocation message.") + + if(INVOCATION_SHOUT, INVOCATION_WHISPER) + if(isnull(initial(spell_type.invocation))) + Fail("Spell: [spell_name] ([spell_type]) set a speaking invocation type but did not set an invocation message.") + + // INVOCATION_NONE: + // It doesn't matter what they have set for invocation text. So not it's skipped. diff --git a/code/modules/unit_tests/spell_mindswap.dm b/code/modules/unit_tests/spell_mindswap.dm new file mode 100644 index 0000000000000..0f7d63440cf06 --- /dev/null +++ b/code/modules/unit_tests/spell_mindswap.dm @@ -0,0 +1,41 @@ +/** + * Validates that the mind swap spell + * properly transfers minds between a caster and a target. + * + * Also checks that the mindswap spell itself was transferred over + * to the new body on cast. + */ +/datum/unit_test/mind_swap_spell + +/datum/unit_test/mind_swap_spell/Run() + + var/mob/living/carbon/human/swapper = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/to_swap = allocate(/mob/living/carbon/human) + + swapper.forceMove(run_loc_floor_bottom_left) + to_swap.forceMove(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) + + swapper.mind_initialize() + to_swap.mind_initialize() + + var/datum/mind/swapper_mind = swapper.mind + var/datum/mind/to_swap_mind = to_swap.mind + + var/datum/action/cooldown/spell/pointed/mind_transfer/mind_swap = new(swapper.mind) + mind_swap.target_requires_key = FALSE + mind_swap.Grant(swapper) + + // Perform a cast from the very base - mimics a click + var/result = mind_swap.InterceptClickOn(swapper, null, to_swap) + TEST_ASSERT(result, "[mind_swap] spell: Mind swap returned \"false\" from InterceptClickOn / cast, despite having valid conditions.") + + TEST_ASSERT_EQUAL(swapper.mind, to_swap_mind, "[mind_swap] spell: Despite returning \"true\" on cast, swap failed to relocate the minds of the caster and the target.") + TEST_ASSERT_EQUAL(to_swap.mind, swapper_mind, "[mind_swap] spell: Despite returning \"true\" on cast, swap failed to relocate the minds of the target and the caster.") + + var/datum/action/cooldown/spell/pointed/mind_transfer/should_be_null = locate() in swapper.actions + var/datum/action/cooldown/spell/pointed/mind_transfer/should_not_be_null = locate() in to_swap.actions + + TEST_ASSERT(!isnull(should_not_be_null), "[mind_swap] spell: The spell was not transferred to the caster's new body, despite successful mind reolcation.") + TEST_ASSERT(isnull(should_be_null), "[mind_swap] spell: The spell remained on the caster's original body, despite successful mind relocation.") + + qdel(mind_swap) diff --git a/code/modules/unit_tests/spell_names.dm b/code/modules/unit_tests/spell_names.dm new file mode 100644 index 0000000000000..df8a42ae3da44 --- /dev/null +++ b/code/modules/unit_tests/spell_names.dm @@ -0,0 +1,32 @@ +/** + * Validates that all spells have a different name. + * + * Spell names are used for debugging in some places + * as well as an option for admins giving out spells, + * so every spell should have a distinct name. + * + * If you're making a subtype with only one or two big changes, + * consider adding an adjective to the name. + * + * "Lesser Fireball" for a subtype of Fireball with a shorter cooldown. + * "Deadly Magic Missile" for a subtype of Magic Missile that does damage, etc. + */ +/datum/unit_test/spell_names + +/datum/unit_test/spell_names/Run() + + var/list/types_to_test = typesof(/datum/action/cooldown/spell) + + var/list/existing_names = list() + for(var/datum/action/cooldown/spell/spell_type as anything in types_to_test) + var/spell_name = initial(spell_type.name) + if(spell_name == "Spell") + continue + + if(spell_name in existing_names) + Fail("Spell: [spell_name] ([spell_type]) had a name identical to another spell. \ + This can cause confusion for admins giving out spells, and while debugging. \ + Consider giving the name an adjective if it's a subtype. (\"Greater\", \"Lesser\", \"Deadly\".)") + continue + + existing_names += spell_name diff --git a/code/modules/unit_tests/spell_shapeshift.dm b/code/modules/unit_tests/spell_shapeshift.dm new file mode 100644 index 0000000000000..0aebbd7e49290 --- /dev/null +++ b/code/modules/unit_tests/spell_shapeshift.dm @@ -0,0 +1,20 @@ +/** + * Validates that all shapeshift type spells + * have a valid possible_shapes setup. + */ +/datum/unit_test/shapeshift_spell_validity + +/datum/unit_test/shapeshift_spell_validity/Run() + + var/list/types_to_test = subtypesof(/datum/action/cooldown/spell/shapeshift) + + for(var/spell_type in types_to_test) + var/datum/action/cooldown/spell/shapeshift/shift = new spell_type() + if(!LAZYLEN(shift.possible_shapes)) + Fail("Shapeshift spell: [shift] ([spell_type]) did not have any possible shapeshift options.") + + for(var/shift_type in shift.possible_shapes) + if(!ispath(shift_type, /mob/living)) + Fail("Shapeshift spell: [shift] had an invalid / non-living shift type ([shift_type]) in their possible shapes list.") + + qdel(shift) diff --git a/code/modules/unit_tests/wizard_loadout.dm b/code/modules/unit_tests/wizard_loadout.dm new file mode 100644 index 0000000000000..862062846e3da --- /dev/null +++ b/code/modules/unit_tests/wizard_loadout.dm @@ -0,0 +1,14 @@ +// Once upon a time, a Game Master decided to upgrade the wizard's spellbook to tgui. +// In doing so, he introduced an infinite loop that crashed many servers and made many wizards sad. +// May this never happen again. + +/// Test loadouts for crashes, runtimes, stack traces and infinite loops. No ASSERTs necessary. +/datum/unit_test/wizard_loadout + +/datum/unit_test/wizard_loadout/Run() + for(var/loadout in ALL_WIZARD_LOADOUTS) + var/obj/item/spellbook/wizard_book = allocate(/obj/item/spellbook) + var/mob/living/carbon/human/wizard = allocate(/mob/living/carbon/human) + wizard.mind_initialize() + wizard.put_in_active_hand(wizard_book, forced = TRUE) +wizard_book.wizard_loadout(wizard, loadout) diff --git a/tgui/packages/tgui/interfaces/Spellbook.js b/tgui/packages/tgui/interfaces/Spellbook.js new file mode 100644 index 0000000000000..0c84d63c8fbe6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Spellbook.js @@ -0,0 +1,581 @@ +import { multiline } from 'common/string'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Dimmer, Divider, Icon, NoticeBox, ProgressBar, Section, Stack } from '../components'; +import { Window } from '../layouts'; +const TAB2NAME = [ + { + title: 'Enscribed Name', + blurb: + "This book answers only to its owner, and of course, must have one. The permanence of the pact between a spellbook and its owner ensures such a powerful artifact cannot fall into enemy hands, or be used in ways that break the Federation's rules such as bartering spells.", + component: () => EnscribedName, + noScrollable: 2, + }, + { + title: 'Table of Contents', + blurb: null, + component: () => TableOfContents, + }, + { + title: 'Offensive', + blurb: 'Spells and items geared towards debilitating and destroying.', + }, + { + title: 'Defensive', + blurb: "Spells and items geared towards improving your survivability or reducing foes' ability to attack.", + }, + { + title: 'Mobility', + blurb: 'Spells and items geared towards improving your ability to move. It is a good idea to take at least one.', + }, + { + title: 'Assistance', + blurb: + 'Spells and items geared towards bringing in outside forces to aid you or improving upon your other items and abilities.', + }, + { + title: 'Challenges', + blurb: + 'The Wizard Federation is looking for shows of power. Arming the station against you will increase the danger, but will grant you more charges for your spellbook.', + locked: true, + noScrollable: 1, + }, + { + title: 'Rituals', + blurb: 'These powerful spells change the very fabric of reality. Not always in your favour.', + }, + { + title: 'Loadouts', + blurb: + 'The Wizard Federation accepts that sometimes, choosing is hard. You can choose from some approved wizard loadouts here.', + component: () => Loadouts, + noScrollable: 2, + }, + { + title: 'Randomize', + blurb: "If you didn't like the loadouts offered, you can embrace chaos. Not recommended for newer wizards.", + component: () => Randomize, + }, +]; +const BUYWORD2ICON = { + Learn: 'plus', + Summon: 'hat-wizard', + Cast: 'meteor', +}; +const EnscribedName = (props, context) => { + const { act, data } = useBackend(context); + const { owner } = data; + return ( + <> + + {owner} + + + + ); +}; +const lineHeightToc = '34.6px'; +const TableOfContents = (props, context) => { + const { act, data } = useBackend(context); + const [tabIndex, setTabIndex] = useLocalState(context, 'tab-index', 1); + return ( + +