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 += "