diff --git a/_maps/RandomZLevels/blackmesa.dmm b/_maps/RandomZLevels/blackmesa.dmm index 2f32a7222da..dd20648700c 100644 --- a/_maps/RandomZLevels/blackmesa.dmm +++ b/_maps/RandomZLevels/blackmesa.dmm @@ -1,6 +1,6 @@ //MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE "aac" = ( -/obj/item/book/granter/spell/summonitem{ +/obj/item/book/granter/action/spell/summonitem{ name = "\proper an extremely flamboyant book" }, /turf/open/misc/xen, @@ -5937,7 +5937,6 @@ /turf/open/floor/iron/smooth, /area/awaymission/black_mesa/black_ops_armory) "exc" = ( -/obj/item/book/granter/traitsr/ventcrawl_book, /obj/effect/spawner/random/bioluminescent_plant, /turf/open/misc/xen, /area/awaymission/black_mesa/xen/acid_lake) diff --git a/_maps/RandomZLevels/caves.dmm b/_maps/RandomZLevels/caves.dmm index afa002f67e9..ca38176f087 100644 --- a/_maps/RandomZLevels/caves.dmm +++ b/_maps/RandomZLevels/caves.dmm @@ -128,7 +128,7 @@ amount = 25 }, /obj/item/coin/antagtoken, -/obj/item/book/granter/spell/summonitem{ +/obj/item/book/granter/action/spell/summonitem{ name = "\proper an extremely flamboyant book" }, /turf/open/floor/engine/cult{ diff --git a/_maps/RandomZLevels/research2.dmm b/_maps/RandomZLevels/research2.dmm index d27926dab6e..fb52b2ae70e 100644 --- a/_maps/RandomZLevels/research2.dmm +++ b/_maps/RandomZLevels/research2.dmm @@ -4394,7 +4394,7 @@ /area/space/nearstation) "nb" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/random, +/obj/item/book/granter/action/spell/random, /turf/open/floor/mineral/plasma, /area/space/nearstation) "nc" = ( diff --git a/_maps/map_files/Blueshift/BlueShift_middle.dmm b/_maps/map_files/Blueshift/BlueShift_middle.dmm index f53cafcb61e..5e94766ac86 100644 --- a/_maps/map_files/Blueshift/BlueShift_middle.dmm +++ b/_maps/map_files/Blueshift/BlueShift_middle.dmm @@ -31834,7 +31834,7 @@ /area/station/maintenance/starboard/fore) "kiJ" = ( /obj/structure/table/wood/fancy, -/obj/item/book/granter/spell/smoke/lesser, +/obj/item/book/granter/action/spell/smoke/lesser, /obj/item/nullrod, /obj/item/organ/internal/heart, /obj/item/reagent_containers/food/drinks/bottle/holywater, diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm index 2f4aec3da52..8ebd155d45a 100644 --- a/_maps/map_files/Deltastation/DeltaStation2.dmm +++ b/_maps/map_files/Deltastation/DeltaStation2.dmm @@ -3099,7 +3099,7 @@ /area/station/hallway/secondary/construction) "aPH" = ( /obj/structure/table/wood/fancy, -/obj/item/book/granter/spell/smoke/lesser, +/obj/item/book/granter/action/spell/smoke/lesser, /obj/item/nullrod, /obj/item/organ/internal/heart, /obj/item/reagent_containers/food/drinks/bottle/holywater, diff --git a/_maps/map_files/Deltastation/DeltaStation2_skyrat.dmm b/_maps/map_files/Deltastation/DeltaStation2_skyrat.dmm index 8b72af45968..94becc4d912 100644 --- a/_maps/map_files/Deltastation/DeltaStation2_skyrat.dmm +++ b/_maps/map_files/Deltastation/DeltaStation2_skyrat.dmm @@ -3179,7 +3179,7 @@ /area/station/hallway/secondary/construction) "aPH" = ( /obj/structure/table/wood/fancy, -/obj/item/book/granter/spell/smoke/lesser, +/obj/item/book/granter/action/spell/smoke/lesser, /obj/item/nullrod, /obj/item/organ/internal/heart, /obj/item/reagent_containers/food/drinks/bottle/holywater, diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm index 05052c41dec..c77a9bab4d1 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm @@ -30833,7 +30833,7 @@ /area/station/hallway/secondary/service) "jFF" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/smoke/lesser{ +/obj/item/book/granter/action/spell/smoke/lesser{ name = "mysterious old book of cloud-chasing" }, /obj/item/reagent_containers/food/drinks/bottle/holywater{ diff --git a/_maps/map_files/IceBoxStation/IceBoxStation_skyrat.dmm b/_maps/map_files/IceBoxStation/IceBoxStation_skyrat.dmm index 2147471864d..0152045e9b9 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation_skyrat.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation_skyrat.dmm @@ -31055,7 +31055,7 @@ /area/station/hallway/secondary/service) "jFF" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/smoke/lesser{ +/obj/item/book/granter/action/spell/smoke/lesser{ name = "mysterious old book of cloud-chasing" }, /obj/item/reagent_containers/food/drinks/bottle/holywater{ diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm index 7c7cc96ed05..9bb34bd513d 100644 --- a/_maps/map_files/KiloStation/KiloStation.dmm +++ b/_maps/map_files/KiloStation/KiloStation.dmm @@ -81030,7 +81030,7 @@ /turf/open/floor/plating, /area/station/cargo/warehouse) "wTM" = ( -/obj/item/book/granter/spell/smoke/lesser, +/obj/item/book/granter/action/spell/smoke/lesser, /obj/structure/table/wood, /turf/open/floor/cult, /area/station/service/chapel/office) diff --git a/_maps/map_files/KiloStation/KiloStation_skyrat.dmm b/_maps/map_files/KiloStation/KiloStation_skyrat.dmm index 6db074bd3dc..144ed4155f2 100644 --- a/_maps/map_files/KiloStation/KiloStation_skyrat.dmm +++ b/_maps/map_files/KiloStation/KiloStation_skyrat.dmm @@ -81118,7 +81118,7 @@ /turf/open/floor/plating, /area/station/cargo/warehouse) "wTM" = ( -/obj/item/book/granter/spell/smoke/lesser, +/obj/item/book/granter/action/spell/smoke/lesser, /obj/structure/table/wood, /turf/open/floor/cult, /area/station/service/chapel/office) diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index bf045c4ab9d..79def30ad42 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -36460,7 +36460,7 @@ /area/station/medical/chemistry) "mXO" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/smoke/lesser{ +/obj/item/book/granter/action/spell/smoke/lesser{ name = "mysterious old book of cloud-chasing" }, /obj/item/reagent_containers/food/drinks/bottle/holywater{ diff --git a/_maps/map_files/MetaStation/MetaStation_skyrat.dmm b/_maps/map_files/MetaStation/MetaStation_skyrat.dmm index 38ba4c231e4..56745afcf93 100644 --- a/_maps/map_files/MetaStation/MetaStation_skyrat.dmm +++ b/_maps/map_files/MetaStation/MetaStation_skyrat.dmm @@ -37089,7 +37089,7 @@ /area/station/medical/chemistry) "mXO" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/smoke/lesser{ +/obj/item/book/granter/action/spell/smoke/lesser{ name = "mysterious old book of cloud-chasing" }, /obj/item/reagent_containers/food/drinks/bottle/holywater{ diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm index d94011acdf7..55363272375 100644 --- a/_maps/map_files/tramstation/tramstation.dmm +++ b/_maps/map_files/tramstation/tramstation.dmm @@ -51765,7 +51765,7 @@ /area/station/service/theater) "ruW" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/smoke/lesser{ +/obj/item/book/granter/action/spell/smoke/lesser{ name = "mysterious old book of cloud-chasing" }, /obj/item/soulstone/anybody/chaplain, diff --git a/_maps/map_files/tramstation/tramstation_skyrat.dmm b/_maps/map_files/tramstation/tramstation_skyrat.dmm index ab6bef18356..1d45046afe5 100644 --- a/_maps/map_files/tramstation/tramstation_skyrat.dmm +++ b/_maps/map_files/tramstation/tramstation_skyrat.dmm @@ -52616,7 +52616,7 @@ /area/station/service/theater) "ruW" = ( /obj/structure/table/wood, -/obj/item/book/granter/spell/smoke/lesser{ +/obj/item/book/granter/action/spell/smoke/lesser{ name = "mysterious old book of cloud-chasing" }, /obj/item/soulstone/anybody/chaplain, diff --git a/code/__DEFINES/actions.dm b/code/__DEFINES/actions.dm index b4ee3d76e2c..ca206810699 100644 --- a/code/__DEFINES/actions.dm +++ b/code/__DEFINES/actions.dm @@ -9,3 +9,12 @@ ///Action button triggered with right click #define TRIGGER_SECONDARY_ACTION (1<<0) + +// 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" diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 6a50ffababc..418832f1556 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -112,6 +112,10 @@ WIZARD_LOADOUT_SOULTAP, \ ) +/// Used in logging spells for roundend results +#define LOG_SPELL_TYPE "type" +#define LOG_SPELL_AMOUNT "amount" + ///File to the traitor flavor #define TRAITOR_FLAVOR_FILE "antagonist_flavor/traitor_flavor.json" diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm index a30fb460e79..ee98b5a4eb9 100644 --- a/code/__DEFINES/dcs/signals/signals_action.dm +++ b/code/__DEFINES/dcs/signals/signals_action.dm @@ -1,5 +1,35 @@ -// /datum/action signals +// 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_heretic.dm b/code/__DEFINES/dcs/signals/signals_heretic.dm index 3a5f68fb948..a3be544fec6 100644 --- a/code/__DEFINES/dcs/signals/signals_heretic.dm +++ b/code/__DEFINES/dcs/signals/signals_heretic.dm @@ -5,12 +5,12 @@ /// 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 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 a charge after use. - #define COMPONENT_USE_CHARGE (1<<0) + /// 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" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_abilities.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_abilities.dm deleted file mode 100644 index e7e1a0bf7a7..00000000000 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_abilities.dm +++ /dev/null @@ -1,18 +0,0 @@ -// Mob ability signals - -/// from base of /datum/action/cooldown/proc/PreActivate(): (datum/action/cooldown/activated) -#define COMSIG_ABILITY_STARTED "mob_ability_base_started" - #define COMPONENT_BLOCK_ABILITY_START (1<<0) -/// from base of /datum/action/cooldown/proc/PreActivate(): (datum/action/cooldown/finished) -#define COMSIG_ABILITY_FINISHED "mob_ability_base_finished" - -/// 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_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm index ff46eb8131f..1b92c35488c 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm @@ -43,8 +43,6 @@ ///from base of element/bane/activate(): (item/weapon, mob/user) #define COMSIG_LIVING_BANED "living_baned" -///Sent when bloodcrawl ends in mob/living/phasein(): (phasein_decal) -#define COMSIG_LIVING_AFTERPHASEIN "living_phasein" ///from base of mob/living/death(): (gibbed) #define COMSIG_LIVING_DEATH "living_death" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 548e26c1af1..40c900a87e8 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -231,8 +231,6 @@ ///from [/mob/living/carbon/human/Move]: () #define COMSIG_SHOES_STEP_ACTION "shoes_step_action" -///from base of /obj/item/clothing/suit/space/proc/toggle_spacesuit(): (obj/item/clothing/suit/space/suit) -#define COMSIG_SUIT_SPACE_TOGGLE "suit_space_toggle" // /obj/item/implant signals ///from base of /obj/item/implant/proc/activate(): () @@ -315,34 +313,6 @@ //called in /obj/item/organ/internal/cyberimp/chest/thrusters/proc/toggle() : () #define COMSIG_THRUSTER_DEACTIVATED "jetmodule_deactivated" -// /obj/effect/proc_holder/spell signals - -///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. - #define COMPONENT_CANCEL_SPELL (1<<0) -///called from /obj/effect/proc_holder/spell/perform (src) -#define COMSIG_MOB_CAST_SPELL "mob_cast_spell" - -/// Sent from /obj/effect/proc_holder/spell/targeted/lichdom/cast(), to the item being imbued: (mob/user) -#define COMSIG_ITEM_IMBUE_SOUL "item_imbue_soul" - /// Returns to block this item from being imbued into a phylactery - #define COMPONENT_BLOCK_IMBUE (1 << 0) -/// Sent from /obj/effect/proc_holder/spell/targeted/summonitem/cast(), to the item being marked : () -#define COMSIG_ITEM_MARK_RETRIEVAL "item_mark_retrieval" - /// Returns to block this item from being marked for instant summons - #define COMPONENT_BLOCK_MARK_RETRIEVAL (1<<0) - -/// Sent from /obj/effect/proc_holder/spell/targeted/charge/cast(), to the item in hand being charged: (obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster) -#define COMSIG_ITEM_MAGICALLY_CHARGED "item_magic_charged" - /// Returns if an item was successfuly recharged - #define COMPONENT_ITEM_CHARGED (1 << 0) - /// Returns if the item had a negative side effect occur while recharging - #define COMPONENT_ITEM_BURNT_OUT (1 << 1) - -/// Sent from /obj/effect/proc_holder/spell/aoe_turf/knock/cast(), to every nearby turf: (obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster) -#define COMSIG_ATOM_MAGICALLY_UNLOCKED "atom_magic_unlock" - // /obj/item/camera signals ///from /obj/item/camera/captureimage(): (atom/target, mob/user) diff --git a/code/__DEFINES/dcs/signals/signals_spell.dm b/code/__DEFINES/dcs/signals/signals_spell.dm new file mode 100644 index 00000000000..4d2c7d2993f --- /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 24db52be639..43466bf71b7 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -155,6 +155,9 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list( #define isclown(A) (istype(A, /mob/living/simple_animal/hostile/retaliate/clown)) +#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 f9669db1c3a..d4b10d7aa19 100644 --- a/code/__DEFINES/magic.dm +++ b/code/__DEFINES/magic.dm @@ -1,37 +1,95 @@ -//schools of magic - unused for years and years on end, finally has a use with chaplains getting punished for using "evil" spells +// Magic schools -//use this if your spell isn't actually a spell, it's set by default (and actually, i really suggest if that's the case you should use datum/actions instead - see spider.dm for an example) +/// Unset / default / "not actually magic" school. #define SCHOOL_UNSET "unset" -//GOOD SCHOOLS (allowed by honorbound gods, some of these you can get on station) +// 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" -#define SCHOOL_RESTORATION "restoration" //heal shit +/// Restoration school, which is mostly healing stuff +#define SCHOOL_RESTORATION "restoration" -//NEUTRAL SPELLS (punished by honorbound gods if you get caught using it) -#define SCHOOL_EVOCATION "evocation" //kill or destroy shit, usually out of thin air -#define SCHOOL_TRANSMUTATION "transmutation" //transform shit -#define SCHOOL_TRANSLOCATION "translocation" //movement based -#define SCHOOL_CONJURATION "conjuration" //summoning +// 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) -#define SCHOOL_NECROMANCY "necromancy" //>>>necromancy -#define SCHOOL_FORBIDDEN "forbidden" //>heretic shit and other fucked up magic +// 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" -//invocation types - what does the wizard need to do to invoke (cast) the spell? - -///Allows being able to cast the spell without saying anything. +// Invocation types - what does the wizard need to do to invoke (cast) the spell? +/// Allows being able to cast the spell without saying or doing anything. #define INVOCATION_NONE "none" -///Forces the wizard to shout (and be able to) to cast the spell. +/// Forces the wizard to shout the invocation to cast the spell. #define INVOCATION_SHOUT "shout" -///Forces the wizard to emote (and be able to) to cast the spell. -#define INVOCATION_EMOTE "emote" -///Forces the wizard to whisper (and be able to) to cast the spell. +/// Forces the wizard to whisper the invocation to cast the spell. #define INVOCATION_WHISPER "whisper" +/// Forces the wizard to emote to cast the spell. +#define INVOCATION_EMOTE "emote" +// 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 curses, abductors, jelly people) +/// Tinfoil hat magic resistance that blocks mental magic (telepathy / mind links, mind curses, abductors) #define MAGIC_RESISTANCE_MIND (1<<1) -/// Holy magic resistance that blocks unholy magic (revenant, cult, vampire, voice of god) +/// Holy magic resistance that blocks unholy magic (revenant, vampire, voice of god) #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/traits.dm b/code/__DEFINES/traits.dm index 800686a6222..f8eaac530d8 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -300,7 +300,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_GUNFLIP "gunflip" /// Increases chance of getting special traumas, makes them harder to cure #define TRAIT_SPECIAL_TRAUMA_BOOST "special_trauma_boost" -#define TRAIT_BLOODCRAWL_EAT "bloodcrawl_eat" #define TRAIT_SPACEWALK "spacewalk" /// Gets double arcade prizes #define TRAIT_GAMERGOD "gamer-god" @@ -322,6 +321,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_MARTIAL_ARTS_IMMUNE "martial_arts_immune" /// You've been cursed with a living duffelbag, and can't have more added #define TRAIT_DUFFEL_CURSE_PROOF "duffel_cursed" +/// Immune to being afflicted by time stop (spell) +#define TRAIT_TIME_STOP_IMMUNE "time_stop_immune" /// Revenants draining you only get a very small benefit. #define TRAIT_WEAK_SOUL "weak_soul" /// This mob has no soul @@ -412,6 +413,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Whether or not orbiting is blocked or not #define TRAIT_ORBITING_FORBIDDEN "orbiting_forbidden" +/// 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" /// Item still allows you to examine items while blind and actively held. #define TRAIT_BLIND_TOOL "blind_tool" @@ -443,8 +449,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai // Normally only present in the mind of a Research Director. #define TRAIT_ROD_SUPLEX "rod_suplex" -/// This mob is currently in rod form. -#define TRAIT_ROD_FORM "rod_form" +/// This mob is phased out of reality from magic, either a jaunt or rod form +#define TRAIT_MAGICALLY_PHASED "magically_phased" //SKILLS #define TRAIT_UNDERWATER_BASKETWEAVING_KNOWLEDGE "underwater_basketweaving" @@ -777,7 +783,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// trait associated to not having fine manipulation appendages such as hands #define LACKING_MANIPULATION_APPENDAGES_TRAIT "lacking-manipulation-appengades" #define HANDCUFFED_TRAIT "handcuffed" -/// Trait granted by [/obj/item/warpwhistle] +/// Trait granted by [/obj/item/warp_whistle] #define WARPWHISTLE_TRAIT "warpwhistle" ///Turf trait for when a turf is transparent #define TURF_Z_TRANSPARENT_TRAIT "turf_z_transparent" diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index 3df60080109..bd6d2fac721 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -121,7 +121,6 @@ #define VV_HK_DIRECT_CONTROL "direct_control" #define VV_HK_GIVE_DIRECT_CONTROL "give_direct_control" #define VV_HK_OFFER_GHOSTS "offer_ghosts" -#define VV_HK_SDQL_SPELL "sdql_spell" // /mob/living #define VV_HK_GIVE_SPEECH_IMPEDIMENT "impede_speech" @@ -151,22 +150,4 @@ //outfits #define VV_HK_TO_OUTFIT_EDITOR "outfit_editor" -// /obj/effect/proc_holder/spell -/// Require casting_clothes to cast spell. -#define VV_HK_SPELL_SET_ROBELESS "spell_set_robeless" -/// Require cult armor to cast spell. -#define VV_HK_SPELL_SET_CULT "spell_set_cult" -/// Require the mob to be ishuman() to cast spell. -#define VV_HK_SPELL_SET_HUMANONLY "spell_set_humanonly" -/// Require mob to not be a brain or pAI to cast spell. -#define VV_HK_SPELL_SET_NONABSTRACT "spell_set_nonabstract" -/// Spell can now be cast without casting_clothes. -#define VV_HK_SPELL_UNSET_ROBELESS "spell_unset_robeless" -/// Spell can now be cast without cult armour. -#define VV_HK_SPELL_UNSET_CULT "spell_unset_cult" -/// Any /mob can cast this spell. -#define VV_HK_SPELL_UNSET_HUMANONLY "spell_unset_humanonly" -/// Abstract mobs such as brains or pAIs can cast this spell. -#define VV_HK_SPELL_UNSET_NONABSTRACT "spell_unset_nonabstract" - #define VV_HK_WEAKREF_RESOLVE "weakref_resolve" diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 946b90a8d98..3d2d827a139 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -120,7 +120,6 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_PRIMITIVE" = TRAIT_PRIMITIVE, //unable to use mechs. Given to Ash Walkers "TRAIT_GUNFLIP" = TRAIT_GUNFLIP, "TRAIT_SPECIAL_TRAUMA_BOOST" = TRAIT_SPECIAL_TRAUMA_BOOST, - "TRAIT_BLOODCRAWL_EAT" = TRAIT_BLOODCRAWL_EAT, "TRAIT_SPACEWALK" = TRAIT_SPACEWALK, "TRAIT_GAMERGOD" = TRAIT_GAMERGOD, "TRAIT_GIANT" = TRAIT_GIANT, diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index b01fef62c78..a42c03f7371 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -389,8 +389,6 @@ min_val = 0 integer = FALSE // It is in hours, but just in case one wants to specify minutes. -/datum/config_entry/flag/sdql_spells - /datum/config_entry/flag/native_fov /datum/config_entry/flag/disallow_title_music diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index fbebd597318..22ec21825c9 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -92,10 +92,22 @@ SUBSYSTEM_DEF(statpanels) if(target.mob) var/mob/target_mob = target.mob - if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(target_mob.mob_spell_list) || length(target_mob.mind?.spell_list))) - if(num_fires % default_wait == 0) - set_spells_tab(target, target_mob) + // Handle the action panels of the stat panel + + var/update_actions = FALSE + // We're on a spell tab, update the tab so we can see cooldowns progressing and such + if(target.stat_tab in target.spell_tabs) + update_actions = TRUE + // We're not on a spell tab per se, but we have cooldown actions, and we've yet to + // set up our spell tabs at all + if(!length(target.spell_tabs) && locate(/datum/action/cooldown) in target_mob.actions) + update_actions = TRUE + + if(update_actions && num_fires % default_wait == 0) + set_action_tabs(target, target_mob) + + // Handle the examined turf of the stat panel if(target_mob?.listed_turf && num_fires % default_wait == 0) if(!target_mob.TurfAdjacent(target_mob.listed_turf) || isnull(target_mob.listed_turf)) @@ -167,14 +179,15 @@ SUBSYSTEM_DEF(statpanels) sdql2A += sdql2B target.stat_panel.send_message("update_sdql2", sdql2A) -/datum/controller/subsystem/statpanels/proc/set_spells_tab(client/target, mob/target_mob) - var/list/proc_holders = target_mob.get_proc_holders() +/// Set up the various action tabs. +/datum/controller/subsystem/statpanels/proc/set_action_tabs(client/target, mob/target_mob) + var/list/actions = target_mob.get_actions_for_statpanel() target.spell_tabs.Cut() - for(var/proc_holder_list as anything in proc_holders) - target.spell_tabs |= proc_holder_list[1] + for(var/action_data in actions) + target.spell_tabs |= action_data[1] - target.stat_panel.send_message("update_spells", list(spell_tabs = target.spell_tabs, proc_holders_encoded = proc_holders)) + target.stat_panel.send_message("update_spells", list(spell_tabs = target.spell_tabs, actions = actions)) /datum/controller/subsystem/statpanels/proc/set_turf_examine_tab(client/target, mob/target_mob) var/list/overrides = list() @@ -240,10 +253,22 @@ SUBSYSTEM_DEF(statpanels) return TRUE var/mob/target_mob = target.mob - if((target.stat_tab in target.spell_tabs) || !length(target.spell_tabs) && (length(target_mob.mob_spell_list) || length(target_mob.mind?.spell_list))) - set_spells_tab(target, target_mob) + + // Handle actions + + var/update_actions = FALSE + if(target.stat_tab in target.spell_tabs) + update_actions = TRUE + + if(!length(target.spell_tabs) && locate(/datum/action/cooldown) in target_mob.actions) + update_actions = TRUE + + if(update_actions) + set_action_tabs(target, target_mob) return TRUE + // Handle turfs + if(target_mob?.listed_turf) if(!target_mob.TurfAdjacent(target_mob.listed_turf)) target.stat_panel.send_message("removed_listedturf") diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm new file mode 100644 index 00000000000..c20b4dbe943 --- /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 00000000000..f0a43fab105 --- /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 00000000000..933ed0561e4 --- /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 00000000000..9d93ef9e81a --- /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 00000000000..70d49662219 --- /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/beserk.dm b/code/datums/actions/items/beserk.dm new file mode 100644 index 00000000000..9f8519906a0 --- /dev/null +++ b/code/datums/actions/items/beserk.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 00000000000..5768a79db63 --- /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 00000000000..6c572548cca --- /dev/null +++ b/code/datums/actions/items/cult_dagger.dm @@ -0,0 +1,38 @@ + +/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 00000000000..24fddb52942 --- /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/items/organ_action.dm b/code/datums/actions/items/organ_action.dm new file mode 100644 index 00000000000..19a8f700373 --- /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 00000000000..69262c108a7 --- /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 00000000000..b8aa7c98907 --- /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 00000000000..c825c72dc51 --- /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 00000000000..1af240a6b02 --- /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 00000000000..943da403e7a --- /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 00000000000..bcfcb5437a2 --- /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 00000000000..46ffd26e499 --- /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/split_personality.dm b/code/datums/brain_damage/split_personality.dm index a5dd9101463..588963437cd 100644 --- a/code/datums/brain_damage/split_personality.dm +++ b/code/datums/brain_damage/split_personality.dm @@ -23,12 +23,12 @@ /datum/brain_trauma/severe/split_personality/proc/make_backseats() stranger_backseat = new(owner, src) - var/obj/effect/proc_holder/spell/targeted/personality_commune/stranger_spell = new(src) - stranger_backseat.AddSpell(stranger_spell) + var/datum/action/cooldown/spell/personality_commune/stranger_spell = new(src) + stranger_spell.Grant(stranger_backseat) owner_backseat = new(owner, src) - var/obj/effect/proc_holder/spell/targeted/personality_commune/owner_spell = new(src) - owner_backseat.AddSpell(owner_spell) + var/datum/action/cooldown/spell/personality_commune/owner_spell = new(src) + owner_spell.Grant(owner_backseat) /datum/brain_trauma/severe/split_personality/proc/get_ghost() diff --git a/code/datums/components/anti_magic.dm b/code/datums/components/anti_magic.dm index 7abe5f7027c..a931df70d5d 100644 --- a/code/datums/components/anti_magic.dm +++ b/code/datums/components/anti_magic.dm @@ -74,7 +74,10 @@ if(!casting_restriction_alert) // Check to see if we have any spells that are blocked due to antimagic - for(var/obj/effect/proc_holder/spell/magic_spell in equipper.mind?.spell_list) + for(var/datum/action/cooldown/spell/magic_spell in equipper.actions) + if(!(magic_spell.spell_requirements & SPELL_REQUIRES_NO_ANTIMAGIC)) + continue + if(antimagic_flags & magic_spell.antimagic_flags) to_chat(equipper, span_warning("[parent] is interfering with your ability to cast magic!")) casting_restriction_alert = TRUE diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm index 4dd01422f45..bf22148117d 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/cult/narsie/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/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm index dd6c8f4780a..6ff5217689d 100644 --- a/code/datums/components/riding/riding_mob.dm +++ b/code/datums/components/riding/riding_mob.dm @@ -143,11 +143,8 @@ var/mob/living/ridden_creature = parent - for(var/ability in ridden_creature.abilities) - var/obj/effect/proc_holder/proc_holder = ability - if(!proc_holder.action) - return - proc_holder.action.GiveAction(rider) + for(var/datum/action/action as anything in ridden_creature.actions) + action.GiveAction(rider) /// Takes away the riding parent's abilities from the rider /datum/component/riding/creature/proc/remove_abilities(mob/living/rider) @@ -156,13 +153,11 @@ var/mob/living/ridden_creature = parent - for(var/ability in ridden_creature.abilities) - var/obj/effect/proc_holder/proc_holder = ability - if(!proc_holder.action) - return - if(rider == proc_holder.ranged_ability_user) - proc_holder.remove_ranged_ability() - proc_holder.action.HideFrom(rider) + for(var/datum/action/action as anything in ridden_creature.actions) + if(istype(action, /datum/action/cooldown) && rider.click_intercept == action) + var/datum/action/cooldown/cooldown_action = action + cooldown_action.unset_click_ability(rider, refund_cooldown = TRUE) + action.HideFrom(rider) /datum/component/riding/creature/riding_can_z_move(atom/movable/movable_parent, direction, turf/start, turf/destination, z_move_flags, mob/living/rider) if(!(z_move_flags & ZMOVE_CAN_FLY_CHECKS)) diff --git a/code/datums/components/seclight_attachable.dm b/code/datums/components/seclight_attachable.dm index ec408401a5c..8eb63d8dfe0 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 c0a4c306268..b684ad913a3 100644 --- a/code/datums/components/stationloving.dm +++ b/code/datums/components/stationloving.dm @@ -63,13 +63,13 @@ return COMPONENT_MOVABLE_BLOCK_PRE_MOVE -/datum/component/stationloving/proc/check_soul_imbue() +/datum/component/stationloving/proc/check_soul_imbue(datum/source) SIGNAL_HANDLER if(disallow_soul_imbue) return COMPONENT_BLOCK_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 0c40efa7dde..6d28994fe99 100644 --- a/code/datums/components/storage/storage.dm +++ b/code/datums/components/storage/storage.dm @@ -65,7 +65,7 @@ ///altclick interact var/quickdraw = FALSE - var/datum/action/item_action/storage_gather_mode/modeswitch_action + var/datum/weakref/modeswitch_action_ref //Screen variables: Do not mess with these vars unless you know what you're doing. They're not defines so storage that isn't in the same location can be supported in the future. var/screen_max_columns = 7 //These two determine maximum screen sizes. @@ -176,17 +176,18 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches) /datum/component/storage/proc/update_actions() SIGNAL_HANDLER - 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/action_trigger) - if(I.item_flags & IN_INVENTORY) - 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 0f959d48ebe..af5640b84a5 100644 --- a/code/datums/martial/plasma_fist.dm +++ b/code/datums/martial/plasma_fist.dm @@ -37,8 +37,11 @@ /datum/martial_art/plasma_fist/proc/Tornado(mob/living/A, mob/living/D) A.say("TORNADO SWEEP!", forced="plasma fist") dance_rotate(A, CALLBACK(GLOBAL_PROC, .proc/playsound, A.loc, 'sound/weapons/punch1.ogg', 15, TRUE, -1)) - 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)") return diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 67b3ce3be13..f783aeff48e 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -46,8 +46,6 @@ var/special_role var/list/restricted_roles = list() - var/list/spell_list = list() // Wizard mode & "Give Spell" badmin button. - var/datum/martial_art/martial_art var/static/default_martial_art = new/datum/martial_art var/miming = FALSE // Mime's vow of silence @@ -173,7 +171,6 @@ if(iscarbon(new_character)) var/mob/living/carbon/C = new_character C.last_mind = src - transfer_actions(new_character) transfer_martial_arts(new_character) RegisterSignal(new_character, COMSIG_LIVING_DEATH, .proc/set_death_time) if(active || force_key_move) @@ -773,8 +770,9 @@ uplink_exists = traitor_datum.uplink_ref if(!uplink_exists) uplink_exists = find_syndicate_uplink(check_unlocked = TRUE) - if(!uplink_exists && !(locate(/obj/effect/proc_holder/spell/self/special_equipment_fallback) in spell_list)) - AddSpell(new /obj/effect/proc_holder/spell/self/special_equipment_fallback(null, src)) + if(!uplink_exists && !(locate(/datum/action/special_equipment_fallback) in current.actions)) + var/datum/action/special_equipment_fallback/fallback = new(src) + fallback.Grant(current) /datum/mind/proc/take_uplink() qdel(find_syndicate_uplink()) @@ -806,25 +804,6 @@ add_antag_datum(head) special_role = ROLE_REV_HEAD -/datum/mind/proc/AddSpell(obj/effect/proc_holder/spell/S) - spell_list += S - S.action.Grant(current) - -//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) - current?.client.stat_panel.send_message("check_spells") - -/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 @@ -834,27 +813,6 @@ else martial_art.teach(new_character) -/datum/mind/proc/transfer_actions(mob/living/new_character) - if(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.updateButtons() - INVOKE_ASYNC(S, /obj/effect/proc_holder/spell.proc/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/_mutations.dm b/code/datums/mutations/_mutations.dm index 9e02286d553..9fc81716b51 100644 --- a/code/datums/mutations/_mutations.dm +++ b/code/datums/mutations/_mutations.dm @@ -16,8 +16,8 @@ var/text_lose_indication = "" /// Visual indicators upon the character of the owner of this mutation var/static/list/visual_indicators = list() - /// The proc holder (ew) o - var/obj/effect/proc_holder/spell/power + /// The path of action we grant to our user on mutation gain + var/datum/action/cooldown/spell/power_path /// Which mutation layer to use var/layer_used = MUTATIONS_LAYER /// To restrict mutation to only certain species @@ -118,7 +118,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) addtimer(CALLBACK(src, .proc/modify, 0.5 SECONDS)) //gonna want children calling ..() to run first @@ -142,8 +142,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) /mob/living/carbon/proc/update_mutations_overlay() @@ -168,12 +170,21 @@ overlays_standing[mutation.layer_used] = mut_overlay apply_overlay(mutation.layer_used) -/datum/mutation/human/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/human/proc/modify() + if(modified || !power_path || !owner) return - power.charge_max *= GET_MUTATION_ENERGY(src) - power.charge_counter *= GET_MUTATION_ENERGY(src) - modified = TRUE + var/datum/action/cooldown/spell/modified_power = locate(power_path) in owner.actions + if(!modified_power) + 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/human/proc/copy_mutation(datum/mutation/human/mutation_to_copy) if(!mutation_to_copy) @@ -202,15 +213,16 @@ else qdel(src) -/datum/mutation/human/proc/grant_spell() - if(!ispath(power) || !owner) +/datum/mutation/human/proc/grant_power() + if(!ispath(power_path) || !owner) return FALSE - 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 48e8c41b078..00000000000 --- a/code/datums/mutations/actions.dm +++ /dev/null @@ -1,446 +0,0 @@ -/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 = /obj/effect/proc_holder/spell/targeted/telepathy - instability = 10 - energy_coeff = 1 - - -/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 = /obj/effect/proc_holder/spell/targeted/olfaction - instability = 30 - synchronizer_coeff = 1 - var/reek = 200 - -/datum/mutation/human/olfaction/modify() - if(power) - var/obj/effect/proc_holder/spell/targeted/olfaction/S = power - S.sensitivity = GET_MUTATION_SYNCHRONIZER(src) - -/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 = 100 - 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() - var/sensitivity = 1 - -/obj/effect/proc_holder/spell/targeted/olfaction/cast(list/targets, mob/living/user = usr) - //can we sniff? is there miasma in the air? - var/datum/gas_mixture/air = user.loc.return_air() - var/list/cached_gases = air.gases - - if(cached_gases[/datum/gas/miasma]) - user.adjust_disgust(sensitivity * 45) - to_chat(user, 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 = user.get_active_held_item() - if(sniffed) - var/old_target = tracking_target - possible = list() - var/list/prints = GET_ATOM_FINGERPRINTS(sniffed) - if(prints) - for(var/mob/living/carbon/C in GLOB.carbon_list) - if(prints[md5(C.dna.unique_identity)]) - possible |= C - if(!length(possible)) - to_chat(user,span_warning("Despite your best efforts, there are no scents to be found on [sniffed]...")) - return - tracking_target = tgui_input_list(user, "Scent to remember", "Scent Tracking", sort_names(possible)) - if(isnull(tracking_target)) - if(isnull(old_target)) - to_chat(user,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.")) - return - tracking_target = old_target - on_the_trail(user) - return - to_chat(user,span_notice("You pick up the scent of [tracking_target]. The hunt begins.")) - on_the_trail(user) - return - - if(!tracking_target) - to_chat(user,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.")) - 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,span_warning("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,span_warning("You smell out the trail to yourself. Yep, it's you.")) - return - if(usr.z < tracking_target.z) - to_chat(user,span_warning("The trail leads... way up above you? Huh. They must be really, really far away.")) - return - else if(usr.z > tracking_target.z) - to_chat(user,span_warning("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,span_notice("You consider [tracking_target]'s scent. The trail leads [direction_text].")) - -/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 = /obj/effect/proc_holder/spell/cone/staggered/firebreath - instability = 30 - energy_coeff = 1 - power_coeff = 1 - -/datum/mutation/human/firebreath/modify() - // If we have a power chromosome... - if(power && GET_MUTATION_POWER(src) > 1) - var/obj/effect/proc_holder/spell/cone/staggered/firebreath/our_spell = power - our_spell.cone_levels += 2 // Cone fwooshes further, and... - our_spell.self_throw_range += 1 // the breath throws the user back more - - -/obj/effect/proc_holder/spell/cone/staggered/firebreath - name = "Fire Breath" - desc = "You breathe a cone of fire directly in front of you." - school = SCHOOL_EVOCATION - invocation = "" - invocation_type = INVOCATION_NONE - charge_max = 400 - clothes_req = FALSE - range = 20 - base_icon_state = "fireball" - action_icon_state = "fireball0" - still_recharging_msg = "You can't muster any flames!" - sound = 'sound/magic/demon_dies.ogg' //horrifying lizard noises - respect_density = TRUE - cone_levels = 3 - antimagic_flags = NONE // cannot be restricted or blocked by antimagic - /// The range our user is thrown backwards after casting the spell - var/self_throw_range = 1 - -/obj/effect/proc_holder/spell/cone/staggered/firebreath/before_cast(list/targets) - . = ..() - if(!iscarbon(usr)) - return - - var/mob/living/carbon/our_lizard = usr - 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!")) - -/obj/effect/proc_holder/spell/cone/staggered/firebreath/cast(list/targets, mob/user) - . = ..() - // When casting, throw them backwards a few tiles. - var/original_dir = user.dir - user.throw_at(get_edge_target_turf(user, turn(user.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. - user.setDir(original_dir) - -// Makes the cone shoot out into a 3 wide column of flames. -/obj/effect/proc_holder/spell/cone/staggered/firebreath/calculate_cone_shape(current_level) - return (2 * current_level) - 1 - -/obj/effect/proc_holder/spell/cone/staggered/firebreath/do_turf_cone_effect(turf/target_turf, 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) - -/obj/effect/proc_holder/spell/cone/staggered/firebreath/do_mob_cone_effect(mob/living/target_mob, 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() - -/obj/effect/proc_holder/spell/cone/staggered/firebreath/do_obj_cone_effect(obj/target_obj, 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))) - -/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 = /obj/effect/proc_holder/spell/self/void - energy_coeff = 1 - synchronizer_coeff = 1 - -/datum/mutation/human/void/on_life(delta_time, times_fired) - if(!isturf(owner.loc)) - return - if(DT_PROB((0.25+((100-dna.stability)/40)) * GET_MUTATION_SYNCHRONIZER(src), delta_time)) //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 = SCHOOL_EVOCATION - clothes_req = FALSE - charge_max = 600 - 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 - -/obj/effect/proc_holder/spell/self/void/cast(list/targets, mob/user = usr) - . = ..() - new /obj/effect/immortality_talisman/void(get_turf(user), user) - -/datum/mutation/human/self_amputation - name = "Autotomy" - desc = "Allows a creature to voluntary discard a random appendage." - quality = POSITIVE - text_gain_indication = "Your joints feel loose." - instability = 30 - power = /obj/effect/proc_holder/spell/self/self_amputation - - energy_coeff = 1 - synchronizer_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 = 100 - action_icon_state = "autotomy" - -/obj/effect/proc_holder/spell/self/self_amputation/cast(list/targets, mob/user = usr) - if(!iscarbon(user)) - return - - var/mob/living/carbon/C = user - if(HAS_TRAIT(C, TRAIT_NODISMEMBER)) - return - - var/list/parts = list() - for(var/X in C.bodyparts) - var/obj/item/bodypart/BP = X - if(BP.body_part != HEAD && BP.body_part != CHEST) - if(BP.dismemberable) - parts += BP - if(!length(parts)) - to_chat(usr, span_notice("You can't shed any more limbs!")) - return - - var/obj/item/bodypart/BP = pick(parts) - BP.dismember() - -/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 = "Your feel like you can throw your voice." - instability = 15 - power = /obj/effect/proc_holder/spell/self/tongue_spike - - energy_coeff = 1 - synchronizer_coeff = 1 - -/obj/effect/proc_holder/spell/self/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." - clothes_req = FALSE - human_req = TRUE - charge_max = 100 - action_icon = 'icons/mob/actions/actions_genetic.dmi' - action_icon_state = "spike" - var/spike_path = /obj/item/hardened_spike - -/obj/effect/proc_holder/spell/self/tongue_spike/cast(list/targets, mob/user = usr) - if(!iscarbon(user)) - return - - var/mob/living/carbon/C = user - if(HAS_TRAIT(C, TRAIT_NODISMEMBER)) - return - var/obj/item/organ/internal/tongue/tongue - for(var/org in C.internal_organs) - if(istype(org, /obj/item/organ/internal/tongue)) - tongue = org - break - - if(!tongue) - to_chat(C, span_notice("You don't have a tongue to shoot!")) - return - - tongue.Remove(C, special = TRUE) - var/obj/item/hardened_spike/spike = new spike_path(get_turf(C), C) - tongue.forceMove(spike) - spike.throw_at(get_edge_target_turf(C,C.dir), 14, 4, C) - -/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) - var/mob/living/carbon/human/fired_by - /// if we missed our target - var/missed = TRUE - -/obj/item/hardened_spike/Initialize(mapload, firedby) - . = ..() - fired_by = firedby - addtimer(CALLBACK(src, .proc/checkembedded), 5 SECONDS) - -/obj/item/hardened_spike/proc/checkembedded() - if(missed) - unembedded() - -/obj/item/hardened_spike/embedded(atom/target) - if(isbodypart(target)) - missed = FALSE - -/obj/item/hardened_spike/unembedded() - var/turf/T = get_turf(src) - visible_message(span_warning("[src] cracks and twists, changing shape!")) - for(var/i in contents) - var/obj/o = i - o.forceMove(T) - 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 = "Your feel like you can really connect with people by throwing your voice." - instability = 15 - locked = TRUE - power = /obj/effect/proc_holder/spell/self/tongue_spike/chem - energy_coeff = 1 - synchronizer_coeff = 1 - -/obj/effect/proc_holder/spell/self/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." - action_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 - var/been_places = FALSE - var/datum/action/innate/send_chems/chems - -/obj/item/hardened_spike/chem/embedded(mob/living/carbon/human/embedded_mob) - if(been_places) - return - been_places = TRUE - chems = new - chems.transfered = embedded_mob - chems.spikey = src - to_chat(fired_by, span_notice("Link established! Use the \"Transfer Chemicals\" ability to send your chemicals to the linked target!")) - chems.Grant(fired_by) - -/obj/item/hardened_spike/chem/unembedded() - to_chat(fired_by, span_warning("Link lost!")) - QDEL_NULL(chems) - ..() - -/datum/action/innate/send_chems - icon_icon = 'icons/mob/actions/actions_genetic.dmi' - background_icon_state = "bg_spell" - check_flags = AB_CHECK_CONSCIOUS - button_icon_state = "spikechemswap" - name = "Transfer Chemicals" - desc = "Send all of your reagents into whomever the chem spike is embedded in. One use." - var/obj/item/hardened_spike/chem/spikey - var/mob/living/carbon/human/transfered - -/datum/action/innate/send_chems/Activate() - if(!ishuman(transfered) || !ishuman(owner)) - return - var/mob/living/carbon/human/transferer = owner - - 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/bodypart/L = spikey.checkembedded() - - //this is where it would deal damage, if it transfers chems it removes itself so no damage - spikey.forceMove(get_turf(L)) - transfered.visible_message(span_notice("[spikey] falls out of [transfered]!")) - -//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 = /obj/effect/proc_holder/spell/self/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) - -/obj/effect/proc_holder/spell/self/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." - clothes_req = FALSE - human_req = FALSE - charge_max = 4 SECONDS //the same time to lay a web - action_icon = 'icons/mob/actions/actions_genetic.dmi' - action_icon_state = "lay_web" - -/obj/effect/proc_holder/spell/self/lay_genetic_web/cast(list/targets, mob/user = usr) - var/failed = FALSE - if(!isturf(user.loc)) - to_chat(user, span_warning("You can't lay webs here!")) - failed = TRUE - var/turf/T = get_turf(user) - var/obj/structure/spider/stickyweb/genetic/W = locate() in T - if(W) - to_chat(user, span_warning("There's already a web here!")) - failed = TRUE - if(failed) - revert_cast(user) - return FALSE - - user.visible_message(span_notice("[user] begins to secrete a sticky substance."),span_notice("You begin to lay a web.")) - if(!do_after(user, 4 SECONDS, target = T)) - to_chat(user, span_warning("Your web spinning was interrupted!")) - return - else - new /obj/structure/spider/stickyweb/genetic(T, user) diff --git a/code/datums/mutations/antenna.dm b/code/datums/mutations/antenna.dm index 9b71063a54f..a5b220abde6 100644 --- a/code/datums/mutations/antenna.dm +++ b/code/datums/mutations/antenna.dm @@ -46,62 +46,81 @@ quality = POSITIVE text_gain_indication = "You hear distant voices at the corners of your mind." text_lose_indication = "The distant voices fade." - power = /obj/effect/proc_holder/spell/targeted/mindread + power_path = /datum/action/cooldown/spell/pointed/mindread instability = 40 difficulty = 8 locked = TRUE -/obj/effect/proc_holder/spell/targeted/mindread +/datum/action/cooldown/spell/pointed/mindread name = "Mindread" desc = "Read the target's mind." - charge_max = 50 - range = 7 - clothes_req = FALSE - action_icon_state = "mindread" + button_icon_state = "mindread" + cooldown_time = 5 SECONDS + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE_MIND -/obj/effect/proc_holder/spell/targeted/mindread/cast(list/targets, mob/living/carbon/human/user = usr) - if(!user.can_cast_magic(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 - for(var/mob/living/M in targets) - if(M.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) - to_chat(usr, span_warning("As you reach into [M]'s mind, you are stopped by a mental blockage. It seems you've been foiled.")) - return - if(M.stat == DEAD) - to_chat(user, span_boldnotice("[M] is dead!")) - return - if(M.mind) - to_chat(user, span_boldnotice("You plunge into [M]'s mind...")) - if(prob(20)) - to_chat(M, span_danger("You feel something foreign enter your mind."))//chance to alert the read-ee - var/list/recent_speech = list() - var/list/say_log = list() - var/log_source = M.logging - for(var/log_type in log_source)//this whole loop puts the read-ee's say logs into say_log in an easy to access way - var/nlog_type = text2num(log_type) - if(nlog_type & LOG_SAY) - var/list/reversed = log_source[log_type] - if(islist(reversed)) - say_log = reverse_range(reversed.Copy()) - break - if(LAZYLEN(say_log)) - for(var/spoken_memory in say_log) - if(recent_speech.len >= 3)//up to 3 random lines of speech, favoring more recent speech - break - if(prob(50)) - //log messages with tags like telepathy are displayed like "(Telepathy to Ckey/(target)) "greetings"" by splitting the text by using a " delimiter we can grab just the greetings part - recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3] - if(recent_speech.len) - to_chat(user, span_boldnotice("You catch some drifting memories of their past conversations...")) - for(var/spoken_memory in recent_speech) - to_chat(user, span_notice("[recent_speech[spoken_memory]]")) - if(iscarbon(M)) - var/mob/living/carbon/human/H = M - to_chat(user, span_boldnotice("You find that their intent is to [H.combat_mode ? "Harm" : "Help"]...")) - if(H.mind) - to_chat(user, span_boldnotice("You uncover that [H.p_their()] true identity is [H.mind.name].")) - else - to_chat(user, span_warning("You can't find a mind to read inside of [M]!")) + 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) + var/list/reversed = log_source[log_type] + if(islist(reversed)) + say_log = reverse_range(reversed.Copy()) + break + + for(var/spoken_memory in say_log) + //up to 3 random lines of speech, favoring more recent speech + if(length(recent_speech) >= 3) + break + if(prob(50)) + continue + // log messages with tags like telepathy are displayed like "(Telepathy to Ckey/(target)) "greetings""" + // by splitting the text by using a " delimiter, we can grab JUST the greetings part + recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3] + + if(length(recent_speech)) + to_chat(owner, span_boldnotice("You catch some drifting memories of their past conversations...")) + for(var/spoken_memory in recent_speech) + to_chat(owner, span_notice("[recent_speech[spoken_memory]]")) + + if(iscarbon(cast_on)) + var/mob/living/carbon/carbon_cast_on = cast_on + to_chat(owner, span_boldnotice("You find that their intent is to [carbon_cast_on.combat_mode ? "harm" : "help"]...")) + to_chat(owner, span_boldnotice("You uncover that [carbon_cast_on.p_their()] true identity is [carbon_cast_on.mind.name].")) /datum/mutation/human/mindreader/New(class_ = MUT_OTHER, timer, datum/mutation/human/copymut) ..() diff --git a/code/datums/mutations/autotomy.dm b/code/datums/mutations/autotomy.dm new file mode 100644 index 00000000000..8f7b66f0b6c --- /dev/null +++ b/code/datums/mutations/autotomy.dm @@ -0,0 +1,42 @@ +/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/body.dm b/code/datums/mutations/body.dm index b6eb0e54da5..5fff1da9679 100644 --- a/code/datums/mutations/body.dm +++ b/code/datums/mutations/body.dm @@ -224,13 +224,12 @@ glowth = new(owner) modify() +// Override modify here without a parent call, because we don't actually give an action. /datum/mutation/human/glow/modify() if(!glowth) return - var/power = GET_MUTATION_POWER(src) - - glowth.set_light_range_power_color(range * power, glow, glow_color) + glowth.set_light_range_power_color(range * GET_MUTATION_POWER(src), glow, glow_color) /// Returns the color for the glow effect /datum/mutation/human/glow/proc/glow_color() diff --git a/code/datums/mutations/chameleon.dm b/code/datums/mutations/chameleon.dm index e804b5f29e3..ebd0209df7c 100644 --- a/code/datums/mutations/chameleon.dm +++ b/code/datums/mutations/chameleon.dm @@ -6,10 +6,6 @@ difficulty = 16 text_gain_indication = "You feel one with your surroundings." text_lose_indication = "You feel oddly exposed." - /// SKYRAT EDIT BEGIN - instability = 35 - power = /obj/effect/proc_holder/spell/self/chameleon_skin_activate - /// SKYRAT EDIT END /datum/mutation/human/chameleon/on_acquiring(mob/living/carbon/human/owner) if(..()) diff --git a/code/datums/mutations/cold.dm b/code/datums/mutations/cold.dm index 1dd531490ac..a49dfa26160 100644 --- a/code/datums/mutations/cold.dm +++ b/code/datums/mutations/cold.dm @@ -6,16 +6,18 @@ instability = 10 difficulty = 10 synchronizer_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/mineral/snow - charge_max = 50 - delete_old = FALSE - action_icon_state = "snow" + button_icon_state = "snow" + cooldown_time = 5 SECONDS + spell_requirements = NONE + + item_type = /obj/item/stack/sheet/mineral/snow + delete_old = FALSE /datum/mutation/human/cryokinesis name = "Cryokinesis" @@ -25,19 +27,17 @@ instability = 20 difficulty = 12 synchronizer_coeff = 1 - power = /obj/effect/proc_holder/spell/aimed/cryo + 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 = 150 - cooldown_min = 150 - 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 - + 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 00000000000..9869d41283e --- /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/holy_mutation/honorbound.dm b/code/datums/mutations/holy_mutation/honorbound.dm index 6b7eac5cb4c..46de73bea5e 100644 --- a/code/datums/mutations/holy_mutation/honorbound.dm +++ b/code/datums/mutations/holy_mutation/honorbound.dm @@ -6,7 +6,7 @@ The user feels compelled to follow supposed \"rules of combat\" but in reality they physically are unable to. \ Their brain is rewired to excuse any curious inabilities that arise from this odd effect." quality = POSITIVE //so it gets carried over on revives - power = /obj/effect/proc_holder/spell/pointed/declare_evil + power_path = /datum/action/cooldown/spell/pointed/declare_evil locked = TRUE text_gain_indication = "You feel honorbound!" text_lose_indication = "You feel unshackled from your code of honor!" @@ -167,7 +167,7 @@ guilty(thrown_by) //spell checking -/datum/mutation/human/honorbound/proc/spell_check(mob/user, obj/effect/proc_holder/spell/spell_cast) +/datum/mutation/human/honorbound/proc/spell_check(mob/user, datum/action/cooldown/spell/spell_cast) SIGNAL_HANDLER punishment(user, spell_cast.school) @@ -201,72 +201,118 @@ lightningbolt(user) SEND_SIGNAL(owner, COMSIG_ADD_MOOD_EVENT, "honorbound", /datum/mood_event/holy_smite)//permanently lose your moodlet after this -/obj/effect/proc_holder/spell/pointed/declare_evil +/datum/action/cooldown/spell/pointed/declare_evil name = "Declare Evil" desc = "If someone is so obviously an evil of this world you can spend a huge amount of favor to declare them guilty." - school = SCHOOL_HOLY - charge_max = 0 - clothes_req = FALSE - range = 7 - cooldown_min = 0 + button_icon_state = "declaration" ranged_mousepointer = 'icons/effects/mouse_pointers/honorbound.dmi' - action_icon_state = "declaration" + + school = SCHOOL_HOLY + cooldown_time = 0 + + invocation = "This is an error!" + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_HUMAN + active_msg = "You prepare to declare a sinner..." deactive_msg = "You decide against a declaration." -/obj/effect/proc_holder/spell/pointed/declare_evil/cast(list/targets, mob/living/carbon/human/user, silent = FALSE) - if(!ishuman(user)) - return FALSE - var/datum/mutation/human/honorbound/honormut = user.dna.check_mutation(/datum/mutation/human/honorbound) - var/datum/religion_sect/honorbound/honorsect = GLOB.religious_sect - if(honorsect.favor < 150) - to_chat(user, span_warning("You need at least 150 favor to declare someone evil!")) - return FALSE - if(!honormut) - return FALSE - if(!targets.len) - if(!silent) - to_chat(user, span_warning("Nobody to declare evil here!")) - return FALSE - if(targets.len > 1) - if(!silent) - to_chat(user, span_warning("Too many people to declare! Pick ONE!")) - return FALSE - var/declaration_message = "[targets[1]]! By the divine light of [GLOB.deity], You are an evil of this world that must be wrought low!" - if(!user.can_speak(declaration_message)) - to_chat(user, span_warning("You can't get the declaration out!")) - return FALSE - if(!can_target(targets[1], user, silent)) - return FALSE - GLOB.religious_sect.adjust_favor(-150, user) - user.say(declaration_message) - honormut.guilty(targets[1], declaration = TRUE) - return TRUE + /// The amount of favor required to declare on someone + var/required_favor = 150 + /// A ref to our owner's honorbound mutation + var/datum/mutation/human/honorbound/honor_mutation + /// The declaration that's shouted in invocation. Set in New() + var/declaration = "By the divine light of my deity, you are an evil of this world that must be wrought low!" -/obj/effect/proc_holder/spell/pointed/declare_evil/can_target(atom/target, mob/user, silent) +/datum/action/cooldown/spell/pointed/declare_evil/New() + . = ..() + declaration = "By the divine light of [GLOB.deity], you are an evil of this world that must be wrought low!" + +/datum/action/cooldown/spell/pointed/declare_evil/Destroy() + // If we had an owner, Destroy() called Remove(), and already handled this + if(honor_mutation) + UnregisterSignal(honor_mutation, COMSIG_PARENT_QDELETING) + honor_mutation = null + return ..() + +/datum/action/cooldown/spell/pointed/declare_evil/Grant(mob/grant_to) + if(!ishuman(grant_to)) + return FALSE + + var/mob/living/carbon/human/human_owner = grant_to + var/datum/mutation/human/honorbound/honor_mut = human_owner.dna?.check_mutation(/datum/mutation/human/honorbound) + if(QDELETED(honor_mut)) + return FALSE + + RegisterSignal(honor_mut, COMSIG_PARENT_QDELETING, .proc/on_honor_mutation_lost) + honor_mutation = honor_mut + return ..() + +/datum/action/cooldown/spell/pointed/declare_evil/Remove(mob/living/remove_from) + . = ..() + UnregisterSignal(honor_mutation, COMSIG_PARENT_QDELETING) + honor_mutation = null + +/// If we lose our honor mutation somehow, self-delete (and clear references) +/datum/action/cooldown/spell/pointed/declare_evil/proc/on_honor_mutation_lost(datum/source) + SIGNAL_HANDLER + + qdel(src) + +/datum/action/cooldown/spell/pointed/declare_evil/can_cast_spell(feedback = TRUE) . = ..() if(!.) return FALSE - if(!isliving(target)) - if(!silent) - to_chat(user, span_warning("You can only declare living beings evil!")) + + // This shouldn't technically be a possible state, but you never know + if(!honor_mutation) return FALSE - var/mob/living/victim = target - if(victim.stat == DEAD) - if(!silent) - to_chat(user, span_warning("Declaration on the dead? Really?")) - return FALSE - var/datum/mind/guilty_conscience = victim.mind - if(!victim.key ||!guilty_conscience) //sec and medical are immune to becoming guilty through attack (we don't check holy because holy shouldn't be able to attack eachother anyways) - if(!silent) - to_chat(user, span_warning("There is no evil a vacant mind can do.")) - return FALSE - if(guilty_conscience.holy_role)//also handles any kind of issues with self declarations - if(!silent) - to_chat(user, span_warning("Followers of [GLOB.deity] cannot be evil!")) - return FALSE - if(guilty_conscience.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY) - if(!silent) - to_chat(user, span_warning("Members of security are uncorruptable! You cannot declare one evil!")) + if(GLOB.religious_sect.favor < required_favor) + if(feedback) + to_chat(owner, span_warning("You need at least 150 favor to declare someone evil!")) return FALSE + return TRUE + +/datum/action/cooldown/spell/pointed/declare_evil/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return FALSE + if(!isliving(cast_on)) + to_chat(owner, span_warning("You can only declare living beings evil!")) + return FALSE + + var/mob/living/living_cast_on = cast_on + if(living_cast_on.stat == DEAD) + to_chat(owner, span_warning("Declaration on the dead? Really?")) + return FALSE + + // sec and medical are immune to becoming guilty through attack + // (we don't check holy, because holy shouldn't be able to attack eachother anyways) + if(!living_cast_on.key || !living_cast_on.mind) + to_chat(owner, span_warning("There is no evil a vacant mind can do.")) + return FALSE + + // also handles any kind of issues with self declarations + if(living_cast_on.mind.holy_role) + to_chat(owner, span_warning("Followers of [GLOB.deity] cannot be evil!")) + return FALSE + + // cannot declare security as evil + if(living_cast_on.mind.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY) + to_chat(owner, span_warning("Members of security are uncorruptable! You cannot declare one evil!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/declare_evil/before_cast(mob/living/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + invocation = "[cast_on]! [declaration]" + +/datum/action/cooldown/spell/pointed/declare_evil/cast(mob/living/cast_on) + . = ..() + GLOB.religious_sect.adjust_favor(-required_favor, owner) + honor_mutation.guilty(cast_on, declaration = TRUE) diff --git a/code/datums/mutations/olfaction.dm b/code/datums/mutations/olfaction.dm new file mode 100644 index 00000000000..e014806233a --- /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 e9234b0c201..9beb1beb96c 100644 --- a/code/datums/mutations/sight.dm +++ b/code/datums/mutations/sight.dm @@ -45,57 +45,67 @@ synchronizer_coeff = 1 power_coeff = 1 energy_coeff = 1 - power = /obj/effect/proc_holder/spell/self/thermal_vision_activate - -/datum/mutation/human/thermal/modify() - if(!power) - return FALSE - var/obj/effect/proc_holder/spell/self/thermal_vision_activate/modified_power = power - modified_power.eye_damage = 10 * GET_MUTATION_SYNCHRONIZER(src) - modified_power.thermal_duration = 10 * GET_MUTATION_POWER(src) - modified_power.charge_max = (25 * GET_MUTATION_ENERGY(src)) SECONDS - -/obj/effect/proc_holder/spell/self/thermal_vision_activate - name = "Activate Thermal Vision" - desc = "You can see thermal signatures, at the cost of your eyesight." - charge_max = 25 SECONDS - var/eye_damage = 10 - var/thermal_duration = 10 - clothes_req = FALSE - action_icon = 'icons/mob/actions/actions_changeling.dmi' - action_icon_state = "augmented_eyesight" - -/obj/effect/proc_holder/spell/self/thermal_vision_activate/cast(list/targets, mob/user = usr) - . = ..() - - if(HAS_TRAIT(user,TRAIT_THERMAL_VISION)) - return - - ADD_TRAIT(user, TRAIT_THERMAL_VISION, GENETIC_MUTATION) - user.update_sight() - to_chat(user, text("You focus your eyes intensely, as your vision becomes filled with heat signatures.")) - - addtimer(CALLBACK(src, .proc/thermal_vision_deactivate), thermal_duration SECONDS) - -/obj/effect/proc_holder/spell/self/thermal_vision_activate/proc/thermal_vision_deactivate(mob/user = usr) - if(!HAS_TRAIT_FROM(user,TRAIT_THERMAL_VISION, GENETIC_MUTATION)) - return - - REMOVE_TRAIT(user, TRAIT_THERMAL_VISION, GENETIC_MUTATION) - user.update_sight() - to_chat(user, text("You blink a few times, your vision returning to normal as a dull pain settles in your eyes.")) - - var/mob/living/carbon/user_mob = user - if(!istype(user_mob)) - return - - user_mob.adjustOrganLoss(ORGAN_SLOT_EYES, eye_damage) + power_path = /datum/action/cooldown/spell/thermal_vision /datum/mutation/human/thermal/on_losing(mob/living/carbon/human/owner) if(..()) return - REMOVE_TRAIT(owner, TRAIT_THERMAL_VISION, GENETIC_MUTATION) - owner.update_sight() + + // 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 + + 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/human/xray diff --git a/code/datums/mutations/telepathy.dm b/code/datums/mutations/telepathy.dm new file mode 100644 index 00000000000..8619c2bddc4 --- /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 00000000000..1bd02df0b3e --- /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 951d6edc6a7..4328e397c6a 100644 --- a/code/datums/mutations/touch.dm +++ b/code/datums/mutations/touch.dm @@ -6,46 +6,49 @@ difficulty = 16 text_gain_indication = "You feel power flow through your hands." text_lose_indication = "The energy in your hands subsides." - power = /obj/effect/proc_holder/spell/targeted/touch/shock + power_path = /datum/action/cooldown/spell/touch/shock instability = 30 -/obj/effect/proc_holder/spell/targeted/touch/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." + 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 - charge_max = 100 - clothes_req = FALSE - action_icon_state = "zap" + 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/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." - catchphrase = null - on_use_sound = 'sound/weapons/zapbang.ogg' icon_state = "zapper" inhand_icon_state = "zapper" - -/obj/item/melee/touch_attack/shock/afterattack(atom/target, mob/living/carbon/user, proximity) - if(!proximity) - return - if(iscarbon(target)) - var/mob/living/carbon/C = target - if(C.electrocute_act(15, user, 1, SHOCK_NOGLOVES | SHOCK_NOSTUN))//doesnt stun. never let this stun - C.dropItemToGround(C.get_active_held_item()) - C.dropItemToGround(C.get_inactive_held_item()) - C.adjust_timed_status_effect(15 SECONDS, /datum/status_effect/confusion) - C.visible_message(span_danger("[user] electrocutes [target]!"),span_userdanger("[user] electrocutes you!")) - return ..() - else - user.visible_message(span_warning("[user] fails to electrocute [target]!")) - return ..() - else if(isliving(target)) - var/mob/living/L = target - L.electrocute_act(15, user, 1, SHOCK_NOSTUN) - L.visible_message(span_danger("[user] electrocutes [target]!"),span_userdanger("[user] electrocutes you!")) - return ..() - else - to_chat(user,span_warning("The electricity doesn't seem to affect [target]...")) - return ..() diff --git a/code/datums/mutations/void_magnet.dm b/code/datums/mutations/void_magnet.dm new file mode 100644 index 00000000000..7900b4c099f --- /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 00000000000..2d696938e6c --- /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/fields/timestop.dm b/code/datums/proximity_monitor/fields/timestop.dm index c9c544dff0d..bde85c6f4d9 100644 --- a/code/datums/proximity_monitor/fields/timestop.dm +++ b/code/datums/proximity_monitor/fields/timestop.dm @@ -28,12 +28,12 @@ freezerange = radius for(var/A in immune_atoms) immune[A] = TRUE - for(var/mob/living/L in GLOB.player_list) - if(locate(/obj/effect/proc_holder/spell/aoe_turf/timestop) in L.mind.spell_list) //People who can stop time are immune to its effects - immune[L] = TRUE - for(var/mob/living/simple_animal/hostile/guardian/G in GLOB.parasites) - if(G.summoner && locate(/obj/effect/proc_holder/spell/aoe_turf/timestop) in G.summoner.mind.spell_list) //It would only make sense that a person's stand would also be immune. - immune[G] = 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) diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm index e188b31c8d0..06861e64a9c 100644 --- a/code/datums/status_effects/neutral.dm +++ b/code/datums/status_effects/neutral.dm @@ -92,7 +92,7 @@ rewarded = caster /datum/status_effect/bounty/on_apply() - 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.")) + 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/gun/shotgun/rack.ogg', 75, FALSE) return ..() @@ -103,13 +103,12 @@ /datum/status_effect/bounty/proc/rewards() if(rewarded && rewarded.mind && rewarded.stat != DEAD) - to_chat(owner, span_boldnotice("You hear something behind you talking... Bounty claimed.")) + to_chat(owner, span_boldnotice("You hear something behind you talking... \"Bounty claimed.\"")) playsound(owner, 'sound/weapons/gun/shotgun/shot.ogg', 75, FALSE) to_chat(rewarded, span_greentext("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_appearance() + for(var/datum/action/cooldown/spell/spell in rewarded.actions) + spell.reset_spell_cooldown() + rewarded.adjustBruteLoss(-25) rewarded.adjustFireLoss(-25) rewarded.adjustToxLoss(-25) diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm index f018aea090f..b19a303868a 100644 --- a/code/game/area/areas/shuttles.dm +++ b/code/game/area/areas/shuttles.dm @@ -246,7 +246,7 @@ /obj/effect/forcefield/arena_shuttle name = "portal" - timeleft = 0 + initial_duration = 0 var/list/warp_points = list() /obj/effect/forcefield/arena_shuttle/Initialize(mapload) @@ -283,7 +283,7 @@ /obj/effect/forcefield/arena_shuttle_entrance name = "portal" - timeleft = 0 + initial_duration = 0 var/list/warp_points = list() /obj/effect/forcefield/arena_shuttle_entrance/Bumped(atom/movable/AM) diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index 6df921a7289..e48decc7bba 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -167,30 +167,32 @@ GLOBAL_LIST_EMPTY(objectives) //SKYRAT EDIT ADDITION receiver.failed_special_equipment += equipment_path receiver.try_give_equipment_fallback() -/obj/effect/proc_holder/spell/self/special_equipment_fallback +/datum/action/special_equipment_fallback name = "Request Objective-specific Equipment" desc = "Call down a supply pod containing the equipment required for specific objectives." - action_icon = 'icons/obj/device.dmi' - action_icon_state = "beacon" - charge_max = 0 - clothes_req = FALSE - nonabstract_req = TRUE - phase_allowed = TRUE - antimagic_flags = NONE - invocation_type = INVOCATION_NONE + icon_icon = 'icons/obj/device.dmi' + button_icon_state = "beacon" -/obj/effect/proc_holder/spell/self/special_equipment_fallback/cast(list/targets, mob/user) - var/datum/mind/mind = user.mind - if(!mind) - CRASH("[src] has no owner!") - if(mind.failed_special_equipment?.len) +/datum/action/special_equipment_fallback/Trigger(trigger_flags) + . = ..() + if(!.) + return FALSE + + var/datum/mind/our_mind = target + if(!istype(our_mind)) + CRASH("[type] - [src] has an incorrect target!") + if(our_mind.current != owner) + CRASH("[type] - [src] was owned by a mob which was not the current of the target mind!") + + if(LAZYLEN(our_mind.failed_special_equipment)) podspawn(list( - "target" = get_turf(user), + "target" = get_turf(owner), "style" = STYLE_SYNDICATE, - "spawn" = mind.failed_special_equipment + "spawn" = our_mind.failed_special_equipment, )) - mind.failed_special_equipment = null - mind.RemoveSpell(src) + our_mind.failed_special_equipment = null + qdel(src) + return TRUE /datum/objective/assassinate name = "assasinate" @@ -885,12 +887,12 @@ GLOBAL_LIST_EMPTY(possible_items_special) if(!isliving(M.current)) continue var/list/all_items = M.current.get_all_contents() //this should get things in cheesewheels, books, etc. - for(var/obj/I in all_items) //Check for wanted items - if(istype(I, /obj/item/book/granter/spell)) - var/obj/item/book/granter/spell/spellbook = I - if(!spellbook.used || !spellbook.oneuse) //if the book still has powers... + for(var/obj/thing in all_items) //Check for wanted items + if(istype(thing, /obj/item/book/granter/action/spell)) + var/obj/item/book/granter/action/spell/spellbook = thing + if(spellbook.uses > 0) //if the book still has powers... stolen_count++ //it counts. nice. - else if(is_type_in_typecache(I, wanted_items)) + else if(is_type_in_typecache(thing, wanted_items)) stolen_count++ return stolen_count >= amount diff --git a/code/game/machinery/airlock_control.dm b/code/game/machinery/airlock_control.dm index 816b4a34429..d40539ab17b 100644 --- a/code/game/machinery/airlock_control.dm +++ b/code/game/machinery/airlock_control.dm @@ -75,7 +75,7 @@ frequency = new_frequency radio_connection = SSradio.add_object(src, frequency, RADIO_AIRLOCK) -/obj/machinery/door/airlock/on_magic_unlock(datum/source, obj/effect/proc_holder/spell/aoe_turf/knock/spell, mob/living/caster) +/obj/machinery/door/airlock/on_magic_unlock(datum/source, datum/action/cooldown/spell/aoe/knock/spell, mob/living/caster) // Airlocks should unlock themselves when knock is casted, THEN open up. locked = FALSE return ..() diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index cc12eacbbe0..d2e870f1645 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -500,7 +500,7 @@ . = ..() /// Signal proc for [COMSIG_ATOM_MAGICALLY_UNLOCKED]. Open up when someone casts knock. -/obj/machinery/door/proc/on_magic_unlock(datum/source, obj/effect/proc_holder/spell/aoe_turf/knock/spell, mob/living/caster) +/obj/machinery/door/proc/on_magic_unlock(datum/source, datum/action/cooldown/spell/aoe/knock/spell, mob/living/caster) SIGNAL_HANDLER INVOKE_ASYNC(src, .proc/open) diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm index e4dacb783d2..6e8864acbdc 100644 --- a/code/game/objects/effects/decals/cleanable.dm +++ b/code/game/objects/effects/decals/cleanable.dm @@ -104,11 +104,28 @@ return TRUE return . +/** + * Checks if this decal is a valid decal that can be blood crawled in. + */ /obj/effect/decal/cleanable/proc/can_bloodcrawl_in() if((blood_state != BLOOD_STATE_OIL) && (blood_state != BLOOD_STATE_NOT_BLOODY)) return bloodiness - else - return 0 + + 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 /obj/effect/decal/cleanable/proc/handle_merge_decal(obj/effect/decal/cleanable/merger) if(!merger) diff --git a/code/game/objects/effects/forcefields.dm b/code/game/objects/effects/forcefields.dm index 9c41da052a8..64b2c013338 100644 --- a/code/game/objects/effects/forcefields.dm +++ b/code/game/objects/effects/forcefields.dm @@ -1,35 +1,60 @@ /obj/effect/forcefield - desc = "A space wizard's magic wall." name = "FORCEWALL" + desc = "A space wizard's magic wall." icon_state = "m_shield" anchored = TRUE opacity = FALSE density = TRUE can_atmos_pass = ATMOS_PASS_DENSITY - 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) . = ..() - 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 ..() + +/// Cult forcefields /obj/effect/forcefield/cult - desc = "An unholy shield that blocks all attacks." name = "glowing wall" + desc = "An unholy shield that blocks all attacks." icon = 'icons/effects/cult/effects.dmi' icon_state = "cultshield" can_atmos_pass = ATMOS_PASS_NO - timeleft = 200 + initial_duration = 20 SECONDS /// A form of the cult forcefield that lasts permanently. /// Used on the Shuttle 667. /obj/effect/forcefield/cult/permanent - timeleft = 0 + initial_duration = 0 -///////////Mimewalls/////////// +/// Mime forcefields (invisible walls) /obj/effect/forcefield/mime icon_state = "nothing" @@ -40,4 +65,4 @@ /obj/effect/forcefield/mime/advanced name = "invisible blockade" desc = "You're gonna be here awhile." - timeleft = 600 + initial_duration = 1 MINUTES diff --git a/code/game/objects/effects/phased_mob.dm b/code/game/objects/effects/phased_mob.dm index 5ddd317b092..5f6596675e3 100644 --- a/code/game/objects/effects/phased_mob.dm +++ b/code/game/objects/effects/phased_mob.dm @@ -5,32 +5,59 @@ 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() - // Eject contents if deleted somehow - var/atom/dest = drop_location() - if(!dest) //You're in nullspace you clown - return ..() - var/area/destination_area = get_area(dest) - var/failed_areacheck = FALSE - if(destination_area.area_flags & NOTELEPORT) - failed_areacheck = TRUE - for(var/_phasing_in in contents) - var/atom/movable/phasing_in = _phasing_in - if(!failed_areacheck) - phasing_in.forceMove(drop_location()) - else //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(phasing_in)) - var/mob/living/living_cheaterson = phasing_in - 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) - phasing_in.forceMove(find_safe_turf(z)) + 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 @@ -61,8 +88,3 @@ to_chat(user, span_danger("Some dull, universal force is blocking the way. It's overwhelmingly oppressive force feels dangerous.")) return return newloc - -/// React to signals by deleting the effect. Used for bloodcrawl. -/obj/effect/dummy/phased_mob/proc/deleteself(mob/living/source, obj/effect/decal/cleanable/phase_in_decal) - SIGNAL_HANDLER - qdel(src) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index ca8ce4889d9..da3a749bd8f 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -226,8 +226,11 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons species_exception = string_list(species_exception) . = ..() + + // Handle adding item associated actions for(var/path in actions_types) - new path(src) + add_item_action(path) + actions_types = null if(force_string) @@ -254,10 +257,55 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons 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) + 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) + /// Called if this item is supposed to be a steal objective item objective. Only done at mapload /obj/item/proc/add_stealing_item_objective() return @@ -605,9 +653,11 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons /// Called when a mob drops an item. /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 && !QDELETED(src)) qdel(src) item_flags &= ~IN_INVENTORY @@ -651,10 +701,11 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons visual_equipped(user, slot, initial) 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) + item_flags |= IN_INVENTORY if(!initial) if(equip_sound && (slot_flags & slot)) @@ -663,7 +714,19 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons playsound(src, pickup_sound, PICKUP_SOUND_VOLUME, ignore_walls = FALSE) user.update_equipment_speed_mods() -///sometimes we only want to grant the item's action if it's equipped in a specific slot. +/// 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 return FALSE diff --git a/code/game/objects/items/RCD.dm b/code/game/objects/items/RCD.dm index 6574224f645..68a81ee6701 100644 --- a/code/game/objects/items/RCD.dm +++ b/code/game/objects/items/RCD.dm @@ -1446,6 +1446,9 @@ GLOBAL_VAR_INIT(icon_holographic_window, init_holographic_window()) name = "Destruction Scan" desc = "Scans the surrounding area for destruction. Scanned structures will rebuild significantly faster." +/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 63e6cf79bf4..b6115099303 100644 --- a/code/game/objects/items/RCL.dm +++ b/code/game/objects/items/RCL.dm @@ -342,3 +342,13 @@ icon_state = "rclg-1" inhand_icon_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 8e0e4ca69bb..dbc45760da5 100644 --- a/code/game/objects/items/cards_ids.dm +++ b/code/game/objects/items/cards_ids.dm @@ -1302,6 +1302,7 @@ chameleon_card_action.chameleon_type = /obj/item/card/id/advanced chameleon_card_action.chameleon_name = "ID Card" chameleon_card_action.initialize_disguises() + add_item_action(chameleon_card_action) /obj/item/card/id/advanced/chameleon/Destroy() theft_target = null diff --git a/code/game/objects/items/chainsaw.dm b/code/game/objects/items/chainsaw.dm index 2e34708ea88..cfea8758bf4 100644 --- a/code/game/objects/items/chainsaw.dm +++ b/code/game/objects/items/chainsaw.dm @@ -72,3 +72,6 @@ playsound(src, pick('sound/weapons/bulletflyby.ogg', 'sound/weapons/bulletflyby2.ogg', 'sound/weapons/bulletflyby3.ogg'), 75, TRUE) return TRUE return FALSE + +/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 75646583d75..9e7dd7f3b0b 100644 --- a/code/game/objects/items/chromosome.dm +++ b/code/game/objects/items/chromosome.dm @@ -34,9 +34,13 @@ 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 e6ab1e63897..6d06acf2eb0 100644 --- a/code/game/objects/items/devices/multitool.dm +++ b/code/game/objects/items/devices/multitool.dm @@ -47,25 +47,23 @@ /obj/item/multitool/ai_detect special_desc_requirement = EXAMINE_CHECK_SYNDICATE // Skyrat edit special_desc = "A special sensor embedded stealthily into this device can detect and warn of nearby silicon activity and camera vision range." // Skyrat edit + 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 6577a2de966..0fa659d4953 100644 --- a/code/game/objects/items/devices/spyglasses.dm +++ b/code/game/objects/items/devices/spyglasses.dm @@ -40,6 +40,9 @@ linked_bug.linked_glasses = null . = ..() +/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 75d506b7e75..00000000000 --- a/code/game/objects/items/granters.dm +++ /dev/null @@ -1,493 +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, TRUE) - if(do_after(user, 5 SECONDS, src)) - if(remarks.len) - to_chat(user, span_notice("[pick(remarks)]")) - else - to_chat(user, span_notice("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, span_notice("You start reading [name]...")) - -/obj/item/book/granter/proc/on_reading_stopped(mob/user) - to_chat(user, span_notice("You stop reading...")) - -/obj/item/book/granter/proc/on_reading_finished(mob/user) - to_chat(user, span_notice("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, 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(!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, 5 SECONDS, src)) - 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, span_warning("You already know all about [actionname]!")) - return TRUE - return FALSE - -/obj/item/book/granter/action/on_reading_start(mob/user) - to_chat(user, span_notice("You start reading about [actionname]...")) - -/obj/item/book/granter/action/on_reading_finished(mob/user) - to_chat(user, span_notice("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 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() - -///SPELLS/// - -/obj/item/book/granter/spell - var/spell - var/spellname = "conjure bugs" - - -/obj/item/book/granter/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/spell/proc/on_magic_charge(datum/source, obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster) - SIGNAL_HANDLER - - if(!oneuse) - 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 - - used = FALSE - return COMPONENT_ITEM_CHARGED - -/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(IS_WIZARD(user)) - to_chat(user,span_warning("You're already far more versed in this spell than this flimsy how-to book can provide!")) - else - to_chat(user,span_warning("You've already read this one!")) - return TRUE - return FALSE - -/obj/item/book/granter/spell/on_reading_start(mob/user) - to_chat(user, span_notice("You start reading about casting [spellname]...")) - -/obj/item/book/granter/spell/on_reading_finished(mob/user) - to_chat(user, span_notice("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(span_warning("[src] glows in a black light!")) - -/obj/item/book/granter/spell/onlearned(mob/user) - ..() - if(oneuse) - user.visible_message(span_warning("[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, devastation_range = 1, light_impact_range = 2, flame_range = 2, flash_range = 3, adminlog = FALSE, explosion_cause = src) - 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,span_warning("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/pointed/trigger/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,span_warning("You go blind!")) - user.blind_eyes(10) - -/obj/item/book/granter/spell/mindswap - spell = /obj/effect/proc_holder/spell/pointed/mind_transfer - spellname = "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/mob/living/stored_swap - -/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,span_warning("For a moment you feel like you don't even know who you are anymore.")) - return - if(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/obj/effect/proc_holder/spell/pointed/mind_transfer/swapper = new - if(swapper.cast(list(stored_swap), user, TRUE)) - to_chat(user,span_warning("You're suddenly somewhere else... and someone else?!")) - to_chat(stored_swap,span_warning("Suddenly you're staring at [src] again... where are you, who are you?!")) - else - user.visible_message(span_warning("[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,span_warning("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,span_warning("You're knocked down!")) - user.Paralyze(40) - -/obj/item/book/granter/spell/barnyard - spell = /obj/effect/proc_holder/spell/pointed/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/animal/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,span_notice("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,span_warning("[src] suddenly feels very warm!")) - empulse(src, 1, 1) - -/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,span_warning("[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,span_warning("You already know [martialname]!")) - return TRUE - return FALSE - -/obj/item/book/granter/martial/on_reading_start(mob/user) - to_chat(user, span_notice("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, span_warning("[src] beeps ominously...")) - -/obj/item/book/granter/martial/cqc/recoil(mob/living/carbon/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) - -/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 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/onlearned(mob/living/carbon/user) - ..() - if(oneuse == TRUE) - desc = "It's completely blank." - name = "empty scroll" - icon_state = "blankscroll" - -/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/plasma_fist/nobomb - martial = /datum/martial_art/plasma_fist/nobomb - -// 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,span_notice("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/cocolavatart, - /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?") - -/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" - oneuse = TRUE - 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/carbon/user) - to_chat(user, span_warning("The book turns to dust in your hands.")) - qdel(src) - -/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" - oneuse = TRUE - 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/carbon/user) - to_chat(user, span_warning("The book turns to dust in your hands.")) - qdel(src) diff --git a/code/game/objects/items/granters/_granters.dm b/code/game/objects/items/granters/_granters.dm new file mode 100644 index 00000000000..fed49582d4f --- /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 00000000000..a4d2b46877a --- /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 00000000000..120e47a64d3 --- /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 00000000000..7bf276642b6 --- /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 00000000000..518de4bb033 --- /dev/null +++ b/code/game/objects/items/granters/crafting/desserts.dm @@ -0,0 +1,21 @@ + +/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 00000000000..73e17184621 --- /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 00000000000..4f695e4d3af --- /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 00000000000..1f512f96d2f --- /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 00000000000..2107af802c7 --- /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 00000000000..988d17aa13b --- /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 00000000000..b8b97e6502f --- /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 00000000000..7df82dccd24 --- /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 00000000000..11bdfeeadbf --- /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 00000000000..6e6bc03dc98 --- /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 00000000000..6396b62136a --- /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/sacred_flame.dm b/code/game/objects/items/granters/magic/sacred_flame.dm new file mode 100644 index 00000000000..1e044e8e039 --- /dev/null +++ b/code/game/objects/items/granters/magic/sacred_flame.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 00000000000..a83811f1e19 --- /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 00000000000..58fbdaf24d0 --- /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 00000000000..08f615a991e --- /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 00000000000..697541e0216 --- /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 00000000000..d33fdf6eaae --- /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 00000000000..c50a062eae5 --- /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 00000000000..b048c67ed72 --- /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 1c50132b1eb..8075b72c38b 100644 --- a/code/game/objects/items/implants/implant.dm +++ b/code/game/objects/items/implants/implant.dm @@ -5,9 +5,11 @@ name = "implant" icon = 'icons/obj/implants.dmi' icon_state = "generic" //Shows up as the action button icon + item_flags = DROPDEL + // 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. actions_types = list(/datum/action/item_action/hands_free/activate) - ///true for implant types that can be activated, false for ones that are "always on" like mindshield implants - var/activated = TRUE ///the mob that's implanted with this var/mob/living/imp_in = null ///implant color, used for selecting either the "b" version or the "r" version of the implant case sprite when the implant is in a case. @@ -16,7 +18,7 @@ var/allow_multiple = FALSE ///how many times this can do something, only relevant for implants with limited uses var/uses = -1 - item_flags = DROPDEL + /obj/item/implant/proc/activate() SEND_SIGNAL(src, COMSIG_IMPLANT_ACTIVATED) @@ -24,6 +26,9 @@ /obj/item/implant/ui_action_click() INVOKE_ASYNC(src, .proc/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) if(issilicon(target)) return FALSE @@ -86,10 +91,8 @@ forceMove(target) imp_in = target target.implants += src - if(activated) - for(var/X in actions) - var/datum/action/implant_action = X - implant_action.Grant(target) + for(var/datum/action/implant_action as anything in actions) + implant_action.Grant(target) if(ishuman(target)) var/mob/living/carbon/human/target_human = target target_human.sec_hud_set_implants() @@ -113,8 +116,7 @@ moveToNullspace() imp_in = null source.implants -= src - for(var/X in actions) - var/datum/action/implant_action = X + for(var/datum/action/implant_action as anything in actions) implant_action.Remove(source) if(ishuman(source)) var/mob/living/carbon/human/human_source = source diff --git a/code/game/objects/items/implants/implant_abductor.dm b/code/game/objects/items/implants/implant_abductor.dm index 084b6818163..5c8d822ec4f 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 var/cooldown = 60 SECONDS var/on_cooldown diff --git a/code/game/objects/items/implants/implant_chem.dm b/code/game/objects/items/implants/implant_chem.dm index bbb1e72027f..2171ad12ef0 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 001e6a35075..a94f3e75098 100644 --- a/code/game/objects/items/implants/implant_clown.dm +++ b/code/game/objects/items/implants/implant_clown.dm @@ -1,7 +1,7 @@ ///A passive implant that plays sound/misc/sadtrombone.ogg when you deathgasp for any reason /obj/item/implant/sad_trombone name = "sad trombone implant" - activated = FALSE + 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 a7afe701c2a..f22c6d0b973 100644 --- a/code/game/objects/items/implants/implant_deathrattle.dm +++ b/code/game/objects/items/implants/implant_deathrattle.dm @@ -77,7 +77,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 a14128e0bdd..056ccd0ff9a 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 = FALSE + 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 96165b3fd3a..45a41313fe0 100644 --- a/code/game/objects/items/implants/implant_explosive.dm +++ b/code/game/objects/items/implants/implant_explosive.dm @@ -119,3 +119,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 373658b3864..e8ea5695fd3 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() @@ -34,4 +33,3 @@ name = "implant case - 'Krav Maga'" desc = "A glass case containing an implant that can teach the user the arts of Krav Maga." imp_type = /obj/item/implant/krav_maga - diff --git a/code/game/objects/items/implants/implant_mindshield.dm b/code/game/objects/items/implants/implant_mindshield.dm index 21a6449642c..240c83c793f 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 = FALSE + 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 f8119b296b9..f538de00e8f 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 = FALSE + actions_types = null /obj/item/implant/weapons_auth/get_data() var/dat = {"Implant Specifications:
@@ -33,7 +33,6 @@ /obj/item/implant/radio name = "internal radio implant" - activated = TRUE 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 916eaa3cede..8005e1c56e9 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 + if (!.) + return -/obj/item/implant/spell/removed(mob/target, silent = FALSE, special = 0) + 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, span_boldnotice("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 76244f23f30..815d20161ce 100644 --- a/code/game/objects/items/implants/implant_track.dm +++ b/code/game/objects/items/implants/implant_track.dm @@ -1,7 +1,8 @@ /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 339c0891389..0ea3a2c0fb6 100644 --- a/code/game/objects/items/robot/robot_upgrades.dm +++ b/code/game/objects/items/robot/robot_upgrades.dm @@ -658,6 +658,8 @@ var/mob/living/silicon/robot/Cyborg = usr GLOB.crewmonitor.show(Cyborg,Cyborg) +/datum/action/item_action/crew_monitor + name = "Interface With Crew Monitor" /obj/item/borg/upgrade/transform name = "borg model picker (Standard)" diff --git a/code/game/objects/items/scrolls.dm b/code/game/objects/items/scrolls.dm index a70bb9f7b28..46b1955b1d0 100644 --- a/code/game/objects/items/scrolls.dm +++ b/code/game/objects/items/scrolls.dm @@ -9,9 +9,22 @@ throw_speed = 3 throw_range = 7 resistance_flags = FLAMMABLE - /// Number of uses remaining + actions_types = list(/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll) + /// Number of uses the scroll gets. var/uses = 4 +/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 @@ -22,54 +35,24 @@ . += "It has [uses] use\s remaining." /obj/item/teleportation_scroll/attack_self(mob/user) + . = ..() + if(.) + return + if(!uses) return if(!ishuman(user)) return var/mob/living/carbon/human/human_user = user - if(human_user.incapacitated()) + if(human_user.incapacitated() || !human_user.is_holding(src)) return - if(!human_user.is_holding(src)) + 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 - teleportscroll(human_user) - -/** - * Shows a list of a possible teleport destinations to a user and then teleports him to to his chosen destination - * - * Arguments: - * * user The mob that is being teleported - */ -/obj/item/teleportation_scroll/proc/teleportscroll(mob/user) - if(!length(GLOB.teleportlocs)) - to_chat(user, span_warning("There are no locations available")) + if(!teleport.Activate(user)) return - var/jump_target = tgui_input_list(user, "Area to jump to", "BOOYEA", GLOB.teleportlocs) - if(isnull(jump_target)) - return - if(!src || QDELETED(src) || !user || !user.is_holding(src) || user.incapacitated() || !uses) - return - var/area/thearea = GLOB.teleportlocs[jump_target] - - var/datum/effect_system/fluid_spread/smoke/smoke = new - smoke.set_up(2, holder = src, location = user.loc) - smoke.attach(user) - smoke.start() - var/list/possible_locations = list() - for(var/turf/target_turf in get_area_turfs(thearea.type)) - if(!target_turf.is_blocked_turf()) - possible_locations += target_turf - - if(!length(possible_locations)) - to_chat(user, span_warning("The spell matrix was unable to locate a suitable teleport destination for an unknown reason.")) - return - - if(do_teleport(user, pick(possible_locations), channel = TELEPORT_CHANNEL_MAGIC, forced = TRUE)) - smoke.start() - uses-- - if(!uses) - to_chat(user, span_warning("[src] has run out of uses and crumbles to dust!")) - qdel(src) - else - to_chat(user, span_notice("[src] has [uses] use\s remaining.")) - else - to_chat(user, span_warning("The spell matrix was disrupted by something near the destination.")) + if(--uses <= 0) + to_chat(user, span_warning("[src] runs out of uses and crumbles to dust!")) + qdel(src) + return TRUE diff --git a/code/game/objects/items/signs.dm b/code/game/objects/items/signs.dm index 6c7bb2e4e5c..6a107650442 100644 --- a/code/game/objects/items/signs.dm +++ b/code/game/objects/items/signs.dm @@ -56,6 +56,15 @@ animate(pixel_y = user.pixel_y + (1 * direction), time = 1, easing = SINE_EASING) user.changeNext_move(CLICK_CD_MELEE) +/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) + /datum/crafting_recipe/picket_sign name = "Picket Sign" result = /obj/item/picket_sign diff --git a/code/game/objects/items/storage/holsters.dm b/code/game/objects/items/storage/holsters.dm index ce8cda00113..8115d891699 100644 --- a/code/game/objects/items/storage/holsters.dm +++ b/code/game/objects/items/storage/holsters.dm @@ -117,6 +117,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/holster/chameleon/ComponentInitialize() . = ..() diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index 065179f1aa5..7cfd090200e 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -190,7 +190,7 @@ new /obj/item/clothing/suit/hooded/chaplain_hoodie(src) new /obj/item/card/id/advanced/chameleon(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(KIT_WHITE_WHALE_HOLY_GRAIL) //Unique items that don't appear anywhere else new /obj/item/gun/ballistic/rifle/boltaction/harpoon(src) @@ -508,8 +508,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/officer(src) diff --git a/code/game/objects/items/tanks/watertank.dm b/code/game/objects/items/tanks/watertank.dm index 40ab024c340..df71835cdfa 100644 --- a/code/game/objects/items/tanks/watertank.dm +++ b/code/game/objects/items/tanks/watertank.dm @@ -470,3 +470,6 @@ reagents.trans_to(user, used_amount, multiplier=usage_ratio, methods = INJECT) update_appearance() user.update_inv_back() //for overlays update + +/datum/action/item_action/activate_injector + name = "Activate Injector" diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index b80e748a2bb..79e4295b65b 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -722,7 +722,7 @@ return COMSIG_CARBON_SHOVE_HANDLED /// Signal proc for [COMSIG_ATOM_MAGICALLY_UNLOCKED]. Unlock and open up when we get knock casted. -/obj/structure/closet/proc/on_magic_unlock(datum/source, obj/effect/proc_holder/spell/aoe_turf/knock/spell, mob/living/caster) +/obj/structure/closet/proc/on_magic_unlock(datum/source, datum/action/cooldown/spell/aoe/knock/spell, mob/living/caster) SIGNAL_HANDLER locked = FALSE diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm index 8400cdff964..758e55d7020 100644 --- a/code/game/objects/structures/icemoon/cave_entrance.dm +++ b/code/game/objects/structures/icemoon/cave_entrance.dm @@ -155,7 +155,7 @@ GLOBAL_LIST_INIT(ore_probability, list( if(9) new /obj/item/immortality_talisman(loc) if(10) - new /obj/item/book/granter/spell/summonitem(loc) + new /obj/item/book/granter/action/spell/summonitem(loc) if(11) new /obj/item/clothing/neck/necklace/memento_mori(loc) if(12) @@ -191,6 +191,6 @@ GLOBAL_LIST_INIT(ore_probability, list( if(26) new /obj/item/clothing/shoes/winterboots/ice_boots(loc) if(27) - new /obj/item/book/granter/spell/sacredflame(loc) + new /obj/item/book/granter/action/spell/sacredflame(loc) if(28) new /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom(loc) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index fa1cbf13d22..b885bb12b81 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -85,7 +85,6 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/view_opfors, //SKYRAT EDIT /datum/admins/proc/open_borgopanel, /datum/admins/proc/view_all_circuits, - /datum/admins/proc/view_all_sdql_spells, /datum/admins/proc/known_alts_panel, /datum/admins/proc/paintings_manager, /datum/admins/proc/display_tags, @@ -218,13 +217,13 @@ GLOBAL_PROTECT(admin_verbs_debug) /datum/admins/proc/create_or_modify_area, /client/proc/check_timer_sources, /client/proc/toggle_cdn, - /client/proc/cmd_sdql_spell_menu, /client/proc/adventure_manager, /client/proc/load_circuit, /client/proc/cmd_admin_toggle_fov, /client/proc/cmd_admin_debug_traitor_objectives, /client/proc/spawn_debug_full_crew, /client/proc/validate_puzzgrids, + /client/proc/debug_spell_requirements, ) GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release)) GLOBAL_PROTECT(admin_verbs_possess) @@ -692,12 +691,29 @@ GLOBAL_PROTECT(admin_verbs_hideable) 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/spell in GLOB.spells) - spell_list[copytext_char("[spell]", type_length)] = spell - var/spell_desc = input("Choose the spell to give to that guy", "ABRAKADABRA") as null|anything in sort_list(spell_list) - if(!spell_desc) + 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") @@ -707,37 +723,43 @@ GLOBAL_PROTECT(admin_verbs_hideable) 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(spell_recipient)] the spell [spell_desc][robeless ? " (Forced robeless)" : ""].") - message_admins(span_adminnotice("[key_name_admin(usr)] gave [key_name_admin(spell_recipient)] the spell [spell_desc][spell_desc][robeless ? " (Forced robeless)" : ""].")) + 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)" : ""].") - var/spell_path = spell_list[spell_desc] - var/obj/effect/proc_holder/spell/new_spell = new spell_path() + var/datum/action/cooldown/spell/new_spell = new spell_path(spell_recipient.mind || spell_recipient) if(robeless) - new_spell.clothes_req = FALSE + new_spell.spell_requirements &= ~SPELL_REQUIRES_WIZARD_GARB - if(spell_recipient.mind) - spell_recipient.mind.AddSpell(new_spell) - else - spell_recipient.AddSpell(new_spell) - message_admins(span_danger("Spells given to mindless mobs will not be transferred in mindswap or cloning!")) + 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/removal_target in GLOB.mob_list) set category = "Admin.Fun" set name = "Remove Spell" set desc = "Remove a spell from the selected mob." - var/target_spell_list = length(removal_target?.mind?.spell_list) ? removal_target.mind.spell_list : removal_target.mob_spell_list + 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/obj/effect/proc_holder/spell/removed_spell = input("Choose the spell to remove", "NO ABRAKADABRA") as null|anything in sort_list(target_spell_list) - if(removed_spell) - removal_target.mind.RemoveSpell(removed_spell) - log_admin("[key_name(usr)] removed the spell [removed_spell] from [key_name(removal_target)].") - message_admins(span_adminnotice("[key_name_admin(usr)] removed the spell [removed_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! + 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 = "Admin.Fun" @@ -962,3 +984,41 @@ GLOBAL_PROTECT(admin_verbs_hideable) CHECK_TICK to_chat(admin, "[number_made] crewmembers have been created.") + +/// 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() diff --git a/code/modules/admin/verbs/SDQL2/SDQL_spells/executor_component.dm b/code/modules/admin/verbs/SDQL2/SDQL_spells/executor_component.dm deleted file mode 100644 index 35f8b74e6fa..00000000000 --- a/code/modules/admin/verbs/SDQL2/SDQL_spells/executor_component.dm +++ /dev/null @@ -1,52 +0,0 @@ -/datum/component/sdql_executor - var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS" - var/giver //The ckey of the user that gave this spell - var/suppress_message_admins - var/list/scratchpad = list() //Use this to store vars in between queries and casts. - var/list/saved_overrides = list() - -/datum/component/sdql_executor/Initialize(giver) - src.giver = giver - -//Returns the address of x without the square brackets around it. -#define RAW_ADDRESS(x) copytext("\ref[x]",2,-1) - -/datum/component/sdql_executor/proc/execute(list/targets, mob/user) - if(!CONFIG_GET(flag/sdql_spells)) - return - if(!length(query)) - return - var/query_text = query - var/message_query = query - var/list/targets_and_user_list = list(user) - if(targets) - targets_and_user_list += targets - var/targets_and_user_string = ref_list(targets_and_user_list) - var/targets_string = ref_list(targets) - query_text = replacetextEx_char(query_text, "TARGETS_AND_USER", "[targets_and_user_string]") - message_query = replacetext_char(message_query, "TARGETS_AND_USER", (targets_and_user_list.len > 3) ? "\[[targets_and_user_list.len] items]" : targets_and_user_string) - query_text = replacetextEx_char(query_text, "USER", "{[RAW_ADDRESS(user)]}") - message_query = replacetextEx_char(message_query, "USER", "{[RAW_ADDRESS(user)]}") - query_text = replacetextEx_char(query_text, "TARGETS", "[targets_string]") - message_query = replacetextEx_char(message_query, "TARGETS", (targets?.len > 3) ? "\[[targets.len] items]" : targets_string) - query_text = replacetextEx_char(query_text, "SOURCE", "{[RAW_ADDRESS(parent)]}") - message_query = replacetextEx_char(message_query, "SOURCE", "{[RAW_ADDRESS(parent)]}") - query_text = replacetextEx_char(query_text, "SCRATCHPAD", "({[RAW_ADDRESS(src)]}.scratchpad)") - message_query = replacetextEx_char(message_query, "SCRATCHPAD", "({[RAW_ADDRESS(src)]}.scratchpad)") - if(!usr) //We need to set AdminProcCaller manually because it won't be set automatically by WrapAdminProcCall if usr is null - GLOB.AdminProcCaller = "SDQL_SPELL_OF_[user.ckey]" - GLOB.AdminProcCallCount++ - world.SDQL2_query(query_text, "[key_name(user, TRUE)] (via an SDQL spell given by [giver])", "[key_name(user)] (via an SDQL spell given by [giver])", silent = suppress_message_admins) - GLOB.AdminProcCallCount-- - GLOB.AdminProcCaller = null - -/datum/component/sdql_executor/proc/ref_list(list/L) - if(isnull(L) || !L.len) - return "\[]" - var/ret = "\[" - for(var/i in 1 to L.len-1) - ret += "{[RAW_ADDRESS(L[i])]}," - ret += "{[RAW_ADDRESS(L[L.len])]}]" - return ret - -#undef RAW_ADDRESS diff --git a/code/modules/admin/verbs/SDQL2/SDQL_spells/spell_admin_panel.dm b/code/modules/admin/verbs/SDQL2/SDQL_spells/spell_admin_panel.dm deleted file mode 100644 index 1469982f2b6..00000000000 --- a/code/modules/admin/verbs/SDQL2/SDQL_spells/spell_admin_panel.dm +++ /dev/null @@ -1,77 +0,0 @@ -/// An admin verb to view all sdql spells, plus useful information -/datum/admins/proc/view_all_sdql_spells() - set category = "Admin.Game" - set name = "View All SDQL Spells" - - if(CONFIG_GET(flag/sdql_spells) || tgui_alert(usr, "SDQL spells are disabled. Open the admin panel anyways?", "SDQL Admin Panel", list("Yes", "No")) == "Yes") - var/static/datum/SDQL_spell_panel/SDQL_spell_panel = new - SDQL_spell_panel.ui_interact(usr) - -/datum/SDQL_spell_panel - -/datum/SDQL_spell_panel/ui_static_data(mob/user) - var/list/data = list() - data["spells"] = list() - - for (var/obj/effect/proc_holder/spell/spell as anything in GLOB.sdql_spells) - var/mob/living/owner = spell.owner.resolve() - var/datum/component/sdql_executor/executor = spell.GetComponent(/datum/component/sdql_executor) - if(!executor) - continue - - data["spells"] += list(list( - "ref" = REF(spell), - "name" = "[spell]", - "owner" = owner, - "ownerRef" = REF(owner), - "creator" = executor.giver - )) - - return data - -/datum/SDQL_spell_panel/ui_act(action, list/params) - . = ..() - if (.) - return . - - switch(action) - if("edit_spell") - var/obj/effect/proc_holder/spell/spell = locate(params["spell"]) - if(!spell) - to_chat(usr, span_warning("That spell no longer exists!")) - return - var/datum/component/sdql_executor/executor = spell.GetComponent(/datum/component/sdql_executor) - if(!executor) - to_chat(usr, span_warning("[spell][spell.p_s()] SDQL executor component is gone!")) - return - if(usr.ckey == executor.giver || tgui_alert(usr, "You didn't create this SDQL spell. Edit it anyways?", "SDQL Admin Panel", list("Yes", "No")) == "Yes") - usr.client?.cmd_sdql_spell_menu(spell) - if("follow_owner") - var/mob/living/owner = locate(params["owner"]) - if(!owner) - to_chat(usr, span_warning("That mob no longer exists!")) - return - usr.client?.admin_follow(owner) - if("vv_spell") - var/obj/effect/proc_holder/spell/spell = locate(params["spell"]) - if(!spell) - to_chat(usr, span_warning("That spell no longer exists!")) - return - usr.client?.debug_variables(spell) - if("open_player_panel") - var/mob/living/owner = locate(params["owner"]) - if(!owner) - to_chat(usr, span_warning("That mob no longer exists!")) - return - usr.client?.holder?.show_player_panel(owner) - - return TRUE - -/datum/SDQL_spell_panel/ui_state(mob/user) - return GLOB.admin_state - -/datum/SDQL_spell_panel/ui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "SDQLSpellAdminPanel") - ui.open() diff --git a/code/modules/admin/verbs/SDQL2/SDQL_spells/spell_edit_menu.dm b/code/modules/admin/verbs/SDQL2/SDQL_spells/spell_edit_menu.dm deleted file mode 100644 index 7c74c38ca70..00000000000 --- a/code/modules/admin/verbs/SDQL2/SDQL_spells/spell_edit_menu.dm +++ /dev/null @@ -1,932 +0,0 @@ -GLOBAL_LIST_INIT_TYPED(sdql_spells, /obj/effect/proc_holder/spell, list()) - -/client/proc/cmd_sdql_spell_menu(target in GLOB.mob_list) - set name = "Give/Edit SDQL spell" - set hidden = TRUE - if(CONFIG_GET(flag/sdql_spells)) - var/datum/give_sdql_spell/ui = new(usr, target) - ui.ui_interact(usr) - else - to_chat(usr, span_warning("SDQL spells are disabled.")) - -/datum/give_sdql_spell - var/client/user - var/mob/living/target_mob - var/obj/effect/proc_holder/spell/target_spell - var/spell_type - var/list/saved_vars = list("query" = "", "suppress_message_admins" = FALSE) - var/list/list_vars = list() - var/list/parse_result = null - var/alert - - //This list contains all the vars that it should be okay to edit from the menu - var/static/list/editable_spell_vars = list( - "action_background_icon_state", - "action_icon_state", - "action_icon", - "active_msg", - "aim_assist", - "antimagic_allowed", - "base_icon_state", - "centcom_cancast", - "charge_max", - "charge_type", - "clothes_req", - "cone_level", - "deactive_msg", - "desc", - "drawmessage", - "dropmessage", - "hand_path", - "hand_var_overrides", - "holder_var_amount", - "holder_var_type", - "human_req", - "include_user", - "inner_radius", - "invocation_emote_self", - "invocation_type", - "invocation", - "max_targets", - "message", - "name", - "nonabstract_req", - "overlay_icon_state", - "overlay_icon", - "overlay_lifespan", - "overlay", - "phase_allowed", - "player_lock", - "projectile_type", - "projectile_amount", - "projectile_var_overrides", - "projectiles_per_fire", - "random_target_priority", - "random_target", - "range", - "ranged_mousepointer", - "respect_density", - "selection_type", - "self_castable", - "smoke_amt", - "smoke_spread", - "sound", - "sparks_amt", - "sparks_spread", - "stat_allowed", - "still_recharging_msg", - "target_ignore_prev", - ) - - //If a spell creates a datum with vars it overrides, this list should contain an association with the variable containing the path of the created datum. - var/static/list/special_list_vars = list( - "projectile_var_overrides" = "projectile_type", - "hand_var_overrides" = "hand_path", - ) - - var/static/list/special_var_lists = list( - "projectile_type" = "projectile_var_overrides", - "hand_path" = "hand_var_overrides", - ) - - var/static/list/enum_vars = list( - "invocation_type" = list(INVOCATION_NONE, INVOCATION_WHISPER, INVOCATION_SHOUT, INVOCATION_EMOTE), - "selection_type" = list("view", "range"), - "smoke_spread" = list(0, 1, 2, 3), - "random_target_priority" = list(0, 1), - ) - - //base64 representations of any icons that may need to be displayed - var/action_icon_base64 - var/projectile_icon_base64 - var/hand_icon_base64 - var/overlay_icon_base64 - var/mouse_icon_base64 - -/datum/give_sdql_spell/New(user, target) - if(!CONFIG_GET(flag/sdql_spells)) - to_chat(user, span_warning("SDQL spells are disabled.")) - qdel(src) - return - src.user = CLIENT_FROM_VAR(user) - - if(istype(target, /obj/effect/proc_holder/spell)) - target_spell = target - var/mob/living/spell_owner = target_spell.owner.resolve() - if(spell_owner) - target_mob = spell_owner - else - to_chat(user, span_warning("[target_spell] does not have an owner, or its owner was qdelled. This REALLY shouldn't happen.")) - qdel(src) - return - else if(isliving(target)) - target_mob = target - else - to_chat(user, span_warning("Invalid target.")) - qdel(src) - return - if(target_spell) - load_vars_from(target_spell) - -/datum/give_sdql_spell/ui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "SDQLSpellMenu", "Give SDQL Spell") - ui.open() - ui.set_autoupdate(FALSE) - -/datum/give_sdql_spell/ui_state(mob/user) - return GLOB.admin_state - -/datum/give_sdql_spell/ui_status(mob/user, datum/ui_state/state) - if(QDELETED(target_mob)) - return UI_CLOSE - return ..() - -/datum/give_sdql_spell/ui_close(mob/user) - qdel(src) - -#define SANITIZE_NULLIFY 0 -#define SANITIZE_STRINGIFY 1 - -/datum/give_sdql_spell/ui_data(mob/user, params) - var/list/data = list() - if(target_spell) - data["type"] = copytext("[target_spell.type]", 31, -5) - data["fixed_type"] = TRUE - else - data["type"] = spell_type - data["fixed_type"] = FALSE - data["saved_vars"] = saved_vars - data["list_vars"] = json_sanitize_list_vars(list_vars, SANITIZE_STRINGIFY) - if(parse_result) - data["parse_errors"] = parse_result["parse_errors"] - data["parsed_type"] = parse_result["type"] - data["parsed_vars"] = parse_result["vars"] - data["parsed_list_vars"] = json_sanitize_list_vars(parse_result["list_vars"], SANITIZE_STRINGIFY) - else - data["parse_errors"] = null - data["parsed_type"] = null - data["parsed_vars"] = null - data["parsed_list_vars"] = null - data["action_icon"] = action_icon_base64 - data["projectile_icon"] = projectile_icon_base64 - data["hand_icon"] = hand_icon_base64 - data["overlay_icon"] = overlay_icon_base64 - data["mouse_icon"] = mouse_icon_base64 - data["alert"] = alert - alert = "" - return data - -/datum/give_sdql_spell/ui_static_data(mob/user) - return list( - "types" = list("aimed", "aoe_turf", "cone", "cone/staggered", "pointed", "self", "targeted", "targeted/touch"), - "tooltips" = list( - "query" = "The SDQL query that is executed. Certain keywords are specific to SDQL spell queries.\n\ - $type\n\ - USER is replaced with a reference to the user of the spell.\n\ - TARGETS_AND_USER is replaced with the combined references from TARGETS and USER.\n\ - SOURCE is replaced with a reference to this spell, allowing you to refer to and edit variables within it.\n\ - SCRATCHPAD is a list used to store variables between individual queries within the same cast or between multiple casts.\n\ - NOTE: The SDQL keywords usr and marked do not work.", - "query_aimed" = "TARGETS is replaced with a list containing a reference to the atom hit by the fired projectile.", - "query_aoe_turf" = "TARGETS is replaced with a list containing references to every atom in the spell's area of effect.", - "query_cone" = "TARGETS is replaced with a list containing references to every atom in the cone produced by the spell.", - "query_cone/staggered" = "The query will be executed once for every level of the cone produced by the spell.\n\ - TARGETS is replaced with a list containing references to every atom in the given level of the cone.", - "query_pointed" = "TARGETS is replaced with a list containing a reference to the targeted atom.", - "query_self" = "TARGETS is null.", - "query_targeted" = "TARGETS is replaced with a list containing a reference(s) to the targeted mob(s).", - "query_targeted_touch" = "TARGETS is replaced with a list containing a reference to the atom hit with the touch attack.", - "suppress_message_admins" = "If this is true, the spell will not print out its query to admins' chat panels.\n\ - The query will still be output to the game log.", - "charge_type" = "How the spell's charge works. This affects how charge_max is used.\n\ - When set to \"recharge\", charge_max is the time in deciseconds between casts of the spell.\n\ - When set to \"charges\", the user can only use the spell a number of times equal to charge_max.\n\ - When set to \"holder_var\", charge_max is not used. holder_var_type and holder_var_amount are used instead.\n", - "holder_var_type" = "When charge_type is set to \"holder_var\", this is the name of the var that is modified each time the spell is cast.\n\ - If this is set to \"bruteloss\", \"fireloss\", \"toxloss\", or \"oxyloss\", the user will take the corresponding damage.\n\ - If this is set to \"stun\", \"knockdown\", \"paralyze\", \"immobilize\", or \"unconscious\", the user will suffer the corresponding status effect.\n\ - If this is set to anything else, the variable with the appropriate name will be modified.", - "holder_var_amount" = "The amount of damage taken, the duration of status effect inflicted, or the change made to any other variable.", - "clothes_req" = "Whether the user has to be wearing wizard robes to cast the spell.", - "human_req" = "Whether the user has to be a human to cast the spell. Redundant when clothes_req is true.", - "nonabstract_req" = "If this is true, the spell cannot be cast by brains and pAIs.", - "stat_allowed" = "Whether the spell can be cast if the user is unconscious or dead.", - "phase_allowed" = "Whether the spell can be cast while the user is jaunting or bloodcrawling.", - "antimagic_allowed" = "Whether the spell can be cast while the user is affected by anti-magic effects.", - "invocation_type" = "How the spell is invoked.\n\ - When set to \"none\", the user will not state anything when invocating.\n\ - When set to \"whisper\", the user whispers the invocation, as if with the whisper verb.\n\ - When set to \"shout\", the user says the invocation, as if with the say verb.\n\ - When set to \"emote\", a visible message is produced.", - "invocation" = "What the user says, whispers, or emotes when using the spell.", - "invocation_emote_self" = "What the user sees in their own chat when they use the spell.", - "selection_type" = "Whether the spell can target any mob in range, or only visible mobs in range.", - "range" = "The spell's range, in tiles.", - "message" = "What mobs affected by the spell see in their chat.\n\ - Keep in mind, just because a mob is affected by the spell doesn't mean the query will have any effect on them.", - "player_lock" = "If false, simple mobs can use the spell.", - "overlay" = "Whether an overlay is drawn atop atoms affectecd by the spell.\n\ - Keep in mind, just because an atom is affected by the spell doesn't mean the query will have any effect on it.", - "overlay_lifetime" = "The amount of time in deciseconds the overlay will persist.", - "sparks_spread" = "Whether the spell produces sparks when cast.", - "smoke_spread" = "The kind of smoke, if any, the spell produces when cast.", - "centcom_cancast" = "If true, the spell can be cast on the centcom Z-level.", - "max_targets" = "The maximum number of mobs the spell can target.", - "target_ignore_prev" = "If false, the same mob can be targeted multiple times.", - "include_user" = "If true, the user can target themselves with the spell.", - "random_target" = "If true, the spell will target a random mob(s) in range.", - "random_target_priority" = "Whether the spell will target random mobs in range or the closest mobs in range.", - "inner_radius" = "If this is a non-negative number, the spell will not affect atoms within that many tiles of the user.", - "ranged_mousepointer" = "The icon used for the mouse when aiming the spell.", - "deactive_mesg" = "The message the user sees when canceling the spell.", - "active_msg" = "The message the user sees when activating the spell.", - "projectile_amount" = "The maximum number of projectiles the user can fire with each cast of the spell.", - "projectiles_per_fire" = "The amount of projectiles fired with each click of the mouse.", - "projectile_var_overrides" = "The fired projectiles will have the appropriate variables overridden by the corresponding values in this associative list.\n\ - You should probably set \"name\", \"icon\", and \"icon_state\".\n\ - Refer to code/modules/projectiles/projectile.dm to see what other vars you can override.", - "cone_level" = "How many tiles out the cone will extend.", - "respect_density" = "If true, the cone produced by the spell is blocked by walls.", - "self_castable" = "If true, the user can cast the spell on themselves.", - "aim_assist" = "If true, the spell has turf-based aim assist.", - "drawmessage" = "The message the user sees when activating the spell.", - "dropmessage" = "The message the user sees when canceling the spell.", - "hand_var_overrides" = "The touch attack will have the appropriate variables overridden by the corresponding values in this associative list.\n\ - You should probably set \"name\", \"desc\", \"catchphrase\", \"on_use_sound\" \"icon\", \"icon_state\", and \"inhand_icon_state\".\n\ - Refer to code/modules/spells/spell_types/godhand.dm to see what other vars you can override.", - "scratchpad" = "This list can be used to store variables between individual queries within the same cast or between casts.\n\ - You can declare variables from this menu for convenience. To access this list in a query, use the identifier \"SOURCE.scratchpad\".\n\ - Refer to the _list procs defined in code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm for information on how to modify and edit list vars from within a query.", - ), - ) - -#define LIST_VAR_FLAGS_TYPED 1 -#define LIST_VAR_FLAGS_NAMED 2 - -/datum/give_sdql_spell/proc/load_vars_from(obj/effect/proc_holder/spell/sample) - var/datum/component/sdql_executor/executor = sample.GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[sample]'s SDQL executor component went missing!") - saved_vars["query"] = executor.query - saved_vars["suppress_message_admins"] = executor.suppress_message_admins - load_list_var(executor.scratchpad, "scratchpad") - for(var/V in sample.vars&editable_spell_vars) - if(islist(sample.vars[V])) - if(special_list_vars[V]) - var/list/saved_overrides = executor.saved_overrides[V] - if(saved_overrides) - list_vars[V] = saved_overrides.Copy() - icon_needs_updating("[V]/icon") - else - saved_vars[V] = sample.vars[V] - icon_needs_updating(V) - -/datum/give_sdql_spell/proc/load_list_var(list/L, list_name) - list_vars[list_name] = list() - for(var/V in L) - if(islist(L[V])) - list_vars[list_name][V] = list("type" = "list", "value" = null) - list_vars |= load_list_var(L[V], "[list_name]/[V]") - else if(isnum(L[V])) - list_vars[list_name][V] = list("type" = "num", "value" = L[V]) - else if(ispath(L[V])) - list_vars[list_name][V] = list("type" = "path", "value" = L[V]) - else if(isicon(L[V])) - list_vars[list_name][V] = list("type" = "icon", "value" = L[V]) - else if(istext(L[V]) || isfile(L[V])) - list_vars[list_name][V] = list("type" = "string", "value" = L[V]) - else if(istype(L[V], /datum)) - list_vars[list_name][V] = list("type" = "ref", "value" = L[V]) - else if(isnull(L[V])) - list_vars[list_name][V] = list("type" = "num", "value" = 0) - alert = "Could not determine the type for [list_name]/[V]! Be sure to set it correctly, or you may cause unnecessary runtimes!" - -/datum/give_sdql_spell/ui_act(action, params, datum/tgui/ui) - if(..()) - return - . = TRUE - switch(action) - if("type") - if(!target_spell) - spell_type = params["path"] - load_sample() - if("variable") - var/V = params["name"] - if(V == "holder_var_type") - if(!holder_var_validate(params["value"])) - return - saved_vars[V] = params["value"] - icon_needs_updating(V) - if("bool_variable") - saved_vars[params["name"]] = !saved_vars[params["name"]] - if("path_variable") - var/new_path = tgui_input_list(user, "Select type.", "Add SDQL Spell", typesof(text2path(params["root_path"]))) - if(isnull(new_path)) - return - saved_vars[params["name"]] = new_path - var/datum/sample = new new_path - var/list/overrides = list_vars[special_var_lists[params["name"]]] - overrides = overrides&sample.vars - qdel(sample) - icon_needs_updating(params["name"]) - if("list_variable_add") - if(!list_vars[params["list"]]) - list_vars[params["list"]] = list() - if(special_list_vars[params["list"]]) - var/path = saved_vars[special_list_vars[params["list"]]] - var/datum/sample = new path - var/list/choosable_vars = map_var_list(sample.vars-list_vars[params["list"]], sample) - var/chosen_var = tgui_input_list(user, "Select variable to add.", "Add SDQL Spell", sort_list(choosable_vars)) - if(chosen_var) - if(islist(sample.vars[choosable_vars[chosen_var]])) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "list", "value" = null, "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) - list_vars["[params["list"]]/[choosable_vars[chosen_var]]"] = list() - else if(isnum(sample.vars[choosable_vars[chosen_var]])) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "num", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) - else if(ispath(sample.vars[choosable_vars[chosen_var]])) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "path", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) - else if(isicon(sample.vars[choosable_vars[chosen_var]])) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "icon", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) - else if(istext(sample.vars[choosable_vars[chosen_var]]) || isfile(sample.vars[choosable_vars[chosen_var]])) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "string", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) - else if(istype(sample.vars[choosable_vars[chosen_var]], /datum)) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "ref", "value" = null, "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) - alert = "[params["list"]]/[choosable_vars[chosen_var]] is a reference! Be sure to set it correctly, or you may cause unnecessary runtimes!" - else if(isnull(sample.vars[choosable_vars[chosen_var]])) - list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "num", "value" = 0, "flags" = LIST_VAR_FLAGS_NAMED) - alert = "Could not determine the type for [params["list"]]/[choosable_vars[chosen_var]]! Be sure to set it correctly, or you may cause unnecessary runtimes!" - else - alert = "[params["list"]]/[choosable_vars[chosen_var]] is not of a supported type!" - icon_needs_updating("[params["list"]]/[choosable_vars[chosen_var]]") - qdel(sample) - else - if(!list_vars[params["list"]]["new_var"]) - list_vars[params["list"]] += list("new_var" = list("type" = "num", "value" = 0, "flags" = 0)) - else - alert = "Rename or remove [params["list"]]/new_var before attempting to add another variable to this list!" - if("list_variable_remove") - remove_list_var(params["list"], params["name"]) - if("list_variable_rename") - rename_list_var(params["list"], params["name"], params["new_name"]) - if("list_variable_change_type") - change_list_var_type(params["list"], params["name"], params["value"]) - if("list_variable_change_value") - set_list_var(params["list"], params["name"], params["value"]) - icon_needs_updating("[params["list"]]/[params["name"]]") - if("list_variable_change_bool") - toggle_list_var(params["list"], params["name"]) - if("list_variable_set_ref") - set_list_ref_var(params["list"], params["name"]) - if("save") - var/f = file("data/TempSpellUpload") - fdel(f) - WRITE_FILE(f, json_encode(list("type" = spell_type, "vars" = saved_vars, "list_vars" = json_sanitize_list_vars(list_vars)))) - user << ftp(f,"[replacetext_char(saved_vars["name"], " ", "_")].json") - if("load") - var/spell_file = input("Pick spell json file:", "File") as null|file - if(!spell_file) - return - var/filedata = file2text(spell_file) - var/json = json_decode(filedata) - if(!json) - alert = "JSON decode error!" - return - parse_result = load_from_json(json) - var/list/parse_errors = parse_result["parse_errors"] - if(!parse_errors.len) - finalize_load() - if("close_error") - parse_result = null - if("load_despite_error") - finalize_load() - if("confirm") - if(target_spell) - reassign_vars(target_spell) - target_spell.action.UpdateButtons() - log_admin("[key_name(user)] edited the SDQL spell \"[target_spell]\" owned by [key_name(target_mob)].") - else - var/new_spell = give_spell() - log_admin("[key_name(user)] gave the SDQL spell \"[new_spell]\" to [key_name(target_mob)].") - ui.close() - -/datum/give_sdql_spell/proc/load_sample() - var/path = text2path("/obj/effect/proc_holder/spell/[spell_type]/sdql") - var/datum/sample = new path - if(spell_type) - load_vars_from(sample) - qdel(sample) - -/datum/give_sdql_spell/proc/finalize_load() - spell_type = parse_result["type"] - load_sample() - saved_vars = parse_result["vars"] | saved_vars - list_vars = parse_result["list_vars"] | list_vars - parse_result = null - icon_needs_updating("everything") - -//Change all references in the list vars, either to null (for saving) or to their string representation (for display) -/datum/give_sdql_spell/proc/json_sanitize_list_vars(list/list_vars, mode = SANITIZE_NULLIFY) - var/list/temp_list_vars = deep_copy_list(list_vars) - for(var/V in temp_list_vars) - var/list/L = temp_list_vars[V] - for(var/W in L) - if(temp_list_vars[V][W]["type"] == "ref") - switch(mode) - if(SANITIZE_NULLIFY) - temp_list_vars[V][W]["value"] = null - if(SANITIZE_STRINGIFY) - if(temp_list_vars[V][W]["value"]) - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - else - temp_list_vars[V][W]["value"] = "null" - return temp_list_vars - -#undef SANITIZE_NULLIFY -#undef SANITIZE_STRINGIFY - -/datum/give_sdql_spell/proc/load_from_json(json) - var/list/result_vars = list() - var/list/result_list_vars = list() - var/list/parse_errors = list() - . = list("type" = "", - "vars" = result_vars, - "list_vars" = result_list_vars, - "parse_errors" = parse_errors) - if(!json["type"]) - parse_errors += "The \"type\" property is missing from the json file" - return - var/temp_type = json["type"] - var/datum/D = text2path("/obj/effect/proc_holder/spell/[temp_type]/sdql") - if(!ispath(D)) - parse_errors += "[temp_type] is not a valid SDQL spell type" - return - if(target_spell) - if(!istype(target_spell, D)) - parse_errors += "You cannot change the type of an existing spell" - if(!json["vars"]) - parse_errors += "The \"vars\" property is missing from the json file" - if(!islist(json["vars"])) - parse_errors += "The \"vars\" property must be a json object" - if(!json["list_vars"]) - parse_errors += "The \"list_vars\" property is missing from the json file" - if(!islist(json["list_vars"])) - parse_errors += "The \"list_vars\" property must be a json object" - if(parse_errors.len) - return - .["type"] = temp_type - var/list/temp_vars = json["vars"] - var/list/temp_list_vars = json["list_vars"] - D = new D - for(var/V in temp_vars) - if(!istext(V)) - parse_errors += "JSON property names must be text ([V] is not text)" - continue - if(V == "query") - if(!istext(temp_vars[V])) - parse_errors += "The value of \"query\" must be text" - continue - result_vars[V] = temp_vars[V] - continue - if(V == "suppress_message_admins") - if(!isnum(temp_vars[V])) - parse_errors += "The value of \"suppress_message_admins\" must be a number" - continue - result_vars[V] = !!temp_vars[V] - continue - if(!(V in editable_spell_vars)) - parse_errors += "\"[V]\" is not an editable variable" - continue - if(!(V in D.vars)) //D.vars[V] can runtime unlike V in D.vars - parse_errors += "Spells of type \"[temp_type]\" have no such var [V]" - continue - if(islist(D.vars[V])) - parse_errors += "[D.type]/[V] is a list; vars.[V] should be in the \"list_vars\" property" - continue - if(istext(D.vars[V])) - if(!istext(temp_vars[V])) - parse_errors += "[D.type]/[V] is text; vars.[V] has been converted to text" - temp_vars[V] = "[temp_vars[V]]" - continue - if(V=="holder_var_type") - var/potential_alert = holder_var_validate(temp_vars[V], TRUE) - if(potential_alert) - parse_errors += potential_alert - continue - if(isicon(D.vars[V])) - if(!istext(temp_vars[V])) - parse_errors += "[D.type]/[V] is an icon; vars.[V] has been converted to text" - temp_vars[V] = "[temp_vars[V]]" - if(!fexists(temp_vars[V])) - parse_errors += "[D.type]/[V] is an icon; no such file [temp_vars[V]] exists on the server" - continue - if(ispath(D.vars[V])) - if(!istext(temp_vars[V])) - parse_errors += "[D.type]/[V] is a path; vars.[V] has been converted to text" - temp_vars[V] = "[temp_vars[V]]" - var/path = text2path(temp_vars[V]) - if(!path) - parse_errors += "[D.type]/[V] is a path; vars.[V] ([temp_vars[V]]) does not correspond to an existing type" - continue - if(!ispath(path, D.vars[V])) - parse_errors += "[D.type]/[V] is a path; vars.[V] ([path]) is not derived from [D.vars[V]]" - continue - if(isnum(D.vars[V])) - if(!isnum(temp_vars[V])) - parse_errors += "[D.type]/[V] is a number; vars.[V] should be a number" - continue - if(enum_vars[V]) - var/list/enum = enum_vars[V] - if(!enum.Find(temp_vars[V])) - parse_errors += "[D.type]/[V] is an enumeration; vars.[V] should be one of: [english_list(enum, and_text = " or ")]" - continue - result_vars[V] = temp_vars[V] - for(var/V in temp_list_vars) - if(!istext(V)) - parse_errors += "JSON property names must be text ([V] is not text)" - continue - if(!islist(temp_list_vars[V])) - parse_errors += "list_vars.[V] should be a json object" - continue - if(special_list_vars[V] && (V in D.vars)) - var/sample_path = D.vars[special_list_vars[V]] - var/temp_path - if(temp_vars[special_list_vars[V]]) - temp_path = temp_vars[special_list_vars[V]] - temp_path = text2path(temp_path) - if(!temp_path) - parse_errors += "[D.type]/[special_list_vars[V]] is a path; vars.[temp_vars[special_list_vars[V]]] (temp_vars[special_list_vars[V]]) does not correspond to an existing type" - else if(!ispath(temp_path, D.vars[special_list_vars[V]])) - parse_errors += "[D.type]/[special_list_vars[V]] is a path; vars.[special_list_vars[V]] ([temp_path]) is not derived from [D.vars[special_list_vars[V]]]" - else - sample_path = temp_path - result_list_vars[V] = list() - var/datum/sample = new sample_path - for(var/W in temp_list_vars[V]) - if(!istext(W)) - parse_errors += "JSON property names must be text ([W] in list_vars.[V] is not text)" - continue - if(!(W in sample.vars)) - parse_errors += "[sample.type] has no such var \"[W]\"" - continue - if(!(islist(temp_list_vars[V][W]) && istext(temp_list_vars[V][W]["type"]) && (istext(temp_list_vars[V][W]["value"]) || isnum(temp_list_vars[V][W]["value"]) || isnull(temp_list_vars[V][W]["value"])) && isnum(temp_list_vars[V][W]["flags"]))) - parse_errors += "[V]/[W] is not of the form {type: string, value: num|string|null, flags: number}" - continue - if(!(temp_list_vars[V][W]["flags"] & LIST_VAR_FLAGS_NAMED)) - parse_errors += "[V]/[W] did not have the LIST_VAR_FLAGS_NAMED flag set; it has been set" - temp_list_vars[V][W]["flags"] |= LIST_VAR_FLAGS_NAMED - if(temp_list_vars[V][W]["flags"] & ~(LIST_VAR_FLAGS_NAMED | LIST_VAR_FLAGS_TYPED)) - parse_errors += "[V]/[W] has unused bit flags set; they have been unset" - temp_list_vars[V][W]["flags"] &= LIST_VAR_FLAGS_NAMED | LIST_VAR_FLAGS_TYPED - if(!(temp_list_vars[V][W]["flags"] & LIST_VAR_FLAGS_TYPED)) - if(isnull(sample.vars[W])) - continue - parse_errors += "[sample.type]/[W] is not null; it has had the LIST_VAR_FLAGS_TYPED flag set" - temp_list_vars[V][W]["flags"] |= LIST_VAR_FLAGS_TYPED - if(islist(sample.vars[W])) - temp_list_vars[V][W]["type"] = "list" - else if(isnum(sample.vars[W])) - temp_list_vars[V][W]["type"] = "num" - else if(istext(sample.vars[W])) - temp_list_vars[V][W]["type"] = "string" - else if(ispath(sample.vars[W])) - temp_list_vars[V][W]["type"] = "path" - else if(isicon(sample.vars[W])) - temp_list_vars[V][W]["type"] = "icon" - else if(istype(sample.vars[W], /datum)) - temp_list_vars[V][W]["type"] = "ref" - temp_list_vars[V][W]["value"] = null - else - parse_errors += "[sample.type]/[W] is not of a supported type" - continue - if(islist(sample.vars[W])) - if(temp_list_vars[V][W]["type"] != "list") - parse_errors += "[sample.type]/[W] is a list; list_vars.[V].[W].type has been converted to \"list\"" - temp_list_vars[V][W]["type"] = "list" - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"list\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - if(!temp_list_vars[temp_list_vars[V][W]["value"]]) - parse_errors += "list_vars.[V].[W].type is \"list\"; there is no property of list_vars whose name is list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]])" - continue - else if(isnum(sample.vars[W])) - if(temp_list_vars[V][W]["type"] != "num") - parse_errors += "[sample.type]/[W] is a number; list_vars.[V].[W].type has been converted to \"num\"" - temp_list_vars[V][W]["type"] = "num" - if(!isnum(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"num\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) should be a number" - continue - else if(istext(sample.vars[W])) - if(temp_list_vars[V][W]["type"] != "string") - parse_errors += "[sample.type]/[W] is text; list_vars.[V].[W].type has been converted to \"string\"" - temp_list_vars[V][W]["type"] = "string" - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"list\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - else if(ispath(sample.vars[W])) - if(temp_list_vars[V][W]["type"] != "path") - parse_errors += "[sample.type]/[W] is a path; list_vars.[V].[W].type has been converted to \"path\"" - temp_list_vars[V][W]["type"] = "path" - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - temp_path = text2path(temp_list_vars[V][W]["value"]) - if(!ispath(temp_path)) - parse_errors += "list_vars.[W].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) does not correspond to an existing type" - continue - if(!ispath(temp_path, sample.vars[W])) - parse_errors += "list_vars.[W].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) is not derived from [sample.vars[W]]" - continue - else if(isicon(sample.vars[W])) - if(temp_list_vars[V][W]["type"] != "icon") - parse_errors += "[sample.type]/[W] is an icon; list_vars.[V].[W].type has been converted to \"icon\"" - temp_list_vars[V][W]["type"] = "icon" - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"icon\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - if(!fexists(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"icon\"; no such file \"[temp_list_vars[V][W]["value"]]\" exists on the server" - continue - else if(istype(sample.vars[W], /datum)) - if(temp_list_vars[V][W]["type"] != "ref") - parse_errors += "[sample.type]/[W] is a datum reference; list_vars.[V].[W].type has been converted to \"ref\"" - alert = "Reference vars are not assigned on load from file. Be sure to set them correctly." - temp_list_vars[V][W]["type"] = "ref" - temp_list_vars[V][W]["value"] = null - result_list_vars[V][W] = temp_list_vars[V][W] - qdel(sample) - else - result_list_vars[V] = list() - for(var/W in temp_list_vars[V]) - if(temp_list_vars[V][W]["flags"]) - parse_errors += "list_vars.[V].[W] has unnecessary flags set; they have been unset" - temp_list_vars[V][W]["flags"] = 0 - if(temp_list_vars[V][W]["type"] == "list") - if(temp_list_vars[V][W]["value"]) - parse_errors += "list_vars.[V].[W].type is \"list\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been nulled, as it is not used" - temp_list_vars[V][W]["value"] = null - if(!temp_list_vars["[V]/[W]"]) - parse_errors += "list_vars.[V].[W].type is \"list\"; there is no property of list_vars whose name is \"[V]/[W]\"" - continue - if(temp_list_vars[V][W]["type"] == "num") - if(!isnum(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"num\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) should be a number" - continue - if(temp_list_vars[V][W]["type"] == "string") - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"string\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - if(temp_list_vars[V][W]["type"] == "path") - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - var/temp_path = text2path(temp_list_vars[V][W]["value"]) - if(!ispath(temp_path)) - parse_errors += "list_vars.[W].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) does not correspond to an existing type" - continue - if(temp_list_vars[V][W]["type"] == "icon") - if(!istext(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"icon\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text" - temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]" - if(!fexists(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"icon\"; no such file \"[temp_list_vars[V][W]["value"]]\" exists on the server" - continue - if(temp_list_vars[V][W]["type"] == "ref") - if(!isnull(temp_list_vars[V][W]["value"])) - parse_errors += "list_vars.[V].[W].type is \"ref\"; list_vars.[V].[W].value has been nulled out" - temp_list_vars[V][W]["value"] = null - else - alert = "Reference vars are not assigned on load from file. Be sure to set them correctly." - result_list_vars[V][W] = temp_list_vars[V][W] - qdel(D) - -#undef LIST_VAR_FLAGS_TYPED -#undef LIST_VAR_FLAGS_NAMED - -/datum/give_sdql_spell/proc/map_var_list(list/L, datum/D) - var/list/ret = list() - for(var/V in L) - if(D.vars[V]) - ret["[V] = [string_rep(D.vars[V])]"] = V - return ret - -/datum/give_sdql_spell/proc/string_rep(V) - if(istext(V) || isfile(V) || isicon(V)) - return "\"[V]\"" - else if(isnull(V)) - return "null" - else - return "[V]" - -/datum/give_sdql_spell/proc/holder_var_validate(V, return_alert = FALSE) - switch(V) - if("bruteloss", "fireloss", "toxloss", "oxyloss", "stun", "knockdown", "paralyze", "unconscious") - if(return_alert) - return "" - return TRUE - else - if(target_mob.vars[V]) - if(!isnum(target_mob.vars[V])) - var/new_alert = "[target_mob.type]/[V] is not a number!" - if(return_alert) - return new_alert - alert = new_alert - return FALSE - else - return return_alert ? "" : TRUE - else - var/new_alert = "[target_mob.type] has no such variable [V]!" - if(return_alert) - return new_alert - alert = new_alert - return FALSE - -/datum/give_sdql_spell/proc/icon_needs_updating(var_name) - switch(var_name) - if("action_icon", "action_icon_state", "action_background_icon_state") - var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") - var/image/out_image = image('icons/mob/actions/backgrounds.dmi', null, saved_vars["action_background_icon_state"]) - var/overlay_icon = icon(saved_vars["action_icon"], saved_vars["action_icon_state"]) - out_image.overlays += image(overlay_icon) - out_icon.Insert(getFlatIcon(out_image, no_anim = TRUE)) - action_icon_base64 = icon2base64(out_icon) - - if("projectile_var_overrides/icon", "projectile_var_overrides/icon_state") - var/atom/A = /obj/projectile - var/icon = initial(A.icon) - var/icon_state = initial(A.icon_state) - if(list_vars["projectile_var_overrides"]?["icon"]) - icon = list_vars["projectile_var_overrides"]["icon"]["value"] - if(list_vars["projectile_var_overrides"]?["icon_state"]) - icon_state = list_vars["projectile_var_overrides"]["icon_state"]["value"] - var/icon/out_icon = icon(icon, icon_state, frame = 1) - projectile_icon_base64 = icon2base64(out_icon) - - if("hand_var_overrides/icon", "hand_var_overrides/icon_state") - var/atom/A = /obj/item/melee/touch_attack - var/icon = initial(A.icon) - var/icon_state = initial(A.icon_state) - if(list_vars["hand_var_overrides"]?["icon"]) - icon = list_vars["hand_var_overrides"]["icon"]["value"] - if(list_vars["hand_var_overrides"]?["icon_state"]) - icon_state = list_vars["hand_var_overrides"]["icon_state"]["value"] - var/icon/out_icon = icon(icon, icon_state, frame = 1) - hand_icon_base64 = icon2base64(out_icon) - - if("overlay", "overlay_icon", "overlay_icon_state") - var/icon/out_icon = icon(saved_vars["overlay_icon"], saved_vars["overlay_icon_state"], frame = 1) - overlay_icon_base64 = icon2base64(out_icon) - - if("ranged_mousepointer") - var/icon/out_icon = icon(saved_vars["ranged_mousepointer"], frame = 1) - mouse_icon_base64 = icon2base64(out_icon) - - if("everything") - var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") - var/image/out_image = image('icons/mob/actions/backgrounds.dmi', null, saved_vars["action_background_icon_state"]) - var/overlay_icon = icon(saved_vars["action_icon"], saved_vars["action_icon_state"]) - out_image.overlays += image(overlay_icon) - out_icon.Insert(getFlatIcon(out_image, no_anim = TRUE)) - action_icon_base64 = icon2base64(out_icon) - if(list_vars["projectile_var_overrides"]) - var/atom/A = saved_vars["projectile_type"] - var/icon = initial(A.icon) - var/icon_state = initial(A.icon_state) - if(list_vars["projectile_var_overrides"]?["icon"]) - icon = list_vars["projectile_var_overrides"]["icon"]["value"] - if(list_vars["projectile_var_overrides"]?["icon_state"]) - icon_state = list_vars["projectile_var_overrides"]["icon_state"]["value"] - out_icon = icon(icon, icon_state, frame = 1) - projectile_icon_base64 = icon2base64(out_icon) - if(list_vars["hand_var_overrides"]) - var/atom/A = saved_vars["hand_path"] - var/icon = initial(A.icon) - var/icon_state = initial(A.icon_state) - if(list_vars["hand_var_overrides"]?["icon"]) - icon = list_vars["hand_var_overrides"]["icon"]["value"] - if(list_vars["hand_var_overrides"]?["icon_state"]) - icon_state = list_vars["hand_var_overrides"]["icon_state"]["value"] - out_icon = icon(icon, icon_state, frame = 1) - hand_icon_base64 = icon2base64(out_icon) - out_icon = icon(saved_vars["overlay_icon"], saved_vars["overlay_icon_state"], frame = 1) - overlay_icon_base64 = icon2base64(out_icon) - out_icon = icon(saved_vars["ranged_mousepointer"], frame = 1) - mouse_icon_base64 = icon2base64(out_icon) - -/datum/give_sdql_spell/proc/toggle_list_var(list_name, list_var) - if(list_vars[list_name]?[list_var]) - list_vars[list_name][list_var]["value"] = !list_vars[list_name][list_var]["value"] - -/datum/give_sdql_spell/proc/set_list_var(list_name, list_var, value) - if(list_vars[list_name]?[list_var]) - list_vars[list_name][list_var]["value"] = value - -/datum/give_sdql_spell/proc/set_list_ref_var(list_name, list_var) - if(list_vars[list_name]?[list_var]) - list_vars[list_name][list_var]["value"] = user.holder?.marked_datum - -/datum/give_sdql_spell/proc/rename_list_var(list_name, list_var, new_name) - if(!new_name) - alert = "You can't give a list variable an empty string for a name!" - return - if(list_var == new_name) - return - if(list_vars[list_name]) - var/list/L = list_vars[list_name] - var/ind = L.Find(list_var) - if(ind) - if(list_vars[list_name][new_name]) - alert = "There is already a variable named [new_name] in [list_name]!" - else - var/old_val = list_vars[list_name][list_var] - list_vars[list_name][ind] = new_name - list_vars[list_name][new_name] = old_val - -/datum/give_sdql_spell/proc/change_list_var_type(list_name, list_var, var_type) - if(list_vars[list_name]?[list_var]) - if(list_vars[list_name][list_var]["type"] == "list" && var_type != "list") - purge_list_var("[list_name]/[list_var]") - list_vars[list_name][list_var]["type"] = var_type - switch(var_type) - if("string", "path") - list_vars[list_name][list_var]["value"] = "" - if("bool", "num") - list_vars[list_name][list_var]["value"] = 0 - if("list") - list_vars[list_name][list_var]["value"] = null - list_vars |= list("[list_name]/[list_var]" = list()) - -/datum/give_sdql_spell/proc/remove_list_var(list_name, list_var) - if(list_vars[list_name]) - var/list/L = list_vars[list_name] - var/ind = L.Find(list_var) - if(ind) - if(list_vars[list_name][list_var]["type"] == "list") - purge_list_var("[list_name]/[list_var]") - L.Cut(ind, ind+1) - list_vars[list_name] = L - -/datum/give_sdql_spell/proc/purge_list_var(list_name) - var/ind = list_vars.Find(list_name) - if(ind) - for(var/V in list_vars[list_name]) - if(list_vars[list_name][V]["type"] == "list") - purge_list_var("[list_name]/[V]") - list_vars.Cut(ind, ind+1) - -/datum/give_sdql_spell/proc/generate_list_var(list_name) - if(!list_vars[list_name]) - return null - var/list/ret = list() - for(var/V in list_vars[list_name]) - if(list_vars[list_name][V]["type"] == "list") - ret[V] = generate_list_var("[list_name]/[V]") - else if(list_vars[list_name][V]["type"] == "path") - ret[V] = text2path(list_vars[list_name][V]["value"]) - else if(list_vars[list_name][V]["type"] == "icon") - ret[V] = icon(list_vars[list_name][V]["value"]) - else - ret[V] = list_vars[list_name][V]["value"] - return ret - -/datum/give_sdql_spell/proc/give_spell() - var/path = text2path("/obj/effect/proc_holder/spell/[spell_type]/sdql") - var/obj/effect/proc_holder/spell/new_spell = new path(null, target_mob, user.ckey) - GLOB.sdql_spells += new_spell - reassign_vars(new_spell) - new_spell.action.UpdateButtons() - if(target_mob.mind) - target_mob.mind.AddSpell(new_spell) - else - target_mob.AddSpell(new_spell) - to_chat(user, span_danger("Spells given to mindless mobs will not be transferred in mindswap or cloning!")) - return new_spell - -/datum/give_sdql_spell/proc/reassign_vars(obj/effect/proc_holder/spell/target) - if(!target) - CRASH("edit_spell must be called with a non_null target") - var/datum/component/sdql_executor/executor = target.GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - for(var/V in saved_vars+list_vars) - if(V == "query") - executor.vv_edit_var("query", saved_vars["query"]) - else if(V == "suppress_message_admins") - executor.vv_edit_var("suppress_message_admins", saved_vars["suppress_message_admins"]) - else if(V == "scratchpad") - var/list/new_scratchpad = generate_list_var("scratchpad") - if(new_scratchpad) - executor.vv_edit_var("scratchpad", new_scratchpad) - else if(target.vars[V]) - if(islist(target.vars[V])) - if(special_list_vars[V]) - var/list/overrides_to_save = list_vars[V] - executor.saved_overrides[V] = overrides_to_save.Copy() - var/list/list_var = generate_list_var(V) - if(list_var) - target.vv_edit_var(V, list_var) - else if(isicon(target.vars[V])) - target.vv_edit_var(V, icon(saved_vars[V])) - else - target.vv_edit_var(V, saved_vars[V]) diff --git a/code/modules/admin/verbs/SDQL2/SDQL_spells/spells.dm b/code/modules/admin/verbs/SDQL2/SDQL_spells/spells.dm deleted file mode 100644 index 093e4e8dd16..00000000000 --- a/code/modules/admin/verbs/SDQL2/SDQL_spells/spells.dm +++ /dev/null @@ -1,148 +0,0 @@ -/obj/effect/proc_holder/spell/aimed/sdql - name = "Aimed SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - projectile_type = /obj/projectile - -/obj/effect/proc_holder/spell/aimed/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - RegisterSignal(src, COMSIG_PROJECTILE_ON_HIT, .proc/on_projectile_hit) - -/obj/effect/proc_holder/spell/aimed/sdql/proc/on_projectile_hit(source, firer, target) - SIGNAL_HANDLER - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - INVOKE_ASYNC(executor, /datum/component/sdql_executor/proc/execute, list(target), owner.resolve()) - -/obj/effect/proc_holder/spell/aoe_turf/sdql - name = "AoE SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - -/obj/effect/proc_holder/spell/aoe_turf/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/aoe_turf/sdql/cast(list/targets, mob/user) - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - executor.execute(targets, user) - -/obj/effect/proc_holder/spell/cone/sdql - name = "Cone SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - var/list/targets = list() - -/obj/effect/proc_holder/spell/cone/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/cone/sdql/do_mob_cone_effect(mob/living/target_mob, level) - targets |= target_mob - -/obj/effect/proc_holder/spell/cone/sdql/do_obj_cone_effect(obj/target_obj, level) - targets |= target_obj - -/obj/effect/proc_holder/spell/cone/sdql/do_turf_cone_effect(turf/target_turf, level) - targets |= target_turf - -/obj/effect/proc_holder/spell/cone/sdql/cast(list/targets, mob/user) - . = ..() - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - executor.execute(targets, user) - targets = list() - -/obj/effect/proc_holder/spell/cone/staggered/sdql - name = "Staggered Cone SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - var/list/targets = list() - -/obj/effect/proc_holder/spell/cone/staggered/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/cone/staggered/sdql/do_mob_cone_effect(mob/living/target_mob, level) - targets |= target_mob - -/obj/effect/proc_holder/spell/cone/staggered/sdql/do_obj_cone_effect(obj/target_obj, level) - targets |= target_obj - -/obj/effect/proc_holder/spell/cone/staggered/sdql/do_turf_cone_effect(turf/target_turf, level) - targets |= target_turf - -/obj/effect/proc_holder/spell/cone/staggered/sdql/do_cone_effects(list/target_turf_list, level) - . = ..() - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - executor.execute(target_turf_list, owner.resolve()) - targets = list() - -/obj/effect/proc_holder/spell/pointed/sdql - name = "Pointed SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - -/obj/effect/proc_holder/spell/pointed/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/pointed/sdql/cast(list/targets, mob/user) - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - executor.execute(targets, user) - -/obj/effect/proc_holder/spell/self/sdql - name = "Self SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - -/obj/effect/proc_holder/spell/self/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/self/sdql/cast(list/targets, mob/user) - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - executor.execute(targets, user) - -/obj/effect/proc_holder/spell/targeted/sdql - name = "Targeted SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - -/obj/effect/proc_holder/spell/targeted/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/targeted/sdql/cast(list/targets, mob/user) - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - executor.execute(targets, user) - -/obj/effect/proc_holder/spell/targeted/touch/sdql - name = "Touch SDQL Spell" - desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu." - var/list/hand_var_overrides = list() //The touch attack has its vars changed to the ones put in this list. - -/obj/effect/proc_holder/spell/targeted/touch/sdql/Initialize(mapload, new_owner, giver) - . = ..() - AddComponent(/datum/component/sdql_executor, giver) - -/obj/effect/proc_holder/spell/targeted/touch/sdql/ChargeHand(mob/living/carbon/user) - if(..()) - for(var/V in hand_var_overrides) - if(attached_hand.vars[V]) - attached_hand.vv_edit_var(V, hand_var_overrides[V]) - RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK, .proc/on_touch_attack) - user.update_inv_hands() - -/obj/effect/proc_holder/spell/targeted/touch/sdql/proc/on_touch_attack(source, target, user) - SIGNAL_HANDLER - var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor) - if(!executor) - CRASH("[src]'s SDQL executor component went missing!") - INVOKE_ASYNC(executor, /datum/component/sdql_executor/proc/execute, list(target), user) diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm index 0e22ab0c6f2..c9f9eaf330c 100644 --- a/code/modules/antagonists/_common/antag_spawner.dm +++ b/code/modules/antagonists/_common/antag_spawner.dm @@ -261,14 +261,15 @@ /obj/item/antag_spawner/slaughter_demon/spawn_antag(client/C, turf/T, kind = "", datum/mind/user) - var/obj/effect/dummy/phased_mob/holder = new /obj/effect/dummy/phased_mob(T) - var/mob/living/simple_animal/hostile/imp/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.key = C.key S.mind.set_assigned_role(SSjob.GetJobType(/datum/job/slaughter_demon)) S.mind.special_role = ROLE_SLAUGHTER_DEMON S.mind.add_antag_datum(antag_type) - 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 203c22acd42..5db683974b3 100644 --- a/code/modules/antagonists/cult/blood_magic.dm +++ b/code/modules/antagonists/cult/blood_magic.dm @@ -218,68 +218,45 @@ 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 + 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/Destroy() - var/obj/effect/proc_holder/horror/destroy = PH - . = ..() - if(destroy && !QDELETED(destroy)) - QDEL_NULL(destroy) - -/datum/action/innate/cult/blood_spell/horror/Activate() - PH.toggle(owner) //the important bit - return TRUE - -/obj/effect/proc_holder/horror - active = FALSE - ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' - var/datum/action/innate/cult/blood_spell/attached_action - -/obj/effect/proc_holder/horror/Destroy() - var/datum/action/innate/cult/blood_spell/AA = attached_action - . = ..() - if(AA && !QDELETED(AA)) - QDEL_NULL(AA) - -/obj/effect/proc_holder/horror/proc/toggle(mob/user) - if(active) - remove_ranged_ability(span_cult("You dispel the magic...")) - else - add_ranged_ability(user, span_cult("You prepare to horrify a target...")) - -/obj/effect/proc_holder/horror/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - if(ranged_ability_user.incapacitated() || !IS_CULTIST(caller)) - remove_ranged_ability() - return - var/turf/T = get_turf(ranged_ability_user) - if(!isturf(T)) +/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 - if(target in view(7, get_turf(ranged_ability_user))) - var/mob/living/carbon/human/human_target = target - if(!istype(human_target) || IS_CULTIST(human_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,/atom/.proc/remove_alt_appearance,"cult_apoc",TRUE), 2400, TIMER_OVERRIDE|TIMER_UNIQUE) - to_chat(ranged_ability_user,span_cult("[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.UpdateButtons() - if(attached_action.charges <= 0) - remove_ranged_ability(span_cult("You have exhausted the spell's power!")) - qdel(src) + + if(!ishuman(clicked_on) || get_dist(caller, clicked_on) > 7) + return FALSE + + var/mob/living/carbon/human/human_clicked = clicked_on + if(IS_CULTIST(human_clicked)) + return FALSE + + return ..() + +/datum/action/innate/cult/blood_spell/horror/do_ability(mob/living/caller, params, mob/living/carbon/human/clicked_on) + + clicked_on.hallucination = max(clicked_on.hallucination, 120) + SEND_SOUND(caller, sound('sound/effects/ghost.ogg', FALSE, TRUE, 50)) + + 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" diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index 20a0778d4da..60b65c9ffa4 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -252,7 +252,7 @@ name = "Cult" ///The blood mark target - var/blood_target + var/atom/blood_target ///Image of the blood mark target var/image/blood_target_image ///Timer for the blood mark expiration @@ -496,6 +496,64 @@ return FALSE //can't convert machines, shielded, or braindead return TRUE +/// 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/outfit/cultist name = "Cultist (Preview only)" diff --git a/code/modules/antagonists/cult/cult_comms.dm b/code/modules/antagonists/cult/cult_comms.dm index 3ee18c3f7a0..0d6f0a3d72c 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_IMMOBILE|AB_CHECK_CONSCIOUS + ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' /datum/action/innate/cult/IsAvailable() if(!IS_CULTIST(owner)) @@ -230,252 +231,218 @@ 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 - -/datum/action/innate/cult/master/cultmark/New(Target) - CM = new() - CM.attached_action = src - ..() + 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/IsAvailable() - if(cooldown > world.time) - if(!CM.active) - to_chat(owner, span_cultlarge("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 + + if(!(clicked_on in view(7, caller_turf))) + return FALSE + return ..() -/datum/action/innate/cult/master/cultmark/Destroy() - QDEL_NULL(CM) - return ..() +/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.") -/datum/action/innate/cult/master/cultmark/Activate() - CM.toggle(owner) //the important bit + 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/mouse_pointers/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(span_cult("You cease the marking ritual.")) - else - add_ranged_ability(user, span_cult("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 - - var/datum/antagonist/cult/C = caller.mind.has_antag_datum(/datum/antagonist/cult,TRUE) - - if(target in view(7, get_turf(ranged_ability_user))) - if(C.cult_team.blood_target) - to_chat(ranged_ability_user, span_cult("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, /mob.proc/update_action_buttons_icon), attached_action.base_cooldown) - C.cult_team.blood_target_image = image('icons/effects/mouse_pointers/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 as anything in get_antag_minds(/datum/antagonist/cult)) - if(B.current && B.current.stat != DEAD && B.current.client) - to_chat(B.current, span_cultlarge("[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(span_cult("The marking rite is complete! It will last for 90 seconds.")) - C.cult_team.blood_target_reset_timer = addtimer(CALLBACK(GLOBAL_PROC, .proc/reset_blood_target,C.cult_team), 900, TIMER_STOPPABLE) - return TRUE - return FALSE - -/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,span_cultlarge("The blood mark has expired!")) - B.current.client.images -= team.blood_target_image - QDEL_NULL(team.blood_target_image) - team.blood_target = null - - -/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." - -/datum/action/innate/cult/master/cultmark/ghost/IsAvailable() - if(istype(owner, /mob/dead/observer) && IS_CULTIST(owner.mind.current)) - return TRUE - else - qdel(src) - /datum/action/innate/cult/ghostmark //Ghost version name = "Blood Mark your Target" - desc = "Marks whatever you are orbitting - for the entire cult to track." + desc = "Marks whatever you are orbiting for the entire cult to track." button_icon_state = "cult_mark" - var/tracking = FALSE - var/cooldown = 0 - var/base_cooldown = 600 + /// 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) /datum/action/innate/cult/ghostmark/IsAvailable() - if(istype(owner, /mob/dead/observer) && IS_CULTIST(owner.mind.current)) - 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,span_cultbold("Your previous mark is gone - you are now ready to create a new blood mark.")) + return ..() && istype(owner, /mob/dead/observer) /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, span_cultbold("You have cleared the cult's blood target!")) - deltimer(C.cult_team.blood_target_reset_timer) - return - else - to_chat(owner, span_cultbold("The cult has already designated a target!")) - return - if(cooldown>world.time) - to_chat(owner, span_cultbold("You aren't ready to place another blood mark yet!")) - return - target = owner.orbiting?.parent || get_turf(owner) - if(!target) - return - C.cult_team.blood_target = target - var/atom/atom_target = target - var/area/A = get_area(atom_target) - cooldown = world.time + base_cooldown - addtimer(CALLBACK(owner, /mob.proc/update_action_buttons_icon), base_cooldown) - C.cult_team.blood_target_image = image('icons/effects/mouse_pointers/cult_target.dmi', atom_target, "glow", ABOVE_MOB_LAYER) - C.cult_team.blood_target_image.appearance_flags = RESET_COLOR - C.cult_team.blood_target_image.pixel_x = -atom_target.pixel_x - C.cult_team.blood_target_image.pixel_y = -atom_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 as anything in get_antag_minds(/datum/antagonist/cult)) - if(B.current && B.current.stat != DEAD && B.current.client) - to_chat(B.current, span_cultlarge("[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,span_cultbold("You have marked the [atom_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, .proc/reset_blood_target,C.cult_team), base_cooldown, TIMER_STOPPABLE) - addtimer(CALLBACK(src, .proc/reset_button), base_cooldown) + 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.") + 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 + + to_chat(owner, span_cultbold("The cult has already designated a target!")) + return FALSE + + if(!COOLDOWN_FINISHED(src, cult_mark_cooldown)) + to_chat(owner, span_cultbold("You aren't ready to place another blood mark yet!")) + return FALSE + + var/atom/mark_target = owner.orbiting?.parent || get_turf(owner) + if(!mark_target) + return FALSE + + 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 + + to_chat(owner, span_cult("The marking failed!")) + return FALSE + +/datum/action/innate/cult/ghostmark/proc/update_button_status() + if(!owner) + return + + 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 + + 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 ///////// - - /datum/action/innate/cult/master/pulse name = "Eldritch Pulse" 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, span_cultlarge("You need to wait [DisplayTimeText(cooldown - world.time)] before you can pulse again!")) + + if(!(clicked_on in view(7, caller_turf))) return FALSE + + if(clicked_on == caller) + 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) - return ..() +/datum/action/innate/cult/master/pulse/do_ability(mob/living/caller, params, atom/clicked_on) + var/atom/throwee = throwee_ref?.resolve() + if(QDELETED(throwee)) + to_chat(caller, span_cult("You lost your target!")) + throwee = null + throwee_ref = null + return FALSE -/datum/action/innate/cult/master/pulse/Activate() - PM.toggle(owner) //the important bit - return TRUE + 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/mouse_pointers/throw_target.dmi' - var/datum/action/innate/cult/master/pulse/attached_action + var/turf/throwee_turf = get_turf(throwee) -/obj/effect/proc_holder/pulse/Destroy() - attached_action = null - return ..() + 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 + + 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) + + return TRUE -/obj/effect/proc_holder/pulse/proc/toggle(mob/user) - if(active) - remove_ranged_ability(span_cult("You cease your preparations...")) - attached_action.throwing = FALSE else - add_ranged_ability(user, span_cult("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 -/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(target in view(7, get_turf(ranged_ability_user))) - var/mob/mob_target = target - var/is_cultist = istype(mob_target) && IS_CULTIST(mob_target) - if((!(is_cultist || 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,span_cult("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(span_cult("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, /mob.proc/update_action_buttons_icon), attached_action.base_cooldown) + 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 + + return FALSE diff --git a/code/modules/antagonists/fugitive/fugitive_outfits.dm b/code/modules/antagonists/fugitive/fugitive_outfits.dm index 5a4627a1110..3d176a9d39b 100644 --- a/code/modules/antagonists/fugitive/fugitive_outfits.dm +++ b/code/modules/antagonists/fugitive/fugitive_outfits.dm @@ -43,8 +43,7 @@ equipped_on.hair_color = "#000000" equipped_on.facial_hair_color = equipped_on.hair_color equipped_on.update_body() - if(equipped_on.mind) - equipped_on.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null)) + var/list/no_drops = list() no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_FEET) no_drops += equipped_on.get_item_by_slot(ITEM_SLOT_ICLOTHING) @@ -54,6 +53,9 @@ for(var/obj/item/trait_needed as anything in no_drops) ADD_TRAIT(trait_needed, TRAIT_NODROP, CURSED_ITEM_TRAIT(trait_needed.type)) + 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" uniform = /obj/item/clothing/under/color/white diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index b3d63da325e..478448cfaa4 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -194,7 +194,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/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/on_item_afterattack) RegisterSignal(our_mob, COMSIG_MOB_LOGIN, .proc/fix_influence_network) @@ -202,7 +202,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)) /datum/antagonist/heretic/on_body_transfer(mob/living/old_body, mob/living/new_body) . = ..() @@ -212,13 +212,13 @@ 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 // Heretic spells are of the forbidden school, otherwise we don't care @@ -234,7 +234,7 @@ // We shouldn't be able to cast this! Cancel it. source.balloon_alert(source, "you need a focus!") - return COMPONENT_CANCEL_SPELL + 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 694cdc3bdbf..419c8457f9b 100644 --- a/code/modules/antagonists/heretic/heretic_knowledge.dm +++ b/code/modules/antagonists/heretic/heretic_knowledge.dm @@ -68,6 +68,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, @@ -77,6 +78,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. @@ -168,23 +170,26 @@ */ /datum/heretic_knowledge/spell abstract_parent_type = /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 + /// 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(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() +/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 8b2209a2cf2..d05d6a76f8c 100644 --- a/code/modules/antagonists/heretic/knowledge/ash_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/ash_lore.dm @@ -82,7 +82,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 = PATH_ASH @@ -104,8 +104,10 @@ return // Also refunds 75% of charge! - for(var/obj/effect/proc_holder/spell/targeted/touch/mansus_grasp/grasp in source.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) @@ -160,7 +162,7 @@ /datum/heretic_knowledge/summon/ashy, /datum/heretic_knowledge/summon/rusty, ) - spell_to_add = /obj/effect/proc_holder/spell/targeted/fiery_rebirth + spell_to_add = /datum/action/cooldown/spell/aoe/fiery_rebirth cost = 1 route = PATH_ASH @@ -200,8 +202,13 @@ /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) + user.client?.give_award(/datum/award/achievement/misc/ash_ascension, user) for(var/trait in traits_to_apply) ADD_TRAIT(user, trait, MAGIC_TRAIT) diff --git a/code/modules/antagonists/heretic/knowledge/blade_lore.dm b/code/modules/antagonists/heretic/knowledge/blade_lore.dm index ec5590335b5..17065104029 100644 --- a/code/modules/antagonists/heretic/knowledge/blade_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/blade_lore.dm @@ -340,7 +340,7 @@ /datum/heretic_knowledge/final/blade_final, /datum/heretic_knowledge/rifle, ) - spell_to_add = /obj/effect/proc_holder/spell/aimed/furious_steel + spell_to_add = /datum/action/cooldown/spell/pointed/projectile/furious_steel cost = 1 route = PATH_BLADE @@ -374,8 +374,8 @@ RegisterSignal(user, COMSIG_HERETIC_BLADE_ATTACK, .proc/on_eldritch_blade) user.apply_status_effect(/datum/status_effect/protective_blades/recharging, null, 8, 30, 0.25 SECONDS, 1 MINUTES) - var/obj/effect/proc_holder/spell/aimed/furious_steel/steel_spell = locate() in user.mind.spell_list - steel_spell?.charge_max /= 3 + var/datum/action/cooldown/spell/pointed/projectile/furious_steel/steel_spell = locate() in user.actions + steel_spell?.cooldown_time /= 3 /datum/heretic_knowledge/final/blade_final/proc/on_eldritch_blade(mob/living/source, mob/living/target, obj/item/melee/sickly_blade/blade) SIGNAL_HANDLER diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index 6d1c03c9f16..b1b588e63fb 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -82,22 +82,22 @@ if(LAZYLEN(created_items) >= limit) target.balloon_alert(source, "at ghoul limit!") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE if(HAS_TRAIT(target, TRAIT_HUSK)) target.balloon_alert(source, "husked!") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE if(!IS_VALID_GHOUL_MOB(target)) target.balloon_alert(source, "invalid body!") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE target.grab_ghost() // The grab failed, so they're mindless or playerless. We can't continue if(!target.mind || !target.client) target.balloon_alert(source, "no soul!") - return COMPONENT_BLOCK_CHARGE_USE + return COMPONENT_BLOCK_HAND_USE make_ghoul(source, target) @@ -302,7 +302,10 @@ /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) + user.client?.give_award(/datum/award/achievement/misc/flesh_ascension, user) var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index 5d6b3d0e48a..884b9db4681 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -72,7 +72,7 @@ SIGNAL_HANDLER target.rust_heretic_act() - return COMPONENT_USE_CHARGE + return COMPONENT_USE_HAND /datum/heretic_knowledge/rust_regen name = "Leeching Walk" @@ -153,7 +153,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 = PATH_RUST @@ -181,7 +181,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 = 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 7ec72ecc68d..4a315575d61 100644 --- a/code/modules/antagonists/heretic/knowledge/side_flesh_void.dm +++ b/code/modules/antagonists/heretic/knowledge/side_flesh_void.dm @@ -29,7 +29,7 @@ /datum/heretic_knowledge/spell/void_phase, /datum/heretic_knowledge/summon/raw_prophet, ) - spell_to_add = /obj/effect/proc_holder/spell/pointed/blood_siphon + spell_to_add = /datum/action/cooldown/spell/pointed/blood_siphon cost = 1 route = PATH_SIDE @@ -43,6 +43,6 @@ /datum/heretic_knowledge/summon/stalker, /datum/heretic_knowledge/spell/void_pull, ) - spell_to_add = /obj/effect/proc_holder/spell/pointed/cleave + spell_to_add = /datum/action/cooldown/spell/pointed/cleave cost = 1 route = PATH_SIDE diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm index afb7c8e2608..d430d657ecb 100644 --- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm @@ -21,7 +21,7 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) desc = "Starts your journey into the Mansus. \ Grants you the Mansus Grasp, a powerful and upgradable \ disabling spell that can be cast regardless of having a focus." - 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 = PATH_START diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm index c8721dcac36..5a87ff98c37 100644 --- a/code/modules/antagonists/heretic/knowledge/void_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm @@ -130,7 +130,7 @@ /datum/heretic_knowledge/spell/blood_siphon, /datum/heretic_knowledge/rune_carver, ) - spell_to_add = /obj/effect/proc_holder/spell/pointed/void_phase + spell_to_add = /datum/action/cooldown/spell/pointed/void_phase cost = 1 route = PATH_VOID @@ -163,7 +163,7 @@ /datum/heretic_knowledge/spell/cleave, /datum/heretic_knowledge/summon/maid_in_mirror, ) - spell_to_add = /obj/effect/proc_holder/spell/targeted/void_pull + spell_to_add = /datum/action/cooldown/spell/aoe/void_pull cost = 1 route = PATH_VOID diff --git a/code/modules/antagonists/heretic/magic/aggressive_spread.dm b/code/modules/antagonists/heretic/magic/aggressive_spread.dm index 58591be4a1f..01eca69edbd 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 - school = SCHOOL_FORBIDDEN - charge_max = 300 //twice as long as mansus grasp - clothes_req = FALSE - range = 3 + spell_requirements = NONE -/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() + aoe_radius = 3 -/obj/effect/proc_holder/spell/aoe_turf/rust_conversion/small +/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 e43b7fb4c94..4c11c4e84c9 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 - school = SCHOOL_FORBIDDEN - 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) +/datum/action/cooldown/spell/fire_sworn/Remove(mob/living/remove_from) + remove_from.remove_status_effect(/datum/status_effect/fire_ring) + return ..() + +/datum/action/cooldown/spell/fire_sworn/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/datum/action/cooldown/spell/fire_sworn/cast(mob/living/cast_on) . = ..() - current_user = user - has_fire_ring = TRUE - addtimer(CALLBACK(src, .proc/remove, user), duration, TIMER_OVERRIDE|TIMER_UNIQUE) + cast_on.apply_status_effect(/datum/status_effect/fire_ring, duration, fire_radius) -/obj/effect/proc_holder/spell/targeted/fire_sworn/proc/remove() - has_fire_ring = FALSE - current_user = null +/// 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 -/obj/effect/proc_holder/spell/targeted/fire_sworn/process(delta_time) - . = ..() - if(!has_fire_ring) - return - if(current_user.stat == DEAD) - remove() - return - if(!isturf(current_user.loc)) +/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 - for(var/turf/nearby_turf as anything in RANGE_TURFS(1, current_user)) + if(!isturf(owner.loc)) + return + + 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." + 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 - charge_max = 300 //twice as long as mansus grasp - clothes_req = FALSE + 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" + spell_requirements = NONE -/obj/effect/proc_holder/spell/aoe_turf/fire_cascade/cast(list/targets, mob/user = usr) - INVOKE_ASYNC(src, .proc/fire_cascade, user, range) + /// The radius the flames will go around the caster. + var/flame_radius = 4 -/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)) +/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 - school = SCHOOL_FORBIDDEN - charge_max = 300 - range = 15 - clothes_req = FALSE + spell_requirements = NONE -/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/fire_line, user, T) - T = line_target(10, range, X, user) - INVOKE_ASYNC(src, .proc/fire_line, user, T) - T = line_target(0, range, X, user) - INVOKE_ASYNC(src, .proc/fire_line, user, T) - T = line_target(-10, range, X, user) - INVOKE_ASYNC(src, .proc/fire_line, user, T) - T = line_target(25, range, X, user) - INVOKE_ASYNC(src, .proc/fire_line, user, T) - return ..() + /// The length of the flame line spit out. + var/flame_line_length = 15 -/obj/effect/proc_holder/spell/pointed/ash_final/proc/line_target(offset, range, atom/at , atom/user) +/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 @@ -117,7 +135,7 @@ T = check 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)) diff --git a/code/modules/antagonists/heretic/magic/ash_jaunt.dm b/code/modules/antagonists/heretic/magic/ash_jaunt.dm index 9d77924f66a..9c0d403fb23 100644 --- a/code/modules/antagonists/heretic/magic/ash_jaunt.dm +++ b/code/modules/antagonists/heretic/magic/ash_jaunt.dm @@ -1,32 +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 - school = SCHOOL_FORBIDDEN - 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 +/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/proc_holder/spell/targeted/ethereal_jaunt/shift/ash/play_sound() - return - /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 7917b26758a..574c967b9af 100644 --- a/code/modules/antagonists/heretic/magic/blood_cleave.dm +++ b/code/modules/antagonists/heretic/magic/blood_cleave.dm @@ -1,28 +1,33 @@ -/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 - school = SCHOOL_FORBIDDEN - charge_max = 350 - clothes_req = FALSE - range = 9 + spell_requirements = NONE -/obj/effect/proc_holder/spell/pointed/cleave/cast(list/targets, mob/user) - if(!targets.len) - user.balloon_alert(user, "no targets!") - return FALSE - if(!can_target(targets[1], user)) - return FALSE + cast_range = 9 + /// The radius of the cleave effect + var/cleave_radius = 1 - for(var/mob/living/carbon/human/nearby_human in range(1, targets[1])) - targets |= nearby_human +/datum/action/cooldown/spell/pointed/cleave/is_valid_target(atom/cast_on) + return ..() && ishuman(cast_on) - for(var/mob/living/carbon/human/victim as anything in targets) - if(victim == user) +/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.can_block_magic()) victim.visible_message( @@ -42,18 +47,15 @@ var/obj/item/bodypart/bodypart = pick(victim.bodyparts) var/datum/wound/slash/critical/crit_wound = new() crit_wound.apply_wound(bodypart) - victim.adjustFireLoss(20) + victim.apply_damage(20, BURN, wound_bonus = CANT_WOUND) + 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/eldritch.dmi' diff --git a/code/modules/antagonists/heretic/magic/blood_siphon.dm b/code/modules/antagonists/heretic/magic/blood_siphon.dm index 30c1c9de2e5..406d3123d78 100644 --- a/code/modules/antagonists/heretic/magic/blood_siphon.dm +++ b/code/modules/antagonists/heretic/magic/blood_siphon.dm @@ -1,51 +1,59 @@ -/obj/effect/proc_holder/spell/pointed/blood_siphon +/datum/action/cooldown/spell/pointed/blood_siphon name = "Blood Siphon" - desc = "A touch spell that heals your wounds while damaging the enemy. It has a chance to transfer wounds between you and your 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 - school = SCHOOL_EVOCATION - charge_max = 150 - clothes_req = FALSE - range = 9 + spell_requirements = NONE -/obj/effect/proc_holder/spell/pointed/blood_siphon/cast(list/targets, mob/user) - if(!isliving(user)) - return + cast_range = 9 - var/mob/living/real_target = targets[1] - var/mob/living/living_user = user - playsound(user, 'sound/magic/demon_attack1.ogg', 75, TRUE) - if(real_target.can_block_magic()) - user.balloon_alert(user, "spell blocked!") - real_target.visible_message( - span_danger("The spell bounces off of [real_target]!"), +/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 - real_target.visible_message( - span_danger("[real_target] turns pale as a red glow envelops [real_target.p_them()]!"), + 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!"), ) - real_target.adjustBruteLoss(20) - living_user.adjustBruteLoss(-20) + var/mob/living/living_owner = owner + cast_on.adjustBruteLoss(20) + living_owner.adjustBruteLoss(-20) - if(!living_user.blood_volume) - return + if(!cast_on.blood_volume || !living_owner.blood_volume) + return TRUE - real_target.blood_volume -= 20 - if(living_user.blood_volume < BLOOD_VOLUME_MAXIMUM) // we dont want to explode from casting - living_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(real_target)) - return + if(!iscarbon(cast_on) || !iscarbon(owner)) + return TRUE - var/mob/living/carbon/carbon_target = real_target - var/mob/living/carbon/carbon_user = living_user + 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)) @@ -56,9 +64,4 @@ 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 34fd2d972bd..195af5e6089 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_blind.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_blind.dm @@ -1,5 +1,9 @@ // Given to heretic monsters. -/obj/effect/proc_holder/spell/pointed/trigger/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 f2e8e22249d..d2424126b91 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_emplosion.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_emplosion.dm @@ -1,13 +1,14 @@ // 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 - school = SCHOOL_FORBIDDEN - 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 31dbc0f1748..9265a8e4f22 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm @@ -1,9 +1,10 @@ // 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, diff --git a/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm b/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm index 16c6055571c..d4a70d81cc0 100644 --- a/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm +++ b/code/modules/antagonists/heretic/magic/eldritch_telepathy.dm @@ -1,7 +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 - antimagic_flags = MAGIC_RESISTANCE_MIND +/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 10f0d4eea57..7cb5913c1c7 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 - school = SCHOOL_FORBIDDEN - 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 -/obj/effect/proc_holder/spell/targeted/shed_human_form/cast(list/targets, mob/user) +/datum/action/cooldown/spell/shed_human_form/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/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(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(!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(/datum/status_effect/grouped/stasis, STASIS_ASCENSION_EFFECT) - for(var/mob/living/carbon/human/nearby_human in view(9, outside) - target) + 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 + var/datum/brain_trauma/trauma = pick(subtypesof(BRAIN_TRAUMA_MILD) + subtypesof(BRAIN_TRAUMA_SEVERE)) + nearby_human.gain_trauma(trauma, TRAUMA_RESILIENCE_LOBOTOMY) - if(iscarbon(mob_inside)) - var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/armsy = target - if(mob_inside.remove_status_effect(/datum/status_effect/grouped/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 +/datum/action/cooldown/spell/worm_contract name = "Force Contract" desc = "Forces your body to contract onto a single tile." - invocation_type = INVOCATION_NONE + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "worm_contract" + school = SCHOOL_FORBIDDEN - 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" + cooldown_time = 30 SECONDS -/obj/effect/proc_holder/spell/targeted/worm_contract/cast(list/targets, mob/user) + 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) . = ..() - if(!istype(user, /mob/living/simple_animal/hostile/heretic_summon/armsy)) - to_chat(user, span_userdanger("You try to contract your muscles, but nothing happens...")) - return - - var/mob/living/simple_animal/hostile/heretic_summon/armsy/lord_of_night = user - lord_of_night.contract_next_chain_into_single_tile() + cast_on.contract_next_chain_into_single_tile() diff --git a/code/modules/antagonists/heretic/magic/furious_steel.dm b/code/modules/antagonists/heretic/magic/furious_steel.dm index 059f127ffe0..2fc6322e4f1 100644 --- a/code/modules/antagonists/heretic/magic/furious_steel.dm +++ b/code/modules/antagonists/heretic/magic/furious_steel.dm @@ -1,77 +1,96 @@ -/obj/effect/proc_holder/spell/aimed/furious_steel +/datum/action/cooldown/spell/pointed/projectile/furious_steel name = "Furious Steel" desc = "Summon three silver blades which orbit you. \ While orbiting you, these blades will protect you from from attacks, but will be consumed on use. \ Additionally, you can click to fire the blades at a target, dealing damage and causing bleeding." - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "furious_steel0" - action_background_icon_state = "bg_ecult" - base_icon_state = "furious_steel" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "furious_steel0" + sound = 'sound/weapons/guillotine.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 30 SECONDS invocation = "F'LSH'NG S'LV'R!" invocation_type = INVOCATION_SHOUT - school = SCHOOL_FORBIDDEN - clothes_req = FALSE - charge_max = 30 SECONDS - range = 20 - projectile_amount = 3 - projectiles_per_fire = 1 - projectile_type = /obj/projectile/floating_blade - sound = 'sound/weapons/guillotine.ogg' + + spell_requirements = NONE + + base_icon_state = "furious_steel" active_msg = "You summon forth three blades of furious silver." deactive_msg = "You conceal the blades of furious silver." + cast_range = 20 + projectile_type = /obj/projectile/floating_blade + projectile_amount = 3 + /// A ref to the status effect surrounding our heretic on activation. var/datum/status_effect/protective_blades/blade_effect -/obj/effect/proc_holder/spell/aimed/furious_steel/Destroy() - QDEL_NULL(blade_effect) +/datum/action/cooldown/spell/pointed/projectile/furious_steel/Grant(mob/grant_to) + . = ..() + if(!owner) + return + + if(IS_HERETIC(owner)) + RegisterSignal(owner, SIGNAL_REMOVETRAIT(TRAIT_ALLOW_HERETIC_CASTING), .proc/on_focus_lost) + +/datum/action/cooldown/spell/pointed/projectile/furious_steel/Remove(mob/remove_from) + UnregisterSignal(remove_from, SIGNAL_REMOVETRAIT(TRAIT_ALLOW_HERETIC_CASTING)) return ..() -/obj/effect/proc_holder/spell/aimed/furious_steel/on_activation(mob/user) - if(!isliving(user)) +/// Signal proc for [SIGNAL_REMOVETRAIT], via [TRAIT_ALLOW_HERETIC_CASTING], to remove the effect when we lose the focus trait +/datum/action/cooldown/spell/pointed/projectile/furious_steel/proc/on_focus_lost(mob/source) + SIGNAL_HANDLER + + unset_click_ability(source, refund_cooldown = TRUE) + +/datum/action/cooldown/spell/pointed/projectile/furious_steel/InterceptClickOn(mob/living/caller, params, atom/click_target) + // Let the caster prioritize using items like guns over blade casts + if(caller.get_active_held_item()) + return FALSE + // Let the caster prioritize melee attacks like punches and shoves over blade casts + if(get_dist(caller, click_target) <= 1) + return FALSE + + return ..() + +/datum/action/cooldown/spell/pointed/projectile/furious_steel/on_activation(mob/on_who) + . = ..() + if(!.) return - var/mob/living/living_user = user - // Aimed spells snowflake and activate without checking cast_check, very cool - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(living_user) - if(our_heretic && !our_heretic.ascended && !HAS_TRAIT(living_user, TRAIT_ALLOW_HERETIC_CASTING)) - user.balloon_alert(living_user, "you need a focus!") + + if(!isliving(on_who)) return // Delete existing if(blade_effect) + stack_trace("[type] had an existing blade effect in on_activation. This might be an exploit, and should be investigated.") + UnregisterSignal(blade_effect, COMSIG_PARENT_QDELETING) QDEL_NULL(blade_effect) - . = ..() - blade_effect = living_user.apply_status_effect(/datum/status_effect/protective_blades, null, 3, 25, 0.66 SECONDS) + var/mob/living/living_user = on_who + blade_effect = living_user.apply_status_effect(/datum/status_effect/protective_blades, null, projectile_amount, 25, 0.66 SECONDS) RegisterSignal(blade_effect, COMSIG_PARENT_QDELETING, .proc/on_status_effect_deleted) -/obj/effect/proc_holder/spell/aimed/furious_steel/cast_check(skipcharge = 0,mob/user = usr) - . = ..() - if(!.) - // We shouldn't cast for some reason, likely due to losing our focus - delete the blades - QDEL_NULL(blade_effect) - -/obj/effect/proc_holder/spell/aimed/furious_steel/on_deactivation(mob/user) +/datum/action/cooldown/spell/pointed/projectile/furious_steel/on_deactivation(mob/on_who, refund_cooldown = TRUE) . = ..() QDEL_NULL(blade_effect) -/obj/effect/proc_holder/spell/aimed/furious_steel/InterceptClickOn(mob/living/caller, params, atom/target) - if(get_dist(caller, target) <= 1) // Let the caster prioritize melee attacks over blade casts - return FALSE - return ..() - -/obj/effect/proc_holder/spell/aimed/furious_steel/cast(list/targets, mob/living/user) +/datum/action/cooldown/spell/pointed/projectile/furious_steel/before_cast(atom/cast_on) if(isnull(blade_effect) || !length(blade_effect.blades)) - return FALSE + unset_click_ability(owner, refund_cooldown = TRUE) + return SPELL_CANCEL_CAST + return ..() -/obj/effect/proc_holder/spell/aimed/furious_steel/ready_projectile(obj/projectile/to_launch, atom/target, mob/user, iteration) - . = ..() - to_launch.def_zone = check_zone(user.zone_selected) - -/obj/effect/proc_holder/spell/aimed/furious_steel/fire_projectile(mob/living/user, atom/target) +/datum/action/cooldown/spell/pointed/projectile/furious_steel/fire_projectile(mob/living/user, atom/target) . = ..() qdel(blade_effect.blades[1]) -/obj/effect/proc_holder/spell/aimed/furious_steel/proc/on_status_effect_deleted(datum/source) +/datum/action/cooldown/spell/pointed/projectile/furious_steel/ready_projectile(obj/projectile/to_launch, atom/target, mob/user, iteration) + . = ..() + to_launch.def_zone = check_zone(user.zone_selected) + +/// If our blade status effect is deleted, clear our refs and deactivate +/datum/action/cooldown/spell/pointed/projectile/furious_steel/proc/on_status_effect_deleted(datum/status_effect/protective_blades/source) SIGNAL_HANDLER blade_effect = null diff --git a/code/modules/antagonists/heretic/magic/madness_touch.dm b/code/modules/antagonists/heretic/magic/madness_touch.dm index 240dee710ea..ac9e2b3b87c 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" + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "mad_touch" + school = SCHOOL_FORBIDDEN - charge_max = 150 - clothes_req = FALSE + cooldown_time = 15 SECONDS invocation_type = INVOCATION_NONE - range = 2 + 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 - 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.can_block_magic(antimagic_flags)) - tar.visible_message(span_danger("The spell bounces off of [target]!"), span_danger("The spell bounces off of you!")) - return - if(target.mind && !IS_HERETIC(target)) - to_chat(user, span_warning("[target.name] has been cursed!")) - SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, "gates_of_mansus", /datum/mood_event/gates_of_mansus) + 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 + + 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 1b5cdea5b75..d3e62bc9b1d 100644 --- a/code/modules/antagonists/heretic/magic/manse_link.dm +++ b/code/modules/antagonists/heretic/magic/manse_link.dm @@ -1,55 +1,50 @@ -// Manse link action for Raw Prophets -// Actually an action larping as a spell, because spells don't track what they're attached to. -/datum/action/cooldown/manse_link +/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" - background_icon_state = "bg_ecult" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' + + school = SCHOOL_FORBIDDEN cooldown_time = 20 SECONDS - text_cooldown = FALSE - click_to_activate = TRUE + + invocation = "PI'RC' TH' M'ND." + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + + cast_range = 7 + /// The time it takes to link to a mob. var/link_time = 6 SECONDS - /// The range of the cast. Expanded beyond normal view range by default, as Raw Prophets have a larger sight range. - var/range = 10 - /// The text the caster is forced tos ay. - var/invocation_text = "PI'RC' TH' M'ND" -/datum/action/cooldown/manse_link/New(Target) +/datum/action/cooldown/spell/pointed/manse_link/New(Target) . = ..() 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/cooldown/manse_link/InterceptClickOn(mob/living/caller, params, atom/clicked_on) - if(!isliving(clicked_on)) - return FALSE - if(clicked_on == caller) - return FALSE - if(get_dist(caller, clicked_on) > range) - to_chat(caller, span_warning("[clicked_on] is too far to establish a link.")) // Not a balloon alert due being so zoomed out. +/datum/action/cooldown/spell/pointed/manse_link/is_valid_target(atom/cast_on) + . = ..() + if(!.) return FALSE - return ..() + return isliving(cast_on) -/datum/action/cooldown/manse_link/Activate(atom/victim) - owner.say("#[invocation_text]", forced = "spell") +/datum/action/cooldown/spell/pointed/manse_link/before_cast(mob/living/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return - // Short cooldown placed during the channel to prevent spam links. - StartCooldown(10 SECONDS) - - // If we link successfuly, we can start the full cooldown duration. - if(do_linking(victim)) - StartCooldown() - - return TRUE + // If we fail to link, cancel the spell. + if(!do_linking(cast_on)) + return . | SPELL_CANCEL_CAST /** * The actual process of linking [linkee] to our network. */ -/datum/action/cooldown/manse_link/proc/do_linking(mob/living/linkee) +/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!")) diff --git a/code/modules/antagonists/heretic/magic/mansus_grasp.dm b/code/modules/antagonists/heretic/magic/mansus_grasp.dm index 27a51b72e98..98dece5f54e 100644 --- a/code/modules/antagonists/heretic/magic/mansus_grasp.dm +++ b/code/modules/antagonists/heretic/magic/mansus_grasp.dm @@ -1,22 +1,64 @@ -/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." - hand_path = /obj/item/melee/touch_attack/mansus_fist + 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 - charge_max = 10 SECONDS - clothes_req = FALSE - action_icon = 'icons/mob/actions/actions_ecult.dmi' - action_icon_state = "mansus_grasp" - action_background_icon_state = "bg_ecult" - antimagic_flags = NONE // no casting restriction but there is a can_block_magic check in afterattack to allow antimagic + 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 + +/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." + 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" - catchphrase = "R'CH T'H TR'TH!" - on_use_sound = 'sound/items/welder.ogg' /obj/item/melee/touch_attack/mansus_fist/Initialize(mapload) . = ..() @@ -30,67 +72,19 @@ * 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) + var/datum/action/cooldown/spell/touch/mansus_grasp/grasp = spell_which_made_us?.resolve() + grasp?.spell_feedback() + + remove_hand_with_no_refund(user) /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!") - use_charge(user) - -/obj/item/melee/touch_attack/mansus_fist/afterattack(atom/target, mob/user, proximity_flag, click_parameters) - if(!proximity_flag || !isliving(target) || !IS_HERETIC(user) || target == user) - return - if(ishuman(target)) - var/mob/living/carbon/human/human_target = target - if(human_target.can_block_magic()) - human_target.visible_message( - span_danger("The spell bounces off of [target]!"), - span_danger("The spell bounces off of you!"), - ) - return ..() - if(!on_mob_hit(target, user)) - return - - return ..() - -/obj/item/melee/touch_attack/mansus_fist/afterattack_secondary(atom/target, mob/user, proximity_flag, click_parameters) - . = ..() - if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) - return - - if(!proximity_flag || !IS_HERETIC(user) || target == user) - return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN - if(isliving(target)) // if it's a living mob, go with our normal afterattack - return SECONDARY_ATTACK_CALL_NORMAL - - if(SEND_SIGNAL(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK_SECONDARY, target) & COMPONENT_USE_CHARGE) - use_charge(user) - return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN - - return SECONDARY_ATTACK_CONTINUE_CHAIN - -/** - * 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 - - hit.apply_damage(10, BRUTE, wound_bonus = CANT_WOUND) - if(iscarbon(hit)) - var/mob/living/carbon/carbon_hit = hit - 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 + remove_hand_with_no_refund(user) /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 @@ -108,7 +102,7 @@ carbon_user.emote("scream") 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) diff --git a/code/modules/antagonists/heretic/magic/mirror_walk.dm b/code/modules/antagonists/heretic/magic/mirror_walk.dm index 52461cbc36f..3c81d027e34 100644 --- a/code/modules/antagonists/heretic/magic/mirror_walk.dm +++ b/code/modules/antagonists/heretic/magic/mirror_walk.dm @@ -1,22 +1,15 @@ -/// Macro to check if the passed mob is currently in jaunting "in the mirror". -#define IS_MIRROR_PHASED(mob) istype(user.loc, /obj/effect/dummy/phased_mob/mirror_walk) - -/obj/effect/proc_holder/spell/targeted/mirror_walk +/datum/action/cooldown/spell/jaunt/mirror_walk name = "Mirror Walk" desc = "Allows you to traverse invisibly and freely across the station within the realm of the mirror. \ You can only enter and exit the realm of mirrors when nearby reflective surfaces and items, \ such as windows, mirrors, and reflective walls or equipment." - action_icon = 'icons/mob/actions/actions_minor_antag.dmi' - action_icon_state = "ninja_cloak" - action_background_icon_state = "bg_ecult" - charge_max = 6 SECONDS - cooldown_min = 0 - clothes_req = FALSE - antimagic_flags = NONE - phase_allowed = TRUE - range = -1 - include_user = TRUE - overlay = null + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "ninja_cloak" + + cooldown_time = 6 SECONDS + jaunt_type = /obj/effect/dummy/phased_mob/mirror_walk + spell_requirements = NONE /// The time it takes to enter the mirror / phase out / enter jaunt. var/phase_out_time = 1.5 SECONDS @@ -28,110 +21,88 @@ /obj/structure/mirror, )) -/obj/effect/proc_holder/spell/targeted/mirror_walk/on_lose(mob/living/user) - if(IS_MIRROR_PHASED(user)) - var/obj/effect/dummy/phased_mob/mirror_walk/phase = user.loc - phase.eject_user() - qdel(phase) - -/obj/effect/proc_holder/spell/targeted/mirror_walk/cast_check(skipcharge = FALSE, mob/user = usr) +/datum/action/cooldown/spell/jaunt/mirror_walk/can_cast_spell(feedback = TRUE) . = ..() if(!.) return FALSE - var/we_are_phasing = IS_MIRROR_PHASED(user) - var/turf/user_turf = get_turf(user) - var/area/user_area = get_area(user) - if(!user_turf || !user_area) - return FALSE // nullspaced? - - if(user_area.area_flags & NOTELEPORT) - to_chat(user, span_warning("An otherwordly force is preventing you from [we_are_phasing ? "exiting":"entering"] the mirror's realm here!")) + var/we_are_phasing = is_jaunting(owner) + var/turf/owner_turf = get_turf(owner) + if(!is_reflection_nearby(get_turf(owner_turf))) + if(feedback) + to_chat(owner, span_warning("There are no reflective surfaces nearby to [we_are_phasing ? "exit":"enter"] the mirror's realm here!")) return FALSE - if(user_turf.turf_flags & NOJAUNT) - to_chat(user, span_warning("An otherwordly force is preventing you from [we_are_phasing ? "exiting":"entering"] the mirror's realm here!")) + if(owner_turf.is_blocked_turf(exclude_mobs = TRUE)) + if(feedback) + to_chat(owner, span_warning("Something is blocking you from [we_are_phasing ? "exiting":"entering"] the mirror's realm here!")) return FALSE return TRUE -/obj/effect/proc_holder/spell/targeted/mirror_walk/cast(list/targets, mob/living/user = usr) - var/we_are_phasing = IS_MIRROR_PHASED(user) - var/turf/user_turf = get_turf(user) +/datum/action/cooldown/spell/jaunt/mirror_walk/cast(mob/living/cast_on) + . = ..() + if(is_jaunting(cast_on)) + return exit_jaunt(cast_on) + else + return enter_jaunt(cast_on) - if(!is_reflection_nearby(user_turf)) - to_chat(user, span_warning("There are no reflective surfaces nearby to [we_are_phasing ? "exit":"enter"] the mirror's realm here!")) - return FALSE - - if(user_turf.is_blocked_turf(exclude_mobs = TRUE)) - to_chat(user, span_warning("Something is blocking you from [we_are_phasing ? "exiting":"entering"] the mirror's realm here!")) - return FALSE - - // If our loc is a phased mob, we're currently jaunting so we should exit - if(we_are_phasing) - try_exit_phase(user) +/datum/action/cooldown/spell/jaunt/mirror_walk/enter_jaunt(mob/living/jaunter, turf/loc_override) + var/atom/nearby_reflection = is_reflection_nearby(jaunter) + if(!nearby_reflection) + to_chat(jaunter, span_warning("There are no reflective surfaces nearby to enter the mirror's realm!")) return - // Otherwise try to enter like normal - try_enter_phase(user) + jaunter.Beam(nearby_reflection, icon_state = "light_beam", time = phase_out_time) + nearby_reflection.visible_message(span_warning("[nearby_reflection] begins to shimmer and shake slightly!")) + if(!do_after(jaunter, phase_out_time, nearby_reflection, IGNORE_USER_LOC_CHANGE|IGNORE_INCAPACITATED)) + return -/obj/effect/proc_holder/spell/targeted/mirror_walk/proc/try_exit_phase(mob/living/user) - var/obj/effect/dummy/phased_mob/mirror_walk/phase = user.loc - var/atom/nearby_reflection = is_reflection_nearby(phase) + playsound(jaunter, 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1) + jaunter.visible_message( + span_boldwarning("[jaunter] phases out of reality, vanishing before your very eyes!"), + span_notice("You jump into the reflection coming off of [nearby_reflection], entering the mirror's realm."), + ) + + // Pass the turf of the nearby reflection to the parent call + // as that's the location we're actually jaunting into + return ..(jaunter, get_turf(nearby_reflection)) + +/datum/action/cooldown/spell/jaunt/mirror_walk/exit_jaunt(mob/living/unjaunter, turf/loc_override) + var/turf/phase_turf = get_turf(unjaunter) + var/atom/nearby_reflection = is_reflection_nearby(phase_turf) if(!nearby_reflection) - to_chat(user, span_warning("There are no reflective surfaces nearby to exit from the mirror's realm!")) + to_chat(unjaunter, span_warning("There are no reflective surfaces nearby to exit from the mirror's realm!")) return FALSE - var/turf/phase_turf = get_turf(phase) - // It would likely be a bad idea to teleport into an ai monitored area (ai sat) var/area/phase_area = get_area(phase_turf) if(istype(phase_area, /area/station/ai_monitored)) - to_chat(user, span_warning("It's probably not a very wise idea to exit the mirror's realm here.")) + to_chat(unjaunter, span_warning("It's probably not a very wise idea to exit the mirror's realm here.")) return FALSE nearby_reflection.Beam(phase_turf, icon_state = "light_beam", time = phase_in_time) nearby_reflection.visible_message(span_warning("[nearby_reflection] begins to shimmer and shake slightly!")) - if(!do_after(user, phase_in_time, nearby_reflection)) - return + if(!do_after(unjaunter, phase_in_time, nearby_reflection)) + return FALSE - playsound(get_turf(user), 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) - user.visible_message( - span_boldwarning("[user] phases into reality before your very eyes!"), + // We can move around while phasing in, but we'll always end up where we started it. + // Pass the jaunter's turf at the start of the proc back to the parent call. + . = ..(unjaunter, phase_turf) + if(!.) + return FALSE + + playsound(unjaunter, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) + unjaunter.visible_message( + span_boldwarning("[unjaunter] phases into reality before your very eyes!"), span_notice("You jump out of the reflection coming off of [nearby_reflection], exiting the mirror's realm."), ) - // We can move around while phasing in, - // but we'll always end up where we started it. - phase.forceMove(phase_turf) - phase.eject_user() - qdel(phase) - // Chilly! - phase_turf.TakeTemperature(-20) + if(isopenturf(phase_turf)) + phase_turf.TakeTemperature(-20) -/obj/effect/proc_holder/spell/targeted/mirror_walk/proc/try_enter_phase(mob/living/user) - var/atom/nearby_reflection = is_reflection_nearby(user) - if(!nearby_reflection) - to_chat(user, span_warning("There are no reflective surfaces nearby to enter the mirror's realm!")) - return - - user.Beam(nearby_reflection, icon_state = "light_beam", time = phase_out_time) - nearby_reflection.visible_message(span_warning("[nearby_reflection] begins to shimmer and shake slightly!")) - if(!do_after(user, phase_out_time, nearby_reflection, IGNORE_USER_LOC_CHANGE|IGNORE_INCAPACITATED)) - return - - playsound(get_turf(user), 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1) - user.visible_message( - span_boldwarning("[user] phases out of reality, vanishing before your very eyes!"), - span_notice("You jump into the reflection coming off of [nearby_reflection], entering the mirror's realm."), - ) - - user.SetAllImmobility(0) - user.setStaminaLoss(0) - - var/obj/effect/dummy/phased_mob/mirror_walk/phase = new(get_turf(nearby_reflection)) - user.forceMove(phase) + return TRUE /** * Goes through all nearby atoms in sight of the @@ -141,7 +112,7 @@ * Returns an object reference to a "reflective" object in view if one was found, * or null if no object was found that was determined to be "reflective". */ -/obj/effect/proc_holder/spell/targeted/mirror_walk/proc/is_reflection_nearby(atom/caster) +/datum/action/cooldown/spell/jaunt/mirror_walk/proc/is_reflection_nearby(atom/caster) for(var/atom/thing as anything in view(2, caster)) if(isitem(thing)) var/obj/item/item_thing = thing @@ -167,10 +138,3 @@ /obj/effect/dummy/phased_mob/mirror_walk name = "reflection" - -/obj/effect/dummy/phased_mob/mirror_walk/proc/eject_user() - var/mob/living/jaunter = locate() in contents - if(QDELETED(jaunter)) - CRASH("[type] called eject_user() without a mob/living within its contents.") - - jaunter.forceMove(drop_location()) diff --git a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm index ff433db6a11..bb00a99e863 100644 --- a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm +++ b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm @@ -1,38 +1,53 @@ -/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 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." + 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 - school = SCHOOL_FORBIDDEN - 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" + spell_requirements = SPELL_REQUIRES_HUMAN -/obj/effect/proc_holder/spell/targeted/fiery_rebirth/cast(list/targets, mob/user) - if(!ishuman(user)) - return - var/mob/living/carbon/human/human_user = user - human_user.extinguish_mob() +/datum/action/cooldown/spell/aoe/fiery_rebirth/cast(mob/living/carbon/human/cast_on) + cast_on.extinguish_mob() + return ..() - 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)) +/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 + if(IS_HERETIC_OR_MONSTER(nearby_mob)) + continue + if(nearby_mob.stat == DEAD || !nearby_mob.on_fire) 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.death() - target.adjustFireLoss(20) - new /obj/effect/temp_visual/eldritch_smoke(target.drop_location()) - human_user.adjustBruteLoss(-10, FALSE) - human_user.adjustFireLoss(-10, FALSE) - human_user.adjustToxLoss(-10, FALSE) - human_user.adjustOxyLoss(-10, FALSE) - human_user.adjustStaminaLoss(-10) + 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()) + + //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) + + // 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/eldritch.dmi' diff --git a/code/modules/antagonists/heretic/magic/rust_wave.dm b/code/modules/antagonists/heretic/magic/rust_wave.dm index c0ffddd748b..4fd8b9c5cab 100644 --- a/code/modules/antagonists/heretic/magic/rust_wave.dm +++ b/code/modules/antagonists/heretic/magic/rust_wave.dm @@ -1,40 +1,41 @@ // 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" + 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 - school = SCHOOL_FORBIDDEN - 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.can_block_magic() || 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 @@ -59,20 +60,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 + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "rust_wave" + school = SCHOOL_FORBIDDEN - 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" + cooldown_time = 35 SECONDS + invocation = "SPR'D TH' WO'D" invocation_type = INVOCATION_WHISPER + spell_requirements = NONE -/obj/projectile/magic/spell/rust_wave + projectile_type = /obj/projectile/magic/aoe/rust_wave + +/obj/projectile/magic/aoe/rust_wave name = "Patron's Reach" icon_state = "eldritch_projectile" alpha = 180 @@ -84,7 +88,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 +106,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 0e6396acbee..2b0efb9d089 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 = "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." - 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 - school = SCHOOL_FORBIDDEN - 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) + return ..() + +/datum/action/cooldown/spell/pointed/void_phase/cast(atom/cast_on) . = ..() - var/target = targets[1] - var/turf/targeted_turf = get_turf(target) + var/turf/source_turf = get_turf(owner) + var/turf/targeted_turf = get_turf(cast_on) - playsound(user,'sound/magic/voidblink.ogg',100) - playsound(targeted_turf,'sound/magic/voidblink.ogg',100) - - 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)) - continue - living_mob.adjustBruteLoss(40) + // 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(1, targeted_turf) - user) - if(IS_HERETIC_OR_MONSTER(living_mob)) + 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) - do_teleport(user,targeted_turf,TRUE,no_effects = TRUE,channel=TELEPORT_CHANNEL_MAGIC) + 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.apply_damage(40, BRUTE, wound_bonus = CANT_WOUND) + + do_teleport( + owner, + targeted_turf, + precision = 1, + no_effects = TRUE, + channel = TELEPORT_CHANNEL_MAGIC, + ) /obj/effect/temp_visual/voidin icon = 'icons/effects/96x96.dmi' diff --git a/code/modules/antagonists/heretic/magic/void_pull.dm b/code/modules/antagonists/heretic/magic/void_pull.dm index 3d2b523f4d0..f8063f8635f 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, damages people already around you. If they are 4 tiles or closer they are also knocked down and a micro-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 - school = SCHOOL_FORBIDDEN - clothes_req = FALSE - range = -1 - include_user = TRUE - charge_max = 400 + spell_requirements = NONE -/obj/effect/proc_holder/spell/targeted/void_pull/cast(list/targets, mob/user) + 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 + +// 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 + if(IS_HERETIC_OR_MONSTER(nearby_mob)) continue - living_mob.adjustBruteLoss(30) - 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) + things += nearby_mob - if(get_dist(user, livies) < 4) - livies.AdjustKnockdown(3 SECONDS) - livies.AdjustParalyzed(0.5 SECONDS) + return things - for(var/i in 1 to 3) - livies.forceMove(get_step_towards(livies,user)) +// 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) + + // 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/heretic/mobs/maid_in_mirror.dm b/code/modules/antagonists/heretic/mobs/maid_in_mirror.dm index 205ed6d2ce6..ec7f94354ee 100644 --- a/code/modules/antagonists/heretic/mobs/maid_in_mirror.dm +++ b/code/modules/antagonists/heretic/mobs/maid_in_mirror.dm @@ -22,7 +22,7 @@ /obj/item/clothing/suit/armor, /obj/item/organ/internal/lungs, ) - spells_to_add = list(/obj/effect/proc_holder/spell/targeted/mirror_walk) + actions_to_add = list(/datum/action/cooldown/spell/jaunt/mirror_walk) /// Whether we take damage when we're examined var/weak_on_examine = TRUE diff --git a/code/modules/antagonists/nightmare/nightmare_organs.dm b/code/modules/antagonists/nightmare/nightmare_organs.dm index dbd8932ac38..686835eebd6 100644 --- a/code/modules/antagonists/nightmare/nightmare_organs.dm +++ b/code/modules/antagonists/nightmare/nightmare_organs.dm @@ -8,23 +8,21 @@ name = "tumorous mass" desc = "A fleshy growth that was dug out of the skull of a Nightmare." icon_state = "brain-x-d" - var/obj/effect/proc_holder/spell/targeted/shadowwalk/shadowwalk + var/datum/action/cooldown/spell/jaunt/shadow_walk/our_jaunt /obj/item/organ/internal/brain/nightmare/Insert(mob/living/carbon/M, special = FALSE) . = ..() if(M.dna.species.id != SPECIES_NIGHTMARE) M.set_species(/datum/species/shadow/nightmare) visible_message(span_warning("[M] thrashes as [src] takes root in [M.p_their()] body!")) - var/obj/effect/proc_holder/spell/targeted/shadowwalk/SW = new - M.AddSpell(SW) - shadowwalk = SW + + our_jaunt = new(M) + our_jaunt.Grant(M) /obj/item/organ/internal/brain/nightmare/Remove(mob/living/carbon/M, special = FALSE) - if(shadowwalk) - M.RemoveSpell(shadowwalk) + QDEL_NULL(our_jaunt) return ..() - /obj/item/organ/internal/heart/nightmare name = "heart of darkness" desc = "An alien organ that twists and writhes when exposed to light." diff --git a/code/modules/antagonists/revenant/revenant.dm b/code/modules/antagonists/revenant/revenant.dm index 4b24727df6f..035bd913864 100644 --- a/code/modules/antagonists/revenant/revenant.dm +++ b/code/modules/antagonists/revenant/revenant.dm @@ -74,12 +74,27 @@ AddElement(/datum/element/simple_flying) ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT) ADD_TRAIT(src, TRAIT_SIXTHSENSE, INNATE_TRAIT) - AddSpell(new /obj/effect/proc_holder/spell/targeted/night_vision/revenant(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) + RegisterSignal(src, COMSIG_LIVING_BANED, .proc/on_baned) random_revenant_name() diff --git a/code/modules/antagonists/revenant/revenant_abilities.dm b/code/modules/antagonists/revenant/revenant_abilities.dm index 8bc04f6fc52..f3638e42e13 100644 --- a/code/modules/antagonists/revenant/revenant_abilities.dm +++ b/code/modules/antagonists/revenant/revenant_abilities.dm @@ -113,245 +113,260 @@ essence_drained = 0 //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" + 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" - antimagic_flags = MAGIC_RESISTANCE_MIND + background_icon_state = "bg_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" + 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" - antimagic_flags = MAGIC_RESISTANCE_HOLY - 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 //If it's locked and needs to be unlocked before use - var/unlock_amount = 100 //How much essence it costs to unlock - var/cast_amount = 50 //How much essence it costs to use + background_icon_state = "bg_revenant" + icon_icon = 'icons/mob/actions/actions_revenant.dmi' -/obj/effect/proc_holder/spell/aoe_turf/revenant/Initialize(mapload) + 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) . = ..() + if(!istype(target, /mob/living/simple_animal/revenant)) + stack_trace("[type] was given to a non-revenant mob, please don't.") + qdel(src) + return + if(locked) name = "[initial(name)] ([unlock_amount]SE)" else 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(!istype(user)) //Badmins, no. Badmins, don't do it. - 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(!istype(user)) //If you're not a revenant, it works. Please, please, please don't give this to a non-revenant. - name = "[initial(name)]" - if(locked) - panel = "Revenant Abilities" - locked = FALSE - return TRUE +/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 + 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(user, span_revennotice("You have unlocked [initial(name)]!")) + to_chat(cast_on, span_revennotice("You have unlocked [initial(name)]!")) panel = "Revenant Abilities" locked = FALSE - charge_counter = charge_max - return FALSE - if(!user.castcheck(-cast_amount)) - charge_counter = charge_max - return FALSE - name = "[initial(name)] ([cast_amount]E)" - user.reveal(reveal) - user.stun(stun) - if(action) - action.UpdateButtons() - 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/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) +/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 - L.visible_message(span_warning("\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/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 view(shock_range, L)) - if(M == user) + 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) - do_sparks(4, FALSE, M) - playsound(M, 'sound/machines/defib_zap.ogg', 50, TRUE, -1) - if(!M.can_block_magic(antimagic_flags)) - M.electrocute_act(shock_damage, L, flags = SHOCK_NOGLOVES) + 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/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(TRUE) - if(T.type == /turf/closed/wall && prob(15) && !HAS_TRAIT(T, TRAIT_RUSTY)) - new /obj/effect/temp_visual/revenant(T) - T.AddElement(/datum/element/rust) - if(T.type == /turf/closed/wall/r_wall && prob(10) && !HAS_TRAIT(T, TRAIT_RUSTY)) - 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) + + 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)) + 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/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) +// 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.bot_cover_flags &= ~BOT_COVER_LOCKED bot.bot_cover_flags |= BOT_COVER_OPEN - bot.emag_act(user) - for(var/mob/living/carbon/human/human in T) - if(human == user) + bot.emag_act(caster) + for(var/mob/living/carbon/human/human in victim) + if(human == caster) continue if(human.can_block_magic(antimagic_flags)) continue 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.emag_act(user) - for(var/mob/living/silicon/robot/S in T) //Only works on cyborgs, not AI - playsound(S, 'sound/machines/warning-buzzer.ogg', 50, TRUE) - 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/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.can_block_magic(antimagic_flags)) - to_chat(user, span_warning("The spell had no effect on [mob]!")) + to_chat(caster, span_warning("The spell had no effect on [mob]!")) continue new /obj/effect/temp_visual/revenant(mob.loc) if(iscarbon(mob)) @@ -372,15 +387,15 @@ 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.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 bdea0928bca..0dc55c942c0 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() var/datum/objective/santa_objective = new() diff --git a/code/modules/antagonists/slaughter/slaughter.dm b/code/modules/antagonists/slaughter/slaughter.dm index 4cef95818b1..40e65cfc236 100644 --- a/code/modules/antagonists/slaughter/slaughter.dm +++ b/code/modules/antagonists/slaughter/slaughter.dm @@ -63,8 +63,8 @@ /obj/effect/decal/cleanable/blood/innards, \ /obj/item/organ/internal/heart/demon) del_on_death = 1 - ///Sound played when consuming a body - var/feast_sound = 'sound/magic/demon_consume.ogg' + + var/crawl_type = /datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon /// How long it takes for the alt-click slam attack to come off cooldown var/slam_cooldown_time = 45 SECONDS /// The actual instance var for the cooldown @@ -76,15 +76,26 @@ /// How much our wound_bonus hitstreak bonus caps at (peak demonry) var/wound_bonus_hitstreak_max = 12 -/mob/living/simple_animal/hostile/imp/slaughter/Initialize(mapload, obj/effect/dummy/phased_mob/bloodpool)//Bloodpool is the blood pool we spawn in +/mob/living/simple_animal/hostile/imp/slaughter/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_BLOODCRAWL_EAT, "innate") - var/obj/effect/proc_holder/spell/bloodcrawl/bloodspell = new - AddSpell(bloodspell) - if(istype(loc, /obj/effect/dummy/phased_mob)) - bloodspell.phased = TRUE - if(bloodpool) - bloodpool.RegisterSignal(src, list(COMSIG_LIVING_AFTERPHASEIN,COMSIG_PARENT_QDELETING), /obj/effect/dummy/phased_mob/.proc/deleteself) + 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) + /// Performs the classic slaughter demon bodyslam on the attack_target. Yeets them a screen away. /mob/living/simple_animal/hostile/imp/slaughter/proc/bodyslam(atom/attack_target) @@ -134,11 +145,6 @@ icon_state = "innards" random_icon_states = null -/mob/living/simple_animal/hostile/imp/slaughter/phasein() - . = ..() - add_movespeed_modifier(/datum/movespeed_modifier/slaughter) - addtimer(CALLBACK(src, .proc/remove_movespeed_modifier, /datum/movespeed_modifier/slaughter), 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/internal/heart/demon name = "demon heart" @@ -154,28 +160,34 @@ /obj/item/organ/internal/heart/demon/attack(mob/M, mob/living/carbon/user, obj/target) if(M != user) return ..() - 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!")) + 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) - 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, 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!")) + + 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/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/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/internal/heart/demon/Stop() return 0 // Always beating. @@ -194,7 +206,6 @@ attack_sound = 'sound/items/bikehorn.ogg' attack_vis_effect = null - feast_sound = 'sound/misc/scary_horn.ogg' deathsound = 'sound/misc/sadtrombone.ogg' icon_state = "bowmon" @@ -203,6 +214,7 @@ 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() @@ -211,10 +223,6 @@ if(SSevents.holidays && SSevents.holidays[APRIL_FOOLS]) icon_state = "honkmon" -/mob/living/simple_animal/hostile/imp/slaughter/laughter/Destroy() - release_friends() - . = ..() - /mob/living/simple_animal/hostile/imp/slaughter/laughter/ex_act(severity) switch(severity) if(EXPLODE_DEVASTATE) @@ -224,48 +232,6 @@ if(EXPLODE_LIGHT) adjustBruteLoss(30) -/mob/living/simple_animal/hostile/imp/slaughter/laughter/proc/release_friends() - if(!consumed_mobs) - return - - var/turf/T = get_turf(src) - - for(var/mob/living/M in consumed_mobs) - if(!M) - continue - - // Unregister the signal first, otherwise it'll trigger the "ling revived inside us" code - UnregisterSignal(M, COMSIG_MOB_STATCHANGE) - - M.forceMove(T) - if(M.revive(full_heal = TRUE, admin_revive = TRUE)) - M.grab_ghost(force = TRUE) - playsound(T, feast_sound, 50, TRUE, -1) - to_chat(M, span_clown("You leave [src]'s warm embrace, and feel ready to take on the world.")) - -/mob/living/simple_animal/hostile/imp/slaughter/laughter/bloodcrawl_swallow(mob/living/victim) - // Keep their corpse so rescue is possible - consumed_mobs += victim - RegisterSignal(victim, COMSIG_MOB_STATCHANGE, .proc/on_victim_statchange) - -/* 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. - */ -/mob/living/simple_animal/hostile/imp/slaughter/laughter/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(src)) - victim.exit_blood_effect() - victim.visible_message(span_warning("[victim] falls out of the air, covered in blood, with a confused look on their face.")) - consumed_mobs -= victim - UnregisterSignal(victim, COMSIG_MOB_STATCHANGE) - /mob/living/simple_animal/hostile/imp/slaughter/engine_demon name = "engine demon" faction = list("hell", "neutral") diff --git a/code/modules/antagonists/slaughter/slaughterevent.dm b/code/modules/antagonists/slaughter/slaughterevent.dm index 89b1941ecee..b2463f6b642 100644 --- a/code/modules/antagonists/slaughter/slaughterevent.dm +++ b/code/modules/antagonists/slaughter/slaughterevent.dm @@ -30,13 +30,16 @@ message_admins("No valid spawn locations found, aborting...") return MAP_ERROR - var/obj/effect/dummy/phased_mob/holder = new /obj/effect/dummy/phased_mob((pick(spawn_locs))) - var/mob/living/simple_animal/hostile/imp/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) + player_mind.transfer_to(S) player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/slaughter_demon)) player_mind.special_role = ROLE_SLAUGHTER_DEMON player_mind.add_antag_datum(/datum/antagonist/slaughter) - 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 70596a17dd3..34511dbcf3e 100644 --- a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm +++ b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm @@ -90,19 +90,7 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module)) /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 - /// The linked proc holder that contains the actual ability code - var/obj/effect/proc_holder/ranged_ai/linked_ability - /// The path of our linked ability - var/linked_ability_type - -/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 @@ -114,32 +102,6 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module)) 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 - /// Appears when the user activates the ability - var/enable_text = "Hello World!" - /// Appears when the user deactivates the ability - var/disable_text = "Goodbye Cruel World!" - 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 base module type, which holds info about each ability. /datum/ai_module var/name = "generic module" @@ -427,44 +389,44 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module)) 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/New() - ..() - desc = "[desc] It has [uses] use\s remaining." - -/datum/action/innate/ai/ranged/override_machine/proc/animate_machine(obj/machinery/M) - if(M && !QDELETED(M)) - new/mob/living/simple_animal/hostile/mimic/copy/machine(get_turf(M), M, owner, 1) - -/obj/effect/proc_holder/ranged_ai/override_machine - active = FALSE 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, span_warning("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, span_warning("That machine can't be overridden!")) - return - ranged_ability_user.playsound_local(ranged_ability_user, 'sound/misc/interference.ogg', 50, 0, use_reverb = FALSE) - attached_action.adjust_uses(-1) - if(attached_action?.uses) - attached_action.desc = "[initial(attached_action.desc)] It has [attached_action.uses] use\s remaining." - attached_action.UpdateButtons() - target.audible_message(span_userdanger("You hear a loud electrical buzzing sound coming from [target]!")) - addtimer(CALLBACK(attached_action, /datum/action/innate/ai/ranged/override_machine.proc/animate_machine, target), 50) //kabeep! - remove_ranged_ability(span_danger("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) + /// Destroy RCDs: Detonates all non-cyborg RCDs on the station. /datum/ai_module/destructive/destroy_rcd name = "Destroy RCDs" @@ -504,47 +466,46 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module)) 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 + 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." /datum/action/innate/ai/ranged/overload_machine/New() ..() desc = "[desc] It has [uses] use\s remaining." -/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] ([M.type]) at [ADMIN_VERBOSEJMP(T)].") - log_game("[key_name(usr)] overloaded [M.name] ([M.type]) at [AREACOORD(T)].") - explosion(M, heavy_impact_range = 2, light_impact_range = 3) - if(M) //to check if the explosion killed it before we try to delete it - qdel(M) +/datum/action/innate/ai/ranged/overload_machine/proc/detonate_machine(mob/living/caller, obj/machinery/to_explode) + if(QDELETED(to_explode)) + return -/obj/effect/proc_holder/ranged_ai/overload_machine - active = FALSE - 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." + 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) -/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, span_warning("You can only overload machines!")) - return - if(is_type_in_typecache(target, GLOB.blacklisted_malf_machines)) - to_chat(ranged_ability_user, span_warning("You cannot overload that device!")) - return - ranged_ability_user.playsound_local(ranged_ability_user, SFX_SPARKS, 50, 0) - attached_action.adjust_uses(-1) - if(attached_action?.uses) - attached_action.desc = "[initial(attached_action.desc)] It has [attached_action.uses] use\s remaining." - attached_action.UpdateButtons() - target.audible_message(span_userdanger("You hear a loud electrical buzzing sound coming from [target]!")) - addtimer(CALLBACK(attached_action, /datum/action/innate/ai/ranged/overload_machine.proc/detonate_machine, target), 50) //kaboom! - remove_ranged_ability(span_danger("Overcharging machine...")) +/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 /// Blackout: Overloads a random number of lights across the station. Three uses. 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 00000000000..0d5982d6689 --- /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 00000000000..beec27fc99d --- /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 00000000000..d200b897588 --- /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 00000000000..4227927ae22 --- /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 00000000000..6dbc92d4a26 --- /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/offensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm new file mode 100644 index 00000000000..75fbb720a79 --- /dev/null +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm @@ -0,0 +1,115 @@ +// Offensive wizard spells +/datum/spellbook_entry/fireball + name = "Fireball" + desc = "Fires an explosive fireball at a target. Considered a classic among all wizards." + spell_type = /datum/action/cooldown/spell/pointed/projectile/fireball + category = "Offensive" + +/datum/spellbook_entry/spell_cards + name = "Spell Cards" + desc = "Blazing hot rapid-fire homing cards. Send your foes to the shadow realm with their mystical power!" + spell_type = /datum/action/cooldown/spell/pointed/projectile/spell_cards + category = "Offensive" + +/datum/spellbook_entry/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." + spell_type = /datum/action/cooldown/spell/rod_form + category = "Offensive" + +/datum/spellbook_entry/disintegrate + name = "Smite" + desc = "Charges your hand with an unholy energy that can be used to cause a touched victim to violently explode." + spell_type = /datum/action/cooldown/spell/touch/smite + category = "Offensive" + +/datum/spellbook_entry/blind + name = "Blind" + desc = "Temporarily blinds a single target." + spell_type = /datum/action/cooldown/spell/pointed/blind + category = "Offensive" + cost = 1 + +/datum/spellbook_entry/mutate + name = "Mutate" + desc = "Causes you to turn into a hulk and gain laser vision for a short while." + spell_type = /datum/action/cooldown/spell/apply_mutations/mutate + category = "Offensive" + +/datum/spellbook_entry/fleshtostone + name = "Flesh to Stone" + desc = "Charges your hand with the power to turn victims into inert statues for a long period of time." + spell_type = /datum/action/cooldown/spell/touch/flesh_to_stone + category = "Offensive" + +/datum/spellbook_entry/teslablast + 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." + spell_type = /datum/action/cooldown/spell/tesla + category = "Offensive" + +/datum/spellbook_entry/lightningbolt + name = "Lightning Bolt" + desc = "Fire a lightning bolt at your foes! It will jump between targets, but can't knock them down." + spell_type = /datum/action/cooldown/spell/pointed/projectile/lightningbolt + category = "Offensive" + cost = 1 + +/datum/spellbook_entry/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." + spell_type = /datum/action/cooldown/spell/conjure_item/infinite_guns/gun + category = "Offensive" + cost = 3 + no_coexistance_typecache = list(/datum/action/cooldown/spell/conjure_item/infinite_guns/arcane_barrage) + +/datum/spellbook_entry/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." + spell_type = /datum/action/cooldown/spell/conjure_item/infinite_guns/arcane_barrage + category = "Offensive" + cost = 3 + no_coexistance_typecache = list(/datum/action/cooldown/spell/conjure_item/infinite_guns/gun) + +/datum/spellbook_entry/barnyard + name = "Barnyard Curse" + desc = "This spell dooms an unlucky soul to possess the speech and facial attributes of a barnyard animal." + spell_type = /datum/action/cooldown/spell/pointed/barnyardcurse + category = "Offensive" + +/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 + category = "Offensive" + +/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 + category = "Offensive" + +/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/mjollnir + category = "Offensive" + +/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 + category = "Offensive" + +/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 + category = "Offensive" + +/datum/spellbook_entry/item/highfrequencyblade + name = "High Frequency Blade" + desc = "An incredibly swift enchanted blade resonating at a frequency high enough to be able to slice through anything." + item_path = /obj/item/highfrequencyblade/wizard + category = "Offensive" + cost = 3 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 00000000000..37bdef69a64 --- /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 00000000000..df83035fc7f --- /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 15a903f0897..d48121c6d39 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 = "\improper Space Wizard" roundend_category = "wizards/witches" @@ -145,7 +148,12 @@ objectives += hijack_objective /datum/antagonist/wizard/on_removal() - 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() @@ -215,27 +223,58 @@ . = ..() if(!owner) CRASH("Antag datum with no owner.") - 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 life-saving 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/pointed/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.") + 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 @@ -275,9 +314,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/academy name = "Academy Teacher" @@ -287,17 +329,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/obj/item/implant/exile/Implant = new/obj/item/implant/exile(M) - Implant.implant(M) + 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/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") @@ -325,12 +369,18 @@ else parts += span_redtext("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 9f9dcc87072..d6a0b3c1bc1 100644 --- a/code/modules/awaymissions/mission_code/Academy.dm +++ b/code/modules/awaymissions/mission_code/Academy.dm @@ -311,7 +311,7 @@ //Random One-use spellbook T.visible_message(span_userdanger("A magical looking book drops to the floor!")) do_smoke(0, holder = src, location = 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(span_userdanger("A Dice Servant appears in a cloud of smoke!")) @@ -331,9 +331,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 @@ -365,31 +364,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 - invocation = "JE VES" + 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 - var/mob/living/target_mob + smoke_type = /datum/effect_system/fluid_spread/smoke + smoke_amt = 2 - action_icon_state = "summons" + var/datum/weakref/summon_weakref -/obj/effect/proc_holder/spell/targeted/summonmob/cast(list/targets,mob/user = usr) - if(!target_mob) +/datum/action/cooldown/spell/summon_mob/New(Target, mob/living/summoned_mob) + . = ..() + if(summoned_mob) + summon_weakref = WEAKREF(summoned_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 a3056482498..f2d1e8b53e4 100644 --- a/code/modules/clothing/chameleon.dm +++ b/code/modules/clothing/chameleon.dm @@ -394,6 +394,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) . = ..() @@ -425,6 +426,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) . = ..() @@ -455,6 +457,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) . = ..() @@ -486,6 +489,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) . = ..() @@ -516,6 +520,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) . = ..() @@ -567,6 +572,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) /obj/item/clothing/mask/chameleon/emp_act(severity) . = ..() @@ -624,6 +630,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) . = ..() @@ -655,6 +662,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) . = ..() @@ -681,6 +689,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() . = ..() @@ -710,6 +719,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) . = ..() @@ -732,6 +742,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) . = ..() @@ -752,6 +763,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) . = ..() @@ -778,6 +790,7 @@ chameleon_action.chameleon_blacklist = typecacheof(/obj/item/clothing/neck/cloak/skill_reward) chameleon_action.chameleon_name = "Neck Accessory" chameleon_action.initialize_disguises() + add_item_action(chameleon_action) /obj/item/clothing/neck/chameleon/Destroy() qdel(chameleon_action) @@ -814,6 +827,7 @@ chameleon_action.chameleon_name = "Gun" chameleon_action.chameleon_blacklist = typecacheof(/obj/item/gun/energy/minigun) chameleon_action.initialize_disguises() + add_item_action(chameleon_action) recharge_newshot() set_chameleon_disguise(/obj/item/gun/energy/laser) diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index 6b728f9c07b..fe2b7e46d43 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -53,7 +53,7 @@ var/obj/item/food/clothing/moth_snack /obj/item/clothing/Initialize(mapload) - if((clothing_flags & VOICEBOX_TOGGLABLE)) + if(clothing_flags & VOICEBOX_TOGGLABLE) actions_types += /datum/action/item_action/toggle_voice_box . = ..() AddElement(/datum/element/venue_price, FOOD_PRICE_CHEAP) diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index da4b0633845..e7971d0f2eb 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -475,6 +475,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) . = ..() diff --git a/code/modules/clothing/glasses/hud.dm b/code/modules/clothing/glasses/hud.dm index b734f0037f3..67c637ce1e3 100644 --- a/code/modules/clothing/glasses/hud.dm +++ b/code/modules/clothing/glasses/hud.dm @@ -131,6 +131,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) . = ..() @@ -210,6 +211,9 @@ var/datum/atom_hud/our_hud = GLOB.huds[hud_type] our_hud.show_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." @@ -254,4 +258,3 @@ desc = "These sunglasses are special, and let you view potential criminals." icon_state = "sun" inhand_icon_state = "sunglasses" - diff --git a/code/modules/clothing/head/hardhat.dm b/code/modules/clothing/head/hardhat.dm index b5b59c0f7f0..391726b9a58 100644 --- a/code/modules/clothing/head/hardhat.dm +++ b/code/modules/clothing/head/hardhat.dm @@ -137,6 +137,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/hardhat/weldhat/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 de154dac732..395ccd20821 100644 --- a/code/modules/clothing/masks/hailer.dm +++ b/code/modules/clothing/masks/hailer.dm @@ -201,6 +201,9 @@ GLOBAL_LIST_INIT(hailer_phrases, list( playsound(src, 'sound/misc/whistle.ogg', 75, FALSE, 4) cooldown = world.time +/datum/action/item_action/halt + name = "HALT!" + #undef PHRASE_COOLDOWN #undef OVERUSE_COOLDOWN #undef AGGR_GOOD_COP diff --git a/code/modules/clothing/spacesuits/_spacesuits.dm b/code/modules/clothing/spacesuits/_spacesuits.dm index 90fbb379dd1..c6a0f477530 100644 --- a/code/modules/clothing/spacesuits/_spacesuits.dm +++ b/code/modules/clothing/spacesuits/_spacesuits.dm @@ -70,24 +70,23 @@ // Space Suit temperature regulation and power usage /obj/item/clothing/suit/space/process(delta_time) - 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, span_warning("The thermal regulator cuts off as [cell] runs out of charge.")) return @@ -195,20 +194,24 @@ to_chat(user, span_notice("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, span_warning("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, span_notice("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/emag_act(mob/user) diff --git a/code/modules/clothing/spacesuits/plasmamen.dm b/code/modules/clothing/spacesuits/plasmamen.dm index f5fae2fd935..1bd1ea5ff47 100644 --- a/code/modules/clothing/spacesuits/plasmamen.dm +++ b/code/modules/clothing/spacesuits/plasmamen.dm @@ -56,7 +56,7 @@ var/visor_icon = "envisor" var/smile_state = "envirohelm_smile" 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|PEPPERPROOF @@ -78,6 +78,13 @@ if(user.canUseTopic(src, BE_CLOSE)) toggle_welding_screen(user) +/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)) if(helmet_on) @@ -303,7 +310,7 @@ desc = "A slight modification on a traditional voidsuit helmet, this helmet was Nanotrasen's first solution to the *logistical problems* that come with employing plasmamen. Despite their limitations, these helmets still see use by historians and old-school plasmamen alike." icon_state = "prototype_envirohelm" inhand_icon_state = "prototype_envirohelm" - actions_types = list(/datum/action/item_action/toggle_welding_screen/plasmaman) + actions_types = list(/datum/action/item_action/toggle_welding_screen) smile_state = "prototype_smile" visor_icon = "prototype_envisor" diff --git a/code/modules/events/wizard/aid.dm b/code/modules/events/wizard/aid.dm index d055cc7c3d1..92747d2cd76 100644 --- a/code/modules/events/wizard/aid.dm +++ b/code/modules/events/wizard/aid.dm @@ -1,4 +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" @@ -9,16 +10,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, span_notice("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 +34,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]" + for(var/mob/living/caster as anything in GLOB.mob_living_list) + if(!length(caster.actions)) + continue - to_chat(L, span_notice("You suddenly feel more competent with your casting!")) + 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 3180ace4903..35cff5792e5 100644 --- a/code/modules/events/wizard/shuffle.dm +++ b/code/modules/events/wizard/shuffle.dm @@ -79,26 +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 || IS_WIZARD(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 - 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/pointed/mind_transfer/swapper = new - while(mobs.len > 1) - var/mob/living/carbon/human/victim = pick(mobs) - mobs -= victim - swapper.cast(list(victim), mobs[mobs.len], TRUE) - mobs -= mobs[mobs.len] + var/datum/action/cooldown/spell/pointed/mind_transfer/swapper = new() - for(var/mob/living/carbon/human/victim in GLOB.alive_mob_list) - var/datum/effect_system/fluid_spread/smoke/smoke = new - smoke.set_up(0, holder = victim, location = victim.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 3ac8c64292e..29449a195e1 100644 --- a/code/modules/instruments/items.dm +++ b/code/modules/instruments/items.dm @@ -215,6 +215,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 80e0e968b60..e306fa3962d 100644 --- a/code/modules/jobs/job_types/mime.dm +++ b/code/modules/jobs/job_types/mime.dm @@ -72,8 +72,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 = TRUE var/datum/atom_hud/fan = GLOB.huds[DATA_HUD_FAN] @@ -85,23 +87,41 @@ icon_state = "bookmime" /obj/item/book/mimery/attack_self(mob/user) + . = ..() + if(.) + return + 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") - user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall(null)) + picked_spell_type = /datum/action/cooldown/spell/conjure/invisible_wall + if("Invisible Chair") - user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair(null)) + picked_spell_type = /datum/action/cooldown/spell/conjure/invisible_chair + if("Invisible Box") - user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box(null)) - else - return - to_chat(user, span_warning("The book disappears into thin air.")) - qdel(src) + 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 diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm index 59839aeb9d9..ec7d8afa10e 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm @@ -226,16 +226,6 @@ /obj/effect/decal/remains/human/grave turf_loc_check = FALSE -/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" - oneuse = FALSE - 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.") - - //***Fluff items for lore/intrigue /obj/item/paper/crumpled/muddy/fluff/elephant_graveyard name = "posted warning" diff --git a/code/modules/mining/lavaland/megafauna_loot.dm b/code/modules/mining/lavaland/megafauna_loot.dm index 3163b81fb09..1e196fbdb6e 100644 --- a/code/modules/mining/lavaland/megafauna_loot.dm +++ b/code/modules/mining/lavaland/megafauna_loot.dm @@ -730,9 +730,8 @@ consumer.set_species(/datum/species/skeleton) if(3) to_chat(user, span_danger("Power courses through you! You can now shift your form at will.")) - if(user.mind) - var/obj/effect/proc_holder/spell/targeted/shapeshift/dragon/dragon_shapeshift = new - user.mind.AddSpell(dragon_shapeshift) + var/datum/action/cooldown/spell/shapeshift/dragon/dragon_shapeshift = new(user.mind || user) + dragon_shapeshift.Grant(user) if(4) to_chat(user, span_danger("You feel like you could walk straight through lava now.")) ADD_TRAIT(user, TRAIT_LAVA_IMMUNE, type) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index 329242adc99..628bc4f5dcb 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -66,7 +66,7 @@ if(16) new /obj/item/immortality_talisman(src) if(17) - new /obj/item/book/granter/spell/summonitem(src) + new /obj/item/book/granter/action/spell/summonitem(src) if(18) new /obj/item/book_of_babel(src) if(19) @@ -97,7 +97,7 @@ if(2) new /obj/item/lava_staff(src) if(3) - new /obj/item/book/granter/spell/sacredflame(src) + new /obj/item/book/granter/action/spell/sacredflame(src) if(4) new /obj/item/dragons_blood(src) @@ -116,7 +116,7 @@ var/loot = rand(1,2) switch(loot) if(1) - new /obj/item/bloodcrawlbottle(src) //SKYRAT EDIT + new /obj/item/mayhem(src) if(2) new /obj/item/soulscythe(src) diff --git a/code/modules/mining/lavaland/tendril_loot.dm b/code/modules/mining/lavaland/tendril_loot.dm index 8ab00be5e10..0716e57e013 100644 --- a/code/modules/mining/lavaland/tendril_loot.dm +++ b/code/modules/mining/lavaland/tendril_loot.dm @@ -716,83 +716,74 @@ lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF custom_materials = null - var/obj/effect/proc_holder/scan/scan + var/datum/action/cooldown/scan/scan_ability /obj/item/clothing/glasses/godeye/Initialize(mapload) . = ..() - scan = new(src) + 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) - user.AddAbility(scan) + 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) - user.RemoveAbility(scan) + 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/effect/proc_holder/scan +/datum/action/cooldown/scan name = "Scan" desc = "Scan an enemy, to get their location and stagger them, increasing their time between attacks." - action_background_icon_state = "bg_clock" - action_icon = 'icons/mob/actions/actions_items.dmi' - action_icon_state = "scan" + 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' - var/cooldown_time = 45 SECONDS - COOLDOWN_DECLARE(scan_cooldown) -/obj/effect/proc_holder/scan/on_lose(mob/living/user) - remove_ranged_ability() +/datum/action/cooldown/scan/IsAvailable() + return ..() && isliving(owner) -/obj/effect/proc_holder/scan/Click(location, control, params) - . = ..() - if(!isliving(usr)) - return TRUE - var/mob/living/user = usr - fire(user) +/datum/action/cooldown/scan/Activate(atom/scanned) + StartCooldown(15 SECONDS) -/obj/effect/proc_holder/scan/fire(mob/living/carbon/user) - if(active) - remove_ranged_ability(span_notice("Your eye relaxes.")) - else - add_ranged_ability(user, span_notice("Your eye starts spinning fast. Left-click a creature to scan it!"), TRUE) + if(owner.stat != CONSCIOUS) + return FALSE + if(!isliving(scanned) || scanned == owner) + owner.balloon_alert(owner, "invalid scanned!") + return FALSE -/obj/effect/proc_holder/scan/InterceptClickOn(mob/living/caller, params, atom/target) - . = ..() - if(.) - return - if(ranged_ability_user.stat) - remove_ranged_ability() - return - if(!COOLDOWN_FINISHED(src, scan_cooldown)) - balloon_alert(ranged_ability_user, "not ready!") - return - if(!isliving(target) || target == ranged_ability_user) - balloon_alert(ranged_ability_user, "invalid target!") - return - var/mob/living/living_target = target - living_target.apply_status_effect(/datum/status_effect/stagger) - var/datum/status_effect/agent_pinpointer/scan_pinpointer = ranged_ability_user.apply_status_effect(/datum/status_effect/agent_pinpointer/scan) - scan_pinpointer.scan_target = living_target - living_target.set_timed_status_effect(100 SECONDS, /datum/status_effect/jitter, only_if_higher = TRUE) - to_chat(living_target, span_warning("You've been staggered!")) - living_target.add_filter("scan", 2, list("type" = "outline", "color" = COLOR_YELLOW, "size" = 1)) - addtimer(CALLBACK(living_target, /atom/.proc/remove_filter, "scan"), 30 SECONDS) - ranged_ability_user.playsound_local(get_turf(ranged_ability_user), 'sound/magic/smoke.ogg', 50, TRUE) - balloon_alert(ranged_ability_user, "[living_target] scanned") - COOLDOWN_START(src, scan_cooldown, cooldown_time) - addtimer(CALLBACK(src, /atom/.proc/balloon_alert, ranged_ability_user, "scan recharged"), cooldown_time) - remove_ranged_ability() + 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 /datum/status_effect/agent_pinpointer/scan diff --git a/code/modules/mob/living/bloodcrawl.dm b/code/modules/mob/living/bloodcrawl.dm deleted file mode 100644 index 395d6a73d96..00000000000 --- a/code/modules/mob/living/bloodcrawl.dm +++ /dev/null @@ -1,160 +0,0 @@ -/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, span_warning("You may not hold items while blood crawling!")) - return FALSE - 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() - - notransform = TRUE - INVOKE_ASYNC(src, .proc/bloodpool_sink, B) - - return TRUE - -/mob/living/proc/bloodpool_sink(obj/effect/decal/cleanable/B) - var/turf/mobloc = get_turf(loc) - - visible_message(span_warning("[src] sinks into the pool of blood!")) - playsound(get_turf(src), 'sound/magic/enter_blood.ogg', 50, TRUE, -1) - // Extinguish, unbuckle, stop being pulled, set our location into the - // dummy object - var/obj/effect/dummy/phased_mob/holder = new /obj/effect/dummy/phased_mob(mobloc) - extinguish_mob() - - // Keep a reference to whatever we're pulling, because forceMove() - // makes us stop pulling - var/pullee = pulling - - holder = holder - forceMove(holder) - - // if we're not pulling anyone, or we can't eat anyone - if(!pullee || !HAS_TRAIT(src, TRAIT_BLOODCRAWL_EAT)) - notransform = FALSE - return - - // if the thing we're pulling isn't alive - if(!isliving(pullee)) - notransform = FALSE - return - - var/mob/living/victim = pullee - var/kidnapped = FALSE - - if(victim.stat == CONSCIOUS) - visible_message(span_warning("[victim] kicks free of the blood pool just before entering it!"), null, span_notice("You hear splashing and struggling.")) - else if(victim.reagents?.has_reagent(/datum/reagent/consumable/ethanol/demonsblood, needs_metabolizing = TRUE)) - visible_message(span_warning("Something prevents [victim] from entering the pool!"), span_warning("A strange force is blocking [victim] from entering!"), span_notice("You hear a splash and a thud.")) - else - victim.forceMove(src) - victim.emote("scream") - visible_message(span_warning("[src] drags [victim] into the pool of blood!"), null, span_notice("You hear a splash.")) - kidnapped = TRUE - - if(kidnapped) - var/success = bloodcrawl_consume(victim) - if(!success) - to_chat(src, span_danger("You happily devour... nothing? Your meal vanished at some point!")) - - notransform = FALSE - return TRUE - -/mob/living/proc/bloodcrawl_consume(mob/living/victim) - to_chat(src, span_danger("You begin to feast on [victim]... You can not move while you are doing this.")) - - var/sound - if(istype(src, /mob/living/simple_animal/hostile/imp/slaughter)) - var/mob/living/simple_animal/hostile/imp/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, TRUE) - sleep(30) - - if(!victim) - return FALSE - - if(victim.reagents?.has_reagent(/datum/reagent/consumable/ethanol/devilskiss, needs_metabolizing = TRUE)) - to_chat(src, span_warning("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(span_warning("[target] violently expels [victim]!")) - victim.exit_blood_effect(target) - found_bloodpool = TRUE - break - - if(!found_bloodpool) - // Fuck it, just eject them, thanks to some split second cleaning - victim.forceMove(get_turf(victim)) - victim.visible_message(span_warning("[victim] appears from nowhere, covered in blood!")) - victim.exit_blood_effect() - return TRUE - - to_chat(src, span_danger("You devour [victim]. Your health is fully restored.")) - revive(full_heal = TRUE, admin_revive = FALSE) - - // No defib possible after laughter - victim.adjustBruteLoss(1000) - victim.death() - bloodcrawl_swallow(victim) - return TRUE - -/mob/living/proc/bloodcrawl_swallow(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, TRUE, -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, /atom/.proc/remove_atom_colour, TEMPORARY_COLOUR_PRIORITY, newcolor), 6 SECONDS) - -/mob/living/proc/phasein(atom/target, forced = FALSE) - if(!forced) - if(notransform) - to_chat(src, span_warning("Finish eating first!")) - return FALSE - target.visible_message(span_warning("[target] starts to bubble...")) - if(!do_after(src, 20, target = target)) - return FALSE - forceMove(get_turf(target)) - client.eye = src - SEND_SIGNAL(src, COMSIG_LIVING_AFTERPHASEIN, target) - visible_message(span_boldwarning("[src] rises out of the pool of blood!")) - exit_blood_effect(target) - return TRUE - -/mob/living/carbon/phasein(atom/target, forced = FALSE) - . = ..() - if(!.) - return - for(var/obj/item/bloodcrawl/blood_hand in held_items) - blood_hand.flags_1 = null - qdel(blood_hand) diff --git a/code/modules/mob/living/brain/brain.dm b/code/modules/mob/living/brain/brain.dm index aa13a0f59a3..9be5ba56743 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_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 22d263e7c63..3a78e70ab36 100644 --- a/code/modules/mob/living/carbon/alien/alien.dm +++ b/code/modules/mob/living/carbon/alien/alien.dm @@ -117,8 +117,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, span_noticealien("You begin to evolve!")) - visible_message(span_alertalien("[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(numba && unique_name) new_xeno.numba = numba 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 268f0664229..1ff88c419d0 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm @@ -6,341 +6,380 @@ These are general powers. Specific powers are stored under the appropriate alien Doesn't work on other aliens/AI.*/ -/obj/effect/proc_holder/alien +/datum/action/cooldown/alien name = "Alien Power" panel = "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 - has_action = TRUE - base_action = /datum/action/spell_action/alien - action_icon = 'icons/mob/actions/actions_xeno.dmi' - action_icon_state = "spell_default" - action_background_icon_state = "bg_alien" -/obj/effect/proc_holder/alien/Click() - if(!iscarbon(usr)) - return 1 - 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) - return 1 - -/obj/effect/proc_holder/alien/on_gain(mob/living/carbon/user) - return - -/obj/effect/proc_holder/alien/on_lose(mob/living/carbon/user) - return - -/obj/effect/proc_holder/alien/fire(mob/living/carbon/user) - return 1 - -/obj/effect/proc_holder/alien/get_panel_text() +/datum/action/cooldown/alien/IsAvailable() . = ..() - if(plasma_cost > 0) - return "[plasma_cost]" + if(!.) + return FALSE + if(!iscarbon(owner)) + return FALSE + var/mob/living/carbon/carbon_owner = owner + if(carbon_owner.getPlasma() < plasma_cost) + return FALSE -/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, span_noticealien("You must be conscious to do this.")) - return FALSE - if(user.getPlasma() < plasma_cost) - if(!silent) - to_chat(user, span_noticealien("Not enough plasma stored.")) - return FALSE - if(check_turf && (!isturf(user.loc) || isspaceturf(user.loc))) - if(!silent) - to_chat(user, span_noticealien("Bad place for a garden!")) - return FALSE 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 +/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 + + 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) + + return TRUE + +/datum/action/cooldown/alien/set_statpanel_format() + . = ..() + 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 + +/datum/action/cooldown/alien/make_structure/IsAvailable() + . = ..() + if(!.) + return FALSE + if(!isturf(owner.loc) || isspaceturf(owner.loc)) + return FALSE + + return TRUE + +/datum/action/cooldown/alien/make_structure/PreActivate(atom/target) + if(!check_for_duplicate()) + return FALSE + + if(!check_for_vents()) + return FALSE + + return ..() + +/datum/action/cooldown/alien/make_structure/Activate(atom/target) + new made_structure_type(owner.loc) + return TRUE + +/// 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 = tgui_alert(user, "Laying eggs and shaping resin here would block access to [atmos_thing]. Do you want to continue?", "Blocking Atmospheric Component", list("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 = "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, span_warning("There's already a weed node here!")) - return FALSE - user.visible_message(span_alertalien("[user] plants some alien weeds!")) - new/obj/structure/alien/weeds/node(user.loc) - 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." + button_icon_state = "alien_whisper" plasma_cost = 10 - action_icon_state = "alien_whisper" -/obj/effect/proc_holder/alien/whisper/fire(mob/living/carbon/user) +/datum/action/cooldown/alien/whisper/Activate(atom/target) var/list/possible_recipients = list() - for(var/mob/living/recipient in oview(user)) - possible_recipients.Add(recipient) + for(var/mob/living/recipient in oview(owner)) + possible_recipients += recipient + if(!length(possible_recipients)) - to_chat(user, span_noticealien("There's no one around to whisper to.")) + to_chat(owner, span_noticealien("There's no one around to whisper to.")) return FALSE - var/mob/living/chosen_recipient = tgui_input_list(user, "Select whisper recipient", "Whisper", sort_names(possible_recipients)) - if(isnull(chosen_recipient)) + + var/mob/living/chosen_recipient = tgui_input_list(owner, "Select whisper recipient", "Whisper", sort_names(possible_recipients)) + if(!chosen_recipient) + return FALSE + + var/to_whisper = tgui_input_text(owner, title = "Alien Whisper") + if(QDELETED(chosen_recipient) || QDELETED(src) || QDELETED(owner) || !IsAvailable() || !to_whisper) return FALSE if(chosen_recipient.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) - to_chat(user, span_warning("As you reach into [chosen_recipient]'s mind, you are stopped by a mental blockage. It seems you've been foiled.")) + 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 - var/msg = tgui_input_text(user, title = "Alien Whisper") - if(isnull(msg)) - return FALSE - if(chosen_recipient.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) - to_chat(user, span_warning("As you reach into [chosen_recipient]'s mind, you are stopped by a mental blockage. It seems you've been foiled.")) - return - log_directed_talk(user, chosen_recipient, msg, LOG_SAY, tag="alien whisper") - to_chat(chosen_recipient, "[span_noticealien("You hear a strange, alien voice in your head...")][msg]") - to_chat(user, span_noticealien("You said: \"[msg]\" to [chosen_recipient]")) - for(var/ded in GLOB.dead_mob_list) - if(!isobserver(ded)) + + 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, chosen_recipient) - to_chat(ded, "[follow_link_user] [span_name("[user]")] [span_alertalien("Alien Whisper --> ")] [follow_link_whispee] [span_name("[chosen_recipient]")] [span_noticealien("[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." plasma_cost = 0 - action_icon_state = "alien_transfer" + 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/alien in oview(user)) - if(isalien(alien)) - aliens_around.Add(alien) + 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(user, span_noticealien("There are no other aliens around.")) + to_chat(owner, span_noticealien("There are no other aliens around.")) return FALSE - var/mob/living/carbon/donation_target = tgui_input_list(user, "Target to transfer to", "Plasma Donation", sort_names(aliens_around)) - if(isnull(donation_target)) - return FALSE - var/amount = tgui_input_number(user, "Amount", "Transfer Plasma to [donation_target]", max_value = user.getPlasma()) - if(!amount || QDELETED(user) || QDELETED(donation_target)) - return FALSE - if (get_dist(user, donation_target) <= 1) - donation_target.adjustPlasma(amount) - user.adjustPlasma(-amount) - to_chat(donation_target, span_noticealien("[user] has transferred [amount] plasma to you.")) - to_chat(user, span_noticealien("You transfer [amount] plasma to [donation_target].")) - else - to_chat(user, span_noticealien("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 -/obj/effect/proc_holder/alien/acid + 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 + +/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." + 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) - add_verb(user, /mob/living/carbon/proc/corrosive_acid) - -/obj/effect/proc_holder/alien/acid/on_lose(mob/living/carbon/user) - remove_verb(user, /mob/living/carbon/proc/corrosive_acid) - -/obj/effect/proc_holder/alien/acid/proc/corrode(atom/target, mob/living/carbon/user = usr) - if(!(target in oview(1,user))) - to_chat(src, span_noticealien("[target] is too far away.")) - return FALSE - if(target.acid_act(200, 1000)) - user.visible_message(span_alertalien("[user] vomits globs of vile stuff all over [target]. It begins to sizzle and melt under the bubbling mess of acid!")) - return TRUE - else - to_chat(user, span_noticealien("You cannot dissolve this object.")) - return FALSE - -/obj/effect/proc_holder/alien/acid/fire(mob/living/carbon/alien/user) - var/list/nearby_targets = list() - for(var/atom/target in oview(1, user)) - nearby_targets.Add(target) - if(!length(nearby_targets)) - to_chat(user, span_noticealien("There's nothing to corrode.")) - return FALSE - var/atom/dissolve_target = tgui_input_list(user, "Select a target to dissolve", "Dissolve", nearby_targets) - if(isnull(dissolve_target)) - return FALSE - if(QDELETED(dissolve_target) || user.incapacitated()) - return FALSE - - return corrode(dissolve_target, user) - -/mob/living/carbon/proc/corrosive_acid(O as obj|turf in oview(1)) // right click menu verb ugh - set name = "Corrosive Acid" - - if(!iscarbon(usr)) +/datum/action/cooldown/alien/acid/corrosion/set_click_ability(mob/on_who) + . = ..() + if(!.) return - var/mob/living/carbon/user = usr - var/obj/effect/proc_holder/alien/acid/A = locate() in user.abilities - if(!A) + + to_chat(on_who, span_noticealien("You prepare to vomit acid. Click a target to acid it!")) + on_who.update_icons() + +/datum/action/cooldown/alien/acid/corrosion/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) return - if(user.getPlasma() > A.plasma_cost && A.corrode(O)) - user.adjustPlasma(-A.plasma_cost) -/obj/effect/proc_holder/alien/neurotoxin - name = "Spit Neurotoxin" - desc = "Spits neurotoxin at someone, paralyzing them for a short time." - action_icon_state = "alien_neurotoxin_0" - active = FALSE + if(refund_cooldown) + to_chat(on_who, span_noticealien("You empty your corrosive acid glands.")) + on_who.update_icons() -/obj/effect/proc_holder/alien/neurotoxin/fire(mob/living/carbon/user) - var/message - if(active) - message = span_notice("You empty your neurotoxin gland.") - remove_ranged_ability(message) - else - message = span_notice("You prepare your neurotoxin gland. Left-click to fire at a target!") - add_ranged_ability(user, message, TRUE) +/datum/action/cooldown/alien/acid/corrosion/PreActivate(atom/target) + if(get_dist(owner, target) > 1) + return FALSE -/obj/effect/proc_holder/alien/neurotoxin/update_icon() - action.button_icon_state = "alien_neurotoxin_[active]" - action.UpdateButtons() return ..() -/obj/effect/proc_holder/alien/neurotoxin/InterceptClickOn(mob/living/caller, params, atom/target) +/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 + + 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 + +/datum/action/cooldown/alien/acid/neurotoxin + name = "Spit Neurotoxin" + desc = "Spits neurotoxin at someone, paralyzing them for a short time." + button_icon_state = "alien_neurotoxin_0" + plasma_cost = 50 + +/datum/action/cooldown/alien/acid/neurotoxin/IsAvailable() + return ..() && isturf(owner.loc) + +/datum/action/cooldown/alien/acid/neurotoxin/set_click_ability(mob/on_who) . = ..() - if(.) + if(!.) return - var/p_cost = 50 - if(!iscarbon(ranged_ability_user) || ranged_ability_user.stat) - remove_ranged_ability() + + 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() + +/datum/action/cooldown/alien/acid/neurotoxin/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) + return + + 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 - var/mob/living/carbon/user = ranged_ability_user - - if(user.getPlasma() < p_cost) - to_chat(user, span_warning("You need at least [p_cost] plasma to spit.")) - remove_ranged_ability() - return FALSE - - 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)) + // 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) - user.visible_message(span_danger("[user] spits neurotoxin!"), span_alertalien("You spit neurotoxin.")) - var/obj/projectile/neurotoxin/neurotoxin = new /obj/projectile/neurotoxin(user.loc) - neurotoxin.preparePixelProjectile(target, user, modifiers) - neurotoxin.firer = user + 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() - user.newtonian_move(get_dir(U, T)) - user.adjustPlasma(-p_cost) - + 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() +// Has to return TRUE, otherwise is skipped. +/datum/action/cooldown/alien/acid/neurotoxin/Activate(atom/target) + return TRUE -/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 = 1 - 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 = 0 - A.update_icons() - ..() - -/obj/effect/proc_holder/alien/resin +/datum/action/cooldown/alien/make_structure/resin name = "Secrete Resin" 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, + ) - action_icon_state = "alien_resin" +// 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 -/obj/effect/proc_holder/alien/resin/fire(mob/living/carbon/user) - if(locate(/obj/structure/alien/resin) in user.loc) - to_chat(user, span_warning("There is already a resin structure there!")) - return FALSE - - if(!check_vent_block(user)) - return FALSE - - var/choice = tgui_input_list(user, "Select a shape to build", "Resin building", structures) - if(isnull(choice)) - return FALSE - if(isnull(structures[choice])) - return FALSE - if (!cost_check(check_turf,user)) - return FALSE - to_chat(user, span_notice("You shape a [choice].")) - user.visible_message(span_notice("[user] vomits up a thick purple substance and begins to shape it.")) - - choice = structures[choice] - new choice(user.loc) return TRUE -/obj/effect/proc_holder/alien/sneak +/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 + + var/obj/structure/choice_path = structures[choice] + if(!ispath(choice_path)) + return FALSE + + 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."), + ) + + new choice_path(owner.loc) + return TRUE + +/datum/action/cooldown/alien/sneak name = "Sneak" desc = "Blend into the shadows to stalk your prey." - active = 0 + button_icon_state = "alien_sneak" + /// The alpha we go to when sneaking. + var/sneak_alpha = 75 - action_icon_state = "alien_sneak" +/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) -/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 = 1 - active = 1 - to_chat(user, span_noticealien("You blend into the shadows...")) else - user.alpha = initial(user.alpha) - user.sneaking = 0 - active = 0 - to_chat(user, span_noticealien("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/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel) if(!vessel) - return 0 - 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/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.UpdateButtons() + 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 ba7a76c8b0a..7d92ace5be2 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm @@ -6,38 +6,45 @@ 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() internal_organs += new /obj/item/organ/internal/alien/plasmavessel/large internal_organs += new /obj/item/organ/internal/alien/resinspinner internal_organs += new /obj/item/organ/internal/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/internal/alien/hivenode/node = user.getorgan(/obj/item/organ/internal/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, span_danger("Without the hivemind, you can't possibly hold the responsibility of leadership!")) - return FALSE - if(node.recent_queen_death) - to_chat(user, span_danger("Your thoughts are still too scattered to take up the position of leadership.")) +/datum/action/cooldown/alien/evolve_to_praetorian/IsAvailable() + . = ..() + if(!.) return FALSE - if(!isturf(user.loc)) - to_chat(user, span_warning("You can't evolve here!")) + if(!isturf(owner.loc)) return FALSE - if(!get_alien_type(/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, span_warning("We already have a living royal!")) + + if(get_alien_type(/mob/living/carbon/alien/humanoid/royal)) 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) + // 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 c4dd26aa62e..28fb151d83b 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm @@ -7,36 +7,48 @@ /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/internal/alien/plasmavessel/large internal_organs += new /obj/item/organ/internal/alien/resinspinner internal_organs += new /obj/item/organ/internal/alien/acid internal_organs += new /obj/item/organ/internal/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." + button_icon_state = "alien_evolve_praetorian" plasma_cost = 500 - action_icon_state = "alien_evolve_praetorian" +/datum/action/cooldown/alien/evolve_to_queen/IsAvailable() + . = ..() + if(!.) + return FALSE -/obj/effect/proc_holder/alien/royal/praetorian/evolve/fire(mob/living/carbon/alien/humanoid/user) - var/obj/item/organ/internal/alien/hivenode/node = user.getorgan(/obj/item/organ/internal/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, span_warning("Without the hivemind, you would be unfit to rule as queen!")) + if(!isturf(owner.loc)) return FALSE - if(node.recent_queen_death) - to_chat(user, span_warning("You are still too burdened with guilt to evolve into a queen.")) + + if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/queen)) return FALSE - if(!get_alien_type(/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, span_warning("We already have an alive queen!")) + + 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 df449a1d790..b86891a8dca 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm @@ -6,8 +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() internal_organs += new /obj/item/organ/internal/alien/plasmavessel diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm index c7c0258cd01..174d06bf4c9 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm @@ -12,8 +12,6 @@ var/leap_on_click = 0 var/pounce_cooldown = 0 var/pounce_cooldown_time = 30 - var/sneaking = 0 //For sneaky-sneaky mode and appropriate slowdown - var/drooling = 0 //For Neruotoxic spit overlays deathsound = 'sound/voice/hiss6.ogg' bodyparts = list( /obj/item/bodypart/chest/alien, @@ -61,11 +59,10 @@ GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list( return A return FALSE - /mob/living/carbon/alien/humanoid/check_breath(datum/gas_mixture/breath) - if(breath && breath.total_moles() > 0 && !sneaking) + 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 ..() /mob/living/carbon/alien/humanoid/set_name() if(numba) diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm index 75268b05303..ff6302b569f 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm @@ -4,7 +4,8 @@ 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 +13,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 +22,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/humanoid/queen.dm b/code/modules/mob/living/carbon/alien/humanoid/queen.dm index dccce537542..9bf9a243e2e 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/queen.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/queen.dm @@ -37,7 +37,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) //there should only be one queen @@ -52,9 +51,15 @@ 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() @@ -63,92 +68,116 @@ internal_organs += new /obj/item/organ/internal/alien/acid internal_organs += new /obj/item/organ/internal/alien/neurotoxin internal_organs += new /obj/item/organ/internal/alien/eggsac - ..() + return ..() //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." + 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(!check_vent_block(user)) - return FALSE - - if(locate(/obj/structure/alien/egg) in get_turf(user)) - to_chat(user, span_alertalien("There's already an egg here.")) - return FALSE - - user.visible_message(span_alertalien("[user] lays an egg!")) - new /obj/structure/alien/egg(user.loc) - return TRUE +/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. -/obj/effect/proc_holder/alien/royal/queen/promote +/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." - plasma_cost = 500 //Plasma cost used on promotion, not spawning the parasite. + button_icon_state = "alien_queen_promote" + /// The promotion only takes plasma when completed, not on activation. + var/promotion_plasma_cost = 500 - 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(/mob/living/carbon/alien/humanoid/royal/praetorian/)) - to_chat(user, span_noticealien("You already have a Praetorian!")) +/datum/action/cooldown/alien/promote/set_statpanel_format() + . = ..() + if(!islist(.)) return - else - for(prom in user) - to_chat(user, span_noticealien("You discard [prom].")) - qdel(prom) - return - prom = new (user.loc) - if(!user.put_in_active_hand(prom, 1)) - to_chat(user, span_warning("You must empty your hands before preparing the parasite.")) - return - else //Just in case telling the player only once is not enough! - to_chat(user, span_noticealien("Use the royal parasite on one of your children to promote her to Praetorian!")) - return + .[PANEL_DISPLAY_STATUS] = "PLASMA - [promotion_plasma_cost]" -/obj/item/queenpromote +/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(get_alien_type(/mob/living/carbon/alien/humanoid/royal/praetorian)) + return FALSE + + return TRUE + +/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) - -/obj/item/queenpromote/attack(mob/living/M, mob/living/carbon/alien/humanoid/user) - if(!isalienadult(M) || isalienroyal(M)) - to_chat(user, span_noticealien("You may only use this with your adult, non-royal children!")) - return - if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/praetorian/)) - to_chat(user, span_noticealien("You already have a Praetorian!")) + if(.) return - var/mob/living/carbon/alien/humanoid/A = M - if(A.stat == CONSCIOUS && A.mind && A.key) - if(!user.usePlasma(500)) - to_chat(user, span_noticealien("You must have 500 plasma stored to use this!")) - return + 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.") - to_chat(A, span_noticealien("The queen has granted you a promotion to Praetorian!")) - user.visible_message(span_alertalien("[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(!isalienadult(to_promote) || isalienroyal(to_promote)) + to_chat(queen, span_noticealien("You may only use this with your adult, non-royal children!")) return - else - to_chat(user, span_warning("This child must be alert and responsive to become a Praetorian!")) -/obj/item/queenpromote/attack_self(mob/user) + if(!promotion.IsAvailable()) + to_chat(queen, span_noticealien("You cannot promote a child right now!")) + return + + if(to_promote.stat != CONSCIOUS || !to_promote.mind || !to_promote.key) + return + + 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/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm index 734a5ef3e3c..a109eb3a770 100644 --- a/code/modules/mob/living/carbon/alien/larva/larva.dm +++ b/code/modules/mob/living/carbon/alien/larva/larva.dm @@ -31,16 +31,18 @@ //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/internal/alien/plasmavessel/small/tiny ..() //This needs to be fixed +// This comment is 12 years old I hope it's fixed by now /mob/living/carbon/alien/larva/get_status_tab_items() . = ..() . += "Progress: [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 9f270e49726..a248173be24 100644 --- a/code/modules/mob/living/carbon/alien/larva/powers.dm +++ b/code/modules/mob/living/carbon/alien/larva/powers.dm @@ -1,57 +1,89 @@ -/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(span_name("[user] scurries to the ground!"), \ - span_noticealien("You are now hiding.")) else - user.layer = MOB_LAYER - user.visible_message(span_notice("[user] slowly peeks up from the ground..."), \ - span_noticealien("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" - -/obj/effect/proc_holder/alien/larva_evolve/fire(mob/living/carbon/alien/user) - if(!islarva(user)) - return - var/mob/living/carbon/alien/larva/larva = user +/datum/action/cooldown/alien/larva_evolve/IsAvailable() + . = ..() + if(!.) + return FALSE + if(!islarva(owner)) + return FALSE + var/mob/living/carbon/alien/larva/larva = owner if(larva.handcuffed || larva.legcuffed) // Cuffing larvas ? Eh ? - to_chat(user, span_warning("You cannot evolve when you are cuffed!")) - return - + return FALSE if(larva.amount_grown < larva.max_grown) - to_chat(user, span_warning("You are not fully grown!")) - return - - to_chat(larva, span_name("You are growing into a beautiful alien! It is time to choose a caste.")) - to_chat(larva, span_info("There are three to choose from:")) - to_chat(larva, span_name("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(larva, span_name("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(larva, span_name("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 = tgui_input_list(larva, "Please choose which alien caste you shall belong to.",,list("Hunter","Sentinel","Drone")) - + return FALSE if(larva.movement_type & VENTCRAWLING) - to_chat(user, span_warning("You cannot evolve while ventcrawling!")) - return + return FALSE - if(user.incapacitated()) //something happened to us while we were choosing. + 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.") + + 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(alien_caste == null) @@ -65,7 +97,8 @@ 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]") larva.alien_evolve(new_xeno) - return - + return TRUE diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm index 16be0609d14..33cb540787a 100644 --- a/code/modules/mob/living/carbon/alien/organs.dm +++ b/code/modules/mob/living/carbon/alien/organs.dm @@ -2,30 +2,6 @@ icon_state = "xgibmid2" visual = FALSE food_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/toxin/acid = 10) - var/list/alien_powers = list() - -/obj/item/organ/internal/alien/Initialize(mapload) - . = ..() - for(var/A in alien_powers) - if(ispath(A)) - alien_powers -= A - alien_powers += new A(src) - -/obj/item/organ/internal/alien/Destroy() - QDEL_LIST(alien_powers) - return ..() - -/obj/item/organ/internal/alien/Insert(mob/living/carbon/M, special = 0) - ..() - for(var/obj/effect/proc_holder/alien/P in alien_powers) - M.AddAbility(P) - - -/obj/item/organ/internal/alien/Remove(mob/living/carbon/M, special = 0) - for(var/obj/effect/proc_holder/alien/P in alien_powers) - M.RemoveAbility(P) - ..() - /obj/item/organ/internal/alien/plasmavessel name = "plasma vessel" @@ -33,11 +9,14 @@ w_class = WEIGHT_CLASS_NORMAL zone = BODY_ZONE_CHEST slot = ORGAN_SLOT_XENO_PLASMAVESSEL - alien_powers = list(/obj/effect/proc_holder/alien/plant, /obj/effect/proc_holder/alien/transfer) food_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/toxin/plasma = 10) + actions_types = list( + /datum/action/cooldown/alien/make_structure/plant_weeds, + /datum/action/cooldown/alien/transfer, + ) /// The current amount of stored plasma. - var/storedPlasma = 100 + var/stored_plasma = 100 /// The maximum plasma this organ can store. var/max_plasma = 250 /// The rate this organ regenerates its owners health at per damage type per second. @@ -49,7 +28,7 @@ name = "large plasma vessel" icon_state = "plasma_large" w_class = WEIGHT_CLASS_BULKY - storedPlasma = 200 + stored_plasma = 200 max_plasma = 500 plasma_rate = 7.5 @@ -60,7 +39,7 @@ name = "small plasma vessel" icon_state = "plasma_small" w_class = WEIGHT_CLASS_SMALL - storedPlasma = 100 + stored_plasma = 100 max_plasma = 150 plasma_rate = 2.5 @@ -69,7 +48,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/internal/alien/plasmavessel/on_life(delta_time, times_fired) //If there are alien weeds on the ground then heal if needed or give some plasma @@ -108,9 +87,9 @@ zone = BODY_ZONE_HEAD slot = ORGAN_SLOT_XENO_HIVENODE w_class = WEIGHT_CLASS_TINY - ///Indicates if the queen died recently, aliens are heavily weakened while this is active. + actions_types = list(/datum/action/cooldown/alien/whisper) + /// Indicates if the queen died recently, aliens are heavily weakened while this is active. var/recent_queen_death = FALSE - alien_powers = list(/obj/effect/proc_holder/alien/whisper) /obj/item/organ/internal/alien/hivenode/Insert(mob/living/carbon/M, special = 0) ..() @@ -162,7 +141,7 @@ icon_state = "stomach-x" zone = BODY_ZONE_PRECISE_MOUTH slot = ORGAN_SLOT_XENO_RESINSPINNER - alien_powers = list(/obj/effect/proc_holder/alien/resin) + actions_types = list(/datum/action/cooldown/alien/make_structure/resin) /obj/item/organ/internal/alien/acid @@ -170,7 +149,7 @@ icon_state = "acid" zone = BODY_ZONE_PRECISE_MOUTH slot = ORGAN_SLOT_XENO_ACIDGLAND - alien_powers = list(/obj/effect/proc_holder/alien/acid) + actions_types = list(/datum/action/cooldown/alien/acid/corrosion) /obj/item/organ/internal/alien/neurotoxin @@ -178,7 +157,7 @@ icon_state = "neurotox" zone = BODY_ZONE_PRECISE_MOUTH slot = ORGAN_SLOT_XENO_NEUROTOXINGLAND - alien_powers = list(/obj/effect/proc_holder/alien/neurotoxin) + actions_types = list(/datum/action/cooldown/alien/acid/neurotoxin) /obj/item/organ/internal/alien/eggsac @@ -187,4 +166,4 @@ zone = BODY_ZONE_PRECISE_GROIN slot = ORGAN_SLOT_XENO_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 d1de191c003..3b6afa28d68 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -408,17 +408,13 @@ . = ..() var/obj/item/organ/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel) if(vessel) - . += "Plasma Stored: [vessel.storedPlasma]/[vessel.max_plasma]" + . += "Plasma Stored: [vessel.stored_plasma]/[vessel.max_plasma]" var/obj/item/organ/internal/heart/vampire/darkheart = getorgan(/obj/item/organ/internal/heart/vampire) if(darkheart) . += "Current blood level: [blood_volume]/[BLOOD_VOLUME_MAXIMUM]." if(locate(/obj/item/assembly/health) in src) . += "Health: [health]" -/mob/living/carbon/get_proc_holders() - . = ..() - . += add_abilities_to_panel() - /mob/living/carbon/attack_ui(slot, params) if(!has_hand_for_held_index(active_hand_index)) return 0 diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index 0e971bc9cec..a9fed1415e4 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -1,19 +1,8 @@ /mob/living/carbon/human/can_equip(obj/item/I, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE) return dna.species.can_equip(I, slot, disable_warning, src, bypass_equip_delay_self) -// 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 +13,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) @@ -38,7 +25,6 @@ return r_store if(ITEM_SLOT_SUITSTORE) return s_store - //SKYRAT EDIT ADDITION BEGIN - ERP_SLOT_SYSTEM if(ITEM_SLOT_VAGINA) return vagina @@ -49,8 +35,46 @@ if(ITEM_SLOT_PENIS) return penis //SKYRAT EDIT ADDITION END + return ..() - 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/get_all_worn_items() . = 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 ebc2acb317f..386baf3d921 100644 --- a/code/modules/mob/living/carbon/human/species_types/golems.dm +++ b/code/modules/mob/living/carbon/human/species_types/golems.dm @@ -733,9 +733,12 @@ BODY_ZONE_CHEST = /obj/item/bodypart/chest/golem/cult, ) - var/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/golem/phase_shift - var/obj/effect/proc_holder/spell/pointed/abyssal_gaze/abyssal_gaze - var/obj/effect/proc_holder/spell/pointed/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 /datum/species/golem/runic/random_name(gender,unique,lastname) var/edgy_first_name = pick("Razor","Blood","Dark","Evil","Cold","Pale","Black","Silent","Chaos","Deadly","Coldsteel") @@ -743,26 +746,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 - C.AddSpell(phase_shift) - abyssal_gaze = new - abyssal_gaze.charge_counter = 0 - C.AddSpell(abyssal_gaze) - dominate = new - dominate.charge_counter = 0 - 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, delta_time, times_fired) if(istype(chem, /datum/reagent/water/holywater)) @@ -1327,8 +1334,11 @@ BODY_ZONE_R_LEG = /obj/item/bodypart/r_leg/golem/snow, BODY_ZONE_CHEST = /obj/item/bodypart/chest/golem/snow, ) - 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 /datum/species/golem/snow/spec_death(gibbed, mob/living/carbon/human/H) H.visible_message(span_danger("[H] turns into a pile of snow!")) @@ -1339,31 +1349,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) +/datum/species/golem/snow/on_species_gain(mob/living/carbon/grant_to, datum/species/old_species) . = ..() - ADD_TRAIT(C, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT) - ball = new - ball.charge_counter = 0 - C.AddSpell(ball) - cryo = new - cryo.charge_counter = 0 - C.AddSpell(cryo) + ADD_TRAIT(grant_to, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT) -/datum/species/golem/snow/on_species_loss(mob/living/carbon/C) - . = ..() - REMOVE_TRAIT(C, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT) - if(ball) - C.RemoveSpell(ball) - if(cryo) - C.RemoveSpell(cryo) + snowball = new(grant_to) + snowball.StartCooldown() + snowball.Grant(grant_to) -/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" + 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/mhydrogen name = "Metallic Hydrogen Golem" diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm index 9fe7e1ddcc3..7cac0335a5a 100644 --- a/code/modules/mob/living/carbon/inventory.dm +++ b/code/modules/mob/living/carbon/inventory.dm @@ -12,7 +12,32 @@ 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/get_all_worn_items() return list( diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 2e4225cbd7f..a61c30ec354 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -33,8 +33,6 @@ else effect.be_replaced() - if(ranged_ability) - ranged_ability.remove_ranged_ability(src) if(buckled) buckled.unbuckle_mob(src,force=1) @@ -795,10 +793,6 @@ clear_alert(ALERT_NOT_ENOUGH_OXYGEN) reload_fullscreen() . = TRUE - if(mind) - for(var/S in mind.spell_list) - var/obj/effect/proc_holder/spell/spell = S - spell.updateButtons() if(excess_healing) INVOKE_ASYNC(src, .proc/emote, "gasp") log_combat(src, src, "revived") @@ -1607,24 +1601,6 @@ GLOBAL_LIST_EMPTY(fire_appearances) /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() - var/list/L = list() - for(var/obj/effect/proc_holder/A in abilities) - L[++L.len] = list("[A.panel]",A.get_panel_text(),A.name,"[REF(A)]") - return L - /mob/living/forceMove(atom/destination) if(!currently_z_moving) stop_pulling() @@ -1729,12 +1705,6 @@ GLOBAL_LIST_EMPTY(fire_appearances) else clear_fullscreen("remote_view", 0) -/mob/living/update_mouse_pointer() - ..() - if (client && 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 6eac0915fad..95c08554d29 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -146,22 +146,17 @@ ///how much blood the mob has var/blood_volume = 0 - ///Any ranged ability the mob has, as a click override - var/obj/effect/proc_holder/ranged_ability ///0 for no override, sets see_invisible = see_override in silicon & carbon life process via update_sight() var/see_override = 0 ///a list of all status effects the mob has var/list/status_effects - var/list/implants = null ///used for database logging var/last_words - var/list/obj/effect/proc_holder/abilities = list() - ///whether this can be picked up and held. var/can_be_held = FALSE /// The w_class of the holder when held. diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 9933ce5911b..70e299be185 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -503,7 +503,21 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( else . = ..() -/mob/living/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof) +/** + * 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) if(!message) return say("#[message]", bubble_type, spans, sanitize, language, ignore_spam, forced, filterproof) diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm index 201142b3414..c6d6b6a5e9d 100644 --- a/code/modules/mob/living/login.dm +++ b/code/modules/mob/living/login.dm @@ -18,9 +18,6 @@ if(ventcrawler) to_chat(src, span_notice("You can ventcrawl! Use alt+click on vents to quickly travel about the station.")) - if(ranged_ability) - ranged_ability.add_ranged_ability(src, span_notice("You currently have [ranged_ability] active!")) - med_hud_set_status() update_fov_client() diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm index f05a08fc487..c6474513577 100644 --- a/code/modules/mob/living/simple_animal/constructs.dm +++ b/code/modules/mob/living/simple_animal/constructs.dm @@ -42,8 +42,6 @@ var/can_repair = FALSE /// Whether this construct can repair itself. Works independently of can_repair. 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 @@ -52,29 +50,25 @@ AddElement(/datum/element/simple_flying) ADD_TRAIT(src, TRAIT_HEALS_FROM_CULT_PYLONS, INNATE_TRAIT) ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT) - var/spellnum = 1 for(var/spell in construct_spells) - 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) - var/obj/effect/proc_holder/spell/the_spell = new spell(null) - the_spell?.action.default_button_position ="6:[pos],4:-2" - AddSpell(the_spell) + 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) - var/pos = 2+spellnum*31 - if(construct_spells.len >= 4) - pos -= 31*(construct_spells.len - 4) - our_rune = new runetype(src) - our_rune.default_button_position = "6:[pos],4:-2" // Set the default position to this random position - our_rune.Grant(src) + update_action_buttons() + if(icon_state) add_overlay("glow_[icon_state]_[theme]") -/mob/living/simple_animal/hostile/construct/Destroy() - QDEL_NULL(our_rune) - return ..() - /mob/living/simple_animal/hostile/construct/Login() . = ..() if(!. || !client) @@ -154,10 +148,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." @@ -221,13 +215,18 @@ attack_verb_simple = "slash" attack_sound = 'sound/weapons/bladeslice.ogg' attack_vis_effect = ATTACK_EFFECT_SLASH - 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." + 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." - 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 + // 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 @@ -239,16 +238,23 @@ . = ..() if(. && isnum(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) + 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,12 +262,18 @@ //////////////////////////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) /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 @@ -288,18 +300,18 @@ 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 - ) - runetype = /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, \ + /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." - 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." can_repair = TRUE can_repair_self = TRUE ///The health HUD applied to this mob. @@ -356,30 +368,32 @@ theme = THEME_HOLY 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 @@ -397,8 +411,10 @@ attack_verb_simple = "butcher" attack_sound = 'sound/weapons/bladeslice.ogg' attack_vis_effect = ATTACK_EFFECT_SLASH - 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 = 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 6ec3f9f3d06..ed9e032902d 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm @@ -40,8 +40,15 @@ return head if(ITEM_SLOT_DEX_STORAGE) 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/guardian/types/dextrous.dm b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm index 210a3f17202..4180929d5f8 100644 --- a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm +++ b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm @@ -58,6 +58,16 @@ return TRUE ..() +/mob/living/simple_animal/hostile/guardian/dextrous/get_item_by_slot(slot_id) + if(slot_id == ITEM_SLOT_DEX_STORAGE) + return internal_storage + return ..() + +/mob/living/simple_animal/hostile/guardian/dextrous/get_slot_by_item(obj/item/looking_for) + if(internal_storage == looking_for) + return ITEM_SLOT_DEX_STORAGE + return ..() + /mob/living/simple_animal/hostile/guardian/dextrous/equip_to_slot(obj/item/I, slot) if(!..()) return diff --git a/code/modules/mob/living/simple_animal/heretic_monsters.dm b/code/modules/mob/living/simple_animal/heretic_monsters.dm index 95764041976..f2f01b091f4 100644 --- a/code/modules/mob/living/simple_animal/heretic_monsters.dm +++ b/code/modules/mob/living/simple_animal/heretic_monsters.dm @@ -32,21 +32,15 @@ loot = list(/obj/effect/gibspawner/human) faction = list(FACTION_HERETIC) simple_mob_flags = SILENCE_RANGED_MESSAGE + /// Innate spells that are added when a beast is created. - var/list/spells_to_add + var/list/actions_to_add /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" @@ -61,10 +55,11 @@ health = 65 sight = SEE_MOBS|SEE_OBJS|SEE_TURFS loot = list(/obj/effect/gibspawner/human, /obj/item/bodypart/l_arm, /obj/item/organ/internal/eyes) - spells_to_add = list( - /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash/long, - /obj/effect/proc_holder/spell/targeted/telepathy/eldritch, - /obj/effect/proc_holder/spell/pointed/trigger/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 weakref to the last target we smacked. Hitting targets consecutively does more damage. var/datum/weakref/last_target @@ -79,16 +74,13 @@ AddComponent(/datum/component/mind_linker, \ network_name = "Mansus Link", \ chat_color = "#568b00", \ - linker_action_path = /datum/action/cooldown/manse_link, \ + linker_action_path = /datum/action/cooldown/spell/pointed/manse_link, \ link_message = on_link_message, \ unlink_message = on_unlink_message, \ post_unlink_callback = CALLBACK(src, .proc/after_unlink), \ speech_action_background_icon_state = "bg_ecult", \ ) - var/datum/action/innate/expand_sight/sight_seer = new(src) - sight_seer.Grant(src) - /mob/living/simple_animal/hostile/heretic_summon/raw_prophet/attack_animal(mob/living/simple_animal/user, list/modifiers) if(user == src) // Easy to hit yourself + very fragile = accidental suicide, prevent that return @@ -155,7 +147,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 @@ -365,9 +357,9 @@ melee_damage_lower = 15 melee_damage_upper = 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) @@ -405,10 +397,10 @@ melee_damage_lower = 15 melee_damage_upper = 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 @@ -423,8 +415,8 @@ melee_damage_lower = 15 melee_damage_upper = 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 42cc79901f4..379042ed416 100644 --- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm +++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm @@ -1,3 +1,4 @@ +#define INTERACTION_SPIDER_KEY "spider_key" /** * # Giant Spider @@ -50,14 +51,10 @@ var/poison_per_bite = 0 ///What reagent the mob injects targets with var/poison_type = /datum/reagent/toxin - ///Whether or not the spider is in the middle of an action. - var/is_busy = FALSE ///How quickly the spider can place down webbing. One is base speed, larger numbers are slower. var/web_speed = 1 ///Whether or not the spider can create sealed webs. var/web_sealer = FALSE - ///The web laying ability - var/datum/action/innate/spider/lay_web/lay_web ///The message that the mother spider left for this spider when the egg was layed. var/directive = "" /// Short description of what this mob is capable of, for radial menu uses @@ -65,8 +62,9 @@ /mob/living/simple_animal/hostile/giant_spider/Initialize(mapload) . = ..() - lay_web = new - lay_web.Grant(src) + var/datum/action/innate/spider/lay_web/webbing = new(src) + webbing.Grant(src) + if(poison_per_bite) AddElement(/datum/element/venomous, poison_type, poison_per_bite) AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move) @@ -145,7 +143,7 @@ datahud.show_to(src) /mob/living/simple_animal/hostile/giant_spider/nurse/AttackingTarget() - if(is_busy) + if(DOING_INTERACTION(src, INTERACTION_SPIDER_KEY)) return if(!istype(target, /mob/living/simple_animal/hostile/giant_spider)) return ..() @@ -159,13 +157,20 @@ if(hurt_spider.stat == DEAD) to_chat(src, span_warning("You're a nurse, not a miracle worker.")) return - visible_message(span_notice("[src] begins wrapping the wounds of [hurt_spider]."),span_notice("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, 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].")) - is_busy = FALSE + visible_message( + span_notice("[src] begins wrapping the wounds of [hurt_spider]."), + span_notice("You begin wrapping the wounds of [hurt_spider]."), + ) + + 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]."), + ) /** * # Tarantula @@ -273,228 +278,238 @@ gold_core_spawnable = NO_SPAWN web_sealer = TRUE menu_description = "Royal spider variant specializing in reproduction and leadership, but has very low amount of health and deals low damage." - ///If the spider is trying to cocoon something, what that something is. - var/atom/movable/cocoon_target - ///How many humans this spider has drained but not layed enriched eggs for. - var/fed = 0 - ///How long it takes for a broodmother to lay eggs. - var/egg_lay_time = 15 SECONDS - ///The ability for the spider to wrap targets. - var/obj/effect/proc_holder/wrap/wrap - ///The ability for the spider to lay basic eggs. - var/datum/action/innate/spider/lay_eggs/lay_eggs - ///The ability for the spider to lay enriched eggs. - var/datum/action/innate/spider/lay_eggs/enriched/lay_eggs_enriched - ///The ability for the spider to set a directive, a message shown to the child spider player when the player takes control. - var/datum/action/innate/spider/set_directive/set_directive - ///A shared list of all the mobs consumed by any spider so that the same target can't be drained several times. - var/static/list/consumed_mobs = list() //the tags of mobs that have been consumed by nurse spiders to lay eggs - ///The ability for the spider to send a message to all currently living spiders. - var/datum/action/innate/spider/comm/letmetalkpls /mob/living/simple_animal/hostile/giant_spider/midwife/Initialize(mapload) . = ..() - wrap = new - AddAbility(wrap) - lay_eggs = new - lay_eggs.Grant(src) - lay_eggs_enriched = new - lay_eggs_enriched.Grant(src) - set_directive = new - set_directive.Grant(src) - letmetalkpls = new - letmetalkpls.Grant(src) + var/datum/action/cooldown/wrap/wrapping = new(src) + wrapping.Grant(src) -/** - * Attempts to cocoon the spider's current cocoon_target. - * - * Attempts to coccon the spider's cocoon_target after a do_after. - * If the target is a human who hasn't been drained before, ups the spider's fed counter so it can lay enriched eggs. - */ -/mob/living/simple_animal/hostile/giant_spider/midwife/proc/cocoon() - if(stat == DEAD || !cocoon_target || cocoon_target.anchored) - return - if(cocoon_target == src) - to_chat(src, span_warning("You can't wrap yourself!")) - return - if(istype(cocoon_target, /mob/living/simple_animal/hostile/giant_spider)) - to_chat(src, span_warning("You can't wrap other spiders!")) - return - if(!Adjacent(cocoon_target)) - to_chat(src, span_warning("You can't reach [cocoon_target]!")) - return - if(is_busy) - to_chat(src, span_warning("You're already doing something else!")) - return - is_busy = TRUE - visible_message(span_notice("[src] begins to secrete a sticky substance around [cocoon_target]."),span_notice("You begin wrapping [cocoon_target] into a cocoon.")) - stop_automated_movement = TRUE - if(do_after(src, 50, target = cocoon_target)) - if(is_busy) - var/obj/structure/spider/cocoon/casing = new(cocoon_target.loc) - if(isliving(cocoon_target)) - var/mob/living/living_target = cocoon_target - if(ishuman(living_target) && (living_target.stat != DEAD || !consumed_mobs[living_target.tag])) //if they're not dead, you can consume them anyway - consumed_mobs[living_target.tag] = TRUE - fed++ - lay_eggs_enriched.UpdateButtons(TRUE) - visible_message(span_danger("[src] sticks a proboscis into [living_target] and sucks a viscous substance out."),span_notice("You suck the nutriment out of [living_target], feeding you enough to lay a cluster of eggs.")) - living_target.death() //you just ate them, they're dead. - else - to_chat(src, span_warning("[living_target] cannot sate your hunger!")) - cocoon_target.forceMove(casing) - if(cocoon_target.density || ismob(cocoon_target)) - casing.icon_state = pick("cocoon_large1","cocoon_large2","cocoon_large3") - cocoon_target = null - is_busy = FALSE - stop_automated_movement = FALSE + var/datum/action/innate/spider/lay_eggs/make_eggs = new(src) + make_eggs.Grant(src) + + var/datum/action/innate/spider/lay_eggs/enriched/make_better_eggs = new(src) + make_better_eggs.Grant(src) + + var/datum/action/innate/spider/set_directive/give_orders = new(src) + give_orders.Grant(src) + + var/datum/action/innate/spider/comm/not_hivemind_talk = new(src) + not_hivemind_talk.Grant(src) /datum/action/innate/spider icon_icon = 'icons/mob/actions/actions_animal.dmi' background_icon_state = "bg_alien" -/datum/action/innate/spider/lay_web +/datum/action/innate/spider/lay_web // Todo: Unify this with the genetics power name = "Spin Web" desc = "Spin a web to slow down potential prey." check_flags = AB_CHECK_CONSCIOUS button_icon_state = "lay_web" -/datum/action/innate/spider/lay_web/Activate() - if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider)) - return +/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 if(!isturf(spider.loc)) - return - var/turf/spider_turf = get_turf(spider) + return FALSE - var/obj/structure/spider/stickyweb/web = locate() in spider_turf - if(web) - if(!spider.web_sealer || istype(web, /obj/structure/spider/stickyweb/sealed)) - to_chat(spider, span_warning("There's already a web here!")) - return - - if(!spider.is_busy) - spider.is_busy = TRUE - 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 - spider.visible_message(span_notice("[spider] begins to secrete a sticky substance."),span_notice("You begin to lay a web.")) - spider.stop_automated_movement = TRUE - if(do_after(spider, 40 * spider.web_speed, target = spider_turf)) - if(spider.is_busy && spider.loc == spider_turf) - if(web) - qdel(web) - new /obj/structure/spider/stickyweb/sealed(spider_turf) - new /obj/structure/spider/stickyweb(spider_turf) - spider.is_busy = FALSE - spider.stop_automated_movement = FALSE - else - to_chat(spider, span_warning("You're already doing something else!")) - -/obj/effect/proc_holder/wrap - name = "Wrap" - panel = "Spider" - 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." - ranged_mousepointer = 'icons/effects/mouse_pointers/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/wrap/update_icon() - action.button_icon_state = "wrap_[active]" - action.UpdateButtons() - return ..() - -/obj/effect/proc_holder/wrap/Click() - if(!istype(usr, /mob/living/simple_animal/hostile/giant_spider/midwife)) - return TRUE - var/mob/living/simple_animal/hostile/giant_spider/midwife/user = usr - activate(user) return TRUE -/obj/effect/proc_holder/wrap/proc/activate(mob/living/user) - var/message - if(active) - message = span_notice("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 = span_notice("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/wrap/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return - if(ranged_ability_user.incapacitated() || !istype(ranged_ability_user, /mob/living/simple_animal/hostile/giant_spider/midwife)) - remove_ranged_ability() + spider.stop_automated_movement = TRUE + + 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) + + 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/giant_spider/midwife/user = ranged_ability_user + 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() - 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, /mob/living/simple_animal/hostile/giant_spider/midwife/.proc/cocoon) - remove_ranged_ability() - return TRUE +/datum/action/cooldown/wrap/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) + return -/obj/effect/proc_holder/wrap/on_lose(mob/living/carbon/user) - remove_ranged_ability() + 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 + + 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 /datum/action/innate/spider/lay_eggs name = "Lay Eggs" desc = "Lay a cluster of eggs, which will soon grow into a normal spider." check_flags = AB_CHECK_CONSCIOUS button_icon_state = "lay_eggs" - var/enriched = FALSE + ///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(!.) - return - if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife)) return FALSE - var/mob/living/simple_animal/hostile/giant_spider/midwife/S = owner - if(enriched && !S.fed) + + 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/giant_spider/midwife)) - return - var/mob/living/simple_animal/hostile/giant_spider/midwife/spider = owner - var/obj/structure/spider/eggcluster/eggs = locate() in get_turf(spider) - if(eggs) - to_chat(spider, span_warning("There is already a cluster of eggs here!")) - else if(enriched && !spider.fed) - to_chat(spider, span_warning("You are too hungry to do this!")) - else if(!spider.is_busy) - spider.is_busy = TRUE - spider.visible_message(span_notice("[spider] begins to lay a cluster of eggs."),span_notice("You begin to lay a cluster of eggs.")) - spider.stop_automated_movement = TRUE - if(do_after(spider, spider.egg_lay_time, target = get_turf(spider))) - if(spider.is_busy) - eggs = locate() in get_turf(spider) - if(!eggs || !isturf(spider.loc)) - var/egg_choice = enriched ? /obj/effect/mob_spawn/ghost_role/spider/enriched : /obj/effect/mob_spawn/ghost_role/spider - var/obj/effect/mob_spawn/ghost_role/spider/new_eggs = new egg_choice(get_turf(spider)) - new_eggs.directive = spider.directive - new_eggs.faction = spider.faction - if(enriched) - spider.fed-- - UpdateButtons(TRUE) - spider.is_busy = FALSE - 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" - enriched = TRUE + 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) /datum/action/innate/spider/set_directive name = "Set Directive" @@ -503,20 +518,18 @@ button_icon_state = "directive" /datum/action/innate/spider/set_directive/IsAvailable() - if(..()) - if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider)) - 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/giant_spider/midwife)) - return 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)) - return + 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 /datum/action/innate/spider/comm name = "Command" @@ -525,15 +538,13 @@ button_icon_state = "command" /datum/action/innate/spider/comm/IsAvailable() - if(..()) - if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife)) - return FALSE - return TRUE + return ..() && istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife) /datum/action/innate/spider/comm/Trigger(trigger_flags) var/input = tgui_input_text(owner, "Input a command for your legions to follow.", "Command") - if(QDELETED(src) || !input || !IsAvailable()) + if(!input || QDELETED(src) || QDELETED(owner) || !IsAvailable()) return FALSE + spider_command(owner, input) return TRUE @@ -550,12 +561,12 @@ return var/my_message my_message = span_spider("Command from [user]: [message]") - for(var/mob/living/simple_animal/hostile/giant_spider/spider in GLOB.spidermobs) + for(var/mob/living/simple_animal/hostile/giant_spider/spider as anything in GLOB.spidermobs) to_chat(spider, my_message) for(var/ghost in GLOB.dead_mob_list) var/link = FOLLOW_LINK(ghost, user) to_chat(ghost, "[link] [my_message]") - usr.log_talk(message, LOG_SAY, tag="spider command") + user.log_talk(message, LOG_SAY, tag = "spider command") /** * # Giant Ice Spider @@ -680,19 +691,17 @@ blood_spawn_chance = 5) /mob/living/simple_animal/hostile/giant_spider/hunter/flesh/AttackingTarget() - if(is_busy) + if(DOING_INTERACTION(src, INTERACTION_SPIDER_KEY)) return if(src == target) if(health >= maxHealth) to_chat(src, span_warning("You're not injured, there's no reason to heal.")) return visible_message(span_notice("[src] begins mending themselves..."),span_notice("You begin mending your wounds...")) - is_busy = TRUE - if(do_after(src, 20, target = src)) + if(do_after(src, 2 SECONDS, target = src, interaction_key = INTERACTION_SPIDER_KEY)) heal_overall_damage(50, 50) new /obj/effect/temp_visual/heal(get_turf(src), "#80F5FF") visible_message(span_notice("[src]'s wounds mend together."),span_notice("You mend your wounds together.")) - is_busy = FALSE return return ..() @@ -711,3 +720,5 @@ /mob/living/simple_animal/hostile/giant_spider/viper/wizard/Initialize(mapload) . = ..() ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) + +#undef INTERACTION_SPIDER_KEY diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm index 83d7c5aab5e..ba0ef910e8e 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm @@ -82,8 +82,8 @@ shotgun_blast.Grant(src) dir_shots.Grant(src) colossus_final.Grant(src) - RegisterSignal(src, COMSIG_ABILITY_STARTED, .proc/start_attack) - RegisterSignal(src, COMSIG_ABILITY_FINISHED, .proc/finished_attack) + RegisterSignal(src, COMSIG_MOB_ABILITY_STARTED, .proc/start_attack) + RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, .proc/finished_attack) AddElement(/datum/element/projectile_shield) /mob/living/simple_animal/hostile/megafauna/colossus/Destroy() @@ -587,8 +587,8 @@ ADD_TRAIT(L, TRAIT_MUTE, STASIS_MUTE) L.status_flags |= GODMODE L.mind.transfer_to(holder_animal) - var/obj/effect/proc_holder/spell/targeted/exit_possession/P = new /obj/effect/proc_holder/spell/targeted/exit_possession - holder_animal.mind.AddSpell(P) + var/datum/action/exit_possession/escape = new(holder_animal) + escape.Grant(holder_animal) remove_verb(holder_animal, /mob/living/verb/pulled) /obj/structure/closet/stasis/dump_contents(kill = 1) @@ -599,7 +599,7 @@ L.notransform = 0 if(holder_animal) holder_animal.mind.transfer_to(L) - L.mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/exit_possession) + holder_animal.gib() if(kill || !isanimal(loc)) L.death(0) ..() @@ -610,33 +610,27 @@ /obj/structure/closet/stasis/ex_act() return -/obj/effect/proc_holder/spell/targeted/exit_possession +/datum/action/exit_possession name = "Exit Possession" - desc = "Exits the body you are possessing." - charge_max = 60 - clothes_req = 0 - invocation_type = INVOCATION_NONE - max_targets = 1 - range = -1 - include_user = TRUE - selection_type = "view" - action_icon = 'icons/mob/actions/actions_spells.dmi' - action_icon_state = "exit_possession" - sound = null + desc = "Exits the body you are possessing. They will explode violently when this occurs." + icon_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "exit_possession" -/obj/effect/proc_holder/spell/targeted/exit_possession/cast(list/targets, mob/living/user = usr) - if(!isfloorturf(user.loc)) - return - var/datum/mind/target_mind = user.mind - for(var/i in user) - if(istype(i, /obj/structure/closet/stasis)) - var/obj/structure/closet/stasis/S = i - S.dump_contents(0) - qdel(S) - break - user.gib() - target_mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/exit_possession) +/datum/action/exit_possession/IsAvailable() + return ..() && isfloorturf(owner.loc) +/datum/action/exit_possession/Trigger(trigger_flags) + . = ..() + if(!.) + return FALSE + + var/obj/structure/closet/stasis/stasis = locate() in owner + if(!stasis) + CRASH("[type] did not find a stasis closet thing in the owner.") + + stasis.dump_contents(FALSE) + qdel(stasis) + qdel(src) #undef ACTIVATE_TOUCH #undef ACTIVATE_SPEECH diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm index 1d4e84f51df..7140399ec91 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm @@ -64,7 +64,7 @@ Difficulty: Extremely Hard ice_shotgun.Grant(src) for(var/obj/structure/frost_miner_prism/prism_to_set in GLOB.frost_miner_prisms) prism_to_set.set_prism_light(LIGHT_COLOR_BLUE, 5) - RegisterSignal(src, COMSIG_ABILITY_STARTED, .proc/start_attack) + RegisterSignal(src, COMSIG_MOB_ABILITY_STARTED, .proc/start_attack) AddElement(/datum/element/knockback, 7, FALSE, TRUE) AddElement(/datum/element/lifesteal, 50) ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT) diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm index 0ddb296bdee..69212074b1c 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm @@ -93,8 +93,8 @@ meteors.Grant(src) mass_fire.Grant(src) lava_swoop.Grant(src) - RegisterSignal(src, COMSIG_ABILITY_STARTED, .proc/start_attack) - RegisterSignal(src, COMSIG_ABILITY_FINISHED, .proc/finished_attack) + RegisterSignal(src, COMSIG_MOB_ABILITY_STARTED, .proc/start_attack) + RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, .proc/finished_attack) RegisterSignal(src, COMSIG_SWOOP_INVULNERABILITY_STARTED, .proc/swoop_invulnerability_started) RegisterSignal(src, COMSIG_LAVA_ARENA_FAILED, .proc/on_arena_fail) diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm index f1559082ef9..9040f80fb43 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm @@ -303,17 +303,11 @@ Difficulty: Hard if(!human_user.mind) return to_chat(human_user, span_danger("Power courses through you! You can now shift your form at will.")) - var/obj/effect/proc_holder/spell/targeted/shapeshift/polar_bear/transformation_spell = new - human_user.mind.AddSpell(transformation_spell) + var/datum/action/cooldown/spell/shapeshift/polar_bear/transformation_spell = new(user.mind || user) + transformation_spell.Grant(user) playsound(human_user.loc, 'sound/items/drink.ogg', rand(10,50), TRUE) qdel(src) -/obj/effect/proc_holder/spell/targeted/shapeshift/polar_bear - name = "Polar Bear Form" - desc = "Take on the shape of a polar bear." - invocation = "RAAAAAAAAWR!" - shapeshift_type = /mob/living/simple_animal/hostile/asteroid/polarbear/lesser - /obj/item/crusher_trophy/wendigo_horn name = "wendigo horn" desc = "A horn from the head of an unstoppable beast." diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm index 39dc4698518..e40f4937e40 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm @@ -80,16 +80,15 @@ return if(G.is_burrowed) holder = G.loc - G.forceMove(T) - QDEL_NULL(holder) + holder.eject_jaunter() + holder = null G.is_burrowed = FALSE G.visible_message(span_danger("[G] emerges from the ground!")) playsound(get_turf(G), 'sound/effects/break_stone.ogg', 50, TRUE, -1) else G.visible_message(span_danger("[G] buries into the ground, vanishing from sight!")) playsound(get_turf(G), 'sound/effects/break_stone.ogg', 50, TRUE, -1) - holder = new /obj/effect/dummy/phased_mob(T) - G.forceMove(holder) + holder = new /obj/effect/dummy/phased_mob(T, G) G.is_burrowed = TRUE /mob/living/simple_animal/hostile/asteroid/goldgrub/GiveTarget(new_target) diff --git a/code/modules/mob/living/simple_animal/hostile/netherworld.dm b/code/modules/mob/living/simple_animal/hostile/netherworld.dm index 752d47813de..2242435324c 100644 --- a/code/modules/mob/living/simple_animal/hostile/netherworld.dm +++ b/code/modules/mob/living/simple_animal/hostile/netherworld.dm @@ -20,13 +20,12 @@ minbodytemp = 0 lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE var/phaser = TRUE - var/datum/action/innate/creature/teleport/teleport var/is_phased = FALSE /mob/living/simple_animal/hostile/netherworld/Initialize(mapload) . = ..() if(phaser) - teleport = new + var/datum/action/innate/creature/teleport/teleport = new(src) teleport.Grant(src) add_cell_sample() @@ -54,14 +53,13 @@ return if(N.is_phased) holder = N.loc - N.forceMove(T) - QDEL_NULL(holder) + holder.eject_jaunter() + holder = null N.is_phased = FALSE playsound(get_turf(N), 'sound/effects/podwoosh.ogg', 50, TRUE, -1) else playsound(get_turf(N), 'sound/effects/podwoosh.ogg', 50, TRUE, -1) - holder = new /obj/effect/dummy/phased_mob(T) - N.forceMove(holder) + holder = new /obj/effect/dummy/phased_mob(T, N) N.is_phased = TRUE /mob/living/simple_animal/hostile/netherworld/proc/can_be_seen(turf/location) diff --git a/code/modules/mob/living/simple_animal/hostile/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm index b67065be44f..0b3b98d407a 100644 --- a/code/modules/mob/living/simple_animal/hostile/ooze.dm +++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm @@ -282,87 +282,87 @@ obj_damage = 15 deathmessage = "deflates and spills its vital juices!" edible_food_types = MEAT | VEGETABLES - ///The ability lets you envelop a carbon in a healing cocoon. Useful for saving critical carbons. - var/datum/action/cooldown/gel_cocoon/gel_cocoon - ///The ability to shoot a mending globule, a sticky projectile that heals over time. - var/obj/effect/proc_holder/globules/globules ghost_controllable = TRUE //SKYRAT EDIT ADDITION - These guys can be helpful... maybe players will be helpful. /mob/living/simple_animal/hostile/ooze/grapes/Initialize(mapload) . = ..() - globules = new - AddAbility(globules) - gel_cocoon = new + var/datum/action/cooldown/globules/glob_shooter = new(src) + glob_shooter.Grant(src) + var/datum/action/cooldown/gel_cocoon/gel_cocoon = new(src) gel_cocoon.Grant(src) -/mob/living/simple_animal/hostile/ooze/grapes/Destroy() - . = ..() - QDEL_NULL(gel_cocoon) - QDEL_NULL(globules) - /mob/living/simple_animal/hostile/ooze/grapes/add_cell_sample() AddElement(/datum/element/swabable, CELL_LINE_TABLE_GRAPE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) ///Ability that allows the owner to fire healing globules at mobs, targetting specific limbs. -/obj/effect/proc_holder/globules +/datum/action/cooldown/globules name = "Fire Mending globule" desc = "Fires a mending globule at someone, healing a specific limb of theirs." - active = FALSE - action_icon = 'icons/mob/actions/actions_slime.dmi' - action_icon_state = "globules" - action_background_icon_state = "bg_hive" - var/cooldown = 5 SECONDS - var/current_cooldown = 0 + background_icon_state = "bg_hive" + icon_icon = 'icons/mob/actions/actions_slime.dmi' + button_icon_state = "globules" + check_flags = AB_CHECK_CONSCIOUS + cooldown_time = 5 SECONDS + click_to_activate = TRUE -/obj/effect/proc_holder/globules/Click(location, control, params) +/datum/action/cooldown/globules/set_click_ability(mob/on_who) . = ..() - if(!isliving(usr)) - return TRUE - var/mob/living/user = usr - fire(user) - -/obj/effect/proc_holder/globules/fire(mob/living/carbon/user) - var/message - if(current_cooldown > world.time) - to_chat(user, span_notice("This ability is still on cooldown.")) + if(!.) return - if(active) - message = span_notice("You stop preparing your mending globules.") - remove_ranged_ability(message) - else - message = span_notice("You prepare to launch a mending globule. Left-click to fire at a target!") - add_ranged_ability(user, message, TRUE) -/obj/effect/proc_holder/globules/InterceptClickOn(mob/living/caller, params, atom/target) + to_chat(on_who, span_notice("You prepare to launch a mending globule. Left-click to fire at a target!")) + +/datum/action/cooldown/globules/unset_click_ability(mob/on_who, refund_cooldown = TRUE) . = ..() - if(.) - return - if(!istype(ranged_ability_user, /mob/living/simple_animal/hostile/ooze) || ranged_ability_user.stat) - remove_ranged_ability() + if(!.) return - var/mob/living/simple_animal/hostile/ooze/ooze = ranged_ability_user + if(refund_cooldown) + to_chat(on_who, span_notice("You stop preparing your mending globules.")) - if(ooze.ooze_nutrition < 5) - to_chat(ooze, span_warning("You need at least 5 nutrition to launch a mending globule.")) - remove_ranged_ability() - return +/datum/action/cooldown/globules/Activate(atom/target) + . = ..() + if(!.) + return FALSE - ooze.visible_message(span_nicegreen("[ooze] launches a mending globule!"), span_notice("You launch a mending globule.")) - var/modifiers = params2list(params) - var/obj/projectile/globule/globule = new (ooze.loc) - globule.preparePixelProjectile(target, ooze, modifiers) - globule.def_zone = ooze.zone_selected - globule.fire() - ooze.adjust_ooze_nutrition(-5) - remove_ranged_ability() - current_cooldown = world.time + cooldown + var/mob/living/simple_animal/hostile/ooze/oozy_owner = owner + if(istype(oozy_owner)) + if(oozy_owner.ooze_nutrition < 5) + to_chat(oozy_owner, span_warning("You need at least 5 nutrition to launch a mending globule.")) + return FALSE return TRUE -/obj/effect/proc_holder/globules/on_lose(mob/living/carbon/user) - remove_ranged_ability() +/datum/action/cooldown/globules/InterceptClickOn(mob/living/caller, params, atom/target) + . = ..() + if(!.) + return FALSE + + // Why is this in InterceptClickOn() and not Activate()? + // Well, we need to use the params of the click intercept + // for passing into preparePixelProjectile, so we'll handle it here instead. + // We just need to make sure Pre-activate and Activate return TRUE so we make it this far + caller.visible_message( + span_nicegreen("[caller] launches a mending globule!"), + span_notice("You launch a mending globule."), + ) + + var/mob/living/simple_animal/hostile/ooze/oozy = caller + if(istype(oozy)) + oozy.adjust_ooze_nutrition(-5) + + var/modifiers = params2list(params) + var/obj/projectile/globule/globule = new(caller.loc) + globule.preparePixelProjectile(target, caller, modifiers) + globule.def_zone = caller.zone_selected + globule.fire() + + return TRUE + +// Needs to return TRUE otherwise PreActivate() will fail, see above +/datum/action/cooldown/globules/Activate(atom/target) + return TRUE ///This projectile embeds into mobs and heals them over time. /obj/projectile/globule diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm index 16c1b1e1ce7..0bf43c28e58 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm @@ -393,13 +393,12 @@ deathsound = 'sound/misc/sadtrombone.ogg' ///This is the list of items we are ready to regurgitate, var/list/prank_pouch = list() - ///This ability lets you fire a single random item from your pouch. - var/obj/effect/proc_holder/regurgitate/my_regurgitate /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/Initialize(mapload) . = ..() - my_regurgitate = new - AddAbility(my_regurgitate) + var/datum/action/cooldown/regurgitate/spit = new(src) + spit.Grant(src) + add_cell_sample() AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/cheesiehonkers, /obj/item/food/cornchips), tame_chance = 30, bonus_tame_chance = 0, after_tame = CALLBACK(src, .proc/tamed)) @@ -468,48 +467,56 @@ prank_pouch -= gone ///This ability will let you fire one random item from your pouch, -/obj/effect/proc_holder/regurgitate +/datum/action/cooldown/regurgitate name = "Regurgitate" desc = "Regurgitates a single item from the depths of your pouch." - action_background_icon_state = "bg_changeling" - action_icon = 'icons/mob/actions/actions_animal.dmi' - action_icon_state = "regurgitate" - active = FALSE + background_icon_state = "bg_changeling" + icon_icon = 'icons/mob/actions/actions_animal.dmi' + button_icon_state = "regurgitate" + check_flags = AB_CHECK_CONSCIOUS + click_to_activate = TRUE -/obj/effect/proc_holder/regurgitate/Click(location, control, params) +/datum/action/cooldown/regurgitate/set_click_ability(mob/on_who) . = ..() - if(!isliving(usr)) + if(!.) + return + + to_chat(on_who, span_notice("Your throat muscles tense up. Left-click to regurgitate a funny morsel!")) + on_who.icon_state = "glutton_tongue" + on_who.update_appearance(UPDATE_ICON) + +/datum/action/cooldown/regurgitate/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(!.) + return + + if(refund_cooldown) + to_chat(on_who, span_notice("Your throat muscles relax.")) + on_who.icon_state = initial(on_who.icon_state) + on_who.update_appearance(UPDATE_ICON) + +/datum/action/cooldown/regurgitate/IsAvailable() + . = ..() + if(!.) + return FALSE + + // Hardcoded to only work with gluttons. Come back next year + return istype(owner, /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton) + +/datum/action/cooldown/regurgitate/Activate(atom/spit_at) + StartCooldown(cooldown_time / 4) + + var/mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/pouch_owner = owner + if(!length(pouch_owner.prank_pouch)) + pouch_owner.icon_state = initial(pouch_owner.icon_state) + to_chat(pouch_owner, span_notice("Your prank pouch is empty.")) return TRUE - var/mob/living/user = usr - fire(user) - -/obj/effect/proc_holder/regurgitate/fire(mob/living/carbon/user) - if(active) - user.icon_state = initial(user.icon_state) - remove_ranged_ability(span_notice("Your throat muscles relax.")) - else - user.icon_state = "glutton_tongue" - add_ranged_ability(user, span_notice("Your throat muscles tense up. Left-click to regurgitate a funny morsel!"), TRUE) - -/obj/effect/proc_holder/regurgitate/InterceptClickOn(mob/living/caller, params, atom/target) - . = ..() - - if(.) - return - - if(!istype(ranged_ability_user, /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton) || ranged_ability_user.stat) - remove_ranged_ability() - return - - var/mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/pouch_owner = ranged_ability_user - if(!pouch_owner.prank_pouch.len) - //active = FALSE - pouch_owner.icon_state = "glutton" - remove_ranged_ability(span_notice("Your prank pouch is empty,.")) - return var/obj/item/projected_morsel = pick(pouch_owner.prank_pouch) projected_morsel.forceMove(pouch_owner.loc) - projected_morsel.throw_at(target, 8, 2, pouch_owner) + projected_morsel.throw_at(spit_at, 8, 2, pouch_owner) flick("glutton_mouth", pouch_owner) playsound(pouch_owner, 'sound/misc/soggy.ogg', 75) + + StartCooldown() + return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/statue.dm b/code/modules/mob/living/simple_animal/hostile/statue.dm index a5c530c3a85..809eaed5814 100644 --- a/code/modules/mob/living/simple_animal/hostile/statue.dm +++ b/code/modules/mob/living/simple_animal/hostile/statue.dm @@ -59,12 +59,13 @@ /mob/living/simple_animal/hostile/netherworld/statue/Initialize(mapload, mob/living/creator) . = ..() // Give spells - var/obj/effect/proc_holder/spell/aoe_turf/flicker_lights/flicker = new(src) - var/obj/effect/proc_holder/spell/aoe_turf/blindness/blind = new(src) - var/obj/effect/proc_holder/spell/targeted/night_vision/night_vision = new(src) - AddSpell(flicker) - AddSpell(blind) - AddSpell(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) @@ -142,68 +143,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 -/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 +/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 + + 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 + cooldown_time = 1 MINUTES + spell_requirements = NONE + aoe_radius = 14 -/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.blind_eyes(4) - return +/datum/action/cooldown/spell/aoe/blindness/cast(atom/cast_on) + cast_on.visible_message(span_danger("[cast_on] glares their eyes.")) + return ..() -//Toggle Night Vision -/obj/effect/proc_holder/spell/targeted/night_vision - name = "Toggle Nightvision \[ON\]" - desc = "Toggle your nightvision mode." +/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 - charge_max = 10 - clothes_req = 0 + things += nearby_mob - message = "You toggle your night vision!" - range = -1 - include_user = 1 + return things -/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/netherworld/statue/sentience_act() - faction -= "neutral" +/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/vatbeast.dm b/code/modules/mob/living/simple_animal/hostile/vatbeast.dm index f35ec302998..2198eabea03 100644 --- a/code/modules/mob/living/simple_animal/hostile/vatbeast.dm +++ b/code/modules/mob/living/simple_animal/hostile/vatbeast.dm @@ -23,19 +23,14 @@ attack_verb_continuous = "slaps" attack_verb_simple = "slap" - var/obj/effect/proc_holder/tentacle_slap/tentacle_slap - /mob/living/simple_animal/hostile/vatbeast/Initialize(mapload) . = ..() - tentacle_slap = new(src, src) - AddAbility(tentacle_slap) + var/datum/action/cooldown/tentacle_slap/slapper = new(src) + slapper.Grant(src) + add_cell_sample() AddComponent(/datum/component/tameable, list(/obj/item/food/fries, /obj/item/food/cheesyfries, /obj/item/food/cornchips, /obj/item/food/carrotfries), tame_chance = 30, bonus_tame_chance = 0, after_tame = CALLBACK(src, .proc/tamed)) -/mob/living/simple_animal/hostile/vatbeast/Destroy() - . = ..() - QDEL_NULL(tentacle_slap) - /mob/living/simple_animal/hostile/vatbeast/proc/tamed(mob/living/tamer) buckle_lying = 0 AddElement(/datum/element/ridable, /datum/component/riding/creature/vatbeast) @@ -44,73 +39,74 @@ /mob/living/simple_animal/hostile/vatbeast/add_cell_sample() AddElement(/datum/element/swabable, CELL_LINE_TABLE_VATBEAST, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) -///Ability that allows the owner to slap other mobs a short distance away -/obj/effect/proc_holder/tentacle_slap +/// Ability that allows the owner to slap other mobs a short distance away. +/// For vatbeats, this ability is shared with the rider. +/datum/action/cooldown/tentacle_slap name = "Tentacle slap" desc = "Slap a creature with your tentacles." - active = FALSE - action_icon = 'icons/mob/actions/actions_animal.dmi' - action_icon_state = "tentacle_slap" - action_background_icon_state = "bg_revenant" + background_icon_state = "bg_revenant" + icon_icon = 'icons/mob/actions/actions_animal.dmi' + button_icon_state = "tentacle_slap" + check_flags = AB_CHECK_CONSCIOUS + cooldown_time = 12 SECONDS + click_to_activate = TRUE ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' - base_action = /datum/action/cooldown/spell_like - ///How long cooldown before we can use the ability again - var/cooldown = 12 SECONDS -/obj/effect/proc_holder/tentacle_slap/Initialize(mapload, mob/living/new_owner) +/datum/action/cooldown/tentacle_slap/UpdateButton(atom/movable/screen/movable/action_button/button, status_only, force) . = ..() - if(!action) + if(!button) return - var/datum/action/cooldown/our_action = action - our_action.cooldown_time = cooldown + if(!status_only && button.our_hud.mymob != owner) + button.name = "Command Tentacle Slap" + button.desc = "Command your steed to slap a creature with its tentacles." -/obj/effect/proc_holder/tentacle_slap/Click(location, control, params) +/datum/action/cooldown/tentacle_slap/set_click_ability(mob/on_who) . = ..() - if(!isliving(usr)) - return TRUE - fire(usr) + if(!.) + return -/obj/effect/proc_holder/tentacle_slap/fire(mob/living/user) - if(active) - remove_ranged_ability(span_notice("You stop preparing to tentacle slap.")) - else - add_ranged_ability(user, span_notice("You prepare [(IS_WEAKREF_OF(user, owner)) ? "your" : "their"] pimp-tentacle. Left-click to slap a target!"), TRUE) + to_chat(on_who, span_notice("You prepare your [on_who == owner ? "":"steed's "]pimp-tentacle. Left-click to slap a target!")) -/obj/effect/proc_holder/tentacle_slap/InterceptClickOn(mob/living/caller, params, atom/target) +/datum/action/cooldown/tentacle_slap/unset_click_ability(mob/on_who, refund_cooldown = TRUE) . = ..() - if(.) + if(!.) return - var/mob/living/beast_owner = owner.resolve() + if(refund_cooldown) + to_chat(on_who, span_notice("You stop preparing your [on_who == owner ? "":"steed's "]pimp-tentacle.")) - if(!beast_owner) - return +/datum/action/cooldown/tentacle_slap/InterceptClickOn(mob/living/caller, params, atom/target) + // Check if we can slap + if(!isliving(target) || target == owner) + return FALSE - if(beast_owner.stat) - remove_ranged_ability() - return + if(!owner.Adjacent(target)) + owner.balloon_alert(caller, "too far!") + return FALSE - if(!beast_owner.Adjacent(target)) - return + // Do the slap + . = ..() + if(!.) + return FALSE - if(!isliving(target)) - return - - var/mob/living/living_target = target - - if(!action.IsAvailable()) //extra check for safety since the ability is shared - remove_ranged_ability() - to_chat(caller, span_notice("This ability is still on cooldown.")) - return - - beast_owner.visible_message("[beast_owner] slaps [living_target] with its tentacle!", span_notice("You slap [living_target] with your tentacle.")) - playsound(beast_owner, 'sound/effects/assslap.ogg', 90) - var/atom/throw_target = get_edge_target_turf(target, beast_owner.dir) - living_target.throw_at(throw_target, 6, 4, beast_owner) - living_target.apply_damage(30) - remove_ranged_ability() - - var/datum/action/cooldown/our_action = action - our_action.StartCooldown() + // Give feedback from the slap. + // Additional feedback for if a rider did it + if(caller != owner) + to_chat(caller, span_notice("You command [owner] to slap [target] with its tentacles.")) return TRUE + +/datum/action/cooldown/tentacle_slap/Activate(atom/to_slap) + var/mob/living/living_to_slap = to_slap + + owner.visible_message( + span_warning("[owner] slaps [to_slap] with its tentacle!"), + span_notice("You slap [to_slap] with your tentacle."), + ) + playsound(owner, 'sound/effects/assslap.ogg', 90) + var/atom/throw_target = get_edge_target_turf(to_slap, owner.dir) + living_to_slap.throw_at(throw_target, 6, 4, owner) + living_to_slap.apply_damage(30, BRUTE) + + StartCooldown() + return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/wizard.dm b/code/modules/mob/living/simple_animal/hostile/wizard.dm index 8da7258a4ce..e844ceb12c1 100644 --- a/code/modules/mob/living/simple_animal/hostile/wizard.dm +++ b/code/modules/mob/living/simple_animal/hostile/wizard.dm @@ -23,56 +23,60 @@ unsuitable_atmos_damage = 7.5 faction = list(ROLE_WIZARD) status_flags = CANPUSH + footstep_type = FOOTSTEP_MOB_SHOE retreat_distance = 3 //out of fireball range minimum_distance = 3 del_on_death = 1 - loot = list(/obj/effect/mob_spawn/corpse/human/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 + loot = list( + /obj/effect/mob_spawn/corpse/human/wizard, + /obj/item/staff, + ) var/next_cast = 0 - - footstep_type = FOOTSTEP_MOB_SHOE + 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 /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) - 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) + fireball = new(src) + fireball.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND) + fireball.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 + magic_missile = new(src) + magic_missile.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND) + magic_missile.Grant(src) + + 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 757d2111581..d8a5cb13622 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -350,6 +350,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(flags) @@ -856,30 +864,33 @@ /mob/proc/get_status_tab_items() . = list() -/// Gets all relevant proc holders for the browser statpenl -/mob/proc/get_proc_holders() - . = list() - if(mind) - . += get_spells_for_statpanel(mind.spell_list) - . += get_spells_for_statpanel(mob_spell_list) - /** * Convert a list of spells into a displyable list for the statpanel * * Shows charge and other important info */ -/mob/proc/get_spells_for_statpanel(list/spells) - var/list/L = list() - for(var/obj/effect/proc_holder/spell/S in spells) - if(S.can_be_cast_by(src)) - switch(S.charge_type) - if("recharge") - L[++L.len] = list("[S.panel]", "[S.charge_counter/10.0]/[S.charge_max/10]", S.name, REF(S)) - if("charges") - L[++L.len] = list("[S.panel]", "[S.charge_counter]/[S.charge_max]", S.name, REF(S)) - if("holdervar") - L[++L.len] = list("[S.panel]", "[S.holder_var_type] [S.holder_var_amount]", S.name, REF(S)) - return L +/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 /mob/proc/swap_hand() var/obj/item/held_item = get_active_held_item() @@ -911,32 +922,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 - - LAZYADD(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)) - LAZYREMOVE(mob_spell_list, S) - qdel(S) - if(client) - client.stat_panel.send_message("check_spells") - /** * Checks to see if the mob can cast normal magic spells. * @@ -1255,7 +1240,6 @@ VV_DROPDOWN_OPTION(VV_HK_DIRECT_CONTROL, "Assume Direct Control") VV_DROPDOWN_OPTION(VV_HK_GIVE_DIRECT_CONTROL, "Give Direct Control") VV_DROPDOWN_OPTION(VV_HK_OFFER_GHOSTS, "Offer Control to Ghosts") - VV_DROPDOWN_OPTION(VV_HK_SDQL_SPELL, "Give SDQL Spell") /mob/vv_do_topic(list/href_list) . = ..() @@ -1307,10 +1291,7 @@ if(!check_rights(NONE)) return offer_control(src) - if(href_list[VV_HK_SDQL_SPELL]) - if(!check_rights(R_DEBUG)) - return - usr.client.cmd_sdql_spell_menu(src) + /** * extra var handling for the logging var */ diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 20238ddb4d1..15fe1b0f192 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -48,8 +48,9 @@ var/cached_multiplicative_actions_slowdown /// List of action hud items the user has var/list/datum/action/actions - /// 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 ///Cursor icon used when holding shift over things var/examine_cursor_icon = 'icons/effects/mouse_pointers/examine_pointer.dmi' @@ -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 - - /// 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/mob_say.dm b/code/modules/mob/mob_say.dm index 5f18a993e9b..797c056dad8 100644 --- a/code/modules/mob/mob_say.dm +++ b/code/modules/mob/mob_say.dm @@ -28,8 +28,14 @@ if(message) SSspeech_controller.queue_say_for_mob(src, message, SPEECH_CONTROLLER_QUEUE_WHISPER_VERB) -///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 = language) ///The me emote verb diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm index e6ae9ad334c..f4ac95853d9 100644 --- a/code/modules/mod/mod_control.dm +++ b/code/modules/mod/mod_control.dm @@ -556,9 +556,6 @@ new_module.on_install() if(wearer) new_module.on_equip() - var/datum/action/item_action/mod/pinned_module/action = new_module.pinned_to[REF(wearer)] - if(action) - action.Grant(wearer) // SKYRAT EDIT START - pAIs in MODsuits if(mod_pai) var/datum/action/item_action/mod/pinned_module/action = new_module.pinned_to[ref(mod_pai)] @@ -577,7 +574,7 @@ if(old_module.active) old_module.on_deactivation(display_message = !deleting, deleting = deleting) old_module.on_uninstall(deleting = deleting) - QDEL_LIST(old_module.pinned_to) + QDEL_LIST_ASSOC_VAL(old_module.pinned_to) old_module.mod = null /obj/item/mod/control/proc/update_access(mob/user, obj/item/card/id/card) diff --git a/code/modules/mod/modules/_module.dm b/code/modules/mod/modules/_module.dm index 49f8de40feb..a130ada864c 100644 --- a/code/modules/mod/modules/_module.dm +++ b/code/modules/mod/modules/_module.dm @@ -303,12 +303,13 @@ /// Pins the module to the user's action buttons /obj/item/mod/module/proc/pin(mob/user) - var/datum/action/item_action/mod/pinned_module/action = pinned_to[REF(user)] - if(action) - qdel(action) - else - action = new(mod, src, user) - action.Grant(user) + var/datum/action/item_action/mod/pinned_module/existing_action = pinned_to[REF(user)] + if(existing_action) + mod.remove_item_action(existing_action) + return + + var/datum/action/item_action/mod/pinned_module/new_action = new(mod, src, user) + mod.add_item_action(new_action) /// On drop key, concels a device item. /obj/item/mod/module/proc/dropkey(mob/living/user) diff --git a/code/modules/mod/modules/modules_timeline.dm b/code/modules/mod/modules/modules_timeline.dm index 14143d74935..d84a1c8c101 100644 --- a/code/modules/mod/modules/modules_timeline.dm +++ b/code/modules/mod/modules/modules_timeline.dm @@ -174,12 +174,12 @@ mod.visible_message(span_warning("[mod.wearer] leaps out of the timeline!")) mod.wearer.SetAllImmobility(0) mod.wearer.setStaminaLoss(0, 0) - phased_mob = new(get_turf(mod.wearer.loc)) - mod.wearer.forceMove(phased_mob) + phased_mob = new(get_turf(mod.wearer.loc), mod.wearer) RegisterSignal(mod, COMSIG_MOD_ACTIVATE, .proc/on_activate_block) else //phasing in - QDEL_NULL(phased_mob) + phased_mob.eject_jaunter() + phased_mob = null UnregisterSignal(mod, COMSIG_MOD_ACTIVATE) mod.visible_message(span_warning("[mod.wearer] drops into the timeline!")) diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 39d3003b919..0c6ef4786db 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -80,8 +80,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar /// Stored pAI in the computer var/obj/item/paicard/inserted_pai = null - var/datum/action/item_action/toggle_computer_light/light_butt - /obj/item/modular_computer/Initialize(mapload) . = ..() @@ -95,7 +93,8 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar soundloop = new(src, enabled) UpdateDisplay() if(has_light) - light_butt = new(src) + add_item_action(/datum/action/item_action/toggle_computer_light) + update_appearance() register_context() Add_Messenger() @@ -116,19 +115,10 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar if(istype(inserted_pai)) QDEL_NULL(inserted_pai) - if(istype(light_butt)) - QDEL_NULL(light_butt) physical = null return ..() -/obj/item/modular_computer/ui_action_click(mob/user, actiontype) - if(istype(actiontype, light_butt)) - toggle_flashlight() - else - ..() - - /obj/item/modular_computer/pre_attack_secondary(atom/A, mob/living/user, params) if(active_program?.tap(A, user, params)) user.do_attack_animation(A) //Emulate this animation since we kill the attack in three lines @@ -558,6 +548,13 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar enabled = 0 update_appearance() +/obj/item/modular_computer/ui_action_click(mob/user, actiontype) + if(istype(actiontype, /datum/action/item_action/toggle_computer_light)) + toggle_flashlight() + return + + return ..() + /** * Toggles the computer's flashlight, if it has one. * diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm index 0ef579f22d5..2c86f43c021 100644 --- a/code/modules/power/cell.dm +++ b/code/modules/power/cell.dm @@ -71,7 +71,7 @@ * * If we, or the item we're located in, is subject to the charge spell, gain some charge back */ -/obj/item/stock_parts/cell/proc/on_magic_charge(datum/source, obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster) +/obj/item/stock_parts/cell/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/charge/spell, mob/living/caster) SIGNAL_HANDLER // This shouldn't be running if we're not being held by a mob, diff --git a/code/modules/projectiles/guns/energy/beam_rifle.dm b/code/modules/projectiles/guns/energy/beam_rifle.dm index b1a5219f2c9..c5a69e323a9 100644 --- a/code/modules/projectiles/guns/energy/beam_rifle.dm +++ b/code/modules/projectiles/guns/energy/beam_rifle.dm @@ -29,6 +29,7 @@ weapon_weight = WEAPON_HEAVY w_class = WEIGHT_CLASS_BULKY ammo_type = list(/obj/item/ammo_casing/energy/beam_rifle/hitscan) + actions_types = list(/datum/action/item_action/zoom_lock_action) cell_type = /obj/item/stock_parts/cell/beam_rifle canMouseDown = TRUE var/aiming = FALSE @@ -72,7 +73,6 @@ var/current_zoom_x = 0 var/current_zoom_y = 0 - var/datum/action/item_action/zoom_lock_action/zoom_lock_action var/mob/listeningTo /obj/item/gun/energy/beam_rifle/debug @@ -95,7 +95,7 @@ return ..() /obj/item/gun/energy/beam_rifle/ui_action_click(mob/user, actiontype) - if(istype(actiontype, zoom_lock_action)) + if(istype(actiontype, /datum/action/item_action/zoom_lock_action)) zoom_lock++ if(zoom_lock > 3) zoom_lock = 0 @@ -109,8 +109,9 @@ if(ZOOM_LOCK_OFF) to_chat(user, span_boldnotice("You disable [src]'s zooming system.")) reset_zooming() - else - ..() + return + + return ..() /obj/item/gun/energy/beam_rifle/proc/set_autozoom_pixel_offsets_immediate(current_angle) if(zoom_lock == ZOOM_LOCK_CENTER_VIEW || zoom_lock == ZOOM_LOCK_OFF) @@ -162,7 +163,6 @@ fire_delay = delay current_tracers = list() START_PROCESSING(SSfastprocess, src) - zoom_lock_action = new(src) /obj/item/gun/energy/beam_rifle/Destroy() STOP_PROCESSING(SSfastprocess, src) diff --git a/code/modules/projectiles/guns/magic.dm b/code/modules/projectiles/guns/magic.dm index c33977c9e3e..5ed5c643e7f 100644 --- a/code/modules/projectiles/guns/magic.dm +++ b/code/modules/projectiles/guns/magic.dm @@ -42,7 +42,7 @@ * * Adds uses to wands or staffs. */ -/obj/item/gun/magic/proc/on_magic_charge(datum/source, obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster) +/obj/item/gun/magic/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/charge/spell, mob/living/caster) SIGNAL_HANDLER . = COMPONENT_ITEM_CHARGED diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm index 5e764cb6318..757c89b994b 100644 --- a/code/modules/projectiles/guns/magic/wand.dm +++ b/code/modules/projectiles/guns/magic/wand.dm @@ -82,7 +82,7 @@ to_chat(user, span_notice("You feel great!")) return to_chat(user, "You irradiate yourself with pure negative energy! \ - [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You Die...","Do you want your possessions identified?")]\ + [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You die...","Do you want your possessions identified?")]\ ") user.death(FALSE) @@ -118,7 +118,7 @@ var/mob/living/L = user if(L.mob_biotypes & MOB_UNDEAD) //positive energy harms the undead to_chat(user, "You irradiate yourself with pure positive energy! \ - [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You Die...","Do you want your possessions identified?")]\ + [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You die...","Do you want your possessions identified?")]\ ") user.death(0) return diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index 6f3926ebb1b..0199c5f82bb 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -337,7 +337,7 @@ /obj/projectile/magic/sapping/on_hit(mob/living/target) . = ..() if(isliving(target)) - SEND_SIGNAL(target, 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" @@ -345,20 +345,16 @@ /obj/projectile/magic/necropotence/on_hit(mob/living/target) . = ..() - if(isliving(target)) - if(!target.mind) - return + if(!isliving(target)) + return - to_chat(target, span_danger("Your body feels drained and there is a burning pain in your chest.")) - target.maxHealth -= 20 - target.health = min(target.health, target.maxHealth) - if(target.maxHealth <= 0) - to_chat(target, span_userdanger("Your weakened soul is completely consumed by the [src]!")) - return - for(var/obj/effect/proc_holder/spell/spell in target.mind.spell_list) - spell.charge_counter = spell.charge_max - spell.recharging = FALSE - spell.update_appearance() + // 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" @@ -403,17 +399,61 @@ to_chat(target, span_notice("Your mind has managed to go unnoticed in the spirit world.")) qdel(trauma) -/// Gives magic projectiles a 3x3 Area of Effect range that will bump into any nearby mobs +/// 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 + + /// 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() - for(var/mob/living/target in range(1, get_turf(src))) - if(target.stat != DEAD && target != firer) - return Bump(target) - ..() + 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" @@ -423,30 +463,33 @@ nodamage = FALSE speed = 0.3 + /// 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 - var/chain - var/mob/living/caster + /// 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) . = ..() tesla_zap(src, zap_range, zap_power, zap_flags) +/obj/projectile/magic/aoe/lightning/Destroy() + 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/aoe/lightning/Destroy() - qdel(chain) - . = ..() - /obj/projectile/magic/fireball name = "bolt of fireball" icon_state = "fireball" @@ -454,20 +497,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 + /// Flash radius of the fireball + var/exp_flash = 3 -/obj/projectile/magic/fireball/on_hit(mob/living/target) +/obj/projectile/magic/fireball/on_hit(atom/target, blocked = FALSE, pierce_hit) . = ..() - if(ismob(target)) - //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 - target.take_overall_damage(0, 10) + 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/T = get_turf(target) - explosion(T, 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) + 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) + . = ..() + 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 f333cdac159..6afa99c1338 100644 --- a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm @@ -1048,7 +1048,7 @@ All effects don't start immediately, but rather get worse over time; the rate is chemical_flags = REAGENT_CAN_BE_SYNTHESIZED liquid_fire_power = 3 //SKYRAT EDIT ADDITION -/datum/reagent/consumable/ethanol/demonsblood //Prevents the imbiber from being dragged into a pool of blood by a slaughter demon. +/datum/reagent/consumable/ethanol/demonsblood name = "Demon's Blood" description = "AHHHH!!!!" color = "#820000" // rgb: 130, 0, 0 @@ -1060,7 +1060,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." chemical_flags = REAGENT_CAN_BE_SYNTHESIZED -/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 = "Creepy time!" color = "#A68310" // rgb: 166, 131, 16 @@ -1072,6 +1099,41 @@ All effects don't start immediately, but rather get worse over time; the rate is glass_desc = "Creepy time!" chemical_flags = REAGENT_CAN_BE_SYNTHESIZED +/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" description = "For when a gin and tonic isn't Russian enough." diff --git a/code/modules/research/xenobiology/crossbreeding/_misc.dm b/code/modules/research/xenobiology/crossbreeding/_misc.dm index 3fcb987a1f1..90316d00e4a 100644 --- a/code/modules/research/xenobiology/crossbreeding/_misc.dm +++ b/code/modules/research/xenobiology/crossbreeding/_misc.dm @@ -136,7 +136,7 @@ Slimecrossing Items icon_state = "slimebarrier_thick" can_atmos_pass = 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 be45d280608..14e4d56fb0b 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_form(mob/living/shape) +/datum/action/cooldown/spell/shapeshift/slime_form/restore_form(mob/living/shape) + . = ..() + if(!.) + return + if(remove_on_restore) - if(shape.mind) - shape.mind.RemoveSpell(src) - return ..() + 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, span_warning("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 b468c318263..1d162c53a73 100644 --- a/code/modules/research/xenobiology/crossbreeding/burning.dm +++ b/code/modules/research/xenobiology/crossbreeding/burning.dm @@ -276,15 +276,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(span_danger("[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) - ..() + 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 20a8251d964..5fdf82a9a79 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -1,609 +1,425 @@ -#define TARGET_CLOSEST 0 -#define TARGET_RANDOM 1 - - -/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 - var/datum/weakref/owner - -/obj/effect/proc_holder/Initialize(mapload, mob/living/new_owner) - . = ..() - owner = WEAKREF(new_owner) - if(has_action) - action = new base_action(src) - -/obj/effect/proc_holder/Destroy() - if(!QDELETED(action)) - qdel(action) - action = null - return ..() - -/obj/effect/proc_holder/proc/on_gain(mob/living/user) - return - -/obj/effect/proc_holder/proc/on_lose(mob/living/user) - return - -/obj/effect/proc_holder/proc/fire(mob/living/user) - return TRUE - -/obj/effect/proc_holder/proc/get_panel_text() - return "" - -GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) //needed for the badmin verb for now - -/obj/effect/proc_holder/Destroy() - QDEL_NULL(action) - if(ranged_ability_user) - remove_ranged_ability() - return ..() - -/obj/effect/proc_holder/singularity_act() - return - -/obj/effect/proc_holder/singularity_pull() - return - -/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, span_warning("[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 - -/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, span_warning("[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_appearance() - -/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_appearance() - -/obj/effect/proc_holder/spell +/** + * # 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" - 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 - ///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 + /// 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 - - 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 decisecond 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/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 - - /// This determines what type of antimagic is needed to block the spell (MAGIC_RESISTANCE, MAGIC_RESISTANCE_MIND, MAGIC_RESISTANCE_HOLY) + /// 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 - - 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/overlay = 0 - var/overlay_icon = 'icons/obj/wizard.dmi' - var/overlay_icon_state = "spell" - var/overlay_lifespan = 0 - - var/sparks_spread = 0 - var/sparks_amt = 0 //cropped at 10 + /// 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_spread = null - /// The amount of smoke to create on case. This is a range so a value of 5 will create enough smoke to cover everything within 5 steps. + 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 - var/centcom_cancast = TRUE //Whether or not the spell should be allowed on z2 +/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 - 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) - return FALSE - - if(player_lock) - if(!user.mind || !(src in user.mind.spell_list) && !(src in user.mob_spell_list)) - to_chat(user, span_warning("You shouldn't have this spell! Something's wrong.")) - return FALSE - else - if(!(src in user.mob_spell_list)) - 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, span_warning("You can't cast this spell here!")) - return FALSE - - if(!skipcharge) - if(!charge_check(user)) - return FALSE - - if(user.stat && !stat_allowed) - to_chat(user, span_warning("Not when you're incapacitated!")) - return FALSE - - if(!user.can_cast_magic(antimagic_flags)) - return FALSE - - if(!phase_allowed && istype(user.loc, /obj/effect/dummy) || HAS_TRAIT(user, TRAIT_ROD_FORM)) - to_chat(user, span_warning("[name] cannot be cast unless you are completely manifested in the material plane!")) - return FALSE - - var/mob/living/L = user - if(istype(L) && (invocation_type == INVOCATION_WHISPER || invocation_type == INVOCATION_SHOUT) && !L.can_speak_vocal()) - to_chat(user, span_warning("You can't get the words out!")) - return FALSE - - if(ishuman(user)) - - var/mob/living/carbon/human/H = user - - if(clothes_req) - if(!(H.wear_suit?.clothing_flags & CASTING_CLOTHES)) - to_chat(H, span_warning("You don't feel strong enough without your robe!")) - return FALSE - if(!(H.head?.clothing_flags & CASTING_CLOTHES)) - to_chat(H, span_warning("You don't feel strong enough without your hat!")) - return FALSE - else - if(clothes_req || human_req) - to_chat(user, span_warning("This spell can only be cast by humans!")) - return FALSE - if(nonabstract_req && (isbrain(user) || ispAI(user))) - to_chat(user, span_warning("This spell can only be cast by physical beings!")) - return FALSE - - - if(!skipcharge) - 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) - if(action) - action.UpdateButtons() - return TRUE - -/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, span_warning("[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(INVOCATION_SHOUT) - if(prob(50))//Auto-mute? Fuck that noise - user.say(invocation, forced = "spell") - else - user.say(replacetext(invocation," ","`"), forced = "spell") - if(INVOCATION_WHISPER) - if(prob(50)) - user.whisper(invocation) - else - user.whisper(replacetext(invocation," ","`")) - if(INVOCATION_EMOTE) - user.visible_message(invocation, invocation_emote_self) //same style as in mob/living/emote.dm - -/obj/effect/proc_holder/spell/proc/playMagSound() - playsound(get_turf(usr), sound,50,TRUE) - -/obj/effect/proc_holder/spell/Initialize(mapload) . = ..() - START_PROCESSING(SSfastprocess, src) + if(!owner) + return - still_recharging_msg = span_warning("[name] is still recharging!") - charge_counter = charge_max + // 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") + +/datum/action/cooldown/spell/Remove(mob/living/remove_from) + + 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/spell/Destroy() - STOP_PROCESSING(SSfastprocess, src) - qdel(action) return ..() -/obj/effect/proc_holder/spell/Click() - if(cast_check()) - choose_targets() - return 1 +/datum/action/cooldown/spell/IsAvailable() + return ..() && can_cast_spell(feedback = FALSE) -/obj/effect/proc_holder/spell/proc/choose_targets(mob/user = usr) //depends on subtype - /targeted or /aoe_turf - 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 + + return ..() + +/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 + + return ..() + +// Where the cast chain starts +/datum/action/cooldown/spell/PreActivate(atom/target) + if(!is_valid_target(target)) + return FALSE + + return Activate(target) + +/// 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!") + + // 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((spell_requirements & SPELL_REQUIRES_MIND) && !owner.mind) + // No point in feedback here, as mindless mobs aren't players + return FALSE + + 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 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 + + 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(!can_invoke(feedback = feedback)) + return FALSE + + 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(!(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 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 + + // 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 + + return TRUE /** - * can_target: Checks if we are allowed to cast the spell on a target. + * 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. * - * Arguments: - * * target The atom that is being targeted by the spell. - * * user The mob using the spell. - * * silent If the checks should not give any feedback messages. + * Return TRUE if cast_on is valid, FALSE otherwise */ -/obj/effect/proc_holder/spell/proc/can_target(atom/target, mob/user, silent = FALSE) +/datum/action/cooldown/spell/proc/is_valid_target(atom/cast_on) return TRUE -/obj/effect/proc_holder/spell/proc/start_recharge() - recharging = TRUE +// 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) -/obj/effect/proc_holder/spell/process(delta_time) - if(recharging && charge_type == "recharge" && (charge_counter < charge_max)) - charge_counter += delta_time * 10 - if(charge_counter >= charge_max) - action.UpdateButtons() - charge_counter = charge_max - recharging = FALSE + // 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/proc/perform(list/targets, recharge = TRUE, mob/user = usr) //if recharge is started is important for the trigger spells - before_cast(targets) - invocation(user) - if(user?.ckey) - user.log_message(span_danger("cast the spell [name]."), LOG_ATTACK) - if(recharge) - recharging = TRUE + // 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() + + // Actually cast the spell. Main effects go here + cast(cast_on) + + if(!(precast_result & SPELL_NO_IMMEDIATE_COOLDOWN)) + // The entire spell is done, start the actual cooldown at its set duration + StartCooldown() + + // 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() + + return TRUE + +/** + * 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 + + SEND_SIGNAL(owner, COMSIG_MOB_AFTER_SPELL_CAST, src, cast_on) + + // 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)) + + 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() + +/// 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 + + if(invocation_type != INVOCATION_NONE) + invocation() if(sound) - playMagSound() - SEND_SIGNAL(user, COMSIG_MOB_CAST_SPELL, src) - cast(targets,user=user) - after_cast(targets) - if(action) - action.UpdateButtons() + playsound(get_turf(owner), sound, 50, TRUE) -/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, text("[message]")) - if(sparks_spread) - do_sparks(sparks_amt, FALSE, location) - if(ispath(smoke_spread, /datum/effect_system/fluid_spread/smoke)) // Dear god this code is :agony: - var/datum/effect_system/fluid_spread/smoke/smoke = new smoke_spread() - smoke.set_up(smoke_amt, holder = src, location = location) - smoke.start() - - -/obj/effect/proc_holder/spell/proc/cast(list/targets,mob/user = usr) - -/obj/effect/proc_holder/spell/proc/view_or_range(distance = world.view, center=usr, type="view") - switch(type) - if("view") - . = view(distance,center) - if("range") - . = range(distance,center) - -/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) - if(action) - action.UpdateButtons() - -/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 - 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 - - -/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/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, user, TRUE)) - 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() + owner.say(replacetext(invocation," ","`"), forced = "spell ([src])") - for(var/mob/living/M in view_or_range(range, user, selection_type)) - if(!include_user && user == M) - continue - if(!can_target(M, user, TRUE)) - continue - possible_targets += M + if(INVOCATION_WHISPER) + if(prob(50)) + owner.whisper(invocation, forced = "spell ([src])") + else + owner.whisper(replacetext(invocation," ","`"), forced = "spell ([src])") - //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/chosen_target - if(!random_target) - chosen_target = tgui_input_list(user, "Choose the target for the spell", "Targeting", sort_names(possible_targets)) - if(isnull(chosen_target)) - return - if(!ismob(chosen_target) || user.incapacitated()) - return - else - switch(random_target_priority) - if(TARGET_RANDOM) - chosen_target = pick(possible_targets) - if(TARGET_CLOSEST) - for(var/mob/living/living_target in possible_targets) - if(chosen_target) - if(get_dist(user, living_target) < get_dist(user, chosen_target)) - if(los_check(user, living_target)) - chosen_target = living_target - else - if(los_check(user, living_target)) - chosen_target = living_target - if(chosen_target in view_or_range(range, user, selection_type)) - targets += chosen_target + if(INVOCATION_EMOTE) + owner.visible_message(invocation, invocation_self_message) - else - var/list/possible_targets = list() - for(var/mob/living/target in view_or_range(range, user, selection_type)) - if(!can_target(target, user, TRUE)) - continue - possible_targets += target - for(var/i in 1 to max_targets) - if(!length(possible_targets)) - break - if(target_ignore_prev) - var/target = pick(possible_targets) - possible_targets -= target - targets += target - else - targets += pick(possible_targets) +/// 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(!include_user && (user in targets)) - targets -= user + if(invocation_type == INVOCATION_NONE) + return TRUE - if(!length(targets)) //doesn't waste the spell - revert_cast(user) - return - - perform(targets, user=user) - -/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, user, TRUE)) - continue - if(!(target in view_or_range(inner_radius,user,selection_type))) - targets += target - - if(!length(targets)) //doesn't waste the spell - revert_cast() - return - - perform(targets,user=user) - -/obj/effect/proc_holder/spell/proc/updateButtons(status_only, force) - action.UpdateButtons(status_only, force) - -/obj/effect/proc_holder/spell/proc/can_be_cast_by(mob/caster) - if((human_req || clothes_req) && !ishuman(caster)) + // 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 + + 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((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 + return TRUE -/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 (get_line(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) +/// 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 + + // Spell is at cap, and we will not bypass it + if(!bypass_cap && (spell_level >= spell_max_level)) + return FALSE + + spell_level++ + cooldown_time = max(cooldown_time - cooldown_reduction_per_rank, 0) + update_spell_name() 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)) +/** + * 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(!charge_check(user,TRUE)) + if(spell_level <= 1) return FALSE - if(user.stat && !stat_allowed) - return FALSE - - if(!user.can_cast_magic(antimagic_flags)) - 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 -/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 +/** + * 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 " -/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 = SCHOOL_RESTORATION - sound = 'sound/magic/staff_healing.ogg' - -/obj/effect/proc_holder/spell/self/basic_heal/cast(list/targets, 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(span_warning("A wreath of gentle light passes over [user]!"), span_notice("You wreath yourself in healing light!")) - user.adjustBruteLoss(-10) - user.adjustFireLoss(-10) - -/obj/effect/proc_holder/spell/vv_get_dropdown() - . = ..() - VV_DROPDOWN_OPTION("", "---------") - if(clothes_req) - VV_DROPDOWN_OPTION(VV_HK_SPELL_SET_ROBELESS, "Set Robeless") - else - VV_DROPDOWN_OPTION(VV_HK_SPELL_UNSET_ROBELESS, "Unset Robeless") - - if(human_req) - VV_DROPDOWN_OPTION(VV_HK_SPELL_UNSET_HUMANONLY, "Unset Require Humanoid Mob") - else - VV_DROPDOWN_OPTION(VV_HK_SPELL_SET_HUMANONLY, "Set Require Humanoid Mob") - - if(nonabstract_req) - VV_DROPDOWN_OPTION(VV_HK_SPELL_UNSET_NONABSTRACT, "Unset Require Body") - else - VV_DROPDOWN_OPTION(VV_HK_SPELL_SET_NONABSTRACT, "Set Require Body") - -/obj/effect/proc_holder/spell/vv_do_topic(list/href_list) - . = ..() - if(href_list[VV_HK_SPELL_SET_ROBELESS]) - clothes_req = FALSE - return - if(href_list[VV_HK_SPELL_UNSET_ROBELESS]) - clothes_req = TRUE - return - if(href_list[VV_HK_SPELL_UNSET_HUMANONLY]) - human_req = FALSE - return - if(href_list[VV_HK_SPELL_SET_HUMANONLY]) - human_req = TRUE - return - if(href_list[VV_HK_SPELL_UNSET_NONABSTRACT]) - nonabstract_req = FALSE - return - if(href_list[VV_HK_SPELL_SET_NONABSTRACT]) - nonabstract_req = TRUE - return + 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 64ca91f0541..00000000000 --- a/code/modules/spells/spell_types/aimed.dm +++ /dev/null @@ -1,207 +0,0 @@ - -/obj/effect/proc_holder/spell/aimed - name = "aimed projectile spell" - base_icon_state = "projectile" - var/projectile_type = /obj/projectile/magic/teleport - var/deactive_msg = "You discharge your projectile..." - var/active_msg = "You charge your 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 - if(!can_cast(user)) - remove_ranged_ability(span_warning("You can no longer cast [name]!")) - return - - if(active) - on_deactivation(user) - else - on_activation(user) - -/** - * Activate the spell for user. - */ -/obj/effect/proc_holder/spell/aimed/proc/on_activation(mob/user) - SHOULD_CALL_PARENT(TRUE) - - current_amount = projectile_amount - add_ranged_ability(user, span_notice("[active_msg] Left-click to shoot it at a target!"), TRUE) - -/** - * Deactivate the spell from user. - */ -/obj/effect/proc_holder/spell/aimed/proc/on_deactivation(mob/user) - SHOULD_CALL_PARENT(TRUE) - - if(charge_type == "recharge") - var/refund_percent = current_amount / projectile_amount - charge_counter = charge_max * refund_percent - start_recharge() - remove_ranged_ability(span_notice("[deactive_msg]")) - - -/obj/effect/proc_holder/spell/aimed/update_icon() - if(!action) - return - - . = ..() - action.button_icon_state = "[base_icon_state][active]" - action.UpdateButtons() - -/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-- - for(var/i in 1 to projectiles_per_fire) - var/obj/projectile/P = new projectile_type(user.loc) - 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) - P.fired_from = src - 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 = SCHOOL_EVOCATION - charge_max = 100 - clothes_req = FALSE - invocation = "P'WAH, UNLIM'TED P'WAH" - invocation_type = INVOCATION_SHOUT - cooldown_min = 20 - base_icon_state = "lightning" - action_icon_state = "lightning0" - sound = 'sound/magic/lightningbolt.ogg' - active = FALSE - projectile_var_overrides = list("zap_range" = 15, "zap_power" = 20000, "zap_flags" = ZAP_MOB_DAMAGE) - 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 - -/obj/effect/proc_holder/spell/aimed/lightningbolt/on_gain(mob/living/user) - . = ..() - ADD_TRAIT(user, TRAIT_TESLA_SHOCKIMMUNE, "lightning_bolt_spell") - -/obj/effect/proc_holder/spell/aimed/lightningbolt/on_lose(mob/living/user) - . = ..() - REMOVE_TRAIT(user, TRAIT_TESLA_SHOCKIMMUNE, "lightning_bolt_spell") - -/obj/effect/proc_holder/spell/aimed/fireball - name = "Fireball" - desc = "This spell fires an explosive fireball at a target." - school = SCHOOL_EVOCATION - charge_max = 60 - clothes_req = FALSE - invocation = "ONI SOMA" - invocation_type = INVOCATION_SHOUT - range = 20 - cooldown_min = 20 //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/fireball/fire_projectile(list/targets, mob/living/user) - var/range = 6 + 2*spell_level - projectile_var_overrides = list("range" = range) - return ..() - -/obj/effect/proc_holder/spell/aimed/spell_cards - name = "Spell Cards" - desc = "Blazing hot rapid-fire homing cards. Send your foes to the shadow realm with their mystical power!" - school = SCHOOL_EVOCATION - charge_max = 50 - clothes_req = FALSE - invocation = "Sigi'lu M'Fan 'Tasia" - invocation_type = INVOCATION_SHOUT - range = 40 - cooldown_min = 10 - projectile_amount = 5 - projectiles_per_fire = 7 - projectile_type = /obj/projectile/magic/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, GLOB.typecache_living, 1, null, CALLBACK(src, .proc/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 00000000000..1d240bad61e --- /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 00000000000..bde25b77933 --- /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 00000000000..fd9e4503de8 --- /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 00000000000..a1513c1ca89 --- /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 00000000000..9e24ccde61a --- /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 00000000000..450544a7a1f --- /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 3463d9fa6ea..00000000000 --- a/code/modules/spells/spell_types/area_teleport.dm +++ /dev/null @@ -1,92 +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 - school = SCHOOL_TRANSLOCATION - - 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/start_recharge) - cast(targets,thearea,user) - after_cast(targets) - -/obj/effect/proc_holder/spell/targeted/area_teleport/before_cast(list/targets) - var/target_area = null - - if(!randomise_selection) - target_area = tgui_input_list(usr, "Area to teleport to", "Teleport", GLOB.teleportlocs) - else - target_area = pick(GLOB.teleportlocs) - if(isnull(target_area)) - return - if(isnull(GLOB.teleportlocs[target_area])) - return - var/area/thearea = GLOB.teleportlocs[target_area] - - return thearea - -/obj/effect/proc_holder/spell/targeted/area_teleport/cast(list/targets,area/thearea,mob/user = usr) - playsound(get_turf(user), sound1, 50,TRUE) - 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(!length(L)) - to_chat(usr, span_warning("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(length(tempL)) - 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,TRUE) - -/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(INVOCATION_SHOUT) - user.say(words, forced = "spell") - playsound(user.loc, pick('sound/misc/null.ogg','sound/misc/null.ogg'), 100, TRUE) - if(INVOCATION_WHISPER) - user.whisper(words, forced = "spell") diff --git a/code/modules/spells/spell_types/bloodcrawl.dm b/code/modules/spells/spell_types/bloodcrawl.dm deleted file mode 100644 index b47941fb346..00000000000 --- a/code/modules/spells/spell_types/bloodcrawl.dm +++ /dev/null @@ -1,54 +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/on_lose(mob/living/user) - if(phased) - user.phasein(get_turf(user), TRUE) - -/obj/effect/proc_holder/spell/bloodcrawl/cast_check(skipcharge = 0,mob/user = usr) - . = ..() - if(!.) - return FALSE - var/area/noteleport_check = get_area(user) - if(noteleport_check && noteleport_check.area_flags & NOTELEPORT) - to_chat(user, span_danger("Some dull, universal force is between you and your other existence, preventing you from blood crawling.")) - return FALSE - -/obj/effect/proc_holder/spell/bloodcrawl/choose_targets(mob/user = usr) - for(var/obj/effect/decal/cleanable/target in range(range, get_turf(user))) - if(target.can_bloodcrawl_in()) - perform(target) - return - revert_cast() - to_chat(user, span_warning("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(istype(user, /mob/living/simple_animal/hostile/imp/slaughter)) - var/mob/living/simple_animal/hostile/imp/slaughter/slaught = user - slaught.current_hitstreak = 0 - slaught.wound_bonus = initial(slaught.wound_bonus) - slaught.bare_wound_bonus = initial(slaught.bare_wound_bonus) - if(phased) - if(user.phasein(target)) - phased = FALSE - else - if(user.phaseout(target)) - phased = TRUE - start_recharge() - return - revert_cast() - to_chat(user, span_warning("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 754b731f780..00000000000 --- a/code/modules/spells/spell_types/charge.dm +++ /dev/null @@ -1,56 +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." - sound = 'sound/magic/charge.ogg' - action_icon_state = "charge" - - school = 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 - -/obj/effect/proc_holder/spell/targeted/charge/cast(list/targets, mob/user = usr) - // Charge people we're pulling first and foremost - if(isliving(user.pulling)) - var/mob/living/pulled_living = user.pulling - var/pulled_has_spells = FALSE - - for(var/obj/effect/proc_holder/spell/spell in pulled_living.mob_spell_list | pulled_living.mind?.spell_list) - spell.charge_counter = spell.charge_max - spell.recharging = FALSE - spell.update_appearance() - 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(user, 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 = user.get_active_held_item() || user.get_inactive_held_item() - if(!to_charge) - to_chat(user, 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, user) - - if(QDELETED(to_charge)) - to_chat(user, span_warning("[src] seems to react adversely with [to_charge]!")) - return - - if(charge_return & COMPONENT_ITEM_BURNT_OUT) - to_chat(user, span_warning("[to_charge] seems to react negatively to [src], becoming uncomfortably warm!")) - - else if(charge_return & COMPONENT_ITEM_CHARGED) - to_chat(user, span_notice("[to_charge] suddenly feels very warm!")) - - else - to_chat(user, span_notice("[to_charge] doesn't seem to be react to [src].")) 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 00000000000..0832b01b97d --- /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 47f3348778d..00000000000 --- 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 = 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(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 - -/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/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 c91a6fb2b05..00000000000 --- a/code/modules/spells/spell_types/conjure.dm +++ /dev/null @@ -1,109 +0,0 @@ -/obj/effect/proc_holder/spell/aoe_turf/conjure - name = "Conjure" - desc = "This spell conjures objs of the specified types in range." - - school = SCHOOL_CONJURATION - - 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/new_vars = 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,TRUE) - 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 - O.ChangeTurf(N, flags = CHANGETURF_INHERIT_AIR) - else - var/atom/summoned_object = new summoned_object_type(spawn_place) - - for(var/varName in new_vars) - if(varName in new_vars) - summoned_object.vv_edit_var(varName, new_vars[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/summon_ed_swarm //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/secbot/ed209) - summon_amt = 10 - range = 3 - new_vars = list( - "emagged" = 2, - "remote_disabled" = 1, - "shoot_sound" = 'sound/weapons/laser.ogg', - "projectile" = /obj/projectile/beam/laser, - "security_mode_flags" = ~(SECBOT_DECLARE_ARRESTS), - "name" = "Wizard's Justicebot", - ) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/link_worlds - 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 - ///List of weakrefs to items summoned - var/list/datum/weakref/item_refs = list() - var/item_type = /obj/item/banhammer - school = 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 && length(item_refs)) - QDEL_LIST(item_refs) - return - 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() - QDEL_LIST(item_refs) - return ..() - -/obj/effect/proc_holder/spell/targeted/conjure_item/proc/make_item() - var/obj/item/item = new item_type - item_refs += WEAKREF(item) - 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 00000000000..9483bb57b43 --- /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 00000000000..036abbc0f9b --- /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 00000000000..45007ee8503 --- /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 00000000000..50124ce1319 --- /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 00000000000..c51d4d114df --- /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 00000000000..7fec43aa8d4 --- /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 00000000000..db122e4c846 --- /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 00000000000..e0694898c09 --- /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 00000000000..9433fbd7df0 --- /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_worlds.dm b/code/modules/spells/spell_types/conjure/link_worlds.dm new file mode 100644 index 00000000000..f227fc1a13e --- /dev/null +++ b/code/modules/spells/spell_types/conjure/link_worlds.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 00000000000..057fef9b9b4 --- /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 00000000000..cce5d1ab797 --- /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 00000000000..e9717a13253 --- /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 00000000000..5abac48b197 --- /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 00000000000..98921da4879 --- /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 00000000000..b3d55fd74d8 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/invisible_box.dm @@ -0,0 +1,44 @@ + + +/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/lighting_packet.dm b/code/modules/spells/spell_types/conjure_item/lighting_packet.dm new file mode 100644 index 00000000000..9ae2010374c --- /dev/null +++ b/code/modules/spells/spell_types/conjure_item/lighting_packet.dm @@ -0,0 +1,39 @@ + +/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 00000000000..bbc783a48ed --- /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 35f9810e489..00000000000 --- a/code/modules/spells/spell_types/construct_spells.dm +++ /dev/null @@ -1,341 +0,0 @@ -//////////////////////////////Construct Spells///////////////////////// - -/obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser - charge_max = 3 MINUTES - 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 = 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, TRUE) - 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 = 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 = 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/wall/reinforced - name = "Greater Construction" - desc = "This spell constructs a reinforced metal wall." - - school = 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 = 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 = 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 = 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 = 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/effect/proc_holder/spell/targeted/smoke/disable - name = "Paralysing Smoke" - desc = "This spell spawns a cloud of paralysing smoke." - - school = 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 = /datum/effect_system/fluid_spread/smoke/sleeping - smoke_amt = 4 - action_icon_state = "smoke" - action_background_icon_state = "bg_cult" - -/obj/effect/proc_holder/spell/pointed/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 - stat_allowed = FALSE - school = SCHOOL_EVOCATION - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_background_icon_state = "bg_demon" - action_icon_state = "abyssal_gaze" - active_msg = "You prepare to instill a deep terror in a target..." - -/obj/effect/proc_holder/spell/pointed/abyssal_gaze/cast(list/targets, mob/user) - if(!LAZYLEN(targets)) - to_chat(user, span_warning("No target found in range!")) - return FALSE - if(!can_target(targets[1], user)) - return FALSE - - var/mob/living/carbon/target = targets[1] - if(target.can_block_magic(MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY)) - to_chat(user, span_warning("The spell had no effect!")) - to_chat(target, span_warning("You feel a freezing darkness closing in on you, but it rapidly dissipates.")) - return FALSE - - to_chat(target, span_userdanger("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(ABYSSAL_GAZE_BLIND) - addtimer(CALLBACK(src, .proc/cure_blindness, target), 40) - if(ishuman(targets[1])) - var/mob/living/carbon/human/humi = targets[1] - humi.adjust_coretemperature(-200) - target.adjust_bodytemperature(-200) - -/** - * cure_blidness: Cures Abyssal Gaze blindness from the target - * - * Arguments: - * * target The mob that is being cured of the blindness. - */ -/obj/effect/proc_holder/spell/pointed/abyssal_gaze/proc/cure_blindness(mob/target) - if(isliving(target)) - var/mob/living/L = target - L.cure_blind(ABYSSAL_GAZE_BLIND) - -/obj/effect/proc_holder/spell/pointed/abyssal_gaze/can_target(atom/target, mob/user, silent) - . = ..() - if(!.) - return FALSE - if(!iscarbon(target)) - if(!silent) - to_chat(user, span_warning("You can only target carbon based lifeforms!")) - return FALSE - return TRUE - -/obj/effect/proc_holder/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." - charge_max = 1 MINUTES - range = 7 - stat_allowed = FALSE - school = SCHOOL_EVOCATION - clothes_req = FALSE - invocation = "none" - invocation_type = INVOCATION_NONE - ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi' - action_icon = 'icons/mob/actions/actions_cult.dmi' - action_background_icon_state = "bg_demon" - action_icon_state = "dominate" - active_msg = "You prepare to dominate the mind of a target..." - -/obj/effect/proc_holder/spell/pointed/dominate/cast(list/targets, mob/user) - if(!LAZYLEN(targets)) - to_chat(user, span_notice("No target found in range.")) - return FALSE - if(!can_target(targets[1], user)) - return FALSE - - var/mob/living/simple_animal/S = targets[1] - S.add_atom_colour("#990000", FIXED_COLOUR_PRIORITY) - S.faction = list("cult") - playsound(get_turf(S), 'sound/effects/ghost.ogg', 100, TRUE) - new /obj/effect/temp_visual/cult/sac(get_turf(S)) - -/obj/effect/proc_holder/spell/pointed/dominate/can_target(atom/target, mob/user, silent) - . = ..() - if(!.) - return FALSE - if(!isanimal(target)) - if(!silent) - to_chat(user, span_warning("Target is not a lesser creature!")) - return FALSE - - var/mob/living/simple_animal/S = target - if(S.mind) - if(!silent) - to_chat(user, span_warning("[S] is too intelligent to dominate!")) - return FALSE - if(S.stat) - if(!silent) - to_chat(user, span_warning("[S] is dead!")) - return FALSE - if(S.sentience_type != SENTIENCE_ORGANIC) - if(!silent) - to_chat(user, span_warning("[S] cannot be dominated!")) - return FALSE - if("cult" in S.faction) - if(!silent) - to_chat(user, span_warning("[S] is already serving Nar'Sie!")) - 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 - antimagic_flags = MAGIC_RESISTANCE_HOLY - 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(src,1)) - 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 54aa74871e4..00000000000 --- a/code/modules/spells/spell_types/emplosion.dm +++ /dev/null @@ -1,19 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/emplosion - name = "Emplosion" - desc = "This spell emplodes an area." - - school = SCHOOL_EVOCATION - 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,TRUE) - for(var/mob/living/target in targets) - if(target.can_block_magic()) - continue - empulse(target.loc, emp_heavy, emp_light) - - 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 27bfba57def..00000000000 --- a/code/modules/spells/spell_types/ethereal_jaunt.dm +++ /dev/null @@ -1,138 +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 = 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 - /// List of valid exit points - var/list/exit_point_list - -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/cast_check(skipcharge = 0,mob/user = usr) - . = ..() - if(!.) - return FALSE - var/area/noteleport_check = get_area(user) - if(noteleport_check && noteleport_check.area_flags & NOTELEPORT) - to_chat(user, span_danger("Some dull, universal force is stopping you from jaunting here.")) - return FALSE - -/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/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.extinguish_mob() - target.forceMove(holder) - target.reset_perspective(holder) - target.notransform=0 //mob is safely inside holder now, no need for protection. - jaunt_steam(mobloc) - if(jaunt_out_time) - ADD_TRAIT(target, TRAIT_IMMOBILIZED, type) - sleep(jaunt_out_time) - REMOVE_TRAIT(target, TRAIT_IMMOBILIZED, type) - var/turf/exit_point = get_turf(holder) //Hopefully this gets updated, otherwise this is our fallback - LAZYINITLIST(exit_point_list) - RegisterSignal(holder, COMSIG_MOVABLE_MOVED, .proc/update_exit_point, target) - sleep(jaunt_duration) - - UnregisterSignal(holder, COMSIG_MOVABLE_MOVED) - if(target.loc != holder) //mob warped out of the warp - qdel(holder) - return - - var/found_exit = FALSE - for(var/turf/possible_exit as anything in exit_point_list) - if(possible_exit.is_blocked_turf_ignore_climbable()) - continue - exit_point = possible_exit - found_exit = TRUE - break - if(!found_exit) - to_chat(target, span_danger("Unable to find an unobstructed space, you find yourself ripped back to where you started.")) - exit_point_list.Cut() - holder.forceMove(exit_point) - - 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) - -/** - * 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) - */ -/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/update_exit_point(mob/living/target) - SIGNAL_HANDLER - var/turf/location = get_turf(target) - 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) - -/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 - movespeed = 2 //quite slow. - 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, .)) - to_chat(user, span_warning("Holy energies block your path!")) - return null diff --git a/code/modules/spells/spell_types/explosion.dm b/code/modules/spells/spell_types/explosion.dm deleted file mode 100644 index 0a497290f33..00000000000 --- a/code/modules/spells/spell_types/explosion.dm +++ /dev/null @@ -1,22 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/explosion - name = "Explosion" - desc = "This spell explodes an area." - - school = SCHOOL_EVOCATION - - /// The devastation range of the resulting explosion. - var/ex_severe = 1 - /// The heavy impact range of the resulting explosion. - var/ex_heavy = 2 - /// The light impact range of the resulting explosion. - var/ex_light = 3 - /// The flash range of the resulting explosion. - 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.can_block_magic()) - continue - explosion(target, devastation_range = ex_severe, heavy_impact_range = ex_heavy, light_impact_range = ex_light, flash_range = ex_flash, explosion_cause = src) - - return diff --git a/code/modules/spells/spell_types/forcewall.dm b/code/modules/spells/spell_types/forcewall.dm deleted file mode 100644 index 43979fe80bf..00000000000 --- 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 = 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),user) - if(user.dir == SOUTH || user.dir == NORTH) - new wall_type(get_step(user, EAST),user) - new wall_type(get_step(user, WEST),user) - else - new wall_type(get_step(user, NORTH),user) - new wall_type(get_step(user, SOUTH),user) - - -/obj/effect/forcefield/wizard - var/mob/wizard - -/obj/effect/forcefield/wizard/Initialize(mapload, mob/summoner) - . = ..() - wizard = summoner - -/obj/effect/forcefield/wizard/CanAllowThrough(atom/movable/mover, border_dir) - . = ..() - if(mover == wizard) - return TRUE - if(isliving(mover)) - var/mob/M = mover - if(M.can_block_magic(charge_cost = 0)) - 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 d6912753b7d..00000000000 --- a/code/modules/spells/spell_types/genetic.dm +++ /dev/null @@ -1,48 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/genetic - name = "Genetic" - desc = "This spell inflicts a set of mutations and disabilities upon the target." - - school = SCHOOL_TRANSMUTATION - - 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.can_block_magic()) - to_chat(user, span_warning("The spell had no effect on [target]!")) - continue - if(!target.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/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)) - 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 93e3d632a8c..00000000000 --- a/code/modules/spells/spell_types/godhand.dm +++ /dev/null @@ -1,171 +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 = "latexballon" - inhand_icon_state = null - item_flags = NEEDS_PERMIT | ABSTRACT | DROPDEL - 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) - . = ..() - ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) - -/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, span_warning("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") - playsound(get_turf(user), on_use_sound, 50, TRUE) - if(--charges <= 0) - 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 smiting touch" - desc = "This hand of mine glows with an awesome power!" - catchphrase = "EI NATH!!" - on_use_sound = 'sound/magic/disintegrate.ogg' - icon_state = "disintegrate" - inhand_icon_state = "disintegrate" - -/obj/item/melee/touch_attack/disintegrate/afterattack(mob/living/target, mob/living/carbon/user, proximity) - if(!proximity || target == user || !istype(target) || !iscarbon(user) || !(user.mobility_flags & MOBILITY_USE)) //exploding after touching yourself would be bad - return - if(!user.can_speak_vocal()) - to_chat(user, span_warning("You can't get the words out!")) - return - do_sparks(4, FALSE, target.loc) - for(var/mob/living/L in view(src, 7)) - if(L != user) - L.flash_act(affect_silicon = FALSE) - if(target.can_block_magic()) - user.visible_message(span_warning("The feedback blows [user]'s arm off!"), \ - span_userdanger("The spell bounces from [target]'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 = target.get_item_by_slot(ITEM_SLOT_OCLOTHING) - if(istype(suit)) - target.visible_message(span_danger("[target]'s [suit] explodes off of them into a puddle of gore!")) - target.dropItemToGround(suit) - qdel(suit) - new /obj/effect/gibspawner(target.loc) - return ..() - target.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" - inhand_icon_state = "fleshtostone" - -/obj/item/melee/touch_attack/fleshtostone/afterattack(mob/living/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, span_warning("You can't reach out!")) - return - if(!user.can_speak_vocal()) - to_chat(user, span_warning("You can't get the words out!")) - return - if(target.can_block_magic()) - to_chat(user, span_warning("The spell can't seem to affect [target]!")) - to_chat(target, span_warning("You feel your flesh turn to stone for a moment, then revert back!")) - return ..() - target.Stun(40) - target.petrify() - return ..() - - -/obj/item/melee/touch_attack/duffelbag - name = "\improper burdening touch" - desc = "Where is the bar from here?" - catchphrase = "HU'SWCH H'ANS!!" - on_use_sound = 'sound/magic/mm_hit.ogg' - icon_state = "duffelcurse" - inhand_icon_state = "duffelcurse" - -/obj/item/melee/touch_attack/duffelbag/afterattack(atom/target, mob/living/carbon/user, proximity) - if(!proximity || target == user || !isliving(target) || !iscarbon(user)) //Roleplay involving touching is equally as bad - return - if(!(user.mobility_flags & MOBILITY_USE)) - to_chat(user, span_warning("You can't reach out!")) - return - if(!user.can_speak_vocal()) - to_chat(user, span_warning("You can't get the words out!")) - return - var/mob/living/carbon/duffelvictim = target - var/elaborate_backstory = pick("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(duffelvictim.can_block_magic()) - to_chat(user, span_warning("The spell can't seem to affect [duffelvictim]!")) - to_chat(duffelvictim, span_warning("You really don't feel like talking about your [elaborate_backstory] with complete strangers today.")) - return ..() - - duffelvictim.flash_act() - duffelvictim.Immobilize(5 SECONDS) - duffelvictim.apply_damage(80, STAMINA) - duffelvictim.Knockdown(5 SECONDS) - - if(HAS_TRAIT(target, TRAIT_DUFFEL_CURSE_PROOF)) - to_chat(user, span_warning("The burden of [duffelvictim]'s duffel bag becomes too much, shoving them to the floor!")) - to_chat(duffelvictim, span_warning("The weight of this bag becomes overburdening!")) - return ..() - - var/obj/item/storage/backpack/duffelbag/cursed/conjuredduffel = new get_turf(target) - - duffelvictim.visible_message(span_danger("A growling duffel bag appears on [duffelvictim]!"), \ - span_danger("You feel something attaching itself to you, and a strong desire to discuss your [elaborate_backstory] at length!")) - - ADD_TRAIT(duffelvictim, TRAIT_DUFFEL_CURSE_PROOF, CURSED_ITEM_TRAIT(conjuredduffel.name)) - conjuredduffel.pickup(duffelvictim) - conjuredduffel.forceMove(duffelvictim) - if(duffelvictim.dropItemToGround(duffelvictim.back)) - duffelvictim.equip_to_slot_if_possible(conjuredduffel, ITEM_SLOT_BACK, TRUE, TRUE) - else - if(!duffelvictim.put_in_hands(conjuredduffel)) - duffelvictim.dropItemToGround(duffelvictim.get_inactive_held_item()) - if(!duffelvictim.put_in_hands(conjuredduffel)) - duffelvictim.dropItemToGround(duffelvictim.get_active_held_item()) - duffelvictim.put_in_hands(conjuredduffel) - else - return ..() - return ..() 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 3817e04198d..00000000000 --- 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 = 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/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/enchanted/arcane_barrage diff --git a/code/modules/spells/spell_types/inflict_handler.dm b/code/modules/spells/spell_types/inflict_handler.dm deleted file mode 100644 index b717009de93..00000000000 --- a/code/modules/spells/spell_types/inflict_handler.dm +++ /dev/null @@ -1,54 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/inflict_handler - name = "Inflict Handler" - desc = "This spell blinds and/or destroys/damages/heals and/or knockdowns/stuns the target." - school = SCHOOL_EVOCATION - antimagic_flags = MAGIC_RESISTANCE - var/amt_paralyze = 0 - var/amt_unconscious = 0 - var/amt_stun = 0 - var/inflict_status - var/list/status_params = list() - //set to negatives for healing - var/amt_dam_fire = 0 - var/amt_dam_brute = 0 - var/amt_dam_oxy = 0 - var/amt_dam_tox = 0 - var/amt_eye_blind = 0 - var/amt_eye_blurry = 0 - var/destroys = "none" //can be "none", "gib" or "disintegrate" - var/summon_type = null //this will put an obj at the target's location - -/obj/effect/proc_holder/spell/targeted/inflict_handler/cast(list/targets,mob/user = usr) - for(var/mob/living/target in targets) - playsound(target,sound, 50,TRUE) - if(target.can_block_magic(antimagic_flags)) - to_chat(user, span_warning("The spell had no effect on [target]!")) - return - switch(destroys) - if("gib") - target.gib() - if("disintegrate") - target.dust() - - if(!target) - continue - //damage/healing - target.adjustBruteLoss(amt_dam_brute) - target.adjustFireLoss(amt_dam_fire) - target.adjustToxLoss(amt_dam_tox) - target.adjustOxyLoss(amt_dam_oxy) - //disabling - target.Paralyze(amt_paralyze) - target.Unconscious(amt_unconscious) - target.Stun(amt_stun) - - target.blind_eyes(amt_eye_blind) - target.blur_eyes(amt_eye_blurry) - //summoning - if(summon_type) - new summon_type(target.loc, target) - - if(inflict_status) - var/list/stat_args = status_params.Copy() - stat_args.Insert(1,inflict_status) - target.apply_status_effect(arglist(stat_args)) 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 00000000000..af311947366 --- /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 00000000000..365234c649a --- /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 00000000000..2c89ba21c7f --- /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 00000000000..64405faf993 --- /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 83b16bd89f7..00000000000 --- a/code/modules/spells/spell_types/knock.dm +++ /dev/null @@ -1,18 +0,0 @@ -/obj/effect/proc_holder/spell/aoe_turf/knock - name = "Knock" - desc = "This spell opens nearby doors and closets." - - school = 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/nearby_turf in targets) - SEND_SIGNAL(nearby_turf, COMSIG_ATOM_MAGICALLY_UNLOCKED, src, user) diff --git a/code/modules/spells/spell_types/lichdom.dm b/code/modules/spells/spell_types/lichdom.dm deleted file mode 100644 index 1b5e24abc88..00000000000 --- a/code/modules/spells/spell_types/lichdom.dm +++ /dev/null @@ -1,74 +0,0 @@ -/obj/effect/proc_holder/spell/targeted/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." - action_icon = 'icons/mob/actions/actions_spells.dmi' - action_icon_state = "skeleton" - centcom_cancast = FALSE - invocation = "NECREM IMORTIUM!" - invocation_type = INVOCATION_SHOUT - school = SCHOOL_NECROMANCY - level_max = 0 // Cannot be improved (yet) - range = -1 - charge_max = 1 SECONDS - cooldown_min = 1 SECONDS - clothes_req = FALSE - include_user = TRUE - -/obj/effect/proc_holder/spell/targeted/lichdom/cast(list/targets, mob/user = usr) - for(var/mob/living/caster in targets) - - if(HAS_TRAIT(caster, TRAIT_NO_SOUL)) - to_chat(caster, span_warning("You don't have a soul to bind!")) - return - - var/obj/item/marked_item = caster.get_active_held_item() - if(marked_item.item_flags & ABSTRACT) - return - if(HAS_TRAIT(marked_item, TRAIT_NODROP)) - to_chat(caster, 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, user) & COMPONENT_BLOCK_IMBUE) - to_chat(caster, span_warning("[marked_item] is not suitable for emplacement of your fragile soul.")) - return - - playsound(user, 'sound/effects/pope_entry.ogg', 100) - - to_chat(caster, span_green("You begin to focus your very being into [marked_item]...")) - if(!do_after(caster, 5 SECONDS, target = marked_item, timed_action_flags = IGNORE_HELD_ITEM)) - to_chat(caster, span_warning("Your soul snaps back to your body as you stop ensouling [marked_item]!")) - return - - marked_item.AddComponent(/datum/component/phylactery, caster.mind) - - caster.set_species(/datum/species/skeleton) - to_chat(caster, 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(caster)) - var/mob/living/carbon/carbon_caster = caster - var/obj/item/organ/internal/brain/lich_brain = carbon_caster.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(caster)) - var/mob/living/carbon/human/human_caster = caster - human_caster.dropItemToGround(human_caster.w_uniform) - human_caster.dropItemToGround(human_caster.wear_suit) - human_caster.dropItemToGround(human_caster.head) - human_caster.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(human_caster), ITEM_SLOT_OCLOTHING) - human_caster.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(human_caster), ITEM_SLOT_HEAD) - human_caster.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(human_caster), ITEM_SLOT_ICLOTHING) - - // You only get one phylactery. - caster.mind.RemoveSpell(src) - // And no soul. You just sold it - ADD_TRAIT(caster, TRAIT_NO_SOUL, LICH_TRAIT) diff --git a/code/modules/spells/spell_types/lightning.dm b/code/modules/spells/spell_types/lightning.dm deleted file mode 100644 index b197deb5bab..00000000000 --- a/code/modules/spells/spell_types/lightning.dm +++ /dev/null @@ -1,87 +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 - school = SCHOOL_EVOCATION - 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, span_notice("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, FALSE) - if(do_after(user, 10 SECONDS, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_HELD_ITEM))) - 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, span_notice("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,FALSE)// 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, span_warning("[target.p_theyre(TRUE)] too far away!")) - Reset(user) - return - - playsound(get_turf(user), 'sound/magic/lightningbolt.ogg', 50, TRUE) - 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.can_block_magic()) - playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, TRUE, -1) - current.visible_message(span_warning("[current] absorbs the spell, remaining unharmed!"), span_userdanger("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, TRUE, -1) - else - current.electrocute_act(bolt_energy,"Lightning Bolt",flags = SHOCK_NOGLOVES) - playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, TRUE, -1) - var/list/possible_targets = new - for(var/mob/living/M in view(range,target)) - 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_target/_list_target.dm b/code/modules/spells/spell_types/list_target/_list_target.dm new file mode 100644 index 00000000000..d595552e987 --- /dev/null +++ b/code/modules/spells/spell_types/list_target/_list_target.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_target/telepathy.dm b/code/modules/spells/spell_types/list_target/telepathy.dm new file mode 100644 index 00000000000..e67fd723342 --- /dev/null +++ b/code/modules/spells/spell_types/list_target/telepathy.dm @@ -0,0 +1,52 @@ + +/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/curse.dm b/code/modules/spells/spell_types/madness_curse.dm similarity index 95% rename from code/modules/spells/spell_types/curse.dm rename to code/modules/spells/spell_types/madness_curse.dm index 473f5ce2f35..330f8aa6ff1 100644 --- a/code/modules/spells/spell_types/curse.dm +++ b/code/modules/spells/spell_types/madness_curse.dm @@ -22,7 +22,7 @@ GLOBAL_VAR_INIT(curse_of_madness_triggered, FALSE) give_madness(to_curse, message) /proc/give_madness(mob/living/carbon/human/to_curse, message) - to_curse.playsound_local(to_curse, 'sound/magic/curse.ogg', 40, 1) + to_curse.playsound_local(get_turf(to_curse), 'sound/magic/curse.ogg', 40, 1) to_chat(to_curse, span_reallybig(span_hypnophrase(message))) to_chat(to_curse, span_warning("Your mind shatters!")) switch(rand(1, 10)) diff --git a/code/modules/spells/spell_types/mime.dm b/code/modules/spells/spell_types/mime.dm deleted file mode 100644 index 311c767171c..00000000000 --- a/code/modules/spells/spell_types/mime.dm +++ /dev/null @@ -1,260 +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 = SCHOOL_MIME - panel = "Mime" - summon_type = list(/obj/effect/forcefield/mime) - invocation_type = INVOCATION_EMOTE - invocation_emote_self = "You form a wall in front of yourself." - summon_lifespan = 300 - charge_max = 300 - clothes_req = FALSE - antimagic_flags = NONE - 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?.mind) - if(!usr.mind.miming) - to_chat(usr, span_warning("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 = SCHOOL_MIME - panel = "Mime" - summon_type = list(/obj/structure/chair/mime) - invocation_type = INVOCATION_EMOTE - invocation_emote_self = "You conjure an invisible chair and sit down." - summon_lifespan = 250 - charge_max = 300 - clothes_req = FALSE - antimagic_flags = NONE - 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?.mind) - if(!usr.mind.miming) - to_chat(usr, span_warning("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 = SCHOOL_MIME - panel = "Mime" - summon_type = list(/obj/item/storage/box/mime) - invocation_type = INVOCATION_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_flags = NONE - 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, /obj/item/storage/box/mime/.proc/emptyStorage, FALSE), (summon_lifespan - 1)) - -/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box/Click() - if(usr?.mind) - if(!usr.mind.miming) - to_chat(usr, span_warning("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 = SCHOOL_MIME - panel = "Mime" - clothes_req = FALSE - human_req = TRUE - antimagic_flags = NONE - 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 = span_warning("You can't break your vow of silence that fast!") - else - still_recharging_msg = span_warning("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, span_notice("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, span_notice("You break your vow of silence.")) - -// 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 = SCHOOL_MIME - panel = "Mime" - wall_type = /obj/effect/forcefield/mime/advanced - invocation_type = INVOCATION_EMOTE - invocation_emote_self = "You form a blockade in front of yourself." - charge_max = 600 - sound = null - clothes_req = FALSE - antimagic_flags = NONE - 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?.mind) - if(!usr.mind.miming) - to_chat(usr, span_warning("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/aimed/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." - school = SCHOOL_MIME - panel = "Mime" - charge_max = 300 - clothes_req = FALSE - antimagic_flags = NONE - invocation_type = INVOCATION_EMOTE - invocation_emote_self = span_danger("You fire your finger gun!") - range = 20 - projectile_type = /obj/projectile/bullet/mime - projectile_amount = 3 - sound = null - active_msg = "You draw your fingers!" - deactive_msg = "You put your fingers at ease. Another time." - active = FALSE - - action_icon = 'icons/mob/actions/actions_mime.dmi' - action_icon_state = "finger_guns0" - action_background_icon_state = "bg_mime" - base_icon_state = "finger_guns" - - -/obj/effect/proc_holder/spell/aimed/finger_guns/Click() - var/mob/living/carbon/human/owner = usr - if(owner.incapacitated()) - to_chat(owner, span_warning("You can't properly point your fingers while incapacitated.")) - return - if(owner.get_active_held_item()) - to_chat(owner, span_warning("You can't properly fire your finger guns with something in your hand.")) - return - if(usr?.mind) - if(!usr.mind.miming) - to_chat(usr, span_warning("You must dedicate yourself to silence first!")) - return - invocation = "[usr.real_name] fires [usr.p_their()] finger gun!" - else - invocation_type ="none" - ..() - -/obj/effect/proc_holder/spell/aimed/finger_guns/InterceptClickOn(mob/living/caller, params, atom/target) - if(caller.get_active_held_item()) - to_chat(caller, span_warning("You can't properly fire your finger guns with something in your hand.")) - return - if(caller.incapacitated()) - to_chat(caller, span_warning("You can't properly point your fingers while incapacitated.")) - if(charge_type == "recharge") - var/refund_percent = current_amount/projectile_amount - charge_counter = charge_max * refund_percent - start_recharge() - remove_ranged_ability() - on_deactivation(caller) - ..() - -/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/aimed/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/personality_commune.dm b/code/modules/spells/spell_types/personality_commune.dm deleted file mode 100644 index d776bb0aef1..00000000000 --- a/code/modules/spells/spell_types/personality_commune.dm +++ /dev/null @@ -1,39 +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" - // Bidaly reminder that spells are not really "owned" by anyone - /// Weakref to the trauma that owns this spell - var/datum/weakref/trauma_ref - 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_ref = WEAKREF(T) - -/obj/effect/proc_holder/spell/targeted/personality_commune/Destroy() - trauma_ref = null - return ..() - -// Pillaged and adapted from telepathy code -/obj/effect/proc_holder/spell/targeted/personality_commune/cast(list/targets, mob/user) - var/datum/brain_trauma/severe/split_personality/trauma = trauma_ref?.resolve() - if(!istype(trauma)) - to_chat(user, span_warning("Something is wrong; Either due a bug or admemes, you are trying to cast this spell without a split personality!")) - return - var/msg = tgui_input_text(usr, "What would you like to tell your other self?", "Commune") - if(!msg) - charge_counter = charge_max - return - to_chat(user, span_boldnotice("You concentrate and send thoughts to your other self: [msg]")) - to_chat(trauma.owner, span_boldnotice("[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)] [span_boldnotice("[user] [name]:")] [span_notice("\"[msg]\" to")] [span_name("[trauma]")]") 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 00000000000..4f5bbf2349e --- /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 00000000000..0cf4d3130a4 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/abyssal_gaze.dm @@ -0,0 +1,54 @@ + +/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 index 4177ca48af7..b6fce652155 100644 --- a/code/modules/spells/spell_types/pointed/barnyard.dm +++ b/code/modules/spells/spell_types/pointed/barnyard.dm @@ -1,53 +1,55 @@ -/obj/effect/proc_holder/spell/pointed/barnyardcurse +/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 - charge_type = "recharge" - charge_max = 150 - charge_counter = 0 - clothes_req = FALSE - stat_allowed = FALSE + cooldown_time = 15 SECONDS + cooldown_reduction_per_rank = 3 SECONDS + invocation = "KN'A FTAGHU, PUCK 'BTHNK!" invocation_type = INVOCATION_SHOUT - range = 7 - cooldown_min = 30 - ranged_mousepointer = 'icons/effects/mouse_pointers/barn_target.dmi' - action_icon_state = "barn" + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + active_msg = "You prepare to curse a target..." - deactive_msg = "You dispel the curse..." - /// List of mobs which are allowed to be a target of the spell - var/static/list/compatible_mobs_typecache = typecacheof(list(/mob/living/carbon/human)) + deactive_msg = "You dispel the curse." -/obj/effect/proc_holder/spell/pointed/barnyardcurse/cast(list/targets, mob/user) - if(!targets.len) - to_chat(user, span_warning("No target found in range!")) - return FALSE - if(!can_target(targets[1], user)) - return FALSE - - var/mob/living/carbon/target = targets[1] - if(target.can_block_magic()) - to_chat(user, span_warning("The spell had no effect!")) - target.visible_message(span_danger("[target]'s face bursts into flames, which instantly burst outward, leaving [target] unharmed!"), \ - span_danger("Your face starts burning up, but the flames are repulsed by your anti-magic protection!")) - return FALSE - - var/choice = pick(GLOB.cursed_animal_masks) - var/obj/item/clothing/mask/magichead = new choice(get_turf(target)) - - target.visible_message(span_danger("[target]'s face bursts into flames, and a barnyard animal's head takes its place!"), \ - span_danger("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() - -/obj/effect/proc_holder/spell/pointed/barnyardcurse/can_target(atom/target, mob/user, silent) +/datum/action/cooldown/spell/pointed/barnyardcurse/is_valid_target(atom/cast_on) . = ..() if(!.) return FALSE - if(!is_type_in_typecache(target, compatible_mobs_typecache)) - if(!silent) - to_chat(user, span_warning("You are unable to curse [target]!")) + 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 index de4d3ff2b80..cd044e5cb40 100644 --- a/code/modules/spells/spell_types/pointed/blind.dm +++ b/code/modules/spells/spell_types/pointed/blind.dm @@ -1,35 +1,51 @@ -/obj/effect/proc_holder/spell/pointed/trigger/blind +/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 - charge_max = 300 - clothes_req = FALSE + cooldown_time = 30 SECONDS + cooldown_reduction_per_rank = 6.25 SECONDS + invocation = "STI KALY" invocation_type = INVOCATION_WHISPER - message = "Your eyes cry out in pain!" - cooldown_min = 50 //12 deciseconds reduction per rank - starting_spells = list("/obj/effect/proc_holder/spell/targeted/inflict_handler/blind", "/obj/effect/proc_holder/spell/targeted/genetic/blind") - ranged_mousepointer = 'icons/effects/mouse_pointers/blind_target.dmi' - action_icon_state = "blind" + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + active_msg = "You prepare to blind a target..." -/obj/effect/proc_holder/spell/targeted/inflict_handler/blind - amt_eye_blind = 10 - amt_eye_blurry = 20 - sound = 'sound/magic/blind.ogg' + /// 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 -/obj/effect/proc_holder/spell/targeted/genetic/blind - mutations = list(/datum/mutation/human/blind) - duration = 300 - charge_max = 400 // needs to be higher than the duration or it'll be permanent - sound = 'sound/magic/blind.ogg' - -/obj/effect/proc_holder/spell/pointed/trigger/blind/can_target(atom/target, mob/user, silent) +/datum/action/cooldown/spell/pointed/blind/is_valid_target(atom/cast_on) . = ..() if(!.) return FALSE - if(!isliving(target)) - if(!silent) - to_chat(user, span_warning("You can only blind living beings!")) + 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 00000000000..f5b8aaa9c8c --- /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 00000000000..9c495d27d75 --- /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 00000000000..47fd05c0f46 --- /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 00000000000..e88e3523571 --- /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 index 888190f0112..bf31d402a4c 100644 --- a/code/modules/spells/spell_types/pointed/mind_transfer.dm +++ b/code/modules/spells/spell_types/pointed/mind_transfer.dm @@ -1,104 +1,125 @@ -/obj/effect/proc_holder/spell/pointed/mind_transfer - name = "Mind Transfer" +/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 - charge_max = 600 - clothes_req = FALSE + 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 - range = 1 - cooldown_min = 200 //100 deciseconds reduction per rank - ranged_mousepointer = 'icons/effects/mouse_pointers/mindswap_target.dmi' - action_icon_state = "mindswap" + active_msg = "You prepare to swap minds with a target..." - antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND + 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, + )) -/obj/effect/proc_holder/spell/pointed/mind_transfer/cast(list/targets, mob/living/user, silent = FALSE) - if(!targets.len) - if(!silent) - to_chat(user, span_warning("No mind found!")) - return FALSE - if(targets.len > 1) - if(!silent) - to_chat(user, span_warning("Too many minds! You're not a hive damnit!")) - return FALSE - if(!can_target(targets[1], user, silent)) - return FALSE - - var/mob/living/victim = targets[1] //The target of the spell whos body will be transferred to. - if(istype(victim, /mob/living/simple_animal/hostile/guardian)) - var/mob/living/simple_animal/hostile/guardian/stand = victim - if(stand.summoner) - victim = stand.summoner - var/datum/mind/VM = victim.mind - if(victim.can_block_magic(antimagic_flags) || VM.has_antag_datum(/datum/antagonist/wizard) || VM.has_antag_datum(/datum/antagonist/cult) || VM.has_antag_datum(/datum/antagonist/changeling) || VM.has_antag_datum(/datum/antagonist/rev) || victim.key[1] == "@") - if(!silent) - to_chat(user, span_warning("[victim.p_their(TRUE)] mind is resisting your spell!")) - return FALSE - - //You should not be able to enter one of the most powerful side-antags as a fucking wizard. - if(istype(victim,/mob/living/simple_animal/hostile/imp/slaughter)) - to_chat(user, span_warning("The devilish contract doesn't include the 'mind swappable' package, please try again another lifetime.")) - return - - //MIND TRANSFER BEGIN - var/mob/dead/observer/ghost = victim.ghostize() - user.mind.transfer_to(victim) - - ghost.mind.transfer_to(user) - if(ghost.key) - user.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. - user.Unconscious(unconscious_amount_caster) - victim.Unconscious(unconscious_amount_victim) - SEND_SOUND(user, 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 - -/obj/effect/proc_holder/spell/pointed/mind_transfer/can_target(atom/target, mob/user, silent) +/datum/action/cooldown/spell/pointed/mind_transfer/can_cast_spell(feedback = TRUE) . = ..() if(!.) return FALSE - if(!isliving(target)) - if(!silent) - to_chat(user, span_warning("You can only swap minds with living beings!")) + if(!isliving(owner)) return FALSE - if(user == target) - if(!silent) - to_chat(user, span_warning("You can't swap minds with yourself!")) + if(owner.suiciding) + if(feedback) + to_chat(owner, span_warning("You're killing yourself! You can't concentrate enough to do this!")) return FALSE - - var/mob/living/victim = target - var/t_He = victim.p_they(TRUE) - - if(ismegafauna(victim)) - if(!silent) - to_chat(user, span_warning("This creature is too powerful to control!")) - return FALSE - if(victim.stat == DEAD) - if(!silent) - to_chat(user, span_warning("You don't particularly want to be dead!")) - return FALSE - if(!victim.key || !victim.mind) - if(!silent) - to_chat(user, span_warning("[t_He] appear[victim.p_s()] to be catatonic! Not even magic can affect [victim.p_their()] vacant mind.")) - return FALSE - if(user.suiciding) - if(!silent) - to_chat(user, span_warning("You're killing yourself! You can't concentrate enough to do this!")) - return FALSE - if(istype(victim, /mob/living/simple_animal/hostile/guardian)) - var/mob/living/simple_animal/hostile/guardian/stand = victim - if(stand.summoner) - if(stand.summoner == user) - if(!silent) - to_chat(user, span_warning("Swapping minds with your own guardian would just put you back into your own head!")) - 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/pointed.dm b/code/modules/spells/spell_types/pointed/pointed.dm deleted file mode 100644 index 94676ba64ac..00000000000 --- a/code/modules/spells/spell_types/pointed/pointed.dm +++ /dev/null @@ -1,104 +0,0 @@ -/obj/effect/proc_holder/spell/pointed - name = "pointed spell" - ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' - action_icon_state = "projectile" - /// 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..." - /// Variable dictating if the user is allowed to cast a spell on himself. - var/self_castable = FALSE - /// Variable dictating if the spell will use turf based aim assist - var/aim_assist = TRUE - -/obj/effect/proc_holder/spell/pointed/Click() - var/mob/living/user = usr - if(!istype(user)) - return - var/msg - if(!can_cast(user)) - msg = span_warning("You can no longer cast [name]!") - remove_ranged_ability(msg) - return - if(active) - msg = span_notice("[deactive_msg]") - remove_ranged_ability(msg) - else - msg = span_notice("[active_msg] Left-click to activate spell on a target!") - add_ranged_ability(user, msg, TRUE) - -/obj/effect/proc_holder/spell/pointed/on_lose(mob/living/user) - remove_ranged_ability() - -/obj/effect/proc_holder/spell/pointed/remove_ranged_ability(msg) - . = ..() - on_deactivation(ranged_ability_user) - -/obj/effect/proc_holder/spell/pointed/add_ranged_ability(mob/living/user, msg, forced) - . = ..() - on_activation(user) - -/** - * on_activation: What happens upon pointed spell activation. - * - * Arguments: - * * user The mob interacting owning the spell. - */ -/obj/effect/proc_holder/spell/pointed/proc/on_activation(mob/user) - return - -/** - * on_activation: What happens upon pointed spell deactivation. - * - * Arguments: - * * user 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 = "[action_icon_state][active ? 1 : null]" - action.UpdateButtons() - -/obj/effect/proc_holder/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/target) - if(..()) - return TRUE - if(aim_assist && isturf(target)) - var/list/possible_targets = list() - for(var/A in target) - if(intercept_check(caller, A, TRUE)) - possible_targets += A - if(possible_targets.len == 1) - target = possible_targets[1] - if(!intercept_check(caller, target)) - return TRUE - if(!cast_check(FALSE, caller)) - return TRUE - perform(list(target), user = caller) - remove_ranged_ability() - return TRUE // Do not do any underlying actions after the spell cast - -/** - * intercept_check: Specific spell checks for InterceptClickOn() targets. - * - * Arguments: - * * user The mob using the ranged spell via intercept. - * * target The atom that is being targeted by the spell via intercept. - * * silent If the checks should produce not any feedback messages for the user. - */ -/obj/effect/proc_holder/spell/pointed/proc/intercept_check(mob/user, atom/target, silent = FALSE) - if(!self_castable && target == user) - if(!silent) - to_chat(user, span_warning("You cannot cast the spell on yourself!")) - return FALSE - if(!(target in view_or_range(range, user, selection_type))) - if(!silent) - to_chat(user, span_warning("[target.p_theyre(TRUE)] too far away!")) - return FALSE - if(!can_target(target, user, silent)) - return FALSE - 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 00000000000..4b6af520517 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/spell_cards.dm @@ -0,0 +1,83 @@ + +/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 8d3b0b7f014..00000000000 --- a/code/modules/spells/spell_types/projectile.dm +++ /dev/null @@ -1,112 +0,0 @@ -/obj/projectile/magic/spell - name = "custom spell projectile" - 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 - return ..() - -//NEEDS MAJOR CODE CLEANUP. - -/obj/effect/proc_holder/spell/targeted/projectile - name = "Projectile" - desc = "This spell summons projectiles which try to hit the targets." - antimagic_flags = MAGIC_RESISTANCE - 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/guns/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 - -/obj/effect/proc_holder/spell/targeted/projectile/proc/fire_projectile(atom/target, mob/user) - var/obj/projectile/magic/spell/projectile = new proj_type() - - 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 00000000000..f9bd303f56f --- /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 00000000000..443c9cf62e5 --- /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/right_and_wrong.dm similarity index 96% rename from code/modules/spells/spell_types/rightandwrong.dm rename to code/modules/spells/spell_types/right_and_wrong.dm index 0db7cbe8cf4..211a4a9414f 100644 --- a/code/modules/spells/spell_types/rightandwrong.dm +++ b/code/modules/spells/spell_types/right_and_wrong.dm @@ -57,15 +57,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/nothing, /obj/item/gun/magic/wand/death, /obj/item/gun/magic/wand/resurrection, diff --git a/code/modules/spells/spell_types/santa.dm b/code/modules/spells/spell_types/santa.dm deleted file mode 100644 index 6a41c95cbdb..00000000000 --- a/code/modules/spells/spell_types/santa.dm +++ /dev/null @@ -1,24 +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 = SCHOOL_CONJURATION - charge_max = 600 - clothes_req = FALSE - invocation = "HO HO HO" - invocation_type = INVOCATION_SHOUT - range = 3 - cooldown_min = 50 - antimagic_flags = NONE - - summon_type = list("/obj/item/a_gift") - summon_lifespan = 0 - summon_amt = 5 - -/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 - antimagic_flags = NONE 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 00000000000..a4acba2d884 --- /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 00000000000..87d7ae287d3 --- /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 00000000000..543daa46779 --- /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 00000000000..e037c1ae689 --- /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 00000000000..69325f9df97 --- /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 00000000000..7423fb8a374 --- /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 00000000000..553c9394f57 --- /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 00000000000..0cc578809d6 --- /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 00000000000..e7211aeb7a2 --- /dev/null +++ b/code/modules/spells/spell_types/self/night_vision.dm @@ -0,0 +1,40 @@ + +//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 00000000000..67e794c9668 --- /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/rod_form.dm b/code/modules/spells/spell_types/self/rod_form.dm similarity index 71% rename from code/modules/spells/spell_types/rod_form.dm rename to code/modules/spells/spell_types/self/rod_form.dm index fd833029b55..467271e5433 100644 --- a/code/modules/spells/spell_types/rod_form.dm +++ b/code/modules/spells/spell_types/self/rod_form.dm @@ -1,46 +1,61 @@ /// The base distance a wizard rod will go without upgrades. #define BASE_WIZ_ROD_RANGE 13 -/obj/effect/proc_holder/spell/targeted/rod_form +/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." - clothes_req = TRUE - human_req = FALSE - charge_max = 250 - cooldown_min = 100 - range = -1 + 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 - include_user = TRUE + cooldown_time = 25 SECONDS + cooldown_reduction_per_rank = 3.75 SECONDS + invocation = "CLANG!" invocation_type = INVOCATION_SHOUT - action_icon_state = "immrod" + 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 -/obj/effect/proc_holder/spell/targeted/rod_form/cast(list/targets, mob/user = usr) - var/area/our_area = get_area(user) - if(istype(our_area, /area/centcom/wizard_station)) - to_chat(user, span_warning("You know better than to trash Wizard Federation property. Best wait until you leave to use [src].")) - return +/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)) - // You travel farther when you upgrade the spell. - var/rod_max_distance = BASE_WIZ_ROD_RANGE + (spell_level * distance_per_spell_rank) - // You do more damage when you upgrade the spell. - var/rod_damage_bonus = (spell_level * damage_per_spell_rank) + new /obj/effect/immovablerod/wizard( + get_turf(cast_on), + distant_turf, + null, + FALSE, + cast_on, + rod_max_distance, + rod_damage_bonus, + ) - for(var/mob/living/caster in targets) - new /obj/effect/immovablerod/wizard( - get_turf(caster), - get_ranged_target_turf(get_turf(caster), caster.dir, (rod_max_distance + 2)), // Just a bit over the distance we got - null, - FALSE, - caster, - rod_max_distance, - rod_damage_bonus, - ) - ADD_TRAIT(caster, TRAIT_ROD_FORM, MAGIC_TRAIT) +/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 @@ -125,6 +140,7 @@ 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 @@ -139,6 +155,6 @@ wizard.notransform = FALSE wizard.forceMove(get_turf(src)) our_wizard = null - REMOVE_TRAIT(wizard, TRAIT_ROD_FORM, MAGIC_TRAIT) + 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 00000000000..b2c7e924f19 --- /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 00000000000..57932ad8288 --- /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 00000000000..d71cb6713bc --- /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 00000000000..cab47375eb3 --- /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/summonitem.dm b/code/modules/spells/spell_types/self/summonitem.dm new file mode 100644 index 00000000000..761c2c7efad --- /dev/null +++ b/code/modules/spells/spell_types/self/summonitem.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 00000000000..ae4c46a3bb7 --- /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 8bf7bdbc961..00000000000 --- a/code/modules/spells/spell_types/shadow_walk.dm +++ /dev/null @@ -1,98 +0,0 @@ -#define SHADOW_REGEN_RATE 1.5 - -/obj/effect/proc_holder/spell/targeted/shadowwalk - name = "Shadow Walk" - desc = "Grants unlimited movement in darkness." - charge_max = 0 - clothes_req = FALSE - antimagic_flags = NONE - 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_check(skipcharge = 0,mob/user = usr) - . = ..() - if(!.) - return FALSE - var/area/noteleport_check = get_area(user) - if(noteleport_check && noteleport_check.area_flags & NOTELEPORT) - to_chat(user, span_danger("Some dull, universal force is stopping you from melting into the shadows here.")) - return FALSE - -/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, TRUE, -1) - visible_message(span_boldwarning("[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, span_warning("It isn't dark enough here!")) - -/obj/effect/dummy/phased_mob/shadow - var/mob/living/jaunter - -/obj/effect/dummy/phased_mob/shadow/Initialize(mapload) - . = ..() - START_PROCESSING(SSobj, src) - -/obj/effect/dummy/phased_mob/shadow/Destroy() - jaunter = null - STOP_PROCESSING(SSobj, src) - . = ..() - -/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) - if (light_amount < 0.2 && (!QDELETED(jaunter))) //heal in the dark - jaunter.heal_overall_damage((SHADOW_REGEN_RATE * delta_time), (SHADOW_REGEN_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 - end_jaunt(TRUE) - -/obj/effect/dummy/phased_mob/shadow/proc/end_jaunt(forced = FALSE) - if(jaunter) - if(forced) - visible_message(span_boldwarning("[jaunter] is revealed by the light!")) - else - visible_message(span_boldwarning("[jaunter] emerges from the darkness!")) - playsound(loc, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1) - qdel(src) - - -#undef SHADOW_REGEN_RATE diff --git a/code/modules/spells/spell_types/shapeshift.dm b/code/modules/spells/spell_types/shapeshift.dm deleted file mode 100644 index 9e856bf9d68..00000000000 --- a/code/modules/spells/spell_types/shapeshift.dm +++ /dev/null @@ -1,241 +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." - school = SCHOOL_TRANSMUTATION - 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" - nonabstract_req = TRUE - - var/revert_on_death = TRUE - var/die_with_shapeshifted_form = TRUE - ///If you want to convert the caster's health and blood to the shift, and vice versa. - var/convert_damage = TRUE - ///The damage type to convert to, as simplemobs don't have advanced damagetypes. - var/convert_damage_type = BRUTE - - var/mob/living/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/secbot/ed209, - /mob/living/simple_animal/hostile/giant_spider/viper/wizard, - /mob/living/simple_animal/hostile/construct/juggernaut/mystic, - ) - -/obj/effect/proc_holder/spell/targeted/shapeshift/cast(list/targets, mob/user = usr) - if(src in user.mob_spell_list) - LAZYREMOVE(user.mob_spell_list, src) - user.mind.AddSpell(src) - if(user.buckled) - user.buckled.unbuckle_mob(src,force=TRUE) - for(var/mob/living/shapeshifted_targets in targets) - if(!shapeshift_type) - var/list/animal_list = list() - var/list/display_animals = list() - for(var/path in possible_shapes) - var/mob/living/simple_animal/animal = path - animal_list[initial(animal.name)] = path - var/image/animal_image = image(icon = initial(animal.icon), icon_state = initial(animal.icon_state)) - display_animals += list(initial(animal.name) = animal_image) - sort_list(display_animals) - var/new_shapeshift_type = show_radial_menu(shapeshifted_targets, shapeshifted_targets, display_animals, custom_check = CALLBACK(src, .proc/check_menu, user), radius = 38, require_near = TRUE) - 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/shapeshift_ability = locate() in shapeshifted_targets - var/currently_ventcrawling = FALSE - if(shapeshift_ability) - if(shapeshifted_targets.movement_type & VENTCRAWLING) - currently_ventcrawling = TRUE - shapeshifted_targets = restore_form(shapeshifted_targets) - else - shapeshifted_targets = Shapeshift(shapeshifted_targets) - - // Can our new form support ventcrawling? - var/ventcrawler = HAS_TRAIT(shapeshifted_targets, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(shapeshifted_targets, TRAIT_VENTCRAWLER_NUDE) - if(ventcrawler) - continue - - // Are we currently ventcrawling? - if(!currently_ventcrawling) - continue - - // You're shapeshifting into something that can't fit into a vent - var/obj/machinery/atmospherics/pipeyoudiein = shapeshifted_targets.loc - var/datum/pipeline/ourpipeline - var/pipenets = pipeyoudiein.return_pipenets() - if(islist(pipenets)) - ourpipeline = pipenets[1] - else - ourpipeline = pipenets - - to_chat(shapeshifted_targets, span_userdanger("Casting [src] inside of [pipeyoudiein] quickly turns you into a bloody mush!")) - var/gibtype = /obj/effect/gibspawner/generic - if(isalien(shapeshifted_targets)) - gibtype = /obj/effect/gibspawner/xeno - for(var/obj/machinery/atmospherics/components/unary/possiblevent in range(10, get_turf(shapeshifted_targets))) - if(possiblevent.parents.len && possiblevent.parents[1] == ourpipeline) - new gibtype(get_turf(possiblevent)) - playsound(possiblevent, 'sound/effects/reee.ogg', 75, TRUE) - priority_announce("We detected a pipe blockage around [get_area(get_turf(shapeshifted_targets))], please dispatch someone to investigate.", "Central Command") - shapeshifted_targets.death() - qdel(shapeshifted_targets) - -/** - * check_menu: Checks if we are allowed to interact with a radial menu - * - * Arguments: - * * user The mob interacting with a menu - */ -/obj/effect/proc_holder/spell/targeted/shapeshift/proc/check_menu(mob/user) - if(!istype(user)) - return FALSE - if(user.incapacitated()) - return FALSE - return TRUE - -/obj/effect/proc_holder/spell/targeted/shapeshift/proc/Shapeshift(mob/living/caster) - var/obj/shapeshift_holder/shapeshift_ability = locate() in caster - if(shapeshift_ability) - to_chat(caster, span_warning("You're already shapeshifted!")) - return - - var/mob/living/shape = new shapeshift_type(caster.loc) - shapeshift_ability = new(shape, src, caster) - - clothes_req = FALSE - human_req = FALSE - return shape - -/obj/effect/proc_holder/spell/targeted/shapeshift/proc/restore_form(mob/living/caster) - var/obj/shapeshift_holder/shapeshift_ability = locate() in caster - if(!shapeshift_ability) - return - - var/mob/living/restored_player = shapeshift_ability.stored - - shapeshift_ability.restore() - - clothes_req = initial(clothes_req) - human_req = initial(human_req) - return restored_player - -/obj/effect/proc_holder/spell/targeted/shapeshift/dragon - name = "Dragon Form" - desc = "Take on the shape a lesser ash drake." - invocation = "RAAAAAAAAWR!" - - - 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/obj/effect/proc_holder/spell/targeted/shapeshift/source - -/obj/shapeshift_holder/Initialize(mapload, obj/effect/proc_holder/spell/targeted/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/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm new file mode 100644 index 00000000000..df154f2cedb --- /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 00000000000..358ff8a44fd --- /dev/null +++ b/code/modules/spells/spell_types/shapeshift/dragon.dm @@ -0,0 +1,8 @@ + +/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 00000000000..73f0bae9496 --- /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 00000000000..a858ac414d9 --- /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 065b511fbba..00000000000 --- a/code/modules/spells/spell_types/soultap.dm +++ /dev/null @@ -1,42 +0,0 @@ -/// The amount of health taken per tap. -#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!" - action_icon = 'icons/mob/actions/actions_spells.dmi' - action_icon_state = "soultap" - invocation = "AT ANY COST!" - invocation_type = INVOCATION_SHOUT - school = SCHOOL_NECROMANCY //i could see why this wouldn't be necromancy but messing with souls or whatever. ectomancy? - charge_max = 1 SECONDS - cooldown_min = 1 SECONDS - level_max = 0 - -/obj/effect/proc_holder/spell/self/tap/cast(list/targets, mob/living/user = usr) - if(HAS_TRAIT(user, TRAIT_NO_SOUL)) - to_chat(user, span_warning("You have no soul to tap into!")) - return - - to_chat(user, span_danger("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, span_userdanger("Your weakened soul is completely consumed by the tap!")) - ADD_TRAIT(user, TRAIT_NO_SOUL, MAGIC_TRAIT) - return - - for(var/obj/effect/proc_holder/spell/spell in user.mind.spell_list) - spell.charge_counter = spell.charge_max - spell.recharging = FALSE - spell.update_appearance() - -#undef HEALTH_LOST_PER_SOUL_TAP 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 6ced2de2318..00000000000 --- a/code/modules/spells/spell_types/spacetime_distortion.dm +++ /dev/null @@ -1,130 +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 = 300 - var/duration = 150 - range = 7 - var/list/effects - var/ready = TRUE - school = SCHOOL_EVOCATION - centcom_cancast = FALSE - sound = 'sound/effects/magic.ogg' - cooldown_min = 300 - 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/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/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.can_block_magic(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/summonitem.dm b/code/modules/spells/spell_types/summonitem.dm deleted file mode 100644 index b5c24f32d11..00000000000 --- a/code/modules/spells/spell_types/summonitem.dm +++ /dev/null @@ -1,108 +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 = 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 - action_icon_state = "summons" - ///The obj marked for recall - var/obj/marked_item - -/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) //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 = span_warning("You aren't holding anything that can be marked for recall!") - else - message = span_warning("You must hold the desired item in your hands to mark it for recall!") - - else if(marked_item && (marked_item in hand_items)) //unlinking item to the spell - message = span_notice("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 = span_warning("You sense your marked item has been destroyed!") - name = "Instant Summons" - marked_item = null - - 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="COMBAT MODE: [uppertext(L.combat_mode)]") - 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(span_warning("[M] suddenly disappears!")) - M.forceMove(L.loc) - M.loc.visible_message(span_warning("[M] suddenly appears!")) - item_to_retrieve = null - break - M.dropItemToGround(item_to_retrieve) - - else - var/obj/retrieved_item = item_to_retrieve.loc - if(retrieved_item.anchored) - return - if(istype(retrieved_item, /obj/machinery/portable_atmospherics)) //Edge cases for moved machinery - var/obj/machinery/portable_atmospherics/P = retrieved_item - P.disconnect() - P.update_appearance() - - item_to_retrieve = retrieved_item - - infinite_recursion += 1 - - if(!item_to_retrieve) - return - - if(item_to_retrieve.loc) - item_to_retrieve.loc.visible_message(span_warning("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(span_warning("The [item_to_retrieve.name] suddenly appears!")) - playsound(get_turf(L), 'sound/magic/summonitems_generic.ogg', 50, TRUE) - else - item_to_retrieve.loc.visible_message(span_warning("The [item_to_retrieve.name] suddenly appears in [L]'s hand!")) - playsound(get_turf(L), 'sound/magic/summonitems_generic.ogg', 50, TRUE) - - - 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 2185d6174f9..00000000000 --- a/code/modules/spells/spell_types/telepathy.dm +++ /dev/null @@ -1,30 +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" - antimagic_flags = MAGIC_RESISTANCE_MIND - var/notice = "notice" - var/boldnotice = "boldnotice" - -/obj/effect/proc_holder/spell/targeted/telepathy/cast(list/targets, mob/living/simple_animal/revenant/user = usr) - for(var/mob/living/M in targets) - var/msg = tgui_input_text(user, "What do you wish to tell [M]?", "Telepathy") - if(!msg) - charge_counter = charge_max - return - log_directed_talk(user, M, msg, LOG_SAY, "[name]") - to_chat(user, "You transmit to [M]: [msg]") - if(!M.can_block_magic(antimagic_flags, charge_cost = 0)) //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] [span_name("[M]")]") diff --git a/code/modules/spells/spell_types/teleport/_teleport.dm b/code/modules/spells/spell_types/teleport/_teleport.dm new file mode 100644 index 00000000000..794da323dc5 --- /dev/null +++ b/code/modules/spells/spell_types/teleport/_teleport.dm @@ -0,0 +1,145 @@ + +/** + * ## Teleport Spell + * + * Teleports the caster to a turf selected by get_destinations(). + */ +/datum/action/cooldown/spell/teleport + sound = 'sound/weapons/zapbang.ogg' + + school = SCHOOL_TRANSLOCATION + + /// What channel the teleport is done under. + var/teleport_channel = TELEPORT_CHANNEL_MAGIC + /// Whether we force the teleport to happen (ie, it cannot be blocked by noteleport areas or blessings or whatever) + var/force_teleport = FALSE + /// A list of flags related to determining if our destination target is valid or not. + var/destination_flags = NONE + /// The sound played on arrival, after the teleport. + var/post_teleport_sound = 'sound/weapons/zapbang.ogg' + +/datum/action/cooldown/spell/teleport/cast(atom/cast_on) + . = ..() + var/list/turf/destinations = get_destinations(cast_on) + if(!length(destinations)) + CRASH("[type] failed to find a teleport destination.") + + do_teleport(cast_on, pick(destinations), asoundout = post_teleport_sound, channel = teleport_channel, forced = force_teleport) + +/// Gets a list of destinations that are valid +/datum/action/cooldown/spell/teleport/proc/get_destinations(atom/center) + CRASH("[type] did not implement get_destinations and either has no effects or implemented the spell incorrectly.") + +/// Checks if the passed turf is a valid destination. +/datum/action/cooldown/spell/teleport/proc/is_valid_destination(turf/selected) + if(isspaceturf(selected) && (destination_flags & TELEPORT_SPELL_SKIP_SPACE)) + return FALSE + if(selected.density && (destination_flags & TELEPORT_SPELL_SKIP_DENSE)) + return FALSE + if(selected.is_blocked_turf(exclude_mobs = TRUE) && (destination_flags & TELEPORT_SPELL_SKIP_BLOCKED)) + return FALSE + + return TRUE + +/** + * ### Radius Teleport Spell + * + * A subtype of teleport that will teleport the caster + * to a random turf within a radius of themselves. + */ +/datum/action/cooldown/spell/teleport/radius_turf + /// The inner radius around the caster that we can teleport to + var/inner_tele_radius = 1 + /// The outer radius around the caster that we can teleport to + var/outer_tele_radius = 2 + +/datum/action/cooldown/spell/teleport/radius_turf/get_destinations(atom/center) + var/list/valid_turfs = list() + var/list/possibles = RANGE_TURFS(outer_tele_radius, center) + if(inner_tele_radius > 0) + possibles -= RANGE_TURFS(inner_tele_radius, center) + + for(var/turf/nearby_turf as anything in possibles) + if(!is_valid_destination(nearby_turf)) + continue + + valid_turfs += nearby_turf + + // If there are valid turfs around us? + // Screw it, allow 'em to teleport to ANY nearby turf. + return length(valid_turfs) ? valid_turfs : possibles + +/datum/action/cooldown/spell/teleport/radius_turf/is_valid_destination(turf/selected) + . = ..() + if(!.) + return FALSE + + // putting them at the edge is dumb + if(selected.x > world.maxx - outer_tele_radius || selected.x < outer_tele_radius) + return FALSE + if(selected.y > world.maxy - outer_tele_radius || selected.y < outer_tele_radius) + return FALSE + + return TRUE + +/** + * ### Area Teleport Spell + * + * A subtype of teleport that will teleport the caster + * to a random turf within a selected (or random) area. + */ +/datum/action/cooldown/spell/teleport/area_teleport + force_teleport = TRUE // Forced, as the Wizard Den is noteleport and wizards couldn't escape otherwise. + destination_flags = TELEPORT_SPELL_SKIP_BLOCKED + /// The last area we chose to teleport / where we're currently teleporting to, if mid-cast + var/last_chosen_area_name + /// If FALSE, the caster can select the destination area. If TRUE, they will teleport to somewhere randomly instead. + var/randomise_selection = FALSE + /// If the invocation appends the selected area when said. Requires invocation mode shout or whisper. + var/invocation_says_area = TRUE + +/datum/action/cooldown/spell/teleport/area_teleport/get_destinations(atom/center) + var/list/valid_turfs = list() + for(var/turf/possible_destination as anything in get_area_turfs(GLOB.teleportlocs[last_chosen_area_name])) + if(!is_valid_destination(possible_destination)) + continue + + valid_turfs += possible_destination + + return valid_turfs + +/datum/action/cooldown/spell/teleport/area_teleport/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + var/area/target_area + if(randomise_selection) + target_area = pick(GLOB.teleportlocs) + else + target_area = tgui_input_list(cast_on, "Chose an area to teleport to.", "Teleport", GLOB.teleportlocs) + + if(QDELETED(src) || QDELETED(cast_on) || !can_cast_spell()) + return . | SPELL_CANCEL_CAST + if(!target_area || isnull(GLOB.teleportlocs[target_area])) + return . | SPELL_CANCEL_CAST + + last_chosen_area_name = target_area + +/datum/action/cooldown/spell/teleport/area_teleport/cast(atom/cast_on) + if(isliving(cast_on)) + var/mob/living/living_cast_on = cast_on + living_cast_on.buckled?.unbuckle_mob(cast_on, force = TRUE) + return ..() + +/datum/action/cooldown/spell/teleport/area_teleport/invocation() + var/area/last_chosen_area = GLOB.teleportlocs[last_chosen_area_name] + + if(!invocation_says_area || isnull(last_chosen_area)) + return ..() + + switch(invocation_type) + if(INVOCATION_SHOUT) + owner.say("[invocation], [uppertext(last_chosen_area.name)]!", forced = "spell ([src])") + if(INVOCATION_WHISPER) + owner.whisper("[invocation], [uppertext(last_chosen_area.name)].", forced = "spell ([src])") diff --git a/code/modules/spells/spell_types/teleport/blink.dm b/code/modules/spells/spell_types/teleport/blink.dm new file mode 100644 index 00000000000..623bef237fb --- /dev/null +++ b/code/modules/spells/spell_types/teleport/blink.dm @@ -0,0 +1,19 @@ +/datum/action/cooldown/spell/teleport/radius_turf/blink + name = "Blink" + desc = "This spell randomly teleports you a short distance." + button_icon_state = "blink" + sound = 'sound/magic/blink.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 2 SECONDS + cooldown_reduction_per_rank = 0.4 SECONDS + + invocation_type = INVOCATION_NONE + + smoke_type = /datum/effect_system/fluid_spread/smoke + smoke_amt = 0 + + inner_tele_radius = 0 + outer_tele_radius = 6 + + post_teleport_sound = 'sound/magic/blink.ogg' diff --git a/code/modules/spells/spell_types/teleport/teleport.dm b/code/modules/spells/spell_types/teleport/teleport.dm new file mode 100644 index 00000000000..a6ce6d1b9a5 --- /dev/null +++ b/code/modules/spells/spell_types/teleport/teleport.dm @@ -0,0 +1,51 @@ +/// The wizard's teleport SPELL +/datum/action/cooldown/spell/teleport/area_teleport/wizard + name = "Teleport" + desc = "This spell teleports you to an area of your selection." + button_icon_state = "teleport" + sound = 'sound/magic/teleport_diss.ogg' + + school = SCHOOL_TRANSLOCATION + cooldown_time = 1 MINUTES + cooldown_reduction_per_rank = 10 SECONDS + + invocation = "SCYAR NILA" + invocation_type = INVOCATION_SHOUT + + smoke_type = /datum/effect_system/fluid_spread/smoke + smoke_amt = 2 + + post_teleport_sound = 'sound/magic/teleport_app.ogg' + +// Santa's teleport, themed as such +/datum/action/cooldown/spell/teleport/area_teleport/wizard/santa + name = "Santa Teleport" + + invocation = "HO HO HO!" + spell_requirements = NONE + antimagic_flags = NONE + + invocation_says_area = FALSE // Santa moves in mysterious ways + +/// Used by the wizard's teleport scroll +/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll + name = "Teleport (scroll)" + cooldown_time = 0 SECONDS + + invocation = null + invocation_type = INVOCATION_NONE + spell_requirements = NONE + + invocation_says_area = FALSE + +/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll/IsAvailable() + return ..() && owner.is_holding(target) + +/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + var/mob/living/carbon/caster = cast_on + if(caster.incapacitated() || !caster.is_holding(target)) + return . | SPELL_CANCEL_CAST diff --git a/code/modules/spells/spell_types/the_traps.dm b/code/modules/spells/spell_types/the_traps.dm deleted file mode 100644 index 6fb86100ab0..00000000000 --- a/code/modules/spells/spell_types/the_traps.dm +++ /dev/null @@ -1,26 +0,0 @@ -/obj/effect/proc_holder/spell/aoe_turf/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." - - charge_max = 250 - cooldown_min = 50 - - clothes_req = TRUE - invocation = "CAVERE INSIDIAS" - invocation_type = INVOCATION_SHOUT - range = 3 - - summon_type = list( - /obj/structure/trap/stun, - /obj/structure/trap/fire, - /obj/structure/trap/chill, - /obj/structure/trap/damage - ) - summon_lifespan = 3000 - summon_amt = 5 - - action_icon_state = "the_traps" - -/obj/effect/proc_holder/spell/aoe_turf/conjure/the_traps/post_summon(obj/structure/trap/T, mob/user) - T.immune_minds += user.mind - T.charges = 1 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 00000000000..d17ef04387d --- /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 00000000000..0416350ca69 --- /dev/null +++ b/code/modules/spells/spell_types/touch/duffelbag_curse.dm @@ -0,0 +1,87 @@ + +/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 00000000000..c6693c7c904 --- /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 00000000000..d39ddb3e30c --- /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 6267706074f..00000000000 --- a/code/modules/spells/spell_types/touch_attacks.dm +++ /dev/null @@ -1,96 +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 - -/obj/effect/proc_holder/spell/targeted/touch/Destroy() - remove_hand() - if(action?.owner) - var/mob/guy_who_needs_to_know = action.owner - to_chat(guy_who_needs_to_know, span_notice("The power of the spell dissipates from your hand.")) - return ..() - -/obj/effect/proc_holder/spell/targeted/touch/proc/remove_hand(recharge = FALSE) - QDEL_NULL(attached_hand) - if(recharge) - 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.UpdateButtons() - -/obj/effect/proc_holder/spell/targeted/touch/cast(list/targets,mob/user = usr) - if(!QDELETED(attached_hand)) - remove_hand(TRUE) - to_chat(user, span_notice("[dropmessage]")) - return - - for(var/mob/living/carbon/C in targets) - if(!attached_hand) - if(ChargeHand(C)) - 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 - else - return ..() - -/obj/effect/proc_holder/spell/targeted/touch/proc/ChargeHand(mob/living/carbon/user) - attached_hand = new hand_path(src) - attached_hand.attached_spell = src - if(!user.put_in_hands(attached_hand)) - remove_hand(TRUE) - if (user.usable_hands == 0) - to_chat(user, span_warning("You dont have any usable hands!")) - else - to_chat(user, span_warning("Your hands are full!")) - return FALSE - to_chat(user, span_notice("[drawmessage]")) - return TRUE - - -/obj/effect/proc_holder/spell/targeted/touch/disintegrate - name = "Smite" - desc = "This spell charges your hand with an unholy energy that can be used to cause a touched victim to violently explode." - hand_path = /obj/item/melee/touch_attack/disintegrate - - school = 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 = 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/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." - hand_path = /obj/item/melee/touch_attack/duffelbag - action_icon_state = "duffelbag_curse" - - school = SCHOOL_CONJURATION - charge_max = 60 - clothes_req = FALSE - cooldown_min = 20 diff --git a/code/modules/spells/spell_types/trigger.dm b/code/modules/spells/spell_types/trigger.dm deleted file mode 100644 index c13c96686c6..00000000000 --- 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 d1c4813bc67..00000000000 --- a/code/modules/spells/spell_types/turf_teleport.dm +++ /dev/null @@ -1,46 +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 - - school = SCHOOL_TRANSLOCATION - - 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,TRUE) - for(var/mob/living/target in targets) - var/list/turfs = new/list() - for(var/turf/T as anything in RANGE_TURFS(outer_tele_radius, target)) - if(T in RANGE_TURFS(inner_tele_radius, target)) - continue - 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.y>>>>>> acfa5e4fdd0 (TGUI Say: Upgrades chat input with modern features (#67116)) diff --git a/modular_skyrat/master_files/code/_globalvars/maint_loot_uncommon.dm b/modular_skyrat/master_files/code/_globalvars/maint_loot_uncommon.dm index 2f859da8319..c5e2a573e4e 100644 --- a/modular_skyrat/master_files/code/_globalvars/maint_loot_uncommon.dm +++ b/modular_skyrat/master_files/code/_globalvars/maint_loot_uncommon.dm @@ -144,7 +144,7 @@ GLOBAL_LIST_INIT(uncommon_loot, list(// uncommon: useful items /obj/item/a_gift = 200, /obj/item/banhammer = 25, /obj/item/bikehorn/airhorn = 25, - /obj/item/book/granter/spell/smoke/lesser = 5, + /obj/item/book/granter/action/spell/smoke/lesser = 5, /obj/item/book/granter/crafting_recipe/pipegun_prime = 5, /obj/item/book/granter/crafting_recipe/trash_cannon = 5, /obj/item/bountytrap = 25, diff --git a/modular_skyrat/master_files/code/datums/martial/cqcplus.dm b/modular_skyrat/master_files/code/datums/martial/cqcplus.dm index fb761ea425c..9bad8bd08dd 100644 --- a/modular_skyrat/master_files/code/datums/martial/cqcplus.dm +++ b/modular_skyrat/master_files/code/datums/martial/cqcplus.dm @@ -25,6 +25,6 @@ /obj/item/book/granter/martial/cqc/plus martial = /datum/martial_art/cqc/plus name = "old but gold manual" - martialname = "close quarters combat plus" + martial_name = "close quarters combat plus" desc = "A small, black manual. There are drawn instructions of tactical hand-to-hand combat. This includes how to deflect projectiles too." greet = span_boldannounce("You've mastered the basics of CQC+.") diff --git a/modular_skyrat/master_files/code/datums/mutations/chameleon.dm b/modular_skyrat/master_files/code/datums/mutations/chameleon.dm deleted file mode 100644 index c83a314c719..00000000000 --- a/modular_skyrat/master_files/code/datums/mutations/chameleon.dm +++ /dev/null @@ -1,24 +0,0 @@ -/obj/effect/proc_holder/spell/self/chameleon_skin_activate - name = "Activate Chameleon Skin" - desc = "The chromatophores in your skin adjust to your surroundings, as long as you stay still." - clothes_req = FALSE - action_icon = 'icons/mob/actions/actions_minor_antag.dmi' - action_icon_state = "ninja_cloak" // SKYRAT EDIT END - -/obj/effect/proc_holder/spell/self/chameleon_skin_activate/cast(list/targets, mob/user = usr) - . = ..() - - if(HAS_TRAIT(user, TRAIT_CHAMELEON_SKIN)) - chameleon_skin_deactivate(user) - return - - ADD_TRAIT(user, TRAIT_CHAMELEON_SKIN, GENETIC_MUTATION) - to_chat(user, "The pigmentation of your skin shifts and starts to take on the colors of your surroundings.") - -/obj/effect/proc_holder/spell/self/chameleon_skin_activate/proc/chameleon_skin_deactivate(mob/user = usr) - if(!HAS_TRAIT_FROM(user, TRAIT_CHAMELEON_SKIN, GENETIC_MUTATION)) - return - - REMOVE_TRAIT(user, TRAIT_CHAMELEON_SKIN, GENETIC_MUTATION) - user.alpha = 255 - to_chat(user, text("Your skin shifts as it shimmers back into its original colors.")) diff --git a/modular_skyrat/master_files/code/modules/antagonists/wizard/equipment/spellbook.dm b/modular_skyrat/master_files/code/modules/antagonists/wizard/equipment/spellbook.dm deleted file mode 100644 index c5e97522312..00000000000 --- a/modular_skyrat/master_files/code/modules/antagonists/wizard/equipment/spellbook.dm +++ /dev/null @@ -1,11 +0,0 @@ -/* -* THUS BEGINS OUR GLORIOUS WIZARD MODULAR SPELLBOOK! -* ALL HAIL THE MAGICIANS IN OUR LIFE! -*/ - -/datum/spellbook_entry/stimpack - name = "Stimulants" - desc = "Gives you a strong burst of energy, getting you on your feet and giving stun resistance for a short time." - spell_type = /obj/effect/proc_holder/spell/targeted/stimpack - category = "Mobility" - cost = 1 diff --git a/modular_skyrat/master_files/code/modules/spells/wizard.dm b/modular_skyrat/master_files/code/modules/spells/wizard.dm deleted file mode 100644 index 971317b8e83..00000000000 --- a/modular_skyrat/master_files/code/modules/spells/wizard.dm +++ /dev/null @@ -1,28 +0,0 @@ -/* -* AND SO BEGINS THE GLORIOUS MODULAR SPELL ADDITION SPOT. -* PUT ALL NEWLY CREATED WIZARD SPELLS HERE! -*/ - -/obj/effect/proc_holder/spell/targeted/stimpack - name = "Magic Stimpack" - desc = "This spell magically injects stimulants straight into your blood. Won't work on species with no reagent reactions!" - - school = "transmutation" - charge_max = 450 - clothes_req = FALSE - invocation = "STIMULUS CHEQ'US" - invocation_type = INVOCATION_SHOUT - range = -1 - include_user = TRUE - - cooldown_min = 300 // 37.5 deciseconds reduction per rank - action_icon_state = "spell_default" - -/obj/effect/proc_holder/spell/targeted/stimpack/cast(mob/living/user) - ..() - to_chat(user, span_notice("Time appears to slow as your bodily functions rapidly speed up.")) - user.SetKnockdown(0) - user.setStaminaLoss(0) - user.set_resting(FALSE) - user.reagents.add_reagent(/datum/reagent/medicine/stimulants, 3) // Ideally this comes out to a bit less than 30 seconds with tidi taken into account. - return TRUE diff --git a/modular_skyrat/modules/awaymissions_skyrat/code/mothership_astrum.dm b/modular_skyrat/modules/awaymissions_skyrat/code/mothership_astrum.dm index 23b26b0c491..745a2b4c12e 100644 --- a/modular_skyrat/modules/awaymissions_skyrat/code/mothership_astrum.dm +++ b/modular_skyrat/modules/awaymissions_skyrat/code/mothership_astrum.dm @@ -227,7 +227,7 @@ loot = list(/obj/item/storage/medkit/expeditionary = 20, /obj/item/shield/riot/tele = 12, /obj/item/dnainjector/shock = 10, - /obj/item/book/granter/spell/summonitem = 20, + /obj/item/book/granter/action/spell/summonitem = 20, /obj/item/storage/backpack/holding = 12, /obj/item/dnainjector/thermal = 5, /obj/item/melee/baton/telescopic = 12) diff --git a/modular_skyrat/modules/chaplain/code/mortis.dm b/modular_skyrat/modules/chaplain/code/mortis.dm index c391b8f7b3f..480e72901c8 100644 --- a/modular_skyrat/modules/chaplain/code/mortis.dm +++ b/modular_skyrat/modules/chaplain/code/mortis.dm @@ -1,6 +1,5 @@ /obj/item/implant/mortis name = "MORTIS implant" - activated = 0 /obj/item/implant/mortis/get_data() var/dat = {"Implant Specifications:
diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage.dm index 6bc308cdd73..495f768b0a1 100644 --- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage.dm +++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage.dm @@ -48,15 +48,6 @@ new_vampire.set_safe_hunger_level() if(SSevents.holidays && SSevents.holidays[HALLOWEEN]) halloween_version = TRUE - if(isnull(batform) && halloween_version) // You can only have a batform during Halloween. - batform = new - new_vampire.AddSpell(batform) - -/datum/species/hemophage/on_species_loss(mob/living/carbon/ex_vampire) - . = ..() - if(!isnull(batform)) - ex_vampire.RemoveSpell(batform) - QDEL_NULL(batform) /datum/species/hemophage/spec_life(mob/living/carbon/human/vampire, delta_time, times_fired) . = ..() @@ -93,11 +84,3 @@ "Historically, Hemophages have caused great societal strife through their very existence. Many have reported dread on having someone reveal they require blood to survive, worse on learning they have been undead, espiecally in 'superstitious' communities. In many places they occupy a sort of second class, unable to live normal lives due to their condition being a sort of skeleton in their closet. Some can actually be found in slaughterhouses or the agricultural industry, gaining easy access to a large supply of animal blood to feed their eternal thirst.", "Others find their way into mostly-vampiric communities, turning others into their own kind; though, the virus can only transmit to hosts that are incredibly low on blood, taking advantage of their reduced immune system efficiency and higher rate of blood creation to be able to survive the initial few days within their host.", "\"What the fuck does any of this mean?\" - Doctor Micheals, reading their CentCom report about the new 'hires'.") - -/obj/effect/proc_holder/spell/targeted/shapeshift/bat - name = "Bat Form" - desc = "Take on the shape a space bat." - invocation = "*snap" - charge_max = 5 SECONDS - cooldown_min = 5 SECONDS - shapeshift_type = /mob/living/simple_animal/hostile/retaliate/bat diff --git a/modular_skyrat/modules/icspawning/code/observer.dm b/modular_skyrat/modules/icspawning/code/observer.dm index f5f0f416a82..ed17f6b78f8 100644 --- a/modular_skyrat/modules/icspawning/code/observer.dm +++ b/modular_skyrat/modules/icspawning/code/observer.dm @@ -58,8 +58,6 @@ else spawned_player.ckey = user.key - if(give_return != "No") - spawned_player.mind.AddSpell(new /obj/effect/proc_holder/spell/self/return_back, FALSE) if(dresscode != "Naked") spawned_player.equipOutfit(dresscode) diff --git a/modular_skyrat/modules/icspawning/code/spell.dm b/modular_skyrat/modules/icspawning/code/spell.dm deleted file mode 100644 index 6b810578cbb..00000000000 --- a/modular_skyrat/modules/icspawning/code/spell.dm +++ /dev/null @@ -1,41 +0,0 @@ -//SKYRAT MODULE IC-SPAWNING https://github.com/Skyrat-SS13/Skyrat-tg/pull/104 -/obj/effect/proc_holder/spell - var/always_can_cast = FALSE - var/mob_spell = FALSE // If the spell is bound to the mob alone, and not the mind - -/obj/effect/proc_holder/spell/self/return_back // Admin only spell, teleports and deletes the body, ghosting the user. - name = "Return" - desc = "Activates your return beacon." - clothes_req = NONE - charge_max = 1 - cooldown_min = 1 - always_can_cast = TRUE - mob_spell = TRUE - - invocation = "Return on!" // pls someone get reference <3 - invocation_type = "whisper" - school = "evocation" - action_icon_state = "lightning" - -/obj/effect/proc_holder/spell/self/return_back/can_cast(mob/user = usr, skipcharge = FALSE, silent = FALSE) - return TRUE - -/obj/effect/proc_holder/spell/self/return_back/cast(mob/living/carbon/human/user) - user.mind.RemoveSpell(src) - - playsound(get_turf(user.loc), 'sound/magic/Repulse.ogg', 100, 1) - - var/mob/dead/observer/ghost = user.ghostize(1) - - var/datum/effect_system/spark_spread/quantum/sparks = new - sparks.set_up(10, 1, user) - sparks.attach(user.loc) - sparks.start() - - qdel(user) - - - // Get them back to their regular name. - ghost.set_ghost_appearance() - if(ghost.client && ghost.client.prefs) - ghost.deadchat_name = ghost.client.prefs?.read_preference(/datum/preference/name/real_name) diff --git a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm index 6b02f2d602a..3b4945df669 100644 --- a/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm +++ b/modular_skyrat/modules/loadouts/loadout_items/loadout_datum_pocket.dm @@ -144,7 +144,7 @@ GLOBAL_LIST_INIT(loadout_pocket_items, generate_loadout_items(/datum/loadout_ite /datum/loadout_item/pocket_items/modular_laptop name = "Modular Laptop" - item_path = /obj/item/modular_computer/laptop/preset/civilian/closed + item_path = /obj/item/modular_computer/laptop/preset/civilian /datum/loadout_item/pocket_items/ringbox_gold name = "Gold Ring Box" diff --git a/modular_skyrat/modules/modular_items/code/miscellaneous.dm b/modular_skyrat/modules/modular_items/code/miscellaneous.dm deleted file mode 100644 index 80ae0b4fb10..00000000000 --- a/modular_skyrat/modules/modular_items/code/miscellaneous.dm +++ /dev/null @@ -1,54 +0,0 @@ -//loot -/obj/item/bloodcrawlbottle - name = "bloodlust in a bottle" - desc = "Drinking this will give you unimaginable powers... and mildly disgust you because of it's metallic taste." - icon = 'icons/obj/wizard.dmi' - icon_state = "vial" - -/obj/item/bloodcrawlbottle/attack_self(mob/user) - to_chat(user, span_notice("You drink the bottle's contents.")) - var/obj/effect/proc_holder/spell/bloodcrawl/S = new() - user.mind.AddSpell(S) - user.log_message("learned the spell bloodcrawl ([S])", LOG_ATTACK, color="orange") - qdel(src) - -/obj/effect/proc_holder/spell/bloodcrawl/lesser - name = "Lesser Blood Crawl" - desc = "Use pools of blood to phase out of existence. Requires large pools of blood, and a lot of your blood." - -/obj/effect/proc_holder/spell/bloodcrawl/lesser/choose_targets(mob/user = usr) - for(var/obj/effect/decal/cleanable/target in range(range, get_turf(user))) - if(target.can_bloodcrawl_in() && target.bloodiness >= 20) - perform(target) - return - revert_cast() - to_chat(user, span_warning("There must be a nearby source of plentiful blood!")) - -/obj/effect/proc_holder/spell/bloodcrawl/lesser/perform(obj/effect/decal/cleanable/target, recharge = 1, mob/living/user = usr) - if(istype(user) && user.canUseTopic(user, TRUE)) - if(phased) - if(user.phasein(target)) - phased = FALSE - else - if(user.phaseout(target)) - phased = TRUE - if(iscarbon(user)) - var/mob/living/carbon/C = user - C.blood_volume -= 150 - start_recharge() - return - revert_cast() - to_chat(user, span_warning("You are unable to blood crawl!")) - -/obj/item/storage/box/hero/ronin - name = "Sword Saint, Wandering Vagabond - 1600's." - desc = "Anyone can give up, it's the easiest thing in the world to do. But to hold it together when everyone else would understand if you fell apart, that's true strength. Become the wandering swordsman you were always meant to be!" - -/obj/item/storage/box/hero/ronin/PopulateContents() - new /obj/item/clothing/under/costume/kamishimo(src) - new /obj/item/clothing/head/rice_hat(src) - new /obj/item/katana/weak/curator(src) - new /obj/item/clothing/shoes/sandal(src) - -/obj/item/modular_computer/laptop/preset/civilian/closed - start_open = FALSE diff --git a/modular_skyrat/modules/moretraitoritems/code/granters.dm b/modular_skyrat/modules/moretraitoritems/code/granters.dm deleted file mode 100644 index 312503ded81..00000000000 --- a/modular_skyrat/modules/moretraitoritems/code/granters.dm +++ /dev/null @@ -1,35 +0,0 @@ -/obj/item/book/granter/traitsr //They're for traits from our skyrat idk what else to say dude - var/traits - var/traitname = "bug" - var/greet = "You’ve mastered the art of breaking code. Congrats." - -/obj/item/book/granter/traitsr/already_known(mob/user) - if(!traits) - return TRUE - if(HAS_TRAIT(src, (traits))) - to_chat(user, span_boldwarning("You already know about [traitname].")) - return - return FALSE - -/obj/item/book/granter/traitsr/on_reading_start(mob/user) - to_chat(user, span_notice("You start reading about [traitname]...")) - -/obj/item/book/granter/traitsr/on_reading_finished(mob/user) - to_chat(user, "[greet]") - ADD_TRAIT(user, (traits), type) //wtf - user.log_message("Gained the ability [traitname] ([traits])", LOG_ATTACK, color="orange") - onlearned(user) - -/obj/item/book/granter/traitsr/onlearned(mob/user) - ..() - if(oneuse) - user.visible_message(span_warning("You rip out the pages of the [src]!")) - -/obj/item/book/granter/traitsr/ventcrawl_book - traits = TRAIT_VENTCRAWLER_ALWAYS - name = "military contortionist guide" - traitname = "ventcrawling expert" - desc = "A special operations handbook for teaching people with at least a basic understanding of infiltration tactics how to most effectively utilize small spaces such as air ducts or pipes." - greet = span_boldannounce("You’ve mastered the art of climbing through air pipes!") - icon_state = "stealthmanual" - remarks = list("Have a healthy diet...", "Know when to use vents to your advantage...", "Don't be seen climbing into vents...", "Best paired with close quarters skills...", "Pressure resistant gear recommended...") diff --git a/modular_skyrat/modules/moretraitoritems/code/syndicate_loadout.dm b/modular_skyrat/modules/moretraitoritems/code/syndicate_loadout.dm index 8be0fd74be3..3d0e3dab1c3 100644 --- a/modular_skyrat/modules/moretraitoritems/code/syndicate_loadout.dm +++ b/modular_skyrat/modules/moretraitoritems/code/syndicate_loadout.dm @@ -1,7 +1,6 @@ /obj/item/storage/box/syndie_kit/spaceassassin/PopulateContents() new /obj/item/knife/combat(src) - new /obj/item/book/granter/traitsr/ventcrawl_book(src) switch(pick(list("red", "green", "dgreen", "blue", "orange", "black"))) if("green") new /obj/item/clothing/head/helmet/space/syndicate/green(src) @@ -120,7 +119,7 @@ new /obj/item/clothing/suit/hooded/chaplain_hoodie(src) new /obj/item/card/id/advanced/chameleon(src) new /obj/item/clothing/shoes/chameleon/noslip(src) - new /obj/item/book/granter/spell/summonitem(src) + new /obj/item/book/granter/action/spell/summonitem(src) /obj/item/storage/box/syndie_kit/loadout/hunter/PopulateContents() new /obj/item/gun/ballistic/rifle/boltaction/harpoon(src) diff --git a/modular_skyrat/modules/morewizardstuffs/code/antag_spawner.dm b/modular_skyrat/modules/morewizardstuffs/code/antag_spawner.dm deleted file mode 100644 index 23e7ae0d16b..00000000000 --- a/modular_skyrat/modules/morewizardstuffs/code/antag_spawner.dm +++ /dev/null @@ -1,75 +0,0 @@ -/obj/item/antag_spawner/impostors //I don't remember why i've added this but this is problably the worst way to make it - name = "magical device" - desc = "A magical device that will multiply your bodies." - icon = 'icons/obj/wizard.dmi' - icon_state ="lovestone" - -/obj/item/antag_spawner/impostors/proc/check_usability(mob/user) - if(!user.mind.has_antag_datum(/datum/antagonist/wizard, TRUE)) - to_chat(user, span_danger("You have no idea on how to use this thing.")) - return FALSE - return TRUE - - -/obj/item/antag_spawner/impostors/attack_self(mob/user) - if(!(check_usability(user))) - return - for(var/datum/mind/M as anything in get_antag_minds(/datum/antagonist/wizard)) - if(!ishuman(M.current)) - continue - var/mob/living/carbon/human/W = M.current - var/list/candidates = poll_ghost_candidates("Would you like to be an imposter wizard?", ROLE_WIZARD) - if(!candidates) - to_chat(user, span_warning("Unable to duplicate! You can either attack the spellbook with the contract to refund your points, or wait and try again later..")) - return - var/mob/dead/observer/C = pick(candidates) - - new /obj/effect/particle_effect/fluid/smoke(W.loc) - - var/mob/living/carbon/human/I = new /mob/living/carbon/human(W.loc) - W.dna.transfer_identity(I, transfer_SE=1) - I.real_name = I.dna.real_name - I.name = I.dna.real_name - I.updateappearance(mutcolor_update=1) - I.domutcheck() - I.key = C.key - var/datum/antagonist/wizard/master = M.has_antag_datum(/datum/antagonist/wizard) - if(!master.wiz_team) - master.create_wiz_team() - var/datum/antagonist/wizard/apprentice/imposter/spawnersr/imposter = new() - imposter.master = M - imposter.wiz_team = master.wiz_team - master.wiz_team.add_member(imposter) - I.mind.add_antag_datum(imposter) - //Remove if possible - I.mind.special_role = "imposter" - // - qdel(src) - -/datum/antagonist/wizard/apprentice/imposter/spawnersr/on_gain() - . = ..() - equip_wizard() - -/datum/antagonist/wizard/apprentice/imposter/spawnersr/equip_wizard() - var/mob/living/carbon/human/master_mob = master.current - var/mob/living/carbon/human/H = owner.current - if(!istype(master_mob) || !istype(H)) - return - if(master_mob.ears) - H.equip_to_slot_or_del(new master_mob.ears.type, ITEM_SLOT_EARS) - if(master_mob.w_uniform) - H.equip_to_slot_or_del(new master_mob.w_uniform.type, ITEM_SLOT_ICLOTHING) - if(master_mob.shoes) - H.equip_to_slot_or_del(new master_mob.shoes.type, ITEM_SLOT_FEET) - if(master_mob.wear_suit) - H.equip_to_slot_or_del(new master_mob.wear_suit.type, ITEM_SLOT_OCLOTHING) - if(master_mob.head) - H.equip_to_slot_or_del(new master_mob.head.type, ITEM_SLOT_HEAD) - if(master_mob.back) - H.equip_to_slot_or_del(new master_mob.back.type, ITEM_SLOT_BACK) - H.equip_to_slot_or_del(new /obj/item/teleportation_scroll, ITEM_SLOT_LPOCKET) - - //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)) diff --git a/modular_skyrat/modules/morewizardstuffs/code/snapceleritas.dm b/modular_skyrat/modules/morewizardstuffs/code/snapceleritas.dm deleted file mode 100644 index f16ac044acf..00000000000 --- a/modular_skyrat/modules/morewizardstuffs/code/snapceleritas.dm +++ /dev/null @@ -1,34 +0,0 @@ -/obj/effect/proc_holder/spell/pointed/celeritas - name = "Celeritas" - desc = "Swap places with a target within range." - school = "transmutation" - charge_max = 200 - cooldown_min = 60 - clothes_req = FALSE - invocation_type = NONE - action_icon_state = "spellcard" - range = 10 - message = "The world rapidly shift!" - -/obj/effect/proc_holder/spell/pointed/celeritas/cast(list/targets, mob/user) - var/mob/living/target = targets[1] - user.emote("snap") - playsound(user, 'sound/weapons/punchmiss.ogg', 75, TRUE) - var/turf/targeted_turf = get_turf(target) - var/turf/user_turf = get_turf(user) - - new /obj/effect/temp_visual/small_smoke/halfsecond(user.drop_location()) - new /obj/effect/temp_visual/small_smoke/halfsecond(targeted_turf) - do_teleport(user, targeted_turf, 0, no_effects = TRUE, channel = TELEPORT_CHANNEL_MAGIC) - do_teleport(target, user_turf, 0, no_effects = TRUE, channel= TELEPORT_CHANNEL_MAGIC) - - -/obj/effect/proc_holder/spell/pointed/celeritas/can_target(atom/target, mob/user, silent) - . = ..() - if(!.) - return FALSE - if(!isliving(target)) - if(!silent) - to_chat(user, span_warning("You are unable to swap with [target]!")) - return FALSE - return TRUE diff --git a/modular_skyrat/modules/morewizardstuffs/code/spellbooks.dm b/modular_skyrat/modules/morewizardstuffs/code/spellbooks.dm deleted file mode 100644 index 3247a756a26..00000000000 --- a/modular_skyrat/modules/morewizardstuffs/code/spellbooks.dm +++ /dev/null @@ -1,29 +0,0 @@ -//OFFENSIVE -/datum/spellbook_entry/summonknives - name = "Summon Knives" - desc = "Summon several knives to throw at your enemies, A bit sharp, Isn't it?" - spell_type = /obj/effect/proc_holder/spell/aimed/summonknives - -/datum/spellbook_entry/knotlaces - name = "Tie shoelaces" - desc = "Make a fool out of them by tying their shoelaces!" - spell_type = /obj/effect/proc_holder/spell/pointed/knotlaces - -//DEFENSIVE - -//MOBILITY -/datum/spellbook_entry/snapceleritas - name = "Celeritas" - desc = "With a snap of your finger, You will be able to swap places with someone!" - spell_type = /obj/effect/proc_holder/spell/pointed/celeritas - category = "Mobility" - -//SUPPORT -/* Holy shit, What was I doing when I made that one? -/datum/spellbook_entry/item/impostors - name = "Magical Device" - desc = "This magical device can be used to have a fake, identical body double. Looks like you, Problably talks like you but especially used to trick the crews with it." - item_path = /obj/item/antag_spawner/impostors - limit = 3 - category = "Assistance" -*/ diff --git a/modular_skyrat/modules/morewizardstuffs/code/summonknives.dm b/modular_skyrat/modules/morewizardstuffs/code/summonknives.dm deleted file mode 100644 index 9be5e282381..00000000000 --- a/modular_skyrat/modules/morewizardstuffs/code/summonknives.dm +++ /dev/null @@ -1,30 +0,0 @@ -/obj/effect/proc_holder/spell/aimed/summonknives - name = "Summon Knives" - desc = "Throw several knives where you aim at!" - invocation = "POIN'T ED'JIS!" - action_icon_state = "projectile" - invocation_type = INVOCATION_SHOUT - charge_max = 200 - cooldown_min = 35 - projectile_type = /obj/projectile/summonedknife - projectile_amount = 3 - projectiles_per_fire = 5 - var/projectile_initial_spread_amount = 30 - var/projectile_location_spread_amount = 12 - -/obj/effect/proc_holder/spell/aimed/summonknives/ready_projectile(obj/projectile/P, atom/target, mob/user, iteration) - 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 - (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) - -/obj/projectile/summonedknife - name = "Knife" - icon = 'modular_skyrat/modules/morewizardstuffs/icons/projectile.dmi' - icon_state = "knife" - damage_type = BRUTE - damage = 10 - diff --git a/modular_skyrat/modules/morewizardstuffs/code/tiedshoes.dm b/modular_skyrat/modules/morewizardstuffs/code/tiedshoes.dm deleted file mode 100644 index 1c1a59d2c7a..00000000000 --- a/modular_skyrat/modules/morewizardstuffs/code/tiedshoes.dm +++ /dev/null @@ -1,37 +0,0 @@ -/obj/effect/proc_holder/spell/pointed/knotlaces - name = "Tie shoelaces" - desc = "Literally just tie the shoelaces of a target together" - school = "transmutation" - action_icon_state = "barn" - charge_max = 100 - cooldown_min = 35 - invocation = "TAN'GEL" - invocation_type = INVOCATION_WHISPER - var/static/list/compatible_mobs_typecache = typecacheof(list(/mob/living/carbon/human)) - -/obj/effect/proc_holder/spell/pointed/knotlaces/cast(list/targets, mob/user) - if(!targets.len) - to_chat(user, span_warning("No target found in range!")) - return FALSE - if(!can_target(targets[1], user)) - return FALSE - - var/mob/living/carbon/target = targets[1] - if(target.can_block_magic(MAGIC_RESISTANCE)) - to_chat(user, span_warning("The spell had no effect!")) - return FALSE - var/obj/item/clothing/shoes/tiedshoes = target.shoes - if (!tiedshoes?.can_be_tied) - to_chat(user, span_warning("[target] does not have knottable shoes!")) - return - tiedshoes.adjust_laces(SHOES_KNOTTED) - -/obj/effect/proc_holder/spell/pointed/knotlaces/can_target(atom/target, mob/user, silent) - . = ..() - if(!.) - return FALSE - if(!is_type_in_typecache(target, compatible_mobs_typecache)) - if(!silent) - to_chat(user, span_warning("You are unable to curse [target]!")) - return FALSE - return TRUE diff --git a/modular_skyrat/modules/morewizardstuffs/icons/projectile.dmi b/modular_skyrat/modules/morewizardstuffs/icons/projectile.dmi deleted file mode 100644 index 72ee1ee5587..00000000000 Binary files a/modular_skyrat/modules/morewizardstuffs/icons/projectile.dmi and /dev/null differ diff --git a/modular_skyrat/modules/opposing_force/code/equipment/spells.dm b/modular_skyrat/modules/opposing_force/code/equipment/spells.dm index e87fbad2616..8991531e947 100644 --- a/modular_skyrat/modules/opposing_force/code/equipment/spells.dm +++ b/modular_skyrat/modules/opposing_force/code/equipment/spells.dm @@ -3,47 +3,47 @@ /datum/opposing_force_equipment/spell/fireball name = "Fireball" - item_type = /obj/item/book/granter/spell/fireball + item_type = /obj/item/book/granter/action/spell/fireball description = "This spell fires an explosive fireball at a target." /datum/opposing_force_equipment/spell/sacredflame name = "Sacred Flame" - item_type = /obj/item/book/granter/spell/sacredflame + item_type = /obj/item/book/granter/action/spell/sacredflame description = "Makes everyone around you more flammable, and lights yourself on fire." /datum/opposing_force_equipment/spell/smoke name = "Smoke" - item_type = /obj/item/book/granter/spell/smoke + item_type = /obj/item/book/granter/action/spell/smoke description = "This spell spawns a cloud of choking smoke at your location." /datum/opposing_force_equipment/spell/blind name = "Blind" - item_type = /obj/item/book/granter/spell/blind + item_type = /obj/item/book/granter/action/spell/blind description = "This spell temporarily blinds a single target." /datum/opposing_force_equipment/spell/mindswap name = "Mindswap" - item_type = /obj/item/book/granter/spell/mindswap + item_type = /obj/item/book/granter/action/spell/mindswap description = "This spell allows the user to switch bodies with a target next to them." admin_note = "WARNING: This spell allows the user to swap minds with someone else, and is overall very strong." /datum/opposing_force_equipment/spell/forcewall name = "Forcewall" - item_type = /obj/item/book/granter/spell/forcewall + item_type = /obj/item/book/granter/action/spell/forcewall description = "Create a magical barrier that only you can pass through." /datum/opposing_force_equipment/spell/knock name = "Knock" - item_type = /obj/item/book/granter/spell/knock + item_type = /obj/item/book/granter/action/spell/knock description = "This spell opens nearby doors and closets." /datum/opposing_force_equipment/spell/charge name = "Charge" - item_type = /obj/item/book/granter/spell/charge + item_type = /obj/item/book/granter/action/spell/charge description = "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." admin_note = "This one can be used for some fuckery (such as recharging martial art granters, sometimes), be a bit careful." /datum/opposing_force_equipment/spell/summonitem name = "Summon Item" - item_type = /obj/item/book/granter/spell/summonitem + item_type = /obj/item/book/granter/action/spell/summonitem description = "This spell can be used to recall a previously marked item to your hand from anywhere in the universe." diff --git a/modular_skyrat/modules/opposing_force/code/equipment/utility.dm b/modular_skyrat/modules/opposing_force/code/equipment/utility.dm index 9b7c3e4053a..0570cacf1f5 100644 --- a/modular_skyrat/modules/opposing_force/code/equipment/utility.dm +++ b/modular_skyrat/modules/opposing_force/code/equipment/utility.dm @@ -76,9 +76,6 @@ /datum/opposing_force_equipment/gear/borer_egg/on_issue(mob/living/target) new /obj/effect/mob_spawn/ghost_role/borer_egg/opfor(get_turf(target)) -/datum/opposing_force_equipment/gear/ventcrawl_book - item_type = /obj/item/book/granter/traitsr/ventcrawl_book - admin_note = "WARNING: Incredibly powerful, use discretion when handing this out." /datum/opposing_force_equipment/gear/holoparasite item_type = /obj/item/guardiancreator/tech/choose/traitor diff --git a/tgstation.dme b/tgstation.dme index f46fff93756..3dadfa429ac 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -248,6 +248,7 @@ #include "code\__DEFINES\dcs\signals\signals_screentips.dm" #include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm" #include "code\__DEFINES\dcs\signals\signals_specie.dm" +#include "code\__DEFINES\dcs\signals\signals_spell.dm" #include "code\__DEFINES\dcs\signals\signals_storage.dm" #include "code\__DEFINES\dcs\signals\signals_subsystem.dm" #include "code\__DEFINES\dcs\signals\signals_swab.dm" @@ -269,7 +270,6 @@ #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movable.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movement.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_x_act.dm" -#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_abilities.dm" #include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_arcade.dm" #include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_carbon.dm" #include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_living.dm" @@ -631,7 +631,6 @@ #include "code\controllers\subsystem\processing\supermatter_cascade.dm" #include "code\controllers\subsystem\processing\tramprocess.dm" #include "code\controllers\subsystem\processing\wet_floors.dm" -#include "code\datums\action.dm" #include "code\datums\alarm.dm" #include "code\datums\armor.dm" #include "code\datums\beam.dm" @@ -686,15 +685,32 @@ #include "code\datums\achievements\misc_achievements.dm" #include "code\datums\achievements\misc_scores.dm" #include "code\datums\achievements\skill_achievements.dm" -#include "code\datums\actions\beam_rifle.dm" +#include "code\datums\actions\action.dm" +#include "code\datums\actions\cooldown_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\beserk.dm" +#include "code\datums\actions\items\boot_dash.dm" +#include "code\datums\actions\items\cult_dagger.dm" +#include "code\datums\actions\items\hands_free.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\blood_warp.dm" #include "code\datums\actions\mobs\charge.dm" #include "code\datums\actions\mobs\dash.dm" #include "code\datums\actions\mobs\fire_breath.dm" +#include "code\datums\actions\mobs\language_menu.dm" #include "code\datums\actions\mobs\lava_swoop.dm" #include "code\datums\actions\mobs\meteors.dm" #include "code\datums\actions\mobs\mobcooldown.dm" #include "code\datums\actions\mobs\projectileattack.dm" +#include "code\datums\actions\mobs\small_sprite.dm" #include "code\datums\actions\mobs\transform_weapon.dm" #include "code\datums\ai\_ai_behavior.dm" #include "code\datums\ai\_ai_controller.dm" @@ -1175,19 +1191,25 @@ #include "code\datums\mood_events\needs_events.dm" #include "code\datums\mutations\_combined.dm" #include "code\datums\mutations\_mutations.dm" -#include "code\datums\mutations\actions.dm" #include "code\datums\mutations\adaptation.dm" #include "code\datums\mutations\antenna.dm" +#include "code\datums\mutations\autotomy.dm" #include "code\datums\mutations\body.dm" #include "code\datums\mutations\chameleon.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\passive.dm" #include "code\datums\mutations\radioactive.dm" #include "code\datums\mutations\sight.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\mutations\holy_mutation\burdened.dm" #include "code\datums\mutations\holy_mutation\honorbound.dm" #include "code\datums\proximity_monitor\field.dm" @@ -1578,7 +1600,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\gun_maintenance.dm" #include "code\game\objects\items\hand_items.dm" #include "code\game\objects\items\handcuffs.dm" @@ -1712,6 +1733,29 @@ #include "code\game\objects\items\food\snacks.dm" #include "code\game\objects\items\food\soup.dm" #include "code\game\objects\items\food\spaghetti.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\sacred_flame.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\atmos_grenades.dm" @@ -2085,10 +2129,6 @@ #include "code\modules\admin\verbs\SDQL2\SDQL_2.dm" #include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm" #include "code\modules\admin\verbs\SDQL2\SDQL_2_wrappers.dm" -#include "code\modules\admin\verbs\SDQL2\SDQL_spells\executor_component.dm" -#include "code\modules\admin\verbs\SDQL2\SDQL_spells\spell_admin_panel.dm" -#include "code\modules\admin\verbs\SDQL2\SDQL_spells\spell_edit_menu.dm" -#include "code\modules\admin\verbs\SDQL2\SDQL_spells\spells.dm" #include "code\modules\admin\view_variables\admin_delete.dm" #include "code\modules\admin\view_variables\color_matrix_editor.dm" #include "code\modules\admin\view_variables\debug_variables.dm" @@ -2356,7 +2396,14 @@ #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\challenges.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\offensive.dm" +#include "code\modules\antagonists\wizard\equipment\spellbook_entries\summons.dm" #include "code\modules\antagonists\xeno\xeno.dm" #include "code\modules\art\paintings.dm" #include "code\modules\art\statues.dm" @@ -3371,7 +3418,6 @@ #include "code\modules\mob\dead\observer\observer_say.dm" #include "code\modules\mob\dead\observer\orbit.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" @@ -4222,48 +4268,79 @@ #include "code\modules\shuttle\white_ship.dm" #include "code\modules\spatial_grid\cell_tracker.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\bloodcrawl.dm" -#include "code\modules\spells\spell_types\charge.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\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\inflict_handler.dm" -#include "code\modules\spells\spell_types\knock.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\personality_commune.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\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\xeno.dm" +#include "code\modules\spells\spell_types\madness_curse.dm" +#include "code\modules\spells\spell_types\right_and_wrong.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\magic_missile.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_worlds.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\lighting_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_target\_list_target.dm" +#include "code\modules\spells\spell_types\list_target\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\pointed.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\summonitem.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\teleport\_teleport.dm" +#include "code\modules\spells\spell_types\teleport\blink.dm" +#include "code\modules\spells\spell_types\teleport\teleport.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\bsa.dm" #include "code\modules\station_goals\dna_vault.dm" #include "code\modules\station_goals\shield.dm" @@ -4642,7 +4719,6 @@ #include "modular_skyrat\master_files\code\datums\id_trim\solfed.dm" #include "modular_skyrat\master_files\code\datums\id_trim\syndicate.dm" #include "modular_skyrat\master_files\code\datums\martial\cqcplus.dm" -#include "modular_skyrat\master_files\code\datums\mutations\chameleon.dm" #include "modular_skyrat\master_files\code\datums\quirks\_quirk.dm" #include "modular_skyrat\master_files\code\datums\quirks\negative.dm" #include "modular_skyrat\master_files\code\datums\quirks\neutral.dm" @@ -4678,7 +4754,6 @@ #include "modular_skyrat\master_files\code\modules\antagonists\ert\ert.dm" #include "modular_skyrat\master_files\code\modules\antagonists\traitor\objectives\kill_pet.dm" #include "modular_skyrat\master_files\code\modules\antagonists\traitor\objectives\smuggling.dm" -#include "modular_skyrat\master_files\code\modules\antagonists\wizard\equipment\spellbook.dm" #include "modular_skyrat\master_files\code\modules\bandana\bandanafix.dm" #include "modular_skyrat\master_files\code\modules\buildmode\submodes\offercontrol.dm" #include "modular_skyrat\master_files\code\modules\cargo\exports\traitor.dm" @@ -4775,7 +4850,6 @@ #include "modular_skyrat\master_files\code\modules\research\machinery\departmental_circuit_imprinter.dm" #include "modular_skyrat\master_files\code\modules\research\techweb\all_nodes.dm" #include "modular_skyrat\master_files\code\modules\shuttle\shuttle.dm" -#include "modular_skyrat\master_files\code\modules\spells\wizard.dm" #include "modular_skyrat\master_files\code\modules\surgery\bodyparts\species_parts\_mutant_bodyparts.dm" #include "modular_skyrat\master_files\code\modules\surgery\bodyparts\species_parts\akula_bodyparts.dm" #include "modular_skyrat\master_files\code\modules\surgery\bodyparts\species_parts\ghoul_bodyparts.dm" @@ -5412,7 +5486,6 @@ #include "modular_skyrat\modules\hyposprays\code\vending_hypospray.dm" #include "modular_skyrat\modules\icspawning\code\cards_ids.dm" #include "modular_skyrat\modules\icspawning\code\observer.dm" -#include "modular_skyrat\modules\icspawning\code\spell.dm" #include "modular_skyrat\modules\icspawning\code\standard.dm" #include "modular_skyrat\modules\implants\code\augments_arms.dm" #include "modular_skyrat\modules\implants\code\augments_chest.dm" @@ -5579,7 +5652,6 @@ #include "modular_skyrat\modules\modular_items\code\goosenade.dm" #include "modular_skyrat\modules\modular_items\code\kinetic_crusher.dm" #include "modular_skyrat\modules\modular_items\code\makeshift.dm" -#include "modular_skyrat\modules\modular_items\code\miscellaneous.dm" #include "modular_skyrat\modules\modular_items\code\modular_glasses.dm" #include "modular_skyrat\modules\modular_items\code\necklace.dm" #include "modular_skyrat\modules\modular_items\code\pastries.dm" @@ -5702,7 +5774,6 @@ #include "modular_skyrat\modules\moretraitoritems\code\drinkingglass.dm" #include "modular_skyrat\modules\moretraitoritems\code\fake_announcement.dm" #include "modular_skyrat\modules\moretraitoritems\code\glue.dm" -#include "modular_skyrat\modules\moretraitoritems\code\granters.dm" #include "modular_skyrat\modules\moretraitoritems\code\headset.dm" #include "modular_skyrat\modules\moretraitoritems\code\mafioso.dm" #include "modular_skyrat\modules\moretraitoritems\code\smuggling_gear.dm" @@ -5711,11 +5782,6 @@ #include "modular_skyrat\modules\moretraitoritems\code\syndiemirror.dm" #include "modular_skyrat\modules\moretraitoritems\code\uplink_kits.dm" #include "modular_skyrat\modules\moretraitoritems\code\weaponry.dm" -#include "modular_skyrat\modules\morewizardstuffs\code\antag_spawner.dm" -#include "modular_skyrat\modules\morewizardstuffs\code\snapceleritas.dm" -#include "modular_skyrat\modules\morewizardstuffs\code\spellbooks.dm" -#include "modular_skyrat\modules\morewizardstuffs\code\summonknives.dm" -#include "modular_skyrat\modules\morewizardstuffs\code\tiedshoes.dm" #include "modular_skyrat\modules\mounted_machine_gun\code\ammobox.dm" #include "modular_skyrat\modules\mounted_machine_gun\code\mounted_machine_gun.dm" #include "modular_skyrat\modules\multicellcharger\code\multi_cell_charger.dm" diff --git a/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx b/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx deleted file mode 100644 index 901bcf842a2..00000000000 --- a/tgui/packages/tgui/interfaces/SDQLSpellAdminPanel.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useBackend } from '../backend'; -import { Button, Table } from '../components'; -import { Window } from '../layouts'; - -type SDQLSpellAdminPanelData = { - spells: { - ref: string; - name: string; - ownerRef: string; - owner: string; - creator: string; - }[]; -}; - -export const SDQLSpellAdminPanel = (props, context) => { - const { act, data } = useBackend(context); - - return ( - - - - - Spell Name - - Spell Owner - - Spell Creator - - Actions - - - {data.spells.map((spell) => { - const createSpellAct = (action: string) => () => { - act(action, { spell: spell.ref }); - }; - - const createOwnerAct = (action: string) => () => { - act(action, { owner: spell.ownerRef }); - }; - - return ( - - {spell.name} - - {spell.owner} - - {spell.creator} - - - - - - - - - - - - ); - })} -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/SDQLSpellMenu.js b/tgui/packages/tgui/interfaces/SDQLSpellMenu.js deleted file mode 100644 index 802d26302f5..00000000000 --- a/tgui/packages/tgui/interfaces/SDQLSpellMenu.js +++ /dev/null @@ -1,951 +0,0 @@ -import { useBackend } from '../backend'; -import { Box, Button, Collapsible, Dropdown, Input, LabeledList, Modal, NumberInput, Section, Stack, Tooltip } from '../components'; -import { Window } from '../layouts'; - -/** - * Gets a list of objects that encode the parameters for the variables relevant - * to the passed spell type. - * @param type The type of the spell. - * @returns A list of objects. Each object contains the name of the variable, - * the variable's data type, - * what options are valid (if the variable is an enum), - * and what the variable's default value should be. - */ -const typevars = (type) => { - let ret = [ - { name: 'name', type: 'string', options: null, default_value: '' }, - { name: 'desc', type: 'string', options: null, default_value: '' }, - { name: 'query', type: 'string', options: null, default_value: '' }, - { - name: 'suppress_message_admins', - type: 'bool', - options: null, - default_value: false, - }, - { name: 'action_icon', type: 'string', options: null, default_value: '' }, - { - name: 'action_icon_state', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'action_background_icon_state', - type: 'string', - options: null, - default_value: '', - }, - { name: 'sound', type: 'string', options: null, default_value: '' }, - { - name: 'charge_type', - type: 'string_enum', - options: ['recharge', 'charges', 'holder_var'], - default_value: 'recharge', - }, - { name: 'charge_max', type: 'int', options: null, default_value: 100 }, - { - name: 'still_recharging_msg', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'holder_var_type', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'holder_var_amount', - type: 'int', - options: null, - default_value: '', - }, - { name: 'clothes_req', type: 'bool', options: null, default_value: false }, - { name: 'human_req', type: 'bool', options: null, default_value: false }, - { - name: 'nonabstract_req', - type: 'bool', - options: null, - default_value: false, - }, - { name: 'stat_allowed', type: 'bool', options: null, default_value: false }, - { - name: 'phase_allowed', - type: 'bool', - options: null, - default_value: false, - }, - { - name: 'antimagic_allowed', - type: 'bool', - options: null, - default_value: false, - }, - { - name: 'invocation_type', - type: 'string_enum', - options: ['none', 'whisper', 'emote', 'shout'], - default_value: 'none', - }, - { name: 'invocation', type: 'string', options: null, default_value: '' }, - { - name: 'invocation_emote_self', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'selection_type', - type: 'string_enum', - options: ['view', 'range'], - default_value: 'view', - }, - { name: 'range', type: 'int', options: null, default_value: 7 }, - { name: 'message', type: 'string', options: null, default_value: '' }, - { name: 'player_lock', type: 'bool', options: null, default_value: true }, - { - name: 'sparks_spread', - type: 'bool', - options: null, - default_value: false, - }, - { name: 'sparks_amt', type: 'int', options: null, default_value: 0 }, - { - name: 'smoke_spread', - type: 'int_enum', - options: ['none', 'harmless', 'harmful', 'sleeping'], - default_value: 'none', - }, - { name: 'smoke_amt', type: 'int', options: null, default_value: 0 }, - { - name: 'centcom_cancast', - type: 'bool', - options: null, - default_value: false, - }, - ]; - switch (type) { - case 'targeted': - ret.push( - { name: 'overlay', type: 'bool', options: null, default_value: false }, - { - name: 'overlay_icon', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'overlay_icon_state', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'overlay_lifespan', - type: 'int', - options: null, - default_value: 0, - }, - { - name: 'max_targets', - type: 'int', - options: null, - default_value: false, - }, - { - name: 'target_ignore_prev', - type: 'bool', - options: null, - default_value: true, - }, - { - name: 'include_user', - type: 'bool', - options: null, - default_value: false, - }, - { - name: 'random_target', - type: 'bool', - options: null, - default_value: false, - }, - { - name: 'random_target_priority', - type: 'int_enum', - options: ['closest', 'random'], - default_value: 'closest', - } - ); - break; - case 'aoe_turf': - ret = ret.filter((variable) => variable.name !== 'selection_type'); - ret.push( - { name: 'inner_radius', type: 'int', options: null, default_value: -1 }, - { name: 'overlay', type: 'bool', options: null, default_value: false }, - { - name: 'overlay_icon', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'overlay_icon_state', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'overlay_lifespan', - type: 'int', - options: null, - default_value: 0, - } - ); - break; - case 'self': - ret = ret.filter( - (variable) => - variable.name !== 'range' && variable.name !== 'selection_type' - ); - break; - case 'aimed': - ret.push( - { - name: 'base_icon_state', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'ranged_mousepointer', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'deactive_msg', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'active_msg', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'projectile_type', - type: 'path', - options: null, - default_value: '/obj/projectile', - }, - { - name: 'projectile_amount', - type: 'int', - options: null, - default_value: 1, - }, - { - name: 'projectiles_per_fire', - type: 'int', - options: null, - default_value: 1, - }, - { - name: 'projectile_var_overrides', - type: 'list', - options: null, - default_value: {}, - } - ); - break; - case 'cone': - case 'cone/staggered': - ret = ret.filter( - (variable) => - variable.name !== 'range' && variable.name !== 'selection_type' - ); - ret.push( - { name: 'cone_level', type: 'int', options: null, default_value: 3 }, - { - name: 'respect_density', - type: 'bool', - options: null, - default_value: false, - } - ); - break; - case 'pointed': - ret.push( - { name: 'overlay', type: 'bool', options: null, default_value: false }, - { - name: 'overlay_icon', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'overlay_icon_state', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'overlay_lifespan', - type: 'int', - options: null, - default_value: 0, - }, - { - name: 'ranged_mousepointer', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'deactive_msg', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'active_msg', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'self_castable', - type: 'bool', - options: null, - default_value: false, - }, - { name: 'aim_assist', type: 'bool', options: null, default_value: true } - ); - break; - case 'targeted/touch': - ret = ret.filter( - (variable) => - variable.name !== 'range' && - variable.name !== 'invocation_type' && - variable.name !== 'selection_type' - ); - ret.push( - { - name: 'drawmessage', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'dropmessage', - type: 'string', - options: null, - default_value: '', - }, - { - name: 'hand_path', - type: 'path', - options: null, - default_value: '/obj/item/melee/touch_attack', - }, - { - name: 'hand_var_overrides', - type: 'list', - options: null, - default_value: {}, - } - ); - break; - default: - return []; - } - ret.push({ - name: 'scratchpad', - type: 'list', - options: null, - default_value: [], - }); - return ret; -}; - -export const SDQLSpellMenu = (props, context) => { - const { act, data } = useBackend(context); - const { type, types, fixed_type, alert, parse_errors } = data; - - return ( - - - - - - - act('type', { path: value })} - /> - - - - - - - - act('confirm')} - /> - - - - - {alert} - - - - - - - - - - {parse_errors && } - - ); -}; - -/** - * Used to determine whether or not to show a UI element corresponding to a - * variable. - * @param entry An object, from the list of objects returned by typevars(), - * corresponding to the variable to be shown or hidden. - * @param saved_vars The list of currently stored variable values. - * @returns Whether or not to show the UI element corresponding to the variable - * represented by the passed entry. - */ -const varCondition = (entry, saved_vars) => { - switch (entry.name) { - case 'charge_max': - return saved_vars['charge_type'] !== 'holder_var'; - case 'holder_var_type': - case 'holder_var_amount': - return saved_vars['charge_type'] === 'holder_var'; - case 'human_req': - return !saved_vars['clothes_req']; - case 'invocation': - return saved_vars['invocation_type'] !== 'none'; - case 'invocation_emote_self': - return saved_vars['invocation_type'] === 'emote'; - case 'overlay_icon': - case 'overlay_icon_state': - case 'overlay_lifespan': - return !!saved_vars['overlay']; - case 'sparks_amt': - return !!saved_vars['sparks_spread']; - case 'smoke_amt': - return !!saved_vars['smoke_spread']; - case 'random_target_priority': - return !!saved_vars['random_target']; - default: - return true; - } -}; - -/** - * A React component that wraps its contents in a tooltip object, - * if one exists for the variable described by the object passed through the - * entry property. - * - * @param entry An object, from the list of objects returned by typevars(), - * corresponding to the variable whose tooltip is to be shown. - */ -const WrapInTooltip = (props, context) => { - const { data } = useBackend(context); - const { entry, children } = props; - const { type, tooltips } = data; - const tip = tooltips[entry.name]?.replace( - '$type', - tooltips[entry.name + '_' + type] - ); - // TODO: Uncomment this block when tooltips no longer suck. - if (tip) { - return ( - - {children} - - ); - } else { - return children; - } -}; - -/** - * A React component that contains a list of the meaningfully-editable variables - * of the spell being edited. - */ -const SDQLSpellOptions = (props, context) => { - const { data } = useBackend(context); - const { type, saved_vars } = data; - - const vars = typevars(type); - - return ( -
- {vars - .filter((entry) => varCondition(entry, saved_vars)) - .map((entry) => ( - - - - - {entry.name}: - - - - - - - - ))} -
- ); -}; - -/** - * A React component that contains the appropriate input element for the - * variable described by the object passed through the entry property. - * @param entry An object, from the list of objects returned by typevars(), - * corresponding to the variable to provide an input element for. - */ -const SDQLSpellInput = (props, context) => { - const { act, data } = useBackend(context); - const { saved_vars } = data; - const { entry } = props; - const { name, type, options, default_value } = entry; - switch (type) { - case 'string': - return ( - act('variable', { name, value })} - /> - ); - case 'int': - return ( - act('variable', { name, value })} - /> - ); - case 'bool': - return ( - act('bool_variable', { name })} - /> - ); - case 'string_enum': - return ( - act('variable', { name, value })} - /> - ); - case 'int_enum': - return ( - - act('variable', { name, value: options.indexOf(value) }) - } - /> - ); - case 'path': - return ( - - - - - ); -}; - -const SDQLSpellParsedList = (props, context) => { - const { list, name, recursive_props } = props; - const { act, data } = useBackend(context); - const { parsed_list_vars } = data; - return ( - - - - {Object.entries(parsed_list_vars[list]) - .sort(([, { type_1 }], [, { type_2 }]) => { - return (type_1 === 'list' ? 1 : 0) - (type_2 === 'list' ? 1 : 0); - }) - .map(([name, { type, value }]) => { - return type === 'list' ? ( - - ) : ( - - {value} - - ); - })} - - - - ); -}; diff --git a/tgui/packages/tgui/interfaces/Spellbook.js b/tgui/packages/tgui/interfaces/Spellbook.js index 51aecc64268..d146d275da0 100644 --- a/tgui/packages/tgui/interfaces/Spellbook.js +++ b/tgui/packages/tgui/interfaces/Spellbook.js @@ -276,8 +276,8 @@ const Loadouts = (props, context) => { author="Jegudiel Worldshaker" blurb={multiline` The power of the mighty Mjolnir! Best not to lose it. - This loadout has Summon Item, Mutate, Blink, and - Force Wall. Mutate is your utility in this case: + This loadout has Summon Item, Mutate, Blink, Force Wall, + Tesla Blast, and Mjolnir. Mutate is your utility in this case: Use it for limited ranged fire and getting out of bad blinks. `} /> @@ -462,11 +462,13 @@ export const Spellbook = (props, context) => {